diff --git a/recipes/aabenraalokalavisen_dk.recipe b/recipes/aabenraalokalavisen_dk.recipe index 432721f305..6eb932aa71 100644 --- a/recipes/aabenraalokalavisen_dk.recipe +++ b/recipes/aabenraalokalavisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Lokalavisen Aabenraa ''' + class AabenraaLokalavisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Lokalavisen Aabenraa' diff --git a/recipes/aarhuslokalavisen_dk.recipe b/recipes/aarhuslokalavisen_dk.recipe index 6910232257..3ad3eec402 100644 --- a/recipes/aarhuslokalavisen_dk.recipe +++ b/recipes/aarhuslokalavisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Lokalavisen Aarhus ''' + class AarhusLokalavisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Lokalavisen Aarhus' diff --git a/recipes/aarhusmidtlokalavisen_dk.recipe b/recipes/aarhusmidtlokalavisen_dk.recipe index 006312fc79..089508a4f7 100644 --- a/recipes/aarhusmidtlokalavisen_dk.recipe +++ b/recipes/aarhusmidtlokalavisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Lokalavisen Aarhus Midt ''' + class AarhusmidtLokalavisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Lokalavisen Aarhus Midt' diff --git a/recipes/aarhusnordlokalavisen_dk.recipe b/recipes/aarhusnordlokalavisen_dk.recipe index 59d72ad788..bf75809806 100644 --- a/recipes/aarhusnordlokalavisen_dk.recipe +++ b/recipes/aarhusnordlokalavisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Lokalavisen Aarhus Nord ''' + class AarhusnordLokalavisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Lokalavisen Aarhus Nord' diff --git a/recipes/aarhussydlokalavisen_dk.recipe b/recipes/aarhussydlokalavisen_dk.recipe index 9869af3561..daa11fe50c 100644 --- a/recipes/aarhussydlokalavisen_dk.recipe +++ b/recipes/aarhussydlokalavisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Lokalavisen Aarhus Syd ''' + class AarhussydLokalavisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Lokalavisen Aarhus Syd' diff --git a/recipes/aarhusvestlokalavisen_dk.recipe b/recipes/aarhusvestlokalavisen_dk.recipe index d0ba99b74a..f2dffe63d4 100644 --- a/recipes/aarhusvestlokalavisen_dk.recipe +++ b/recipes/aarhusvestlokalavisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Lokalavisen Aarhus Ves ''' + class AarhusvestLokalavisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Lokalavisen Aarhus Ves' diff --git a/recipes/albertslundlokalavisen_dk.recipe b/recipes/albertslundlokalavisen_dk.recipe index 172e7dd24d..55d2c26714 100644 --- a/recipes/albertslundlokalavisen_dk.recipe +++ b/recipes/albertslundlokalavisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Albertslund Posten ''' + class AlbertslundLokalavisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Albertslund Posten' diff --git a/recipes/alleroedlokalavisen_dk.recipe b/recipes/alleroedlokalavisen_dk.recipe index 29640dcf4b..939ba1411c 100644 --- a/recipes/alleroedlokalavisen_dk.recipe +++ b/recipes/alleroedlokalavisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Allerød Nyt: RSS feed: Seneste nyt - alleroed.lokalavisen.dk ''' + class AlleroedLokalavisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Allerød Nyt - alleroed.lokalavisen.dk' diff --git a/recipes/altomdata_dk.recipe b/recipes/altomdata_dk.recipe index 47de230d13..235ff6ae6b 100644 --- a/recipes/altomdata_dk.recipe +++ b/recipes/altomdata_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Alt om DATA, Datatid TechLife - Download, test, antivirus, netværk ''' + class WwwAltomdata_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Alt om DATA, Datatid TechLife - Download, test, antivirus, netværk' diff --git a/recipes/amagerbladet_dk.recipe b/recipes/amagerbladet_dk.recipe index 908e008998..ea22f781ec 100644 --- a/recipes/amagerbladet_dk.recipe +++ b/recipes/amagerbladet_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Amagerbladet ''' + class Amagerbladet_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Amagerbladet' diff --git a/recipes/avisen_dk.recipe b/recipes/avisen_dk.recipe index 5f2cd8b020..2a45af8ab4 100644 --- a/recipes/avisen_dk.recipe +++ b/recipes/avisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Avisen.dk ''' + class WwwAvisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Avisen.dk' diff --git a/recipes/cityavisen_dk.recipe b/recipes/cityavisen_dk.recipe index 42269d44cc..a85f7e0088 100644 --- a/recipes/cityavisen_dk.recipe +++ b/recipes/cityavisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe City Avisen ''' + class CityAvisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'City Avisen' diff --git a/recipes/computerworld_dk.recipe b/recipes/computerworld_dk.recipe index 5a53654898..959bc901e9 100644 --- a/recipes/computerworld_dk.recipe +++ b/recipes/computerworld_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Computerworld.dk ''' + class WwwComputerworld_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Computerworld.dk' diff --git a/recipes/contropiano.recipe b/recipes/contropiano.recipe index 2dc0fb16a5..26108dec66 100644 --- a/recipes/contropiano.recipe +++ b/recipes/contropiano.recipe @@ -1,5 +1,6 @@ from calibre.web.feeds.news import BasicNewsRecipe + class AdvancedUserRecipe(BasicNewsRecipe): title = u'Contropiano' oldest_article = 7 diff --git a/recipes/data_news.recipe b/recipes/data_news.recipe index 5ea2448967..39d34bdd32 100644 --- a/recipes/data_news.recipe +++ b/recipes/data_news.recipe @@ -3,6 +3,7 @@ from __future__ import unicode_literals, division, absolute_import, print_function from calibre.web.feeds.news import BasicNewsRecipe + class AdvancedUserRecipe1468055030(BasicNewsRecipe): title = 'DataNews' __author__ = 'oCkz7bJ_' diff --git a/recipes/de_standaard.recipe b/recipes/de_standaard.recipe index 2170eceb84..f64254e1ab 100644 --- a/recipes/de_standaard.recipe +++ b/recipes/de_standaard.recipe @@ -4,6 +4,7 @@ from __future__ import unicode_literals, division, absolute_import, print_functi import re from calibre.web.feeds.news import BasicNewsRecipe + class AdvancedUserRecipe1467571059(BasicNewsRecipe): title = 'De Standaard' __author__ = 'Darko Miletic, Aimylios, oCkz7bJ_' diff --git a/recipes/djurslandsposten_dk.recipe b/recipes/djurslandsposten_dk.recipe index 2f7211514e..a74d0a4ee7 100644 --- a/recipes/djurslandsposten_dk.recipe +++ b/recipes/djurslandsposten_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe DjurslandsPosten ''' + class DjurslandsPosten_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'DjurslandsPosten' diff --git a/recipes/dr_dk.recipe b/recipes/dr_dk.recipe index 10cbf71a17..a017dff9dd 100644 --- a/recipes/dr_dk.recipe +++ b/recipes/dr_dk.recipe @@ -9,6 +9,7 @@ __copyright__ = '2010, Darko Miletic ' DR.dk ''' + class DRNyheder(BasicNewsRecipe): title = 'DR Nyheder' __author__ = 'Darko Miletic' diff --git a/recipes/ebeltoftlokalavisen_dk.recipe b/recipes/ebeltoftlokalavisen_dk.recipe index 870dda7170..be8189a764 100644 --- a/recipes/ebeltoftlokalavisen_dk.recipe +++ b/recipes/ebeltoftlokalavisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Adresseavisen Ebeltoft ''' + class EbeltoftLokalavisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Adresseavisen Ebeltoft' diff --git a/recipes/egedallokalavisen_dk.recipe b/recipes/egedallokalavisen_dk.recipe index 654de63138..2b2a3ba250 100644 --- a/recipes/egedallokalavisen_dk.recipe +++ b/recipes/egedallokalavisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Lokalavisen Egedal ''' + class EgedalLokalavisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Lokalavisen Egedal' diff --git a/recipes/el_universal.recipe b/recipes/el_universal.recipe index e69bf8629f..aa7feeff04 100644 --- a/recipes/el_universal.recipe +++ b/recipes/el_universal.recipe @@ -6,6 +6,7 @@ eluniversal.com.mx from calibre.web.feeds.news import BasicNewsRecipe + class ElUniversal(BasicNewsRecipe): title = 'El Universal' __author__ = 'Darko Miletic' diff --git a/recipes/eltribuno_salta_impreso.recipe b/recipes/eltribuno_salta_impreso.recipe index bf50ffce22..5530b552dd 100644 --- a/recipes/eltribuno_salta_impreso.recipe +++ b/recipes/eltribuno_salta_impreso.recipe @@ -6,6 +6,7 @@ http://www.eltribuno.info/salta/edicion_impresa.aspx from calibre.web.feeds.news import BasicNewsRecipe + class ElTribunoSaltaImpreso(BasicNewsRecipe): title = 'El Tribuno Salta' __author__ = 'Darko Miletic' diff --git a/recipes/erhvervs_avisen_dk.recipe b/recipes/erhvervs_avisen_dk.recipe index c5a693fac8..3d204e054b 100644 --- a/recipes/erhvervs_avisen_dk.recipe +++ b/recipes/erhvervs_avisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Erhvervs•Avisen: RSS feed: Seneste nyt - erhvervsavisen.dk ''' + class Erhvervsavisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Erhvervs Avisen' diff --git a/recipes/esbjerglokalavisen_dk.recipe b/recipes/esbjerglokalavisen_dk.recipe index 6e2727978b..76c0d87104 100644 --- a/recipes/esbjerglokalavisen_dk.recipe +++ b/recipes/esbjerglokalavisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Lokalavisen Esbjerg: RSS feed: Seneste nyt - esbjerg.lokalavisen.dk ''' + class EsbjergLokalavisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Lokalavisen Esbjerg' diff --git a/recipes/favrskovavisen_dk.recipe b/recipes/favrskovavisen_dk.recipe index bc03dfcbe1..55ea741cc4 100644 --- a/recipes/favrskovavisen_dk.recipe +++ b/recipes/favrskovavisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Favrskov Avisen ''' + class FavrskovAvisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Favrskov Avisen' diff --git a/recipes/favrskovlokalavisen_dk.recipe b/recipes/favrskovlokalavisen_dk.recipe index 20fa44c09f..0aee4b3061 100644 --- a/recipes/favrskovlokalavisen_dk.recipe +++ b/recipes/favrskovlokalavisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Favrskovposten ''' + class FavrskovLokalavisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Favrskovposten' diff --git a/recipes/folkebladet_dk.recipe b/recipes/folkebladet_dk.recipe index 2e1afb6093..48d70aa036 100644 --- a/recipes/folkebladet_dk.recipe +++ b/recipes/folkebladet_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Folkebladet ''' + class Folkebladet_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Folkebladet' diff --git a/recipes/folkebladetdjursland_dk.recipe b/recipes/folkebladetdjursland_dk.recipe index 914332c7da..c6f2f945c9 100644 --- a/recipes/folkebladetdjursland_dk.recipe +++ b/recipes/folkebladetdjursland_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Folkebladet Djursland ''' + class FolkebladetDjursland_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Folkebladet Djursland' diff --git a/recipes/folketidende_dk.recipe b/recipes/folketidende_dk.recipe index fca03a3e7f..4bcc121e6c 100644 --- a/recipes/folketidende_dk.recipe +++ b/recipes/folketidende_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe folketidende.dk ''' + class Folketidende_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'folketidende.dk' diff --git a/recipes/fredensborglokalavisen_dk.recipe b/recipes/fredensborglokalavisen_dk.recipe index 8ba23696f3..68d8cc07dc 100644 --- a/recipes/fredensborglokalavisen_dk.recipe +++ b/recipes/fredensborglokalavisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Uge-Nyt ''' + class FredensborgLokalavisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Uge-Nyt' diff --git a/recipes/fredericialokalavisen_dk.recipe b/recipes/fredericialokalavisen_dk.recipe index 22d5c9e62d..39180e2995 100644 --- a/recipes/fredericialokalavisen_dk.recipe +++ b/recipes/fredericialokalavisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Lokalavisen Fredericia ''' + class FredericiaLokalavisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Lokalavisen Fredericia' diff --git a/recipes/frederiksbergbladet_dk.recipe b/recipes/frederiksbergbladet_dk.recipe index 53cb71e742..4002ce0e59 100644 --- a/recipes/frederiksbergbladet_dk.recipe +++ b/recipes/frederiksbergbladet_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Frederiksberg Bladet ''' + class FrederiksbergBladet_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Frederiksberg Bladet' diff --git a/recipes/frederikssundlokalavisen_dk.recipe b/recipes/frederikssundlokalavisen_dk.recipe index 5c93698179..46d4d5ffe4 100644 --- a/recipes/frederikssundlokalavisen_dk.recipe +++ b/recipes/frederikssundlokalavisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Lokalavisen Frederikssund ''' + class FrederikssundLokalavisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Lokalavisen Frederikssund' diff --git a/recipes/furesoelokalavisen_dk.recipe b/recipes/furesoelokalavisen_dk.recipe index 87da12d80e..f9925717a4 100644 --- a/recipes/furesoelokalavisen_dk.recipe +++ b/recipes/furesoelokalavisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Furesø Avis ''' + class FuresoeLokalavisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Furesø Avis' diff --git a/recipes/gentoftelokalavisen_dk.recipe b/recipes/gentoftelokalavisen_dk.recipe index ebbbcc10dc..0ae2fbe62b 100644 --- a/recipes/gentoftelokalavisen_dk.recipe +++ b/recipes/gentoftelokalavisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Villabyerne ''' + class GentofteLokalavisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Villabyerne' diff --git a/recipes/grenaalokalavisen_dk.recipe b/recipes/grenaalokalavisen_dk.recipe index caae207b9b..32973c3d66 100644 --- a/recipes/grenaalokalavisen_dk.recipe +++ b/recipes/grenaalokalavisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Lokalavisen Grenaa ''' + class GrenaaLokalavisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Lokalavisen Grenaa' diff --git a/recipes/gribskovlokalavisen_dk.recipe b/recipes/gribskovlokalavisen_dk.recipe index c98afd917d..3e8eb7d69f 100644 --- a/recipes/gribskovlokalavisen_dk.recipe +++ b/recipes/gribskovlokalavisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Ugeposten Gribskov ''' + class GribskovLokalavisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Ugeposten Gribskov' diff --git a/recipes/haderslevlokalavisen_dk.recipe b/recipes/haderslevlokalavisen_dk.recipe index 6b72b24a18..2460e813a4 100644 --- a/recipes/haderslevlokalavisen_dk.recipe +++ b/recipes/haderslevlokalavisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Lokalavisen Haderslev ''' + class HaderslevLokalavisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Lokalavisen Haderslev' diff --git a/recipes/halsnaeslokalavisen_dk.recipe b/recipes/halsnaeslokalavisen_dk.recipe index bbd4cdbb4a..c77b6b7387 100644 --- a/recipes/halsnaeslokalavisen_dk.recipe +++ b/recipes/halsnaeslokalavisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Halsnæs Avis ''' + class HalsnaesLokalavisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Halsnæs Avis' diff --git a/recipes/hillerod_posten_dk.recipe b/recipes/hillerod_posten_dk.recipe index 743338b335..f356bb622e 100644 --- a/recipes/hillerod_posten_dk.recipe +++ b/recipes/hillerod_posten_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Hillerød Posten ''' + class HilleroedLokalavisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Hillerød Posten' diff --git a/recipes/hoersholmlokalavisen_dk.recipe b/recipes/hoersholmlokalavisen_dk.recipe index ae04b90896..94dfa01f45 100644 --- a/recipes/hoersholmlokalavisen_dk.recipe +++ b/recipes/hoersholmlokalavisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Ugebladet ''' + class HoersholmLokalavisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Ugebladet' diff --git a/recipes/hornsherredlokalavisen_dk.recipe b/recipes/hornsherredlokalavisen_dk.recipe index c8caf8e8fa..0a24551b1c 100644 --- a/recipes/hornsherredlokalavisen_dk.recipe +++ b/recipes/hornsherredlokalavisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Lokalavisen Hornsherred ''' + class HornsherredLokalavisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Lokalavisen Hornsherred' diff --git a/recipes/hvidovreavis_dk.recipe b/recipes/hvidovreavis_dk.recipe index 419e57a46b..a61004820f 100644 --- a/recipes/hvidovreavis_dk.recipe +++ b/recipes/hvidovreavis_dk.recipe @@ -3,6 +3,7 @@ from __future__ import unicode_literals, division, absolute_import, print_function from calibre.web.feeds.news import BasicNewsRecipe + class Hvidovre_Avis_dk(BasicNewsRecipe): title = 'Hvidovre avis' language = 'da' diff --git a/recipes/hvidovrelokalavisen_dk.recipe b/recipes/hvidovrelokalavisen_dk.recipe index 6f816716de..f4da692e0a 100644 --- a/recipes/hvidovrelokalavisen_dk.recipe +++ b/recipes/hvidovrelokalavisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Hvidovre Avis ''' + class HvidovreLokalavisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Hvidovre Avis' diff --git a/recipes/ing_dk.recipe b/recipes/ing_dk.recipe index 9500bc465c..3b2ade2cd7 100644 --- a/recipes/ing_dk.recipe +++ b/recipes/ing_dk.recipe @@ -6,6 +6,8 @@ from calibre.web.feeds.news import BasicNewsRecipe ''' Ingeniøren.dk ''' + + class Ing_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Ingeniøren' diff --git a/recipes/jot_down.recipe b/recipes/jot_down.recipe index 8ebfc87360..5c7e0df477 100644 --- a/recipes/jot_down.recipe +++ b/recipes/jot_down.recipe @@ -12,6 +12,7 @@ http://www.jotdown.es/ import re from calibre.web.feeds.news import BasicNewsRecipe + class jotdown(BasicNewsRecipe): author = 'desUBIKado' description = 'Revista digital con magníficos y extensos artículos' diff --git a/recipes/jv_dk.recipe b/recipes/jv_dk.recipe index a611a1a78e..81a369a09e 100644 --- a/recipes/jv_dk.recipe +++ b/recipes/jv_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe JydskeVestkysten | JV.dk | jv.dk ''' + class WwwJv_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'JydskeVestkysten | JV.dk | jv.dk' diff --git a/recipes/kaloeviglokalavisen_dk.recipe b/recipes/kaloeviglokalavisen_dk.recipe index 29ade1f84c..721d7ef3ad 100644 --- a/recipes/kaloeviglokalavisen_dk.recipe +++ b/recipes/kaloeviglokalavisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Lokalavisen Kalø Vig ''' + class KaloevigLokalavisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Lokalavisen Kalø Vig' diff --git a/recipes/kgsenghavebladet_dk.recipe b/recipes/kgsenghavebladet_dk.recipe index 64c8d00e97..ae2b21aad5 100644 --- a/recipes/kgsenghavebladet_dk.recipe +++ b/recipes/kgsenghavebladet_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Kgs. Enghave Bladet ''' + class KgsEnghaveBladet_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Kgs. Enghave Bladet' diff --git a/recipes/koegelokalavisen_dk.recipe b/recipes/koegelokalavisen_dk.recipe index 9c6bc8b720..fb4d8532cb 100644 --- a/recipes/koegelokalavisen_dk.recipe +++ b/recipes/koegelokalavisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Lørdagsavisen ''' + class KoegeLokalavisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Lørdagsavisen' diff --git a/recipes/koldinglokalavisen_dk.recipe b/recipes/koldinglokalavisen_dk.recipe index febdcbbc65..ceaecdad3b 100644 --- a/recipes/koldinglokalavisen_dk.recipe +++ b/recipes/koldinglokalavisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Lokalavisen Kolding ''' + class KoldingLokalavisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Lokalavisen Kolding' diff --git a/recipes/kristeligt_dagblad_dk.recipe b/recipes/kristeligt_dagblad_dk.recipe index ed5565993b..899980924d 100644 --- a/recipes/kristeligt_dagblad_dk.recipe +++ b/recipes/kristeligt_dagblad_dk.recipe @@ -3,6 +3,7 @@ from __future__ import unicode_literals, division, absolute_import, print_function from calibre.web.feeds.news import BasicNewsRecipe + class KristeligtDagblad(BasicNewsRecipe): title = 'Kristeligt Dagblad' language = 'da' diff --git a/recipes/lescienze.recipe b/recipes/lescienze.recipe index 8d263679db..e8876e2b97 100644 --- a/recipes/lescienze.recipe +++ b/recipes/lescienze.recipe @@ -4,6 +4,7 @@ __author__ = 'Daniele Forsi' from calibre.web.feeds.recipes import BasicNewsRecipe + class LeScienze(BasicNewsRecipe): title = 'Le Scienze' description = 'Edizione italiana di Scientific American' diff --git a/recipes/lyngby-taarbaeklokalavisen_dk.recipe b/recipes/lyngby-taarbaeklokalavisen_dk.recipe index 30d7afff48..4fbe32ad80 100644 --- a/recipes/lyngby-taarbaeklokalavisen_dk.recipe +++ b/recipes/lyngby-taarbaeklokalavisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Det grønne område ''' + class Lyngby_taarbaekLokalavisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Det grønne område' diff --git a/recipes/nakedcapitalism.recipe b/recipes/nakedcapitalism.recipe index 78d30f03dd..6da0b76038 100644 --- a/recipes/nakedcapitalism.recipe +++ b/recipes/nakedcapitalism.recipe @@ -7,6 +7,7 @@ www.nakedcapitalism.com from calibre.web.feeds.news import BasicNewsRecipe + class nakedcapitalism(BasicNewsRecipe): title = 'Naked Capitalism' __author__ = 'Darko Miletic' diff --git a/recipes/noerrebronordvestbladet_dk.recipe b/recipes/noerrebronordvestbladet_dk.recipe index 53efe69d12..3acadd0147 100644 --- a/recipes/noerrebronordvestbladet_dk.recipe +++ b/recipes/noerrebronordvestbladet_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Nørrebro Nordvest bladet ''' + class Minby_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Nørrebro Nordvest bladet' diff --git a/recipes/norddjurslokalavisen_dk.recipe b/recipes/norddjurslokalavisen_dk.recipe index 2b52fcbe56..e566982015 100644 --- a/recipes/norddjurslokalavisen_dk.recipe +++ b/recipes/norddjurslokalavisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Lokalavisen Norddjurs ''' + class NorddjursLokalavisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Lokalavisen Norddjurs' diff --git a/recipes/nordjyske_dk.recipe b/recipes/nordjyske_dk.recipe index 08fe34440b..16b204a896 100644 --- a/recipes/nordjyske_dk.recipe +++ b/recipes/nordjyske_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Nordjyske.dk ''' + class Nordjyske_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Nordjyske.dk' diff --git a/recipes/odenselokalavisen_dk.recipe b/recipes/odenselokalavisen_dk.recipe index 684d8e3ecb..7733a6499e 100644 --- a/recipes/odenselokalavisen_dk.recipe +++ b/recipes/odenselokalavisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Lokalavisen Odense ''' + class OdenseLokalavisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Lokalavisen Odense' diff --git a/recipes/oesterbroavis_dk.recipe b/recipes/oesterbroavis_dk.recipe index 22a87d349b..f3d35fe332 100644 --- a/recipes/oesterbroavis_dk.recipe +++ b/recipes/oesterbroavis_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Østerbro Avis ''' + class OesterbroAvis_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Østerbro Avis' diff --git a/recipes/private_eye.recipe b/recipes/private_eye.recipe index c9c4e50566..54fc22b730 100644 --- a/recipes/private_eye.recipe +++ b/recipes/private_eye.recipe @@ -1,6 +1,7 @@ import re from calibre.web.feeds.news import BasicNewsRecipe + class AdvancedUserRecipe1359406781(BasicNewsRecipe): title = u'Private Eye' publication_type = 'magazine' diff --git a/recipes/randerslokalavisen_dk.recipe b/recipes/randerslokalavisen_dk.recipe index e7e1221f11..2c4b3b0fde 100644 --- a/recipes/randerslokalavisen_dk.recipe +++ b/recipes/randerslokalavisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Din avis Randers ''' + class RandersLokalavisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Din avis Randers' diff --git a/recipes/respekt_magazine.recipe b/recipes/respekt_magazine.recipe index 33c7453b62..686f084643 100644 --- a/recipes/respekt_magazine.recipe +++ b/recipes/respekt_magazine.recipe @@ -14,6 +14,7 @@ import lxml from lxml.builder import E respekt_url = 'http://www.respekt.cz' + class respektRecipe(BasicNewsRecipe): __author__ = 'Tomáš Hnyk' publisher = u'Respekt Publishing a. s.' diff --git a/recipes/roskildelokalavisen_dk.recipe b/recipes/roskildelokalavisen_dk.recipe index 81f7cf65a5..fe06f9704e 100644 --- a/recipes/roskildelokalavisen_dk.recipe +++ b/recipes/roskildelokalavisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Roskilde Avis ''' + class RoskildeLokalavisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Roskilde Avis' diff --git a/recipes/rudersdallokalavisen_dk.recipe b/recipes/rudersdallokalavisen_dk.recipe index 9d5825965d..3761652c1e 100644 --- a/recipes/rudersdallokalavisen_dk.recipe +++ b/recipes/rudersdallokalavisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Rudersdal Avis ''' + class RudersdalLokalavisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Rudersdal Avis' diff --git a/recipes/sciencedaily.recipe b/recipes/sciencedaily.recipe index 605636eccc..1b2fde59cc 100644 --- a/recipes/sciencedaily.recipe +++ b/recipes/sciencedaily.recipe @@ -7,6 +7,7 @@ sciencedaily.com from calibre.web.feeds.news import BasicNewsRecipe + class ScienceDaily(BasicNewsRecipe): title = u'ScienceDaily' __author__ = u'Darko Miletic' diff --git a/recipes/skanderborglokalavisen_dk.recipe b/recipes/skanderborglokalavisen_dk.recipe index 6793142fc3..0829d2f8fa 100644 --- a/recipes/skanderborglokalavisen_dk.recipe +++ b/recipes/skanderborglokalavisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Lokalavisen Skanderborg ''' + class SkanderborgLokalavisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Lokalavisen Skanderborg' diff --git a/recipes/sn_dk.recipe b/recipes/sn_dk.recipe index 4bb11fd4b8..440a7402ab 100644 --- a/recipes/sn_dk.recipe +++ b/recipes/sn_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe sn.dk ''' + class Sn_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'sn.dk' diff --git a/recipes/soenderborglokalavisen_dk.recipe b/recipes/soenderborglokalavisen_dk.recipe index 40b5c81558..fe5e0e5de2 100644 --- a/recipes/soenderborglokalavisen_dk.recipe +++ b/recipes/soenderborglokalavisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Lokalavisen Sønderborg ''' + class SoenderborgLokalavisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Lokalavisen Sønderborg' diff --git a/recipes/syddjurslokalavisen_dk.recipe b/recipes/syddjurslokalavisen_dk.recipe index 22ff284711..021d33a0ee 100644 --- a/recipes/syddjurslokalavisen_dk.recipe +++ b/recipes/syddjurslokalavisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Adresseavisen Syddjurs ''' + class SyddjursLokalavisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Adresseavisen Syddjurs' diff --git a/recipes/the_oz.recipe b/recipes/the_oz.recipe index 1f2da19672..148f32d8cf 100644 --- a/recipes/the_oz.recipe +++ b/recipes/the_oz.recipe @@ -9,11 +9,13 @@ http://www.theaustralian.news.com.au/ from calibre.web.feeds.news import BasicNewsRecipe + def classes(classes): q = frozenset(classes.split(' ')) return dict(attrs={ 'class': lambda x: x and frozenset(x.split()).intersection(q)}) + class DailyTelegraph(BasicNewsRecipe): title = u'The Australian' __author__ = u'Kovid Goyal' diff --git a/recipes/valbybladet_dk.recipe b/recipes/valbybladet_dk.recipe index 1279c2d74b..d65b441344 100644 --- a/recipes/valbybladet_dk.recipe +++ b/recipes/valbybladet_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Valby Bladet ''' + class ValbyBladet_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Valby Bladet' diff --git a/recipes/vanloesebladet_dk.recipe b/recipes/vanloesebladet_dk.recipe index 3e606f2f94..5f738895f7 100644 --- a/recipes/vanloesebladet_dk.recipe +++ b/recipes/vanloesebladet_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Vanløse Bladet ''' + class VanloeseBladet_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Vanløse Bladet' diff --git a/recipes/vardelokalavisen_dk.recipe b/recipes/vardelokalavisen_dk.recipe index a1ac875f6b..cd2c374d93 100644 --- a/recipes/vardelokalavisen_dk.recipe +++ b/recipes/vardelokalavisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Lokalavisen Varde ''' + class VardeLokalavisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Lokalavisen Varde' diff --git a/recipes/vejlelokalavisen_dk.recipe b/recipes/vejlelokalavisen_dk.recipe index ae2fde9efc..50d8523d18 100644 --- a/recipes/vejlelokalavisen_dk.recipe +++ b/recipes/vejlelokalavisen_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Lokalavisen Vejle ''' + class VejleLokalavisen_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Lokalavisen Vejle' diff --git a/recipes/vesterbrobladet_dk.recipe b/recipes/vesterbrobladet_dk.recipe index 55f8abe199..404ddf3996 100644 --- a/recipes/vesterbrobladet_dk.recipe +++ b/recipes/vesterbrobladet_dk.recipe @@ -7,6 +7,7 @@ from calibre.web.feeds.news import BasicNewsRecipe Vesterbro Bladet ''' + class VesterbroBladet_dk(BasicNewsRecipe): __author__ = 'CoderAllan.github.com' title = 'Vesterbro Bladet' diff --git a/recipes/weblogs_sl.recipe b/recipes/weblogs_sl.recipe index 5f75939e38..3af06f252d 100644 --- a/recipes/weblogs_sl.recipe +++ b/recipes/weblogs_sl.recipe @@ -11,6 +11,7 @@ http://www.weblogssl.com/ import re from calibre.web.feeds.news import BasicNewsRecipe + class weblogssl(BasicNewsRecipe): __author__ = 'desUBIKado' description = u'Weblogs colectivos dedicados a seguir la actualidad sobre tecnologia, entretenimiento, estilos de vida, motor, deportes y economia.' diff --git a/setup.cfg b/setup.cfg index 92cb38070e..c86c0b3717 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,4 +1,4 @@ [flake8] max-line-length = 160 builtins = _,dynamic_property,__,P,I,lopen,icu_lower,icu_upper,icu_title,ngettext -ignore = E12,E203,E22,E231,E241,E301,E302,E304,E401,E402,E731,W391 +ignore = E12,E203,E22,E231,E241,E401,E402,E731,W391 diff --git a/setup/check.py b/setup/check.py index 118bfcca96..13d9f53dfb 100644 --- a/setup/check.py +++ b/setup/check.py @@ -10,12 +10,14 @@ import sys, os, json, subprocess, errno, hashlib from setup import Command, build_cache_dir import __builtin__ + def set_builtins(builtins): for x in builtins: if not hasattr(__builtin__, x): setattr(__builtin__, x, True) yield x + class Message: def __init__(self, filename, lineno, msg): @@ -24,6 +26,7 @@ class Message: def __str__(self): return '%s:%s: %s' % (self.filename, self.lineno, self.msg) + class Check(Command): description = 'Check for errors in the calibre source code' diff --git a/src/calibre/__init__.py b/src/calibre/__init__.py index 1efee71249..c37c0fcf86 100644 --- a/src/calibre/__init__.py +++ b/src/calibre/__init__.py @@ -29,18 +29,22 @@ if False: winerror, win32api, isbsd, config_dir _mt_inited = False + + def _init_mimetypes(): global _mt_inited import mimetypes mimetypes.init([P('mime.types')]) _mt_inited = True + def guess_type(*args, **kwargs): import mimetypes if not _mt_inited: _init_mimetypes() return mimetypes.guess_type(*args, **kwargs) + def guess_all_extensions(*args, **kwargs): import mimetypes if not _mt_inited: @@ -57,17 +61,20 @@ def guess_extension(*args, **kwargs): ext = '.pdb' return ext + def get_types_map(): import mimetypes if not _mt_inited: _init_mimetypes() return mimetypes.types_map + def to_unicode(raw, encoding='utf-8', errors='strict'): if isinstance(raw, unicode): return raw return raw.decode(encoding, errors) + def patheq(p1, p2): p = os.path d = lambda x : p.normcase(p.normpath(p.realpath(p.normpath(x)))) @@ -75,6 +82,7 @@ def patheq(p1, p2): return False return d(p1) == d(p2) + def unicode_path(path, abs=False): if isinstance(path, bytes): path = path.decode(filesystem_encoding) @@ -82,6 +90,7 @@ def unicode_path(path, abs=False): path = os.path.abspath(path) return path + def osx_version(): if isosx: import platform @@ -90,6 +99,7 @@ def osx_version(): if m: return int(m.group(1)), int(m.group(2)), int(m.group(3)) + def confirm_config_name(name): return name + '_again' @@ -97,6 +107,7 @@ _filename_sanitize = re.compile(r'[\xae\0\\|\?\*<":>\+/]') _filename_sanitize_unicode = frozenset([u'\\', u'|', u'?', u'*', u'<', u'"', u':', u'>', u'+', u'/'] + list(map(unichr, xrange(32)))) + def sanitize_file_name(name, substitute='_', as_unicode=False): ''' Sanitize the filename `name`. All invalid characters are replaced by `substitute`. @@ -125,6 +136,7 @@ def sanitize_file_name(name, substitute='_', as_unicode=False): one = '_' + one[1:] return one + def sanitize_file_name_unicode(name, substitute='_'): ''' Sanitize the filename `name`. All invalid characters are replaced by `substitute`. @@ -151,6 +163,7 @@ def sanitize_file_name_unicode(name, substitute='_'): one = '_' + one[1:] return one + def sanitize_file_name2(name, substitute='_'): ''' Sanitize filenames removing invalid chars. Keeps unicode names as unicode @@ -160,6 +173,7 @@ def sanitize_file_name2(name, substitute='_'): return sanitize_file_name(name, substitute=substitute) return sanitize_file_name_unicode(name, substitute=substitute) + def prints(*args, **kwargs): ''' Print unicode arguments safely by encoding them to preferred_encoding @@ -227,9 +241,11 @@ def prints(*args, **kwargs): count += len(sep) return count + class CommandLineError(Exception): pass + def setup_cli_handlers(logger, level): import logging if os.environ.get('CALIBRE_WORKER', None) is not None and logger.handlers: @@ -261,6 +277,7 @@ def load_library(name, cdll): return cdll.LoadLibrary(name) return cdll.LoadLibrary(name+'.so') + def filename_to_utf8(name): '''Return C{name} encoded in utf8. Unhandled characters are replaced. ''' if isinstance(name, unicode): @@ -268,6 +285,7 @@ def filename_to_utf8(name): codec = 'cp1252' if iswindows else 'utf8' return name.decode(codec, 'replace').encode('utf8') + def extract(path, dir): extractor = None # First use the file header to identify its type @@ -292,6 +310,7 @@ def extract(path, dir): raise Exception('Unknown archive type') extractor(path, dir) + def get_proxies(debug=True): from urllib import getproxies proxies = getproxies() @@ -315,6 +334,7 @@ def get_proxies(debug=True): prints('Using proxies:', proxies) return proxies + def get_parsed_proxy(typ='http', debug=True): proxies = get_proxies(debug) proxy = proxies.get(typ, None) @@ -346,6 +366,7 @@ def get_parsed_proxy(typ='http', debug=True): prints('Using http proxy', str(ans)) return ans + def get_proxy_info(proxy_scheme, proxy_string): ''' Parse all proxy information from a proxy string (as returned by @@ -372,6 +393,7 @@ def get_proxy_info(proxy_scheme, proxy_string): USER_AGENT = 'Mozilla/5.0 (Windows NT 6.1; Trident/7.0; rv:11.0) like Gecko' USER_AGENT_MOBILE = 'Mozilla/5.0 (Windows; U; Windows CE 5.1; rv:1.8.1a3) Gecko/20060610 Minimo/0.016' + def random_user_agent(choose=None): try: ua_list = random_user_agent.ua_list @@ -398,6 +420,7 @@ def random_user_agent(choose=None): ] return random.choice(ua_list) if choose is None else ua_list[choose] + def browser(honor_time=True, max_time=2, mobile_browser=False, user_agent=None, use_robust_parser=False, verify_ssl_certificates=True): ''' Create a mechanize browser for web scraping. The browser handles cookies, @@ -431,6 +454,7 @@ def browser(honor_time=True, max_time=2, mobile_browser=False, user_agent=None, return opener + def fit_image(width, height, pwidth, pheight): ''' Fit image in box of width pwidth and height pheight. @@ -453,6 +477,7 @@ def fit_image(width, height, pwidth, pheight): return scaled, int(width), int(height) + class CurrentDir(object): def __init__(self, path, workaround_temp_folder_permissions=False): @@ -481,6 +506,8 @@ class CurrentDir(object): _ncpus = None + + def detect_ncpus(): """Detects the number of effective CPUs in the system""" global _ncpus @@ -502,18 +529,22 @@ def detect_ncpus(): relpath = os.path.relpath _spat = re.compile(r'^the\s+|^a\s+|^an\s+', re.IGNORECASE) + + def english_sort(x, y): ''' Comapare two english phrases ignoring starting prepositions. ''' return cmp(_spat.sub('', x), _spat.sub('', y)) + def walk(dir): ''' A nice interface to os.walk ''' for record in os.walk(dir): for f in record[-1]: yield os.path.join(record[0], f) + def strftime(fmt, t=None): ''' A version of strftime that returns unicode strings and tries to handle dates before 1900 ''' @@ -542,12 +573,14 @@ def strftime(fmt, t=None): ans = ans.replace('_early year hack##', str(orig_year)) return ans + def my_unichr(num): try: return safe_chr(num) except (ValueError, OverflowError): return u'?' + def entity_to_unicode(match, exceptions=[], encoding='cp1252', result_exceptions={}): ''' @@ -606,12 +639,15 @@ xml_entity_to_unicode = partial(entity_to_unicode, result_exceptions={ '>' : '>', '&' : '&'}) + def replace_entities(raw, encoding='cp1252'): return _ent_pat.sub(partial(entity_to_unicode, encoding=encoding), raw) + def xml_replace_entities(raw, encoding='cp1252'): return _ent_pat.sub(partial(xml_entity_to_unicode, encoding=encoding), raw) + def prepare_string_for_xml(raw, attribute=False): raw = _ent_pat.sub(entity_to_unicode, raw) raw = raw.replace('&', '&').replace('<', '<').replace('>', '>') @@ -619,9 +655,11 @@ def prepare_string_for_xml(raw, attribute=False): raw = raw.replace('"', '"').replace("'", ''') return raw + def isbytestring(obj): return isinstance(obj, (str, bytes)) + def force_unicode(obj, enc=preferred_encoding): if isbytestring(obj): try: @@ -639,6 +677,7 @@ def force_unicode(obj, enc=preferred_encoding): obj = obj.decode('utf-8') return obj + def as_unicode(obj, enc=preferred_encoding): if not isbytestring(obj): try: @@ -650,12 +689,14 @@ def as_unicode(obj, enc=preferred_encoding): obj = repr(obj) return force_unicode(obj, enc=enc) + def url_slash_cleaner(url): ''' Removes redundant /'s from url's. ''' return re.sub(r'(? 0 + def path_ok(path): return not os.path.isdir(path) and os.access(path, os.R_OK) + def compile_glob(pat): import fnmatch return re.compile(fnmatch.translate(pat), flags=re.I) + def compile_rule(rule): mt = rule['match_type'] if 'with' in mt: @@ -45,6 +50,7 @@ def compile_rule(rule): ans = lambda filename: not func(filename) return ans, rule['action'] == 'add' + def filter_filename(compiled_rules, filename): for q, action in compiled_rules: if q(filename): @@ -52,6 +58,7 @@ def filter_filename(compiled_rules, filename): _metadata_extensions = None + def metadata_extensions(): # Set of all known book extensions + OPF (the OPF is used to read metadata, # but not actually added) @@ -60,6 +67,7 @@ def metadata_extensions(): _metadata_extensions = frozenset(map(unicode, BOOK_EXTENSIONS)) | {'opf'} return _metadata_extensions + def listdir(root, sort_by_mtime=False): items = (os.path.join(root, x) for x in os.listdir(root)) if sort_by_mtime: @@ -74,12 +82,14 @@ def listdir(root, sort_by_mtime=False): if path_ok(path): yield path + def allow_path(path, ext, compiled_rules): ans = filter_filename(compiled_rules, os.path.basename(path)) if ans is None: ans = ext in metadata_extensions() return ans + def find_books_in_directory(dirpath, single_book_per_directory, compiled_rules=(), listdir_impl=listdir): dirpath = os.path.abspath(dirpath) if single_book_per_directory: @@ -101,6 +111,7 @@ def find_books_in_directory(dirpath, single_book_per_directory, compiled_rules=( if formats_ok(formats): yield list(formats.itervalues()) + def import_book_directory_multiple(db, dirpath, callback=None, added_ids=None, compiled_rules=()): from calibre.ebooks.metadata.meta import metadata_from_formats @@ -121,6 +132,7 @@ def import_book_directory_multiple(db, dirpath, callback=None, break return duplicates + def import_book_directory(db, dirpath, callback=None, added_ids=None, compiled_rules=()): from calibre.ebooks.metadata.meta import metadata_from_formats dirpath = os.path.abspath(dirpath) @@ -140,6 +152,7 @@ def import_book_directory(db, dirpath, callback=None, added_ids=None, compiled_r if callable(callback): callback(mi.title) + def recursive_import(db, root, single_book_per_directory=True, callback=None, added_ids=None, compiled_rules=()): root = os.path.abspath(root) @@ -156,6 +169,7 @@ def recursive_import(db, root, single_book_per_directory=True, break return duplicates + def add_catalog(cache, path, title, dbapi=None): from calibre.ebooks.metadata.book.base import Metadata from calibre.ebooks.metadata.meta import get_metadata @@ -189,6 +203,7 @@ def add_catalog(cache, path, title, dbapi=None): return db_id, new_book_added + def add_news(cache, path, arg, dbapi=None): from calibre.ebooks.metadata.meta import get_metadata from calibre.utils.date import utcnow diff --git a/src/calibre/db/backend.py b/src/calibre/db/backend.py index c66e9aa210..357f690d97 100644 --- a/src/calibre/db/backend.py +++ b/src/calibre/db/backend.py @@ -65,6 +65,7 @@ class DynamicFilter(object): # {{{ self.ids = frozenset(ids) # }}} + class DBPrefs(dict): # {{{ 'Store preferences as key:value pairs in the db' @@ -160,6 +161,8 @@ class DBPrefs(dict): # {{{ # }}} # Extra collators {{{ + + def pynocase(one, two, encoding='utf-8'): if isbytestring(one): try: @@ -173,11 +176,13 @@ def pynocase(one, two, encoding='utf-8'): pass return cmp(one.lower(), two.lower()) + def _author_to_author_sort(x): if not x: return '' return author_to_author_sort(x.replace('|', ',')) + def icu_collator(s1, s2): return cmp(sort_key(force_unicode(s1, 'utf-8')), sort_key(force_unicode(s2, 'utf-8'))) @@ -185,6 +190,8 @@ def icu_collator(s1, s2): # }}} # Unused aggregators {{{ + + def Concatenate(sep=','): '''String concatenation aggregator for sqlite''' @@ -199,6 +206,7 @@ def Concatenate(sep=','): return ([], step, finalize) + def SortedConcatenate(sep=','): '''String concatenation aggregator for sqlite, sorted by supplied index''' @@ -213,6 +221,7 @@ def SortedConcatenate(sep=','): return ({}, step, finalize) + def IdentifiersConcat(): '''String concatenation aggregator for the identifiers map''' @@ -224,6 +233,7 @@ def IdentifiersConcat(): return ([], step, finalize) + def AumSortedConcatenate(): '''String concatenation aggregator for the author sort map''' @@ -244,6 +254,7 @@ def AumSortedConcatenate(): # }}} + class Connection(apsw.Connection): # {{{ BUSY_TIMEOUT = 10000 # milliseconds @@ -304,6 +315,7 @@ class Connection(apsw.Connection): # {{{ # }}} + class DB(object): PATH_LIMIT = 40 if iswindows else 100 @@ -1626,6 +1638,7 @@ class DB(object): def get_custom_book_data(self, name, book_ids, default=None): book_ids = frozenset(book_ids) + def safe_load(val): try: return json.loads(val, object_hook=from_json) diff --git a/src/calibre/db/backup.py b/src/calibre/db/backup.py index 3b1227904b..3d435457b1 100644 --- a/src/calibre/db/backup.py +++ b/src/calibre/db/backup.py @@ -13,9 +13,11 @@ from threading import Thread, Event from calibre import prints from calibre.ebooks.metadata.opf2 import metadata_to_opf + class Abort(Exception): pass + class MetadataBackup(Thread): ''' Continuously backup changed metadata into OPF files diff --git a/src/calibre/db/cache.py b/src/calibre/db/cache.py index bbf8858241..1427fdfda2 100644 --- a/src/calibre/db/cache.py +++ b/src/calibre/db/cache.py @@ -35,20 +35,24 @@ from calibre.utils.config import prefs, tweaks from calibre.utils.date import now as nowf, utcnow, UNDEFINED_DATE from calibre.utils.icu import sort_key + def api(f): f.is_cache_api = True return f + def read_api(f): f = api(f) f.is_read_api = True return f + def write_api(f): f = api(f) f.is_read_api = False return f + def wrap_simple(lock, func): @wraps(func) def call_func_with_lock(*args, **kwargs): @@ -62,6 +66,7 @@ def wrap_simple(lock, func): return func(*args, **kwargs) return call_func_with_lock + def run_import_plugins(path_or_stream, fmt): fmt = fmt.lower() if hasattr(path_or_stream, 'seek'): @@ -74,6 +79,7 @@ def run_import_plugins(path_or_stream, fmt): path = path_or_stream return run_plugins_on_import(path, fmt) + def _add_newbook_tag(mi): tags = prefs['new_book_tags'] if tags: @@ -86,6 +92,7 @@ def _add_newbook_tag(mi): dynamic_category_preferences = frozenset({'grouped_search_make_user_categories', 'grouped_search_terms', 'user_categories'}) + class Cache(object): ''' @@ -804,6 +811,7 @@ class Cache(object): path = self._field_for('path', book_id).replace('/', os.sep) except: return () + def verify(fmt): try: name = self.fields['formats'].format_fname(book_id, fmt) @@ -918,6 +926,7 @@ class Cache(object): return virtual_fields[fm.get(field, field)].sort_keys_for_books(get_metadata, lang_map) if is_series: idx_func = self.fields[idx].sort_keys_for_books(get_metadata, lang_map) + def skf(book_id): return (func(book_id), idx_func(book_id)) return skf @@ -2160,6 +2169,7 @@ class Cache(object): if progress is not None: progress(_('Completed'), total, total) + def import_library(library_key, importer, library_path, progress=None, abort=None): from calibre.db.backend import DB metadata = importer.metadata[library_key] diff --git a/src/calibre/db/categories.py b/src/calibre/db/categories.py index 2cba7e70f1..d5cfb6754c 100644 --- a/src/calibre/db/categories.py +++ b/src/calibre/db/categories.py @@ -17,6 +17,7 @@ from calibre.utils.icu import sort_key, collation_order CATEGORY_SORTS = ('name', 'popularity', 'rating') # This has to be a tuple not a set + class Tag(object): __slots__ = ('name', 'original_name', 'id', 'count', 'state', 'is_hierarchical', @@ -51,6 +52,7 @@ class Tag(object): def __repr__(self): return str(self) + def find_categories(field_metadata): for category, cat in field_metadata.iteritems(): if (cat['is_category'] and cat['kind'] not in {'user', 'search'}): @@ -59,6 +61,7 @@ def find_categories(field_metadata): cat['display'].get('make_category', False)): yield (category, cat['is_multiple'].get('cache_to_list', None), True) + def create_tag_class(category, fm): cat = fm[category] dt = cat['datatype'] @@ -77,6 +80,7 @@ def create_tag_class(category, fm): return partial(Tag, use_sort_as_name=use_sort_as_name, is_editable=is_editable, category=category) + def clean_user_categories(dbcache): user_cats = dbcache.pref('user_categories', {}) new_cats = {} @@ -108,6 +112,7 @@ category_sort_keys[True]['name'] = \ category_sort_keys[False]['name'] = \ lambda x:sort_key(x.sort or x.name) + def get_categories(dbcache, sort='name', book_ids=None, first_letter_sort=False): if sort not in CATEGORY_SORTS: raise ValueError('sort ' + sort + ' not a valid value') diff --git a/src/calibre/db/delete_service.py b/src/calibre/db/delete_service.py index 1e44f0c85b..c33dda1f7c 100644 --- a/src/calibre/db/delete_service.py +++ b/src/calibre/db/delete_service.py @@ -14,6 +14,7 @@ from calibre.ptempfile import remove_dir from calibre.utils.filenames import remove_dir_if_empty from calibre.utils.recycle_bin import delete_tree, delete_file + class DeleteService(Thread): ''' Provide a blocking file delete implementation with support for the @@ -136,6 +137,8 @@ class DeleteService(Thread): shutil.rmtree(tdir) __ds = None + + def delete_service(): global __ds if __ds is None: @@ -143,12 +146,14 @@ def delete_service(): __ds.start() return __ds + def shutdown(timeout=20): global __ds if __ds is not None: __ds.shutdown(timeout) __ds = None + def has_jobs(): global __ds if __ds is not None: diff --git a/src/calibre/db/fields.py b/src/calibre/db/fields.py index bf27e5287c..1fa013d103 100644 --- a/src/calibre/db/fields.py +++ b/src/calibre/db/fields.py @@ -21,17 +21,20 @@ from calibre.utils.icu import sort_key from calibre.utils.date import UNDEFINED_DATE, clean_date_for_sort, parse_date from calibre.utils.localization import calibre_langcode_to_name + def bool_sort_key(bools_are_tristate): return (lambda x:{True: 1, False: 2, None: 3}.get(x, 3)) if bools_are_tristate else lambda x:{True: 1, False: 2, None: 2}.get(x, 2) IDENTITY = lambda x: x + class InvalidLinkTable(Exception): def __init__(self, name): Exception.__init__(self, name) self.field_name = name + class Field(object): is_many = False @@ -166,6 +169,7 @@ class Field(object): ans.append(c) return ans + class OneToOneField(Field): def for_book(self, book_id, default_value=None): @@ -193,6 +197,7 @@ class OneToOneField(Field): for book_id in candidates: yield cbm.get(book_id, default_value), {book_id} + class CompositeField(OneToOneField): is_composite = True @@ -339,6 +344,7 @@ class CompositeField(OneToOneField): ans.add(book_id) return ans + class OnDeviceField(OneToOneField): def __init__(self, name, table, bools_are_tristate): @@ -403,6 +409,7 @@ class OnDeviceField(OneToOneField): for val, book_ids in val_map.iteritems(): yield val, book_ids + class LazySortMap(object): __slots__ = ('default_sort_key', 'sort_key_func', 'id_map', 'cache') @@ -469,6 +476,7 @@ class ManyToOneField(Field): except KeyError: raise InvalidLinkTable(self.name) + class ManyToManyField(Field): is_many = True @@ -534,6 +542,7 @@ class ManyToManyField(Field): except KeyError: raise InvalidLinkTable(self.name) + class IdentifiersField(ManyToManyField): def for_book(self, book_id, default_value=None): @@ -569,6 +578,7 @@ class IdentifiersField(ManyToManyField): ans.append(c) return ans + class AuthorsField(ManyToManyField): def author_data(self, author_id): @@ -588,6 +598,7 @@ class AuthorsField(ManyToManyField): return ' & '.join(self.table.asort_map[k] for k in self.table.book_col_map[book_id]) + class FormatsField(ManyToManyField): def for_book(self, book_id, default_value=None): @@ -618,6 +629,7 @@ class FormatsField(ManyToManyField): ans.append(c) return ans + class LazySeriesSortMap(object): __slots__ = ('default_sort_key', 'sort_key_func', 'id_map', 'cache') @@ -638,12 +650,14 @@ class LazySeriesSortMap(object): val = self.cache[(item_id, lang)] = self.default_sort_key return val + class SeriesField(ManyToOneField): def sort_keys_for_books(self, get_metadata, lang_map): sso = tweaks['title_series_sorting'] ssk = self._sort_key ts = title_sort + def sk(val, lang): return ssk(ts(val, order=sso, lang=lang)) sk_map = LazySeriesSortMap(self._default_sort_key, sk, self.table.id_map) @@ -718,6 +732,7 @@ class TagsField(ManyToManyField): ans.append(c) return ans + def create_field(name, table, bools_are_tristate): cls = { ONE_ONE: OneToOneField, diff --git a/src/calibre/db/lazy.py b/src/calibre/db/lazy.py index 4010a3b40c..25ea870ba8 100644 --- a/src/calibre/db/lazy.py +++ b/src/calibre/db/lazy.py @@ -23,6 +23,7 @@ Speeds up calibre startup with large libraries/libraries on a network share, with a composite custom column. ''' + def resolved(f): @wraps(f) def wrapper(self, *args, **kwargs): @@ -32,6 +33,7 @@ def resolved(f): return f(self, *args, **kwargs) return wrapper + class MutableBase(object): @resolved @@ -87,6 +89,7 @@ class FormatMetadata(MutableBase, MutableMapping): except: pass + class FormatsList(MutableBase, MutableSequence): def __init__(self, formats, format_metadata): @@ -106,6 +109,7 @@ class FormatsList(MutableBase, MutableSequence): ga = object.__getattribute__ sa = object.__setattr__ + def simple_getter(field, default_value=None): def func(dbref, book_id, cache): try: @@ -116,6 +120,7 @@ def simple_getter(field, default_value=None): return ret return func + def pp_getter(field, postprocess, default_value=None): def func(dbref, book_id, cache): try: @@ -126,6 +131,7 @@ def pp_getter(field, postprocess, default_value=None): return ret return func + def adata_getter(field): def func(dbref, book_id, cache): try: @@ -140,6 +146,7 @@ def adata_getter(field): return {adata[i]['name']:adata[i][k] for i in author_ids} return func + def dt_getter(field): def func(dbref, book_id, cache): try: @@ -150,6 +157,7 @@ def dt_getter(field): return ret return func + def item_getter(field, default_value=None, key=0): def func(dbref, book_id, cache): try: @@ -163,6 +171,7 @@ def item_getter(field, default_value=None, key=0): return default_value return func + def fmt_getter(field): def func(dbref, book_id, cache): try: @@ -179,6 +188,7 @@ def fmt_getter(field): return format_metadata return func + def approx_fmts_getter(dbref, book_id, cache): try: return cache['formats'] @@ -187,6 +197,7 @@ def approx_fmts_getter(dbref, book_id, cache): cache['formats'] = ret = list(db.field_for('formats', book_id)) return ret + def series_index_getter(field='series'): def func(dbref, book_id, cache): try: @@ -202,6 +213,7 @@ def series_index_getter(field='series'): return ret return func + def has_cover_getter(dbref, book_id, cache): try: return cache['has_cover'] @@ -211,6 +223,8 @@ def has_cover_getter(dbref, book_id, cache): return ret fmt_custom = lambda x:list(x) if isinstance(x, tuple) else x + + def custom_getter(field, dbref, book_id, cache): try: return cache[field] @@ -219,6 +233,7 @@ def custom_getter(field, dbref, book_id, cache): cache[field] = ret = fmt_custom(db.field_for(field, book_id)) return ret + def composite_getter(mi, field, dbref, book_id, cache, formatter, template_cache): try: return cache[field] @@ -239,6 +254,7 @@ def composite_getter(mi, field, dbref, book_id, cache, formatter, template_cache return 'ERROR WHILE EVALUATING: %s' % field return ret + def virtual_libraries_getter(dbref, book_id, cache): try: return cache['virtual_libraries'] @@ -248,6 +264,7 @@ def virtual_libraries_getter(dbref, book_id, cache): ret = cache['virtual_libraries'] = ', '.join(vls) return ret + def user_categories_getter(proxy_metadata): cache = ga(proxy_metadata, '_cache') try: @@ -293,6 +310,7 @@ for field in ('formats', 'format_metadata'): getters[field] = fmt_getter(field) # }}} + class ProxyMetadata(Metadata): def __init__(self, db, book_id, formatter=None): diff --git a/src/calibre/db/legacy.py b/src/calibre/db/legacy.py index ad6541bc79..7abaf8eda3 100644 --- a/src/calibre/db/legacy.py +++ b/src/calibre/db/legacy.py @@ -24,6 +24,7 @@ from calibre.db.write import clean_identifier, get_series_values from calibre.utils.date import utcnow from calibre.utils.search_query_parser import set_saved_searches + def cleanup_tags(tags): tags = [x.strip().replace(',', ';') for x in tags if x.strip()] tags = [x.decode(preferred_encoding, 'replace') @@ -36,6 +37,7 @@ def cleanup_tags(tags): ans.append(tag) return ans + def create_backend( library_path, default_prefs=None, read_only=False, progress_callback=lambda x, y:True, restore_all_prefs=False, @@ -45,6 +47,7 @@ def create_backend( progress_callback=progress_callback, load_user_formatter_functions=load_user_formatter_functions) + class LibraryDatabase(object): ''' Emulate the old LibraryDatabase2 interface ''' @@ -748,6 +751,7 @@ for prop in ('author_sort', 'authors', 'comment', 'comments', 'publisher', 'max_ def getter(prop): fm = {'comment':'comments', 'metadata_last_modified': 'last_modified', 'title_sort':'sort', 'max_size':'size'}.get(prop, prop) + def func(self, index, index_is_id=False): return self.get_property(index, index_is_id=index_is_id, loc=self.FIELD_MAP[fm]) return func @@ -793,6 +797,7 @@ for field in ( if has_case_change: field = field[1:] acc = field == 'series' + def func(self, book_id, val, notify=True, commit=True, allow_case_change=acc): ret = self.new_api.set_field(field, {book_id:val}, allow_case_change=allow_case_change) if notify: @@ -804,6 +809,7 @@ for field in ( else: null_field = field in {'title', 'sort', 'uuid'} retval = (True if field == 'sort' else None) + def func(self, book_id, val, notify=True, commit=True): if not val and null_field: return (False if field == 'sort' else None) @@ -857,6 +863,7 @@ LibraryDatabase.get_author_id = MT( for field in ('tags', 'series', 'publishers', 'ratings', 'languages'): def getter(field): fname = field[:-1] if field in {'publishers', 'ratings'} else field + def func(self): return [[tid, tag] for tid, tag in self.new_api.get_id_map(fname).iteritems()] return func @@ -865,6 +872,7 @@ for field in ('tags', 'series', 'publishers', 'ratings', 'languages'): for field in ('author', 'tag', 'series'): def getter(field): field = field if field == 'series' else (field+'s') + def func(self, item_id): return self.new_api.get_item_name(field, item_id) return func @@ -873,6 +881,7 @@ for field in ('author', 'tag', 'series'): for field in ('publisher', 'series', 'tag'): def getter(field): fname = 'tags' if field == 'tag' else field + def func(self, item_id): self.new_api.remove_items(fname, (item_id,)) return func @@ -888,6 +897,7 @@ for func in ( def getter(func): if func.startswith('!'): func = func[1:] + def meth(self, include_composites=True): return getattr(self.field_metadata, func)(include_composites=include_composites) elif func == 'search_term_to_field_key': diff --git a/src/calibre/db/locking.py b/src/calibre/db/locking.py index e30da1ab04..9cb615b7c9 100644 --- a/src/calibre/db/locking.py +++ b/src/calibre/db/locking.py @@ -11,6 +11,7 @@ import traceback, sys from threading import Lock, Condition, current_thread from calibre.utils.config_base import tweaks + class LockingError(RuntimeError): is_locking_error = True @@ -19,9 +20,11 @@ class LockingError(RuntimeError): RuntimeError.__init__(self, msg) self.locking_debug_msg = extra + class DowngradeLockError(LockingError): pass + def create_locks(): ''' Return a pair of locks: (read_lock, write_lock) @@ -48,6 +51,7 @@ def create_locks(): wrapper = DebugRWLockWrapper if tweaks.get('newdb_debug_locking', False) else RWLockWrapper return wrapper(l), wrapper(l, is_shared=False) + class SHLock(object): # {{{ ''' Shareable lock class. Used to implement the Multiple readers-single writer @@ -207,6 +211,7 @@ class SHLock(object): # {{{ # }}} + class RWLockWrapper(object): def __init__(self, shlock, is_shared=True): @@ -225,6 +230,7 @@ class RWLockWrapper(object): def owns_lock(self): return self._shlock.owns_lock() + class DebugRWLockWrapper(RWLockWrapper): def __init__(self, *args, **kwargs): @@ -249,6 +255,7 @@ class DebugRWLockWrapper(RWLockWrapper): __enter__ = acquire __exit__ = release + class SafeReadLock(object): def __init__(self, read_lock): diff --git a/src/calibre/db/restore.py b/src/calibre/db/restore.py index a4bedcabd1..11526cea34 100644 --- a/src/calibre/db/restore.py +++ b/src/calibre/db/restore.py @@ -22,6 +22,7 @@ NON_EBOOK_EXTENSIONS = frozenset([ 'opf', 'swp', 'swo' ]) + class Restorer(Cache): def __init__(self, library_path, default_prefs=None, restore_all_prefs=False, progress_callback=lambda x, y:True): @@ -35,6 +36,7 @@ class Restorer(Cache): def no_op(self, *args, **kwargs): pass + class Restore(Thread): def __init__(self, library_path, progress_callback=None): diff --git a/src/calibre/db/schema_upgrades.py b/src/calibre/db/schema_upgrades.py index 3ab161ab94..943ad4850e 100644 --- a/src/calibre/db/schema_upgrades.py +++ b/src/calibre/db/schema_upgrades.py @@ -12,6 +12,7 @@ import os from calibre import prints from calibre.utils.date import isoformat, DEFAULT_DATE + class SchemaUpgrade(object): def __init__(self, db, library_path, field_metadata): @@ -421,6 +422,7 @@ class SchemaUpgrade(object): 'Cache has_cover' self.db.execute('ALTER TABLE books ADD COLUMN has_cover BOOL DEFAULT 0') data = self.db.get('SELECT id,path FROM books', all=True) + def has_cover(path): if path: path = os.path.join(self.library_path, path.replace('/', os.sep), diff --git a/src/calibre/db/search.py b/src/calibre/db/search.py index 7fde3443ff..71138d0132 100644 --- a/src/calibre/db/search.py +++ b/src/calibre/db/search.py @@ -26,6 +26,7 @@ REGEXP_MATCH = 2 # Utils {{{ + def _matchkind(query, case_sensitive=False): matchkind = CONTAINS_MATCH if (len(query) > 1): @@ -43,6 +44,7 @@ def _matchkind(query, case_sensitive=False): query = icu_lower(query) return matchkind, query + def _match(query, value, matchkind, use_primary_find_in_search=True, case_sensitive=False): if query.startswith('..'): query = query[1:] @@ -83,6 +85,7 @@ def _match(query, value, matchkind, use_primary_find_in_search=True, case_sensit return False # }}} + class DateSearch(object): # {{{ def __init__(self): @@ -203,6 +206,7 @@ class DateSearch(object): # {{{ return matches # }}} + class NumericSearch(object): # {{{ def __init__(self): @@ -294,6 +298,7 @@ class NumericSearch(object): # {{{ # }}} + class BooleanSearch(object): # {{{ def __init__(self): @@ -335,6 +340,7 @@ class BooleanSearch(object): # {{{ # }}} + class KeyPairSearch(object): # {{{ def __call__(self, query, field_iter, candidates, use_primary_find): @@ -375,6 +381,7 @@ class KeyPairSearch(object): # {{{ # }}} + class SavedSearchQueries(object): # {{{ queries = {} opt_name = '' @@ -436,6 +443,7 @@ class SavedSearchQueries(object): # {{{ return sorted(self.queries.iterkeys(), key=sort_key) # }}} + class Parser(SearchQueryParser): # {{{ def __init__(self, dbcache, all_book_ids, gst, date_search, num_search, @@ -565,6 +573,7 @@ class Parser(SearchQueryParser): # {{{ fm['display'].get('composite_sort', '') == 'number')): if location == 'id': is_many = False + def fi(default_value=None): for qid in candidates: yield qid, {qid} @@ -722,6 +731,7 @@ class Parser(SearchQueryParser): # {{{ return matches # }}} + class LRUCache(object): # {{{ 'A simple Least-Recently-Used cache' @@ -778,6 +788,7 @@ class LRUCache(object): # {{{ return self.item_map.iteritems() # }}} + class Search(object): MAX_CACHE_UPDATE = 50 diff --git a/src/calibre/db/tables.py b/src/calibre/db/tables.py index f94785dcab..30cedbeb7b 100644 --- a/src/calibre/db/tables.py +++ b/src/calibre/db/tables.py @@ -16,6 +16,7 @@ from calibre.ebooks.metadata import author_to_author_sort _c_speedup = plugins['speedup'][0].parse_date + def c_parse(val): try: year, month, day, hour, minutes, seconds, tzsecs = _c_speedup(val) @@ -45,6 +46,7 @@ ONE_ONE, MANY_ONE, MANY_MANY = xrange(3) null = object() + class Table(object): def __init__(self, name, metadata, link_table=None): @@ -76,6 +78,7 @@ class Table(object): ascii text. ''' pass + class VirtualTable(Table): ''' @@ -87,6 +90,7 @@ class VirtualTable(Table): self.table_type = table_type Table.__init__(self, name, metadata) + class OneToOneTable(Table): ''' @@ -122,6 +126,7 @@ class OneToOneTable(Table): clean.add(val) return clean + class PathTable(OneToOneTable): def set_path(self, book_id, path, db): @@ -129,6 +134,7 @@ class PathTable(OneToOneTable): db.execute('UPDATE books SET path=? WHERE id=?', (path, book_id)) + class SizeTable(OneToOneTable): def read(self, db): @@ -140,6 +146,7 @@ class SizeTable(OneToOneTable): def update_sizes(self, size_map): self.book_col_map.update(size_map) + class UUIDTable(OneToOneTable): def read(self, db): @@ -163,6 +170,7 @@ class UUIDTable(OneToOneTable): def lookup_by_uuid(self, uuid): return self.uuid_to_id_map.get(uuid, None) + class CompositeTable(OneToOneTable): def read(self, db): @@ -177,6 +185,7 @@ class CompositeTable(OneToOneTable): def remove_books(self, book_ids, db): return set() + class ManyToOneTable(Table): ''' @@ -334,6 +343,7 @@ class ManyToOneTable(Table): self.link_table, lcol, table), (existing_item, item_id, item_id)) return affected_books, new_id + class RatingTable(ManyToOneTable): def read_id_maps(self, db): @@ -348,6 +358,7 @@ class RatingTable(ManyToOneTable): db.execute('DELETE FROM {0} WHERE {1}=0'.format( self.metadata['table'], self.metadata['column'])) + class ManyToManyTable(ManyToOneTable): ''' @@ -511,6 +522,7 @@ class ManyToManyTable(ManyToOneTable): db.executemany('DELETE FROM {0} WHERE id=?'.format(self.metadata['table']), tuple((x,) for x in v)) + class AuthorsTable(ManyToManyTable): def read_id_maps(self, db): @@ -562,6 +574,7 @@ class AuthorsTable(ManyToManyTable): def remove_items(self, item_ids, db): raise ValueError('Direct removal of authors is not allowed') + class FormatsTable(ManyToManyTable): do_clean_on_remove = False @@ -612,6 +625,7 @@ class FormatsTable(ManyToManyTable): pass db.executemany('DELETE FROM data WHERE book=? AND format=?', [(book_id, fmt) for book_id, fmts in formats_map.iteritems() for fmt in fmts]) + def zero_max(book_id): try: return max(self.size_map[book_id].itervalues()) @@ -646,6 +660,7 @@ class FormatsTable(ManyToManyTable): (book_id, fmt, size, fname)) return max(self.size_map[book_id].itervalues()) + class IdentifiersTable(ManyToManyTable): def read_id_maps(self, db): diff --git a/src/calibre/db/tests/add_remove.py b/src/calibre/db/tests/add_remove.py index 0d388f6b82..d9c2b3f88c 100644 --- a/src/calibre/db/tests/add_remove.py +++ b/src/calibre/db/tests/add_remove.py @@ -16,6 +16,7 @@ from calibre.db.tests.base import BaseTest, IMG from calibre.ptempfile import PersistentTemporaryFile from calibre.utils.date import now, UNDEFINED_DATE + def import_test(replacement_data, replacement_fmt=None): def func(path, fmt): if not path.endswith('.'+fmt.lower()): @@ -26,6 +27,7 @@ def import_test(replacement_data, replacement_fmt=None): return f.name return func + class AddRemoveTest(BaseTest): def test_add_format(self): # {{{ diff --git a/src/calibre/db/tests/base.py b/src/calibre/db/tests/base.py index 09af71687a..a6c6d60e92 100644 --- a/src/calibre/db/tests/base.py +++ b/src/calibre/db/tests/base.py @@ -16,6 +16,7 @@ rmtree = partial(shutil.rmtree, ignore_errors=True) IMG = b'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x01\x00`\x00`\x00\x00\xff\xe1\x00\x16Exif\x00\x00II*\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xdb\x00C\x00\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\xff\xdb\x00C\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\xff\xc0\x00\x11\x08\x00\x01\x00\x01\x03\x01"\x00\x02\x11\x01\x03\x11\x01\xff\xc4\x00\x15\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\n\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xc4\x00\x14\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xc4\x00\x14\x11\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xda\x00\x0c\x03\x01\x00\x02\x11\x03\x11\x00?\x00\xbf\x80\x01\xff\xd9' # noqa {{{ }}} + class BaseTest(unittest.TestCase): longMessage = True diff --git a/src/calibre/db/tests/filesystem.py b/src/calibre/db/tests/filesystem.py index 2f90557bb3..201d804d47 100644 --- a/src/calibre/db/tests/filesystem.py +++ b/src/calibre/db/tests/filesystem.py @@ -14,6 +14,7 @@ from calibre.constants import iswindows from calibre.db.tests.base import BaseTest from calibre.ptempfile import TemporaryDirectory + class FilesystemTest(BaseTest): def get_filesystem_data(self, cache, book_id): @@ -172,11 +173,14 @@ class FilesystemTest(BaseTest): def test_find_books_in_directory(self): from calibre.db.adding import find_books_in_directory, compile_rule strip = lambda files: frozenset({os.path.basename(x) for x in files}) + def q(one, two): one, two = {strip(a) for a in one}, {strip(b) for b in two} self.assertEqual(one, two) + def r(action='ignore', match_type='startswith', query=''): return {'action':action, 'match_type':match_type, 'query':query} + def c(*rules): return tuple(map(compile_rule, rules)) diff --git a/src/calibre/db/tests/legacy.py b/src/calibre/db/tests/legacy.py index 2ed5080d43..376940ed24 100644 --- a/src/calibre/db/tests/legacy.py +++ b/src/calibre/db/tests/legacy.py @@ -15,6 +15,8 @@ from operator import itemgetter from calibre.db.tests.base import BaseTest # Utils {{{ + + class ET(object): def __init__(self, func_name, args, kwargs={}, old=None, legacy=None): @@ -32,6 +34,7 @@ class ET(object): self.retval = newres return newres + def compare_argspecs(old, new, attr): # We dont compare the names of the non-keyword arguments as they are often # different and they dont affect the usage of the API. @@ -41,6 +44,7 @@ def compare_argspecs(old, new, attr): if not ok: raise AssertionError('The argspec for %s does not match. %r != %r' % (attr, old, new)) + def run_funcs(self, db, ndb, funcs): for func in funcs: meth, args = func[0], func[1:] @@ -61,6 +65,7 @@ def run_funcs(self, db, ndb, funcs): self.assertEqual(res1, res2, 'The method: %s() returned different results for argument %s' % (meth, args)) # }}} + class LegacyTest(BaseTest): ''' Test the emulation of the legacy interface. ''' @@ -253,6 +258,7 @@ class LegacyTest(BaseTest): for a in args: self.assertEqual(fmt(getattr(db, meth)(*a)), fmt(getattr(ndb, meth)(*a)), 'The method: %s() returned different results for argument %s' % (meth, a)) + def f(x, y): # get_top_level_move_items is broken in the old db on case-insensitive file systems x.discard('metadata_db_prefs_backup.json') return x, y diff --git a/src/calibre/db/tests/locking.py b/src/calibre/db/tests/locking.py index 61d5d9374b..dc828a97d1 100644 --- a/src/calibre/db/tests/locking.py +++ b/src/calibre/db/tests/locking.py @@ -11,6 +11,7 @@ from threading import Thread from calibre.db.tests.base import BaseTest from calibre.db.locking import SHLock, RWLockWrapper, LockingError + class TestLock(BaseTest): """Tests for db locking """ @@ -27,6 +28,7 @@ class TestLock(BaseTest): self.assertFalse(lock.owns_lock()) done = [] + def test(): if not lock.owns_lock(): done.append(True) @@ -40,12 +42,14 @@ class TestLock(BaseTest): def test_multithread_deadlock(self): lock = SHLock() + def two_shared(): r = RWLockWrapper(lock) with r: time.sleep(0.2) with r: pass + def one_exclusive(): time.sleep(0.1) w = RWLockWrapper(lock, is_shared=False) @@ -149,6 +153,7 @@ class TestLock(BaseTest): def test_contention(self): lock = SHLock() done = [] + def lots_of_acquires(): for _ in xrange(1000): shared = random.choice([True,False]) diff --git a/src/calibre/db/tests/main.py b/src/calibre/db/tests/main.py index 77aa621a57..c57c2c8b76 100644 --- a/src/calibre/db/tests/main.py +++ b/src/calibre/db/tests/main.py @@ -10,6 +10,7 @@ __docformat__ = 'restructuredtext en' import os from calibre.utils.run_tests import find_tests_in_dir, run_tests + def find_tests(): base = os.path.dirname(os.path.abspath(__file__)) return find_tests_in_dir(base) diff --git a/src/calibre/db/tests/profiling.py b/src/calibre/db/tests/profiling.py index 44cf4949b0..b0c35ffdbe 100644 --- a/src/calibre/db/tests/profiling.py +++ b/src/calibre/db/tests/profiling.py @@ -12,16 +12,20 @@ from tempfile import gettempdir from calibre.db.legacy import LibraryDatabase db = None + + def initdb(path): global db db = LibraryDatabase(os.path.expanduser(path)) + def show_stats(path): from pstats import Stats s = Stats(path) s.sort_stats('cumulative') s.print_stats(30) + def main(): stats = os.path.join(gettempdir(), 'read_db.stats') pr = cProfile.Profile() diff --git a/src/calibre/db/tests/reading.py b/src/calibre/db/tests/reading.py index 05a5a8a4a4..2f28e78225 100644 --- a/src/calibre/db/tests/reading.py +++ b/src/calibre/db/tests/reading.py @@ -13,6 +13,7 @@ from io import BytesIO from calibre.utils.date import utc_tz from calibre.db.tests.base import BaseTest + class ReadingTest(BaseTest): def test_read(self): # {{{ @@ -493,18 +494,22 @@ class ReadingTest(BaseTest): def test_search_caching(self): # {{{ ' Test caching of searches ' from calibre.db.search import LRUCache + class TestCache(LRUCache): hit_counter = 0 miss_counter = 0 + def get(self, key, default=None): ans = LRUCache.get(self, key, default=default) if ans is not None: self.hit_counter += 1 else: self.miss_counter += 1 + @property def cc(self): self.hit_counter = self.miss_counter = 0 + @property def counts(self): return self.hit_counter, self.miss_counter @@ -559,6 +564,7 @@ class ReadingTest(BaseTest): 'Standard field: %s not the same for book %s' % (field, book_id)) self.assertEqual(mi.format_field(field), pmi.format_field(field), 'Standard field format: %s not the same for book %s' % (field, book_id)) + def f(x): try: x.pop('rec_index', None) diff --git a/src/calibre/db/tests/utils.py b/src/calibre/db/tests/utils.py index 2b27f0c15e..dab458cc18 100644 --- a/src/calibre/db/tests/utils.py +++ b/src/calibre/db/tests/utils.py @@ -12,6 +12,7 @@ from calibre import walk from calibre.db.tests.base import BaseTest from calibre.db.utils import ThumbnailCache + class UtilsTest(BaseTest): def setUp(self): diff --git a/src/calibre/db/tests/writing.py b/src/calibre/db/tests/writing.py index eb875fb788..526e541e3c 100644 --- a/src/calibre/db/tests/writing.py +++ b/src/calibre/db/tests/writing.py @@ -15,6 +15,7 @@ from calibre.ebooks.metadata import author_to_author_sort from calibre.utils.date import UNDEFINED_DATE from calibre.db.tests.base import BaseTest, IMG + class WritingTest(BaseTest): # Utils {{{ diff --git a/src/calibre/db/utils.py b/src/calibre/db/utils.py index 9e24d9523e..a4067236e6 100644 --- a/src/calibre/db/utils.py +++ b/src/calibre/db/utils.py @@ -15,6 +15,7 @@ from threading import Lock from calibre import as_unicode, prints from calibre.constants import cache_dir, get_windows_number_formats, iswindows + def force_to_bool(val): if isinstance(val, (str, unicode)): try: @@ -33,6 +34,7 @@ def force_to_bool(val): _fuzzy_title_patterns = None + def fuzzy_title_patterns(): global _fuzzy_title_patterns if _fuzzy_title_patterns is None: @@ -48,12 +50,14 @@ def fuzzy_title_patterns(): ) return _fuzzy_title_patterns + def fuzzy_title(title): title = icu_lower(title.strip()) for pat, repl in fuzzy_title_patterns(): title = pat.sub(repl, title) return title + def find_identical_books(mi, data): author_map, aid_map, title_map = data found_books = None @@ -79,9 +83,12 @@ def find_identical_books(mi, data): Entry = namedtuple('Entry', 'path size timestamp thumbnail_size') + + class CacheError(Exception): pass + class ThumbnailCache(object): ' This is a persistent disk cache to speed up loading and resizing of covers ' @@ -129,6 +136,7 @@ class ThumbnailCache(object): self.total_size = 0 self.items = OrderedDict() order = self._read_order() + def listdir(*args): try: return os.listdir(os.path.join(*args)) @@ -358,6 +366,8 @@ class ThumbnailCache(object): self._apply_size() number_separators = None + + def atof(string): # Python 2.x does not handle unicode number separators correctly, so we # have to implement our own diff --git a/src/calibre/db/view.py b/src/calibre/db/view.py index c0899591ce..b7cf307a49 100644 --- a/src/calibre/db/view.py +++ b/src/calibre/db/view.py @@ -16,12 +16,14 @@ from calibre.ebooks.metadata import title_sort from calibre.utils.config_base import tweaks, prefs from calibre.db.write import uniq + def sanitize_sort_field_name(field_metadata, field): field = field_metadata.search_term_to_field_key(field.lower().strip()) # translate some fields to their hidden equivalent field = {'title': 'sort', 'authors':'author_sort'}.get(field, field) return field + class MarkedVirtualField(object): def __init__(self, marked_ids): @@ -35,6 +37,7 @@ class MarkedVirtualField(object): g = self.marked_ids.get return lambda book_id:g(book_id, None) + class TableRow(object): def __init__(self, book_id, view): @@ -57,6 +60,7 @@ class TableRow(object): for i in xrange(self.column_count): yield self[i] + def format_is_multiple(x, sep=',', repl=None): if not x: return None @@ -64,11 +68,13 @@ def format_is_multiple(x, sep=',', repl=None): x = (y.replace(sep, repl) for y in x) return sep.join(x) + def format_identifiers(x): if not x: return None return ','.join('%s:%s'%(k, v) for k, v in x.iteritems()) + class View(object): ''' A table view of the database, with rows and columns. Also supports diff --git a/src/calibre/db/write.py b/src/calibre/db/write.py index b5bd42f121..8fcdde8969 100644 --- a/src/calibre/db/write.py +++ b/src/calibre/db/write.py @@ -24,9 +24,11 @@ if ispy3: # Convert data into values suitable for the db {{{ + def sqlite_datetime(x): return isoformat(x, sep=' ') if isinstance(x, datetime) else x + def single_text(x): if x is None: return x @@ -37,6 +39,7 @@ def single_text(x): series_index_pat = re.compile(r'(.*)\s+\[([.0-9]+)\]$') + def get_series_values(val): if not val: return (val, None) @@ -50,6 +53,7 @@ def get_series_values(val): pass return (val, None) + def multiple_text(sep, ui_sep, x): if not x: return () @@ -65,6 +69,7 @@ def multiple_text(sep, ui_sep, x): x = (y.strip().replace(ui_sep, repsep) for y in x if y.strip()) return tuple(' '.join(y.split()) for y in x if y) + def adapt_datetime(x): if isinstance(x, (unicode, bytes)): x = parse_date(x, assume_utc=False, as_utc=False) @@ -72,6 +77,7 @@ def adapt_datetime(x): x = UNDEFINED_DATE return x + def adapt_date(x): if isinstance(x, (unicode, bytes)): x = parse_only_date(x) @@ -79,6 +85,7 @@ def adapt_date(x): x = UNDEFINED_DATE return x + def adapt_number(typ, x): if x is None: return None @@ -87,6 +94,7 @@ def adapt_number(typ, x): return None return typ(x) + def adapt_bool(x): if isinstance(x, (unicode, bytes)): x = x.lower() @@ -100,6 +108,7 @@ def adapt_bool(x): x = bool(int(x)) return x if x is None else bool(x) + def adapt_languages(to_tuple, x): ans = [] for lang in to_tuple(x): @@ -109,11 +118,13 @@ def adapt_languages(to_tuple, x): ans.append(lc) return tuple(ans) + def clean_identifier(typ, val): typ = icu_lower(typ or '').strip().replace(':', '').replace(',', '') val = (val or '').strip().replace(',', '|') return typ, val + def adapt_identifiers(to_tuple, x): if not isinstance(x, dict): x = {k:v for k, v in (y.partition(':')[0::2] for y in to_tuple(x))} @@ -124,10 +135,12 @@ def adapt_identifiers(to_tuple, x): ans[k] = v return ans + def adapt_series_index(x): ret = adapt_number(float, x) return 1.0 if ret is None else ret + def get_adapter(name, metadata): dt = metadata['datatype'] if dt == 'text': @@ -174,6 +187,8 @@ def get_adapter(name, metadata): # }}} # One-One fields {{{ + + def one_one_in_books(book_id_val_map, db, field, *args): 'Set a one-one field in the books table' if book_id_val_map: @@ -183,10 +198,12 @@ def one_one_in_books(book_id_val_map, db, field, *args): field.table.book_col_map.update(book_id_val_map) return set(book_id_val_map) + def set_uuid(book_id_val_map, db, field, *args): field.table.update_uuid_cache(book_id_val_map) return one_one_in_books(book_id_val_map, db, field, *args) + def set_title(book_id_val_map, db, field, *args): ans = one_one_in_books(book_id_val_map, db, field, *args) # Set the title sort field @@ -194,6 +211,7 @@ def set_title(book_id_val_map, db, field, *args): {k:title_sort(v) for k, v in book_id_val_map.iteritems()}, db) return ans + def one_one_in_other(book_id_val_map, db, field, *args): 'Set a one-one field in the non-books table, like comments' deleted = tuple((k,) for k, v in book_id_val_map.iteritems() if v is None) @@ -210,6 +228,7 @@ def one_one_in_other(book_id_val_map, db, field, *args): field.table.book_col_map.update(updated) return set(book_id_val_map) + def custom_series_index(book_id_val_map, db, field, *args): series_field = field.series_field sequence = [] @@ -228,12 +247,14 @@ def custom_series_index(book_id_val_map, db, field, *args): # Many-One fields {{{ + def safe_lower(x): try: return icu_lower(x) except (TypeError, ValueError, KeyError, AttributeError): return x + def get_db_id(val, db, m, table, kmap, rid_map, allow_case_change, case_changes, val_map, is_authors=False): ''' Get the db id for the value val. If val does not exist in the db it is @@ -258,6 +279,7 @@ def get_db_id(val, db, m, table, kmap, rid_map, allow_case_change, case_changes[item_id] = val val_map[val] = item_id + def change_case(case_changes, dirtied, db, table, m, is_authors=False): if is_authors: vals = ((val.replace(',', '|'), item_id) for item_id, val in @@ -272,6 +294,7 @@ def change_case(case_changes, dirtied, db, table, m, is_authors=False): if is_authors: table.asort_map[item_id] = author_to_author_sort(val) + def many_one(book_id_val_map, db, field, allow_case_change, *args): dirtied = set() m = field.metadata @@ -347,6 +370,7 @@ def many_one(book_id_val_map, db, field, allow_case_change, *args): # Many-Many fields {{{ + def uniq(vals, kmap=lambda x:x): ''' Remove all duplicates from vals, while preserving order. kmap must be a callable that returns a hashable value for every item in vals ''' @@ -356,6 +380,7 @@ def uniq(vals, kmap=lambda x:x): seen_add = seen.add return tuple(x for x, k in zip(vals, lvals) if k not in seen and not seen_add(k)) + def many_many(book_id_val_map, db, field, allow_case_change, *args): dirtied = set() m = field.metadata @@ -450,6 +475,7 @@ def many_many(book_id_val_map, db, field, allow_case_change, *args): # }}} + def identifiers(book_id_val_map, db, field, *args): # {{{ table = field.table updates = set() @@ -475,9 +501,11 @@ def identifiers(book_id_val_map, db, field, *args): # {{{ return set(book_id_val_map) # }}} + def dummy(book_id_val_map, *args): return set() + class Writer(object): def __init__(self, field): diff --git a/src/calibre/debug.py b/src/calibre/debug.py index 2b47f4700f..6a59e7db59 100644 --- a/src/calibre/debug.py +++ b/src/calibre/debug.py @@ -11,6 +11,7 @@ from calibre.utils.config import OptionParser from calibre.constants import iswindows from calibre import prints + def option_parser(): parser = OptionParser(usage=_('''\ {0} @@ -89,6 +90,7 @@ Everything after the -- is passed to the script. return parser + def reinit_db(dbpath): from contextlib import closing from calibre import as_unicode @@ -127,6 +129,7 @@ def reinit_db(dbpath): atomic_rename(tmpdb, dbpath) prints('Database successfully re-initialized') + def debug_device_driver(): from calibre.devices import debug debug(ioreg_to_tmp=True, buf=sys.stdout) @@ -149,6 +152,7 @@ def add_simple_plugin(path_to_plugin): os.chdir(odir) shutil.rmtree(tdir) + def print_basic_debug_info(out=None): if out is None: out = sys.stdout @@ -183,6 +187,7 @@ def print_basic_debug_info(out=None): names = ('{0} {1}'.format(p.name, p.version) for p in initialized_plugins() if getattr(p, 'plugin_path', None) is not None) out('Successfully initialized third party plugins:', ' && '.join(names)) + def run_debug_gui(logpath): import time time.sleep(3) # Give previous GUI time to shutdown fully and release locks @@ -192,6 +197,7 @@ def run_debug_gui(logpath): from calibre.gui_launch import calibre calibre(['__CALIBRE_GUI_DEBUG__', logpath]) + def run_script(path, args): # Load all user defined plugins so the script can import from the # calibre_plugins namespace @@ -208,12 +214,14 @@ def run_script(path, args): g['__file__'] = ef execfile(ef, g) + def inspect_mobi(path): from calibre.ebooks.mobi.debug.main import inspect_mobi prints('Inspecting:', path) inspect_mobi(path) print + def main(args=sys.argv): from calibre.constants import debug debug() diff --git a/src/calibre/devices/__init__.py b/src/calibre/devices/__init__.py index 24c6673dce..723e875a4b 100644 --- a/src/calibre/devices/__init__.py +++ b/src/calibre/devices/__init__.py @@ -14,6 +14,7 @@ MONTH_MAP = dict(Jan=1, Feb=2, Mar=3, Apr=4, May=5, Jun=6, Jul=7, Aug=8, Sep=9, INVERSE_DAY_MAP = dict(zip(DAY_MAP.values(), DAY_MAP.keys())) INVERSE_MONTH_MAP = dict(zip(MONTH_MAP.values(), MONTH_MAP.keys())) + def strptime(src): src = src.strip() src = src.split() @@ -21,12 +22,14 @@ def strptime(src): src[2] = str(MONTH_MAP[src[2]]) return time.strptime(' '.join(src), '%w, %d %m %Y %H:%M:%S %Z') + def strftime(epoch, zone=time.gmtime): src = time.strftime("%w, %d %m %Y %H:%M:%S GMT", zone(epoch)).split() src[0] = INVERSE_DAY_MAP[int(src[0][:-1])]+',' src[2] = INVERSE_MONTH_MAP[int(src[2])] return ' '.join(src) + def get_connected_device(): from calibre.customize.ui import device_plugins from calibre.devices.scanner import DeviceScanner @@ -55,6 +58,7 @@ def get_connected_device(): break return dev + def debug(ioreg_to_tmp=False, buf=None, plugins=None, disabled_plugins=None): ''' @@ -193,6 +197,7 @@ def debug(ioreg_to_tmp=False, buf=None, plugins=None, except: pass + def device_info(ioreg_to_tmp=False, buf=None): from calibre.devices.scanner import DeviceScanner diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index 0393b0cc19..aa73a47163 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -13,6 +13,7 @@ from calibre.devices.usbms.driver import USBMS HTC_BCDS = [0x100, 0x0222, 0x0224, 0x0226, 0x227, 0x228, 0x229, 0x0231, 0x9999] + class ANDROID(USBMS): name = 'Android driver' @@ -339,6 +340,7 @@ class ANDROID(USBMS): del proxy['use_subdirs'] del proxy['extra_customization'] + class S60(USBMS): name = 'S60 driver' @@ -358,6 +360,7 @@ class S60(USBMS): VENDOR_NAME = 'NOKIA' WINDOWS_MAIN_MEM = 'S60' + class WEBOS(USBMS): name = 'WebOS driver' diff --git a/src/calibre/devices/apple/driver.py b/src/calibre/devices/apple/driver.py index 81320b389c..3d6823cb69 100644 --- a/src/calibre/devices/apple/driver.py +++ b/src/calibre/devices/apple/driver.py @@ -18,6 +18,7 @@ from calibre.ebooks.metadata import (author_to_author_sort, authors_to_string, from calibre.ebooks.metadata.book.base import Metadata from calibre.utils.config_base import config_dir, prefs + def strftime(fmt='%Y/%m/%d %H:%M:%S', dt=None): from calibre.utils.date import now @@ -30,6 +31,8 @@ def strftime(fmt='%Y/%m/%d %H:%M:%S', dt=None): return _strftime(fmt, now().timetuple()) _log = None + + def logger(): global _log if _log is None: @@ -3679,6 +3682,7 @@ class Book(Metadata): A simple class describing a book in the iTunes Books Library. See ebooks.metadata.book.base ''' + def __init__(self, title, author): Metadata.__init__(self, title, authors=author.split(' & ')) self.author = author diff --git a/src/calibre/devices/binatone/driver.py b/src/calibre/devices/binatone/driver.py index db9705f800..836bda20be 100644 --- a/src/calibre/devices/binatone/driver.py +++ b/src/calibre/devices/binatone/driver.py @@ -10,6 +10,7 @@ Device driver for Bookeen's Cybook Gen 3 from calibre.devices.usbms.driver import USBMS + class README(USBMS): name = 'Binatone Readme Device Interface' diff --git a/src/calibre/devices/blackberry/driver.py b/src/calibre/devices/blackberry/driver.py index 6c3111cb3c..77fc5dfe65 100644 --- a/src/calibre/devices/blackberry/driver.py +++ b/src/calibre/devices/blackberry/driver.py @@ -6,6 +6,7 @@ __docformat__ = 'restructuredtext en' from calibre.devices.usbms.driver import USBMS + class BLACKBERRY(USBMS): name = 'Blackberry Device Interface' @@ -29,6 +30,7 @@ class BLACKBERRY(USBMS): EBOOK_DIR_MAIN = 'eBooks' SUPPORTS_SUB_DIRS = True + class PLAYBOOK(USBMS): name = 'Blackberry Playbook Interface' diff --git a/src/calibre/devices/boeye/driver.py b/src/calibre/devices/boeye/driver.py index 1e37ea8cec..2cd616113b 100644 --- a/src/calibre/devices/boeye/driver.py +++ b/src/calibre/devices/boeye/driver.py @@ -8,6 +8,7 @@ Device driver for BOEYE serial readers from calibre.devices.usbms.driver import USBMS + class BOEYE_BEX(USBMS): name = 'BOEYE BEX reader driver' gui_name = 'BOEYE BEX' @@ -29,6 +30,7 @@ class BOEYE_BEX(USBMS): EBOOK_DIR_MAIN = 'Documents' SUPPORTS_SUB_DIRS = True + class BOEYE_BDX(USBMS): name = 'BOEYE BDX reader driver' gui_name = 'BOEYE BDX' diff --git a/src/calibre/devices/cli.py b/src/calibre/devices/cli.py index f8276105d0..c2181933e9 100755 --- a/src/calibre/devices/cli.py +++ b/src/calibre/devices/cli.py @@ -17,6 +17,7 @@ from calibre.utils.config import device_prefs MINIMUM_COL_WIDTH = 12 # : Minimum width of columns in ls output + class FileFormatter(object): def __init__(self, file): @@ -31,6 +32,7 @@ class FileFormatter(object): @dynamic_property def mode_string(self): doc=""" The mode string for this file. There are only two modes read-only and read-write """ + def fget(self): mode, x = "-", "-" if self.is_dir: @@ -45,6 +47,7 @@ class FileFormatter(object): @dynamic_property def isdir_name(self): doc='''Return self.name + '/' if self is a directory''' + def fget(self): name = self.name if self.is_dir: @@ -55,6 +58,7 @@ class FileFormatter(object): @dynamic_property def name_in_color(self): doc=""" The name in ANSI text. Directories are blue, ebooks are green """ + def fget(self): cname = self.name blue, green, normal = "", "", "" @@ -72,6 +76,7 @@ class FileFormatter(object): @dynamic_property def human_readable_size(self): doc=""" File size in human readable form """ + def fget(self): return human_readable(self.size) return property(doc=doc, fget=fget) @@ -79,6 +84,7 @@ class FileFormatter(object): @dynamic_property def modification_time(self): doc=""" Last modified time in the Linux ls -l format """ + def fget(self): return time.strftime("%Y-%m-%d %H:%M", time.localtime(self.wtime)) return property(doc=doc, fget=fget) @@ -86,10 +92,12 @@ class FileFormatter(object): @dynamic_property def creation_time(self): doc=""" Last modified time in the Linux ls -l format """ + def fget(self): return time.strftime("%Y-%m-%d %H:%M", time.localtime(self.ctime)) return property(doc=doc, fget=fget) + def info(dev): info = dev.get_device_information() print "Device name: ", info[0] @@ -97,6 +105,7 @@ def info(dev): print "Software version:", info[2] print "Mime type: ", info[3] + def ls(dev, path, recurse=False, human_readable_size=False, ll=False, cols=0): def col_split(l, cols): # split list l into columns rows = len(l) / cols @@ -173,6 +182,7 @@ def ls(dev, path, recurse=False, human_readable_size=False, ll=False, cols=0): output.close() return listing + def shutdown_plugins(): for d in device_plugins(): try: @@ -180,6 +190,7 @@ def shutdown_plugins(): except: pass + def main(): from calibre.utils.terminal import geometry cols = geometry()[0] diff --git a/src/calibre/devices/cybook/driver.py b/src/calibre/devices/cybook/driver.py index a4f82bbbdb..7f47d75c65 100644 --- a/src/calibre/devices/cybook/driver.py +++ b/src/calibre/devices/cybook/driver.py @@ -17,6 +17,7 @@ from calibre.devices.usbms.driver import USBMS import calibre.devices.cybook.t2b as t2b import calibre.devices.cybook.t4b as t4b + class CYBOOK(USBMS): name = 'Cybook Gen 3 / Opus Device Interface' @@ -60,6 +61,7 @@ class CYBOOK(USBMS): return device_info[3] == 'Bookeen' and (device_info[4] == 'Cybook Gen3' or device_info[4] == 'Cybook Opus') return True + class ORIZON(CYBOOK): name = 'Cybook Orizon Device Interface' @@ -111,6 +113,7 @@ class ORIZON(CYBOOK): return '' return self.EBOOK_DIR_CARD_A + class MUSE(CYBOOK): name = 'Cybook Muse Device Interface' diff --git a/src/calibre/devices/cybook/t2b.py b/src/calibre/devices/cybook/t2b.py index dbfa2a15f5..be99ca657c 100644 --- a/src/calibre/devices/cybook/t2b.py +++ b/src/calibre/devices/cybook/t2b.py @@ -8,6 +8,7 @@ import StringIO DEFAULT_T2B_DATA = '\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0f\xff\xff\xff\xf0\xff\x0f\xc3\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xf8\x00\x00\xff\xff\xff\xf0\xff\x0f\xc3\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xe0\xff\xf0\xff\xff\xff\xf0\xff\xff\xc3\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xc3\xff\xff\xff\xff\xff\xf0\xff\xff\xc3\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x07\xff\xff\xfc\x00?\xf0\xff\x0f\xc3\x00?\xf0\xc0\xfe\x00?\xff\xff\xff\xff\xff\xff\xff\x0f\xff\xff\xf0<\x0f\xf0\xff\x0f\xc0,\x0f\xf0\x0e\xf0,\x0f\xff\xff\xff\xff\xff\xff\xff\x0f\xff\xff\xff\xff\xc3\xf0\xff\x0f\xc0\xff\x0f\xf0\xff\xf0\xff\xc7\xff\xff\xff\xff\xff\xff\xff\x0f\xff\xff\xff\xff\xc3\xf0\xff\x0f\xc3\xff\xc3\xf0\xff\xc3\xff\xc3\xff\xff\xff\xff\xff\xff\xff\x0f\xff\xff\xff\x00\x03\xf0\xff\x0f\xc3\xff\xc3\xf0\xff\xc3\xff\xc3\xff\xff\xff\xff\xff\xff\xff\x0f\xff\xff\xf0\x1f\xc3\xf0\xff\x0f\xc3\xff\xc3\xf0\xff\xc0\x00\x03\xff\xff\xff\xff\xff\xff\xff\x0b\xff\xff\xf0\xff\xc3\xf0\xff\x0f\xc3\xff\xc3\xf0\xff\xc3\xff\xff\xff\xff\xff\xff\xff\xff\xff\xc3\xff\xff\xf3\xff\xc3\xf0\xff\x0f\xc3\xff\xc3\xf0\xff\xc3\xff\xff\xff\xff\xff\xff\xff\xff\xff\xc0\xff\xfc\xf0\xff\x03\xf0\xff\x0f\xc0\xff\x0f\xf0\xff\xf0\xff\xff\xff\xff\xff\xff\xff\xff\xff\xf0\x0f\x00\xf08\x03\xf0\xff\x0f\xc0,\x0f\xf0\xff\xf0\x1f\x03\xff\xff\xff\xff\xff\xff\xff\xff\x00\x0f\xfc\x00\xc3\xf0\xff\x0f\xc3\x00?\xf0\xff\xff\x00\x0f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xf0\x0f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xf0\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x03\xfe\x94\xff\xff\xff\xff\xff\xff\xff\xff\xff\xc0\x00\x00\x00\x0f\xff\xff\xff\xff\xff\xff\xfc\x7f\xfe\x94\xff\xff\xff\xff\xff\xff\xff\xff\xfc\x0f\xff\xfe\xa9@\xff\xff\xff\xff\xff\xff\xfc?\xfe\xa4\xff\xff\xff\xff\xff\xff\xff\xff\xfc\xff\xff\xff\xe9P\xff\xff\xff\xff\xff\xff\xfe/\xfe\xa8\xff\xff\xff\xff\xff\xff\xff\xff\xfc\xff\xff\xff\xf9T\xff\xff\xff\xff\xf0@\x00+\xfa\xa8?\xff\xff\xff\xff\xff\xff\xff\xfc\xbf\xff\xff\xf9T\xff\xff\xff\xff\xcb\xe4}*\xaa\xaa?\xff\xff\xff\xff\xff\xff\xff\xfc\xbf\xff\xff\xe9T\xff\xff\xff\xff\xc7\xe4\xfd\x1a\xaa\xaa?\xff\xff\xff\xff\xff\xff\xff\xfc\xaf\xea\xaa\xa6\xa4\xff@\x00\x0f\xc3\xe8\xfe\x1a\xaa\xaa?\xff\xff\xff\xff\xff\xff\xff\xfcj\x95UZ\xa4\x00\x7f\xfe\x90\x03\xe8\xfe\n\xaa\xaa?\xff\xff\xff\xff\xff\xff\xff\xfcj\x95UZ\xa4?\xff\xff\xa5C\xe8\xfe\x06\xaa\xaa?\xff\xff\xff\xff\xff\xff\xff\xfcj\x95UZ\xa4?\xff\xff\xeaC\xe8\xbe\x06\xaa\xaa\x0f\xff\xff\xff\xff\xff\xff\xff\xfcj\x95UZ\xa4/\xff\xff\xea\x82\xe8j\x06\xaa\xaa\x0f\xff\xff\xff\xff\xff\xff\xff\xfcj\x95UZ\xa4/\xff\xff\xaa\x82\xe8*F\xaa\xaa\x8f\xff\xff\xff\xff\xff\xff\xff\xfcj\x95UZ\xa4+\xff\xfe\xaa\x82\xe8*\x86\xaa\xaa\x8f\xff\xff\x80\xff\xff\xff\xff\xfcj\x95UV\xa4\x1a\xfa\xaa\xaa\x82\xe8*\x86\xaa\xaa\x8f\xf0\x00T?\xff\xff\xff\xfcj\x95UV\xa4\x1a\xfa\xaa\xaa\x82\xe8*\x81\xaa\xaa\x8c\x03\xff\x95?\xff\xff\xff\xfcj\x95UV\xa4\x1a\xfa\xaa\xaa\x82\xe8*\x81\xaa\xaa\x80\xbf\xff\x95?\xff\xff\xff\xfcj\x95UV\xa4\x1a\xfa\xaa\xaa\x82\xe8*\x81\xaa\xaa\x9b\xff\xff\x95\x0f\xff\xff\xff\xfcj\x95UV\xa4\x1a\xfa\xaa\xaa\x82\xe8\x1a\x81\xaa\xaa\x9a\xff\xfe\x95\x0f\xff\xff\xff\xfcj\x95UV\xa4\x1a\xfa\xaa\xaa\x82\xe8\n\x81\xaa\xaa\xa6\xbf\xfeUO\xff\xff\xff\xfcj\x95UV\xa4\x1a\xfa\xaa\xaa\x82\xa8\n\x91j\xaa\xa5\xaa\xa9ZO\xff\xff\xff\xfcj\x95UV\xa4\x1a\xfa\xaa\xaa\x82\xa8\n\xa0j\xaa\xa5Z\x95ZO\xff\xff\xff\xfcj\x95UV\xa4*\xfa\xaa\xaa\x82\xa9\n\xa0j\xaa\xa5UUZC\xff\xff\xff\xfcj\x95UV\xa4*\xfa\xaa\xaa\x82\xaa\n\xa0j\xaa\xa4UUZS\xff\xff\xff\xfcZ\x95UV\xa4*\xfa\xaa\xaa\x82\xaa\n\xa0j\xaa\xa4UUZS\xff\xff\xff\xfcZ\x95UU\xa4*\xfa\xaa\xaa\x82\xaa\n\xa0j\xaa\xa8UUVS\xff\xff\xff\xfcZ\x95UU\xa4*\xea\xaa\xaa\x82\xaa\x06\xa0Z\xaa\xa8UUV\x93\xff\xff\xff\xfcZ\x95UU\xa4*\xaa\xaa\xaa\x81\xaa\x02\xa0\x1a\xaa\xa8UUV\x90\xff\xff\xff\xfcZ\x95UU\xa4*\xaa\xaa\xaa\x80\xaa\x02\xa0\x1a\xaa\xa8\x15UU\x94\xff\xff\xff\xfcZ\x95UU\xa4*\xaa\xaa\xaa\x80\xaa"\xa0\x1a\xaa\xa8\x15UU\x94\xff\xff\xff\xfcZ\x95UU\xa4*\xaa\xaa\xaa\x80\xaa2\xa4\x16\xaa\xa8\x15UU\x94\xff\xff\xff\xfcZ\x95UU\xa4*\xaa\xaa\xaa\x80\xaa2\xa8\x16\xa6\xa9\x15UU\x94\xff\xff\xff\xfcZ\x95UU\xa4*\xaa\xaa\xaa\x80\xaa2\xa8\x16\xa6\xa9\x05UUT?\xff\xff\xfcZ\x95UU\xa4*\xaa\xaa\xaa\x84\xaa2\xa8\x16\xaa\xaa\x05UUU?\xff\xff\xfcZ\x95UU\xa4*\xaa\xaa\xaa\x88\xaa2\xa8\x06\xaa\xaa\x05UUU?\xff\xff\xfcZ\x95UU\xa4*\xaa\xaa\xaa\x8c\xaa1\xa8\xc5\xaa\xaa\x05UUU?\xff\xff\xfcZ\x95UU\xa4*\xaa\xaa\xaa\x8c\xaa0\xa8E\xa9\xaa\x05UUU/\xff\xff\xfcZ\x95UU\xa4*\xaa\xaa\xaa\x8c\xaa<\xa8\x05\xa9\xaaAUUU\x0f\xff\xff\xfcZ\x95UU\xa4*\xaa\xaa\xaa\x8c\xaa<\xa8\x05\xa9\xaaAUUUO\xff\xff\xfcZ\x95UU\xa4*\xaa\xaa\xaa\x8c\xaa<\xa9\x05\xaa\xaaAUUUO\xff\xff\xfcZ\x95UU\xa4*\xaa\xaa\xaa\x8c\xaa\x1c\xaa\x01\xaa\xaa\x81UUUO\xff\xff\xfcZ\x95UU\xa4*\xaa\xaa\xaa\x8c\xaa\x0c\xaa\x01\xaa\xaa\x81UUUO\xff\xff\xfcZ\x95UU\xa4*\xaa\xaa\xaa\x8c\xaa\x0c\xaa1j\xaa\x80UUUC\xff\xff\xfcZ\x95UU\xa4*\xaa\xaa\xaa\x8c\xaa\x0cj1jj\x90UUUS\xff\xff\xfcZ\x95UU\xa4*\xaa\xaa\xaa\x8c\xaa\x0c*1jj\x90UUUS\xff\xff\xfcZ\x95UU\xa4*\xaa\xaa\xaa\x8c\xaaL*1jj\xa0UUUS\xff\xff\xfcZ\x95UU\xa4*\xaa\xaa\xaa\x8c\xaa\x8f* j\xaa\xa0\x15UUS\xff\xff\xfcZ\x95UU\xa4*\xaa\xaa\xaa\x8c\xaa\x8f*@j\xaa\xa0\x15UUP\xff\xff\xfcZ\x95UU\xa4*\xaa\xaa\xaa\x8c\xaa\x8f*\x8cZ\xaa\xa1\x15UUT\xff\xff\xfcZ\x95UU\xa4j\xaa\xaa\xaa\x8c\xaa\x8f*\x8cZ\x9a\xa0\x15UUT\xff\xff\xfcZ\x95UU\xa4j\xaa\xaa\xaa\x8c\xaa\x8f*\x8cZ\x9a\xa0\x15UUT\xff\xff\xfcZ\x95UU\xa4j\xaa\xaa\xaa\x8c\xaa\x8f\x1a\x8cZ\x9a\xa4\x15UUT?\xff\xfcZ\x95UU\x94j\xaa\xaa\xaa\x8cj\x8f\n\x8cVj\xa4\x05UU\xa4?\xff\xfcVUUU\xa4j\xaa\xaa\xaa\x8cj\x8fJ\x8c\x16\xaa\xa8\xc5UZ\xa5?\xff\xfcUUUV\xa4j\xaa\xaa\xaa\x8cj\x8f\xca\x8f\x16\xaa\xa8\xc5V\xaa\xa5?\xff\xfcUj\xaa\xaa\xa4j\xaa\xaa\xaa\x8cj\x8f\xca\x8f\x1a\xaa\xa8\x05Z\xaaU?\xff\xfcV\xaa\xaa\xaa\xa5j\xaa\xaa\xaa\x8e*\x8f\xca\x83\x1a\xaa\xa4\x01eUU?\xff\xfcZ\xaa\xaa\xaa\xa5j\xaa\xaa\xaa\x8f*\x8f\xca\x83\x1a\xa5U\x01U\x00\x00\x0f\xff\xfcUUUUUZ\xaa\xaa\xaaO%\x8f\xc6\x93\x15\x00\x001@\x0f\xff\xff\xff\xfcP\x00\x00\x00\x15\x00\x00\x00\x00\x0f\x00\x07\xc0\x03\x00\xff\xff0\x1f\xff\xff\xff\xff\xfc\x00\xff\xff\xf8\x00?\xff\xff\xff\x0f?\xc7\xc3\xf7\x0f\xff\xff\xf1\xff\xff\xff\xff\xff\xfc\xff\xff\xff\xff\xf4\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # noqa + def reduce_color(c): if c <= 64: return 0 @@ -18,9 +19,11 @@ def reduce_color(c): else: return 3 + def i2b(n): return "".join([str((n >> y) & 1) for y in range(1, -1, -1)]) + def write_t2b(t2bfile, coverdata=None): ''' t2bfile is a file handle ready to write binary data to disk. diff --git a/src/calibre/devices/cybook/t4b.py b/src/calibre/devices/cybook/t4b.py index adb0248d43..c87d7a69b6 100644 --- a/src/calibre/devices/cybook/t4b.py +++ b/src/calibre/devices/cybook/t4b.py @@ -11,9 +11,11 @@ from io import BytesIO DEFAULT_T4B_DATA = b'\x74\x34\x62\x70\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xc4\x00\x16\xdf\xff\xff\xf7\x6d\xff\xff\xfd\x7a\xff\xff\xff\xe7\x77\x76\xff\xf6\x77\x77\x8d\xff\xff\xe7\x77\x78\xbf\xff\xff\xf7\x77\x77\x77\x7d\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xe4\x03\x78\x61\x07\xff\xff\x90\x04\xff\xff\xfc\x05\xff\xff\xff\xd5\x30\x35\xff\xf0\x13\x32\x00\x5f\xff\xd0\x03\x32\x01\xbf\xff\xe0\x00\x00\x00\x0b\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x50\x8f\xff\xff\x75\xff\xff\x40\x30\xef\xff\xfc\x06\xff\xff\xff\xff\xa0\x9f\xff\xf0\x6f\xff\xf5\x0d\xff\xd0\x4f\xff\xd0\x0f\xff\xe0\x3f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfd\x05\xff\xff\xff\xfe\xff\xfe\x03\xa0\x8f\xff\xfc\x06\xff\xff\xff\xff\xa0\x9f\xff\xf0\x6f\xff\xfb\x0b\xff\xd0\x4f\xff\xf7\x0d\xff\xe0\x3f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xf6\x0e\xff\xff\xff\xff\xff\xfa\x0b\xe0\x3f\xff\xfc\x06\xff\xff\xff\xff\xa0\x9f\xff\xf0\x6f\xff\xf5\x0e\xff\xd0\x4f\xff\xf8\x0e\xff\xe0\x3f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xf1\x1f\xff\xff\xff\xff\xff\xf2\x0f\xf3\x0d\xff\xfc\x06\xff\xff\xff\xff\xa0\x9f\xff\xf0\x39\x88\x30\xaf\xff\xd0\x4f\xff\xf2\x1f\xff\xe0\x18\x88\x88\x8d\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xf0\x3f\xff\xff\xff\xff\xff\xd0\x6f\xfc\x09\xff\xfc\x06\xff\xff\xff\xff\xa0\x9f\xff\xf0\x01\x11\x00\x2c\xff\xd0\x3a\xa8\x20\xcf\xff\xe0\x00\x00\x00\x0b\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xf0\x2f\xff\xff\xff\xff\xff\x60\xaf\xff\x11\xff\xfc\x06\xff\xff\xff\xff\xa0\x9f\xff\xf0\x6f\xff\xfd\x10\xef\xd0\x00\x00\x1e\xff\xff\xe0\x3f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xf2\x0f\xff\xff\xff\xff\xfe\x20\x12\x22\x00\xcf\xfc\x06\xff\xff\xff\xff\xa0\x9f\xff\xf0\x6f\xff\xff\x90\x9f\xd0\x3d\xd8\x09\xff\xff\xe0\x3f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xf8\x0b\xff\xff\xff\xff\xfc\x03\x88\x88\x60\x4f\xfc\x06\xff\xff\xff\xff\xa0\x9f\xff\xf0\x6f\xff\xff\xa0\x8f\xd0\x4f\xff\x40\xcf\xff\xe0\x3f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x01\xef\xff\xff\xfb\xf7\x0d\xff\xff\xf1\x1e\xfc\x06\xff\xff\xff\xff\xa0\x9f\xff\xf0\x6f\xff\xff\x60\xcf\xd0\x4f\xff\xf3\x0d\xff\xe0\x3f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x80\x2c\xff\xfa\x05\xf0\x2f\xff\xff\xf6\x0b\xfc\x03\x88\x88\x88\xff\xb0\xaf\xff\xf0\x5d\xcc\xa3\x05\xff\xd0\x4f\xff\xfe\x11\xef\xe0\x18\x88\x88\x8d\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xf8\x10\x01\x00\x3b\xb0\x9f\xff\xff\xfd\x06\xfc\x00\x00\x00\x00\xd0\x00\x00\xff\xf0\x00\x00\x02\x7f\xff\xe1\x5f\xff\xff\xc0\x5f\xe1\x00\x00\x00\x0b\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xed\xa9\xbd\xff\xed\xff\xff\xff\xff\xde\xff\xdd\xdd\xdd\xdd\xfd\xdd\xdd\xff\xfd\xdd\xdd\xee\xff\xff\xfd\xef\xff\xff\xfe\xdf\xfd\xdd\xdd\xdd\xdf\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xed\xdb\x86\x8e\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xb7\x42\x00\x00\x0b\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfd\x00\x00\x00\x00\x09\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfd\x00\x00\x01\x11\x17\xef\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xee\xee\xee\xee\xee\xee\xee\xee\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\x22\x45\x78\x9b\x95\xef\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xb8\x77\x78\x88\x88\x88\x87\x87\x89\xdf\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\x57\x9a\xaa\xaa\x94\xdf\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfb\x00\x00\x11\x11\x22\x12\x11\x11\x10\x7e\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x76\xaa\xaa\xab\xa4\xcf\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfa\x11\x12\x23\x33\x33\x33\x33\x32\x21\x6e\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xa5\xaa\xa9\x99\xa5\xaf\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfa\x11\x22\x33\x33\x44\x44\x44\x33\x21\x6d\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xb4\xaa\xa9\xa9\xb6\x8f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfa\x11\x23\x34\x44\x54\x55\x44\x44\x31\x6d\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xc5\x9a\x99\x9a\xb8\x7e\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfa\x11\x23\x34\x44\x44\x55\x45\x44\x31\x6d\xff\xff\xff\xff\xff\xff\xff\xff\xee\xdd\xdd\xee\xee\xc5\x9a\x88\x8a\xa9\x6e\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfa\x11\x22\x33\x44\x44\x44\x44\x44\x31\x6d\xff\xff\xff\xff\xff\xff\xff\xfe\xdc\xbb\xab\xcc\xca\x84\x8a\xa9\x99\xaa\x5d\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfa\x11\x22\x33\x44\x44\x44\x44\x44\x31\x6d\xff\xff\xff\xff\xff\xff\xff\xed\xa9\x99\x78\x87\x78\x84\x7a\xaa\x89\x9a\x6c\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfa\x11\x23\x34\x44\x45\x55\x55\x44\x31\x6d\xff\xff\xff\xff\xff\xff\xff\xec\x98\x88\x76\x98\x88\x74\x7a\xaa\xa7\x9a\x6b\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfa\x11\x23\x33\x34\x44\x54\x44\x44\x31\x6d\xff\xff\xff\xff\xff\xff\xff\xdb\x98\x88\x76\x98\x88\x74\x5a\xaa\x89\x9b\x7a\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfa\x11\x22\x33\x44\x45\x55\x54\x43\x31\x6d\xff\xff\xff\xff\xff\xff\xfe\xdb\x98\x88\x76\x98\x88\x84\x4a\xaa\x97\xbb\x79\xef\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfa\x11\x22\x33\x34\x44\x44\x44\x44\x31\x5b\xcc\xcc\xcc\xcc\xcc\xcc\xcb\xba\x98\x88\x76\x88\x88\x85\x39\xa9\x8a\xab\x97\xef\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfa\x11\x22\x33\x33\x34\x44\x44\x43\x31\x34\x44\x44\x55\x55\x55\x55\x44\x46\x98\x88\x76\x78\x88\x85\x28\xa8\x9a\xab\xa5\xef\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xf9\x00\x11\x11\x11\x12\x22\x22\x11\x10\x00\x01\x22\x23\x33\x33\x33\x31\x11\x88\x88\x76\x68\x88\x85\x27\xa9\xa9\xab\xa5\xdf\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfa\x11\x12\x23\x33\x33\x44\x43\x33\x21\x00\x12\x33\x44\x55\x55\x65\x42\x11\x78\x88\x76\x68\x88\x85\x36\xaa\xaa\xab\xb5\xcf\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfa\x11\x22\x33\x34\x44\x54\x44\x44\x21\x01\x23\x44\x55\x66\x66\x66\x53\x21\x78\x88\x86\x68\x88\x85\x45\xaa\xaa\xbb\xb6\xaf\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfa\x11\x22\x33\x34\x44\x44\x44\x33\x21\x01\x23\x45\x56\x67\x77\x77\x54\x21\x78\x88\x86\x68\x88\x85\x54\xaa\xab\xbb\xb7\x8f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfa\x11\x22\x33\x44\x44\x44\x44\x43\x21\x01\x23\x57\x8a\xaa\xaa\x99\x74\x21\x79\x88\x86\x68\x88\x75\x44\x9a\xab\xbb\xb9\x6e\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfa\x11\x22\x33\x34\x44\x44\x55\x43\x21\x01\x24\x56\x78\x89\x99\x88\x74\x21\x69\x88\x87\x68\x88\x75\x53\x9a\xab\xbb\xba\x5e\xff\xff\xff\xff\xff\xed\xa7\x9e\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfa\x11\x22\x33\x44\x34\x44\x54\x44\x21\x01\x23\x45\x56\x67\x77\x77\x64\x21\x69\x88\x87\x68\x88\x65\x53\x8a\xaa\xaa\xba\x5d\xff\xff\xff\xed\xb9\x75\x54\x5b\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfa\x11\x22\x33\x84\x66\x48\x54\x43\x21\x01\x23\x45\x56\x67\x77\x76\x64\x21\x6a\x88\x87\x68\x88\x66\x54\x7a\xaa\x9a\xbb\x6b\xff\xee\xb9\x66\x66\x78\x75\x69\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfa\x11\x22\x33\x64\x54\x47\x54\x43\x21\x01\x23\x45\x66\x77\x67\x67\x64\x21\x6a\x88\x87\x68\x88\x76\x54\x6a\xbb\xba\xbb\x79\xca\x75\x56\x77\x88\x88\x85\x68\xef\xff\xff\xff\xff\xff\xff\xff\xff\xfa\x11\x23\x33\xba\xba\xac\x44\x43\x21\x01\x23\x45\x56\x66\x66\x76\x64\x21\x6a\x88\x87\x67\x88\x66\x54\x5a\xbb\xa9\xbb\x83\x45\x67\x78\x64\x78\x88\x86\x66\xdf\xff\xff\xff\xff\xff\xff\xff\xff\xfa\x11\x22\x33\x87\x77\x79\x54\x43\x21\x01\x23\x45\x56\x66\x67\x77\x54\x21\x6a\x88\x87\x67\x88\x66\x54\x4a\xbb\xab\xbb\x93\x47\x88\x88\x66\x38\x88\x98\x66\xbf\xff\xff\xff\xff\xff\xff\xff\xff\xfa\x11\x22\x33\x44\x44\x44\x54\x44\x21\x01\x23\x45\x56\x66\x67\x77\x64\x21\x6a\x88\x88\x66\x98\x66\x55\x3a\xab\xba\xab\xa4\x47\x88\x87\x68\x43\x89\x99\x57\x8f\xff\xff\xff\xff\xff\xff\xff\xff\xfa\x11\x22\x33\x32\x34\x3a\x44\x43\x21\x01\x23\x45\x56\x66\x67\x66\x64\x31\x69\x88\x88\x65\x98\x66\x55\x39\xbb\xba\xbc\xa4\x47\x88\x87\x77\x85\x37\xaa\x78\x7e\xff\xff\xff\xff\xff\xff\xff\xff\xfa\x11\x22\x33\xad\x76\xc5\x44\x33\x21\x01\x23\x44\x56\x66\x77\x76\x54\x21\x69\x98\x88\x65\x98\x66\x65\x38\xbb\xb9\xbb\xb5\x46\x77\x86\x68\x87\x78\x9a\x87\x7c\xff\xff\xff\xff\xff\xff\xff\xff\xfa\x11\x22\x33\x84\x7b\x44\x44\x33\x21\x01\x23\x44\x56\x67\x76\x76\x63\x21\x79\x98\x88\x76\x87\x66\x65\x46\xaa\x99\xab\xb5\x45\x88\x76\x67\x77\x78\x9a\x96\x8a\xff\xff\xff\xff\xff\xff\xff\xff\xfa\x11\x22\x33\x63\x54\x46\x54\x43\x21\x01\x23\x44\x55\x66\x67\x76\x64\x21\x79\x98\x88\x76\x77\x66\x65\x44\xab\xbb\xaa\xb7\x35\x88\x87\x77\x89\x99\x9a\x96\x89\xef\xff\xff\xff\xff\xff\xff\xff\xfa\x11\x22\x33\xbb\xba\xbc\x44\x43\x21\x01\x23\x44\x56\x67\x66\x76\x63\x21\x79\x98\x88\x76\x68\x66\x65\x53\xab\xba\xaa\xb8\x35\x78\x88\x89\x99\x99\x9a\xa7\x88\xef\xff\xff\xff\xff\xff\xff\xff\xfa\x11\x22\x33\x54\x44\x45\x44\x34\x21\x01\x23\x44\x56\x66\x86\x66\x54\x21\x79\x98\x88\x76\x68\x66\x65\x63\x9a\xba\xab\xb9\x35\x68\x88\x99\x99\x9a\x9a\xa8\x77\xcf\xff\xff\xff\xff\xff\xff\xff\xfa\x11\x22\x33\x33\x43\x43\x44\x43\x21\x01\x23\x44\x55\x66\x76\x66\x54\x21\x7a\x98\x88\x76\x68\x66\x65\x63\x8a\xab\xa9\xba\x35\x58\x88\x99\x99\x99\x9a\xa9\x67\xaf\xff\xff\xff\xff\xff\xff\xff\xfa\x11\x12\x23\x69\x2a\xd9\x34\x33\x21\x01\x23\x44\x55\x67\x67\x66\x53\x21\x7a\x98\x88\x76\x67\x66\x66\x54\x7a\xa9\x9a\xbb\x44\x48\x99\x99\x89\x99\xaa\xaa\x68\x8e\xff\xff\xff\xff\xff\xff\xff\xfa\x11\x22\x33\x97\x98\x47\x44\x34\x21\x01\x23\x44\x56\x66\x76\x66\x53\x21\x7a\x98\x88\x76\x67\x66\x66\x55\x6a\xbb\x9a\xbb\x54\x57\x99\x98\x79\x9a\xaa\xaa\x87\x7d\xff\xff\xff\xff\xff\xff\xff\xfa\x11\x22\x33\x53\x44\x36\x54\x44\x21\x00\x23\x44\x55\x66\x66\x66\x53\x21\x7a\x98\x87\x76\x67\x66\x66\x55\x5a\xaa\xaa\xbb\x73\x66\x99\x66\x59\x77\xaa\xaa\x97\x8b\xff\xff\xff\xff\xff\xff\xff\xfa\x11\x22\x33\xcc\xcc\xcd\x44\x33\x21\x01\x23\x34\x55\x66\x66\x66\x53\x21\x7b\x98\x88\x76\x66\x66\x66\x65\x4a\xaa\xab\xab\x83\x66\x88\x55\x48\x84\x5a\xaa\xa6\x89\xff\xff\xff\xff\xff\xff\xff\xfa\x11\x22\x33\x44\x34\x45\x44\x33\x21\x01\x23\x44\x55\x56\x66\x66\x53\x21\x7b\x98\x88\x76\x66\x66\x66\x56\x49\x9a\xaa\x9b\x94\x55\x88\x66\x78\x86\x98\xaa\xa7\x88\xef\xff\xff\xff\xff\xff\xff\xfa\x11\x22\x33\x33\x33\x44\x44\x33\x21\x00\x22\x34\x55\x66\x66\x66\x53\x21\x8b\xa7\x88\x86\x66\x66\x66\x56\x38\xa7\x99\xaa\x95\x56\x79\x99\x97\x76\x96\xaa\xa8\x88\xdf\xff\xff\xff\xff\xff\xff\xfa\x11\x22\x33\x76\x77\x78\x44\x33\x21\x01\x22\x34\x45\x54\x66\x66\x53\x21\x8b\xa7\x78\x86\x56\x66\x66\x55\x48\xaa\xaa\xaa\xa5\x56\x69\x99\x99\x85\x79\x7a\xa9\x68\xbf\xff\xff\xff\xff\xff\xff\xfa\x11\x12\x33\xaa\xaa\xab\x34\x33\x21\x00\x23\x34\x45\x6b\x66\x66\x53\x21\x8b\xa8\x78\x76\x56\x76\x66\x65\x47\xaa\xa9\x9a\xa6\x66\x59\x99\x99\xa6\x6a\x6a\xaa\x68\x9f\xff\xff\xff\xff\xff\xff\xfa\x11\x12\x23\x43\x33\x33\x34\x33\x21\x00\x22\x34\x45\x68\x86\x66\x53\x21\x8c\xa8\x77\x76\x56\x76\x66\x66\x45\xaa\xa9\xab\xb6\x85\x58\x99\x99\x99\x6a\x69\xaa\x67\x7e\xff\xff\xff\xff\xff\xff\xfa\x11\x12\x23\x33\x34\x49\x75\x33\x21\x00\x22\x34\x45\x66\x66\x65\x53\x21\x8c\xa8\x77\x66\x55\x76\x66\x66\x54\xa9\x99\x9b\xa7\x76\x66\x99\x99\xa6\x69\x88\xaa\x87\x6c\xff\xff\xff\xff\xff\xff\xfa\x11\x12\x23\x33\x33\x37\x34\x33\x21\x00\x12\x34\x45\x59\x86\x66\x53\x21\x8c\xb9\x77\x66\x55\x76\x66\x65\x63\x9a\xaa\xba\xb8\x67\x66\x99\x99\x97\x68\x96\xaa\xa6\x7a\xff\xff\xff\xff\xff\xff\xfa\x11\x22\x23\x54\x44\x46\x44\x43\x21\x00\x22\x34\x58\x59\xa5\x65\x53\x21\x8c\xb9\x77\x76\x56\x76\x66\x65\x63\x8a\xaa\xaa\xb9\x58\x65\x89\x99\x99\x86\xa3\xaa\xa6\x88\xef\xff\xff\xff\xff\xff\xfa\x11\x12\x23\xab\xbb\xbc\x33\x33\x21\x00\x12\x34\x46\x66\x66\x55\x43\x21\x8c\xb9\x77\x76\x56\x76\x66\x65\x64\x7a\xaa\x9a\xba\x49\x66\x89\x99\x99\x95\xa4\x5a\xa7\x87\xdf\xff\xff\xff\xff\xff\xfa\x11\x12\x22\x42\x33\x35\x44\x33\x21\x00\x12\x34\x48\x86\x85\x55\x43\x21\x8c\xb9\x67\x77\x56\x67\x66\x66\x65\x6a\xaa\x9a\xba\x4a\x67\x79\x99\x86\x76\x86\x27\xa8\x67\xcf\xff\xff\xff\xff\xff\xfa\x11\x12\x23\x33\x33\x35\x33\x33\x21\x00\x12\x33\x54\x88\x56\x65\x43\x21\x8d\xba\x66\x77\x56\x57\x66\x66\x65\x5a\xab\xaa\xbb\x59\x76\x59\x99\x99\x97\x98\x24\xa9\x67\xaf\xff\xff\xff\xff\xff\xfa\x11\x12\x22\x21\x36\x9b\x33\x33\x21\x00\x12\x34\x45\x56\x76\x55\x43\x21\x8d\xca\x76\x77\x66\x57\x66\x65\x65\x5a\xaa\x9a\xbb\x78\x96\x58\x99\x99\x88\x9a\x44\xa9\x67\x8e\xff\xff\xff\xff\xff\xfa\x11\x12\x23\x6a\xb7\x44\x43\x33\x21\x00\x12\x34\x44\x55\x85\x55\x43\x21\x8d\xca\x77\x67\x66\x67\x66\x66\x66\x49\xaa\xaa\xab\x87\xc5\x57\x98\x98\x87\x68\x85\x99\x76\x6d\xff\xff\xff\xff\xff\xfa\x11\x12\x23\x96\x33\x43\x33\x33\x21\x00\x12\x33\x44\x65\x55\x55\x43\x21\x8d\xca\x87\x76\x66\x67\x66\x66\x56\x59\xa9\x99\xab\x96\xc6\x65\x98\x89\x99\x68\x98\x99\x86\x6b\xff\xff\xff\xff\xff\xfa\x11\x12\x22\x44\x75\x23\x33\x33\x21\x00\x12\x33\x44\x6a\x65\x55\x43\x21\x8d\xca\x87\x76\x66\x67\x66\x66\x66\x58\xa8\x9a\x9b\xa5\xc9\x65\x88\x89\x99\x77\x98\x99\x95\x68\xff\xff\xff\xff\xff\xfa\x11\x12\x22\x22\x24\x79\x33\x33\x21\x00\x12\x34\x44\x56\x75\x55\x43\x21\x8d\xda\x87\x77\x66\x67\x66\x66\x66\x47\xa9\x9a\xbb\xa6\xcb\x65\x78\x88\x88\x85\x68\x99\x96\x66\xef\xff\xff\xff\xff\xfa\x11\x12\x23\x11\x33\x34\x33\x32\x21\x00\x12\x33\x44\x57\x65\x55\x43\x21\x8d\xda\x87\x77\x65\x76\x76\x66\x67\x56\xaa\xbb\xaa\xa6\xbc\x66\x68\x88\x88\x85\x88\x89\x97\x66\xcf\xff\xff\xff\xff\xfa\x11\x12\x23\x95\x23\x36\x33\x33\x21\x00\x12\x33\x44\x58\x65\x55\x43\x21\x8d\xda\x96\x77\x65\x77\x66\x66\x57\x55\xaa\xaa\xaa\xb6\xad\x66\x58\x88\x88\x85\x78\x88\x98\x66\xaf\xff\xff\xff\xff\xfa\x11\x12\x22\x52\x33\x35\x33\x33\x21\x00\x12\x33\x44\x55\x65\x55\x42\x21\x8e\xdb\x97\x67\x75\x77\x66\x66\x56\x64\xaa\xbb\xaa\xa7\x8e\x85\x58\x87\x88\x87\x88\x88\x88\x56\x8f\xff\xff\xff\xff\xfa\x11\x12\x22\x42\x23\x35\x43\x32\x21\x00\x12\x33\x44\x48\x85\x55\x43\x21\x8e\xdb\x97\x67\x75\x77\x66\x66\x66\x73\x9b\xaa\xaa\xb8\x6e\xb4\x57\x87\x87\x77\x78\x88\x88\x56\x7e\xff\xff\xff\xff\xfa\x11\x12\x22\x42\x33\x28\x33\x32\x21\x00\x12\x33\x44\x55\x55\x55\x43\x21\x8e\xdb\x97\x76\x76\x67\x76\x66\x66\x74\x8a\xba\xaa\xb9\x5d\xd5\x55\x77\x77\x77\x47\x88\x88\x65\x5d\xff\xff\xff\xff\xfa\x10\x12\x22\x63\x11\x78\x33\x32\x20\x00\x12\x33\x34\x47\x84\x55\x42\x21\x8e\xec\x97\x77\x66\x67\x87\x76\x66\x74\x7a\xaa\xaa\xba\x4d\xe7\x54\x77\x77\x77\x56\x87\x88\x74\x5a\xff\xff\xff\xff\xfa\x11\x12\x22\x28\x9a\x83\x33\x32\x20\x00\x12\x33\x44\x55\x55\x55\x42\x21\x9e\xec\x97\x77\x66\x67\x76\x66\x66\x76\x6a\xaa\x99\xba\x4b\xfa\x54\x67\x77\x77\x65\x77\x77\x84\x57\xef\xff\xff\xff\xfa\x11\x12\x22\x22\x33\x33\x33\x22\x10\x00\x12\x33\x33\x56\x45\x54\x42\x11\x9e\xec\x97\x77\x75\x67\x76\x66\x65\x67\x5a\xaa\x99\xba\x59\xfc\x54\x57\x77\x76\x65\x77\x77\x75\x55\xdf\xff\xff\xff\xfa\x11\x12\x22\x22\x33\x33\x33\x22\x20\x00\x12\x33\x35\x45\x65\x54\x42\x21\x9e\xec\xa7\x77\x76\x67\x86\x66\x65\x68\x4a\xa9\x8a\xbb\x77\xfd\x65\x56\x76\x76\x64\x67\x77\x76\x55\xbf\xff\xff\xff\xfa\x11\x12\x22\x22\x32\x33\x23\x22\x20\x00\x12\x23\x45\x55\x54\x44\x42\x11\x9e\xec\xa7\x67\x76\x68\x77\x66\x66\x68\x4a\xb9\x88\xaa\x96\xef\x75\x46\x66\x66\x65\x45\x77\x77\x45\x9f\xff\xff\xff\xfa\x10\x12\x22\x22\x22\x33\x32\x22\x20\x00\x12\x23\x33\x44\x54\x54\x42\x21\x8e\xed\xa8\x76\x76\x58\x77\x66\x66\x67\x59\xba\xa8\xa9\xa5\xef\x94\x46\x66\x66\x66\x46\x67\x77\x45\x7e\xff\xff\xff\xfa\x11\x12\x22\x22\x33\x33\x23\x32\x21\x00\x12\x23\x34\x54\x44\x44\x42\x11\x8e\xfd\xb8\x76\x67\x67\x68\x77\x66\x67\x68\xaa\xa9\x7b\xa5\xcf\xc3\x45\x66\x66\x66\x67\x66\x67\x55\x5d\xff\xff\xff\xf9\x00\x00\x11\x11\x11\x11\x11\x11\x10\x00\x12\x23\x34\x34\x44\x44\x42\x11\x8e\xfd\xb8\x77\x66\x67\x77\x66\x66\x67\x77\xaa\xa8\xa9\xb6\xbf\xe5\x44\x66\x66\x66\x66\x66\x67\x54\x4b\xff\xff\xff\xf9\x00\x01\x11\x11\x11\x11\x11\x11\x10\x00\x12\x23\x33\x44\x44\x44\x32\x11\x8e\xfd\xb8\x77\x76\x67\x77\x66\x66\x57\x76\xba\xa8\x7a\xb6\xaf\xf8\x43\x66\x66\x66\x66\x66\x66\x63\x48\xff\xff\xff\xfa\x10\x11\x22\x22\x22\x22\x32\x22\x10\x00\x12\x23\x34\x44\x44\x44\x32\x11\x8e\xfd\xb9\x77\x76\x57\x87\x66\x66\x66\x85\xbb\xa8\x8b\xa7\x9f\xfb\x43\x56\x66\x66\x66\x66\x66\x64\x45\xef\xff\xff\xfa\x10\x12\x22\x22\x22\x32\x22\x22\x10\x00\x12\x23\x33\x44\x44\x44\x32\x11\x8e\xfe\xb9\x67\x77\x57\x87\x76\x66\x66\x84\xab\x89\xaa\xb8\x8e\xfd\x54\x46\x66\x55\x55\x55\x66\x65\x43\xdf\xff\xff\xfa\x10\x12\x22\x22\x22\x32\x22\x22\x20\x00\x12\x23\x33\x44\x44\x44\x32\x11\x8e\xfe\xca\x66\x77\x57\x87\x77\x66\x67\x84\xab\x9a\xa9\xb9\x6e\xfe\x64\x35\x66\x55\x66\x65\x56\x66\x34\x9f\xff\xff\xfa\x10\x11\x12\x22\x22\x33\x22\x22\x10\x00\x11\x23\x34\x44\x55\x44\x32\x11\x8e\xfe\xca\x76\x77\x66\x87\x76\x66\x57\x84\x9b\x9a\x9a\xaa\x5d\xff\x84\x35\x66\x56\x66\x55\x45\x66\x44\x7f\xff\xff\xfa\x00\x11\x22\x22\x22\x22\x32\x22\x10\x00\x12\x34\x67\x78\x88\x76\x53\x11\x8e\xfe\xca\x76\x67\x66\x88\x66\x66\x56\x85\x7b\xaa\xba\xaa\x4c\xff\xb4\x45\x66\x55\x55\x45\x45\x66\x44\x5d\xff\xff\xfa\x10\x11\x22\x22\x33\x33\x23\x32\x10\x00\x12\x23\x34\x44\x44\x44\x42\x11\x8e\xfe\xca\x77\x66\x66\x88\x66\x66\x66\x87\x6b\xba\xab\xbb\x4b\xff\xd4\x44\x65\x54\x44\x44\x45\x55\x43\x4c\xff\xff\xfa\x00\x11\x22\x22\x22\x22\x22\x22\x10\x00\x11\x22\x33\x34\x44\x44\x32\x11\x8d\xed\xb9\x87\x76\x66\x78\x76\x66\x66\x78\x4b\xba\x98\x65\x18\xff\xe6\x33\x55\x54\x44\x55\x55\x54\x42\x5d\xff\xff\xea\x00\x11\x11\x22\x22\x22\x22\x21\x10\x00\x11\x12\x22\x22\x33\x32\x21\x10\x6a\xba\x98\x87\x77\x66\x66\x77\x76\x66\x77\x23\x11\x11\x11\x05\xcd\xd8\x33\x55\x55\x55\x54\x44\x34\x7b\xdf\xee\xee\xd8\x00\x01\x11\x11\x11\x11\x11\x11\x00\x00\x00\x11\x11\x12\x22\x12\x11\x00\x59\x99\x87\x87\x77\x66\x66\x76\x76\x66\x77\x10\x11\x11\x11\x02\xaa\xa9\x33\x45\x55\x44\x44\x58\xbd\xee\xff\xdd\xcc\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x11\x11\x11\x10\x00\x69\x99\x87\x97\x77\x76\x76\x76\x65\x55\x67\x20\x10\x00\x00\x05\x99\x98\x43\x24\x33\x46\x89\xbc\xcd\xde\xee\xdc\xcc\xba\x42\x22\x22\x22\x22\x22\x22\x22\x22\x42\x11\x22\x22\x22\x22\x22\x21\x24\x79\x99\x88\x97\x66\x77\x77\x86\x67\x77\x88\x50\x00\x23\x46\x88\x99\x99\x63\x25\x78\x9a\xab\xbc\xcd\xde\xee\xee\xed\xdc\xbb\xaa\xa9\x99\x99\x99\x88\x88\x88\x88\x77\x77\x77\x77\x77\x77\x77\x78\x99\x99\x98\x88\x88\x88\x88\x88\x88\x88\x99\x87\x78\x99\x99\xaa\xaa\xaa\xa9\xaa\xbb\xcc\xcd\xdd\xee\xef\xff\xff\xff\xff\xee\xee\xed\xdd\xdd\xdc\xcc\xcc\xcc\xbb\xbb\xbb\xbb\xbb\xbb\xba\xaa\xaa\xab\xba\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xbb\xbb\xbb\xbb\xcc\xcc\xcc\xcd\xdd\xdd\xde\xee\xee\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xee\xed\xdd\xdd\xdd\xee\xee\xee\xee\xee\xee\xee\xee\xee\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' # noqa + def reduce_color(c): return max(0, min(255, c))//16 + def write_t4b(t4bfile, coverdata=None): ''' t4bfile is a file handle ready to write binary data to disk. diff --git a/src/calibre/devices/eb600/driver.py b/src/calibre/devices/eb600/driver.py index 3b34e6c8af..7652cafebd 100644 --- a/src/calibre/devices/eb600/driver.py +++ b/src/calibre/devices/eb600/driver.py @@ -18,6 +18,7 @@ import re from calibre.devices.usbms.driver import USBMS + class EB600(USBMS): name = 'Netronix EB600 Device Interface' @@ -49,6 +50,7 @@ class EB600(USBMS): EBOOK_DIR_CARD_A = '' SUPPORTS_SUB_DIRS = True + class TOLINO(EB600): name = 'Tolino Shine Device Interface' @@ -120,6 +122,7 @@ class TOLINO(EB600): return getattr(self, 'ebook_dir_for_upload', self.EBOOK_DIR_MAIN) return self.EBOOK_DIR_MAIN + class COOL_ER(EB600): name = 'Cool-er device interface' @@ -134,6 +137,7 @@ class COOL_ER(EB600): EBOOK_DIR_MAIN = 'my docs' + class SHINEBOOK(EB600): name = 'ShineBook device Interface' @@ -177,6 +181,7 @@ class POCKETBOOK360(EB600): def can_handle(cls, dev, debug=False): return dev[-1] == '1.00' and not dev[-2] and not dev[-3] + class POCKETBOOKHD(EB600): name = 'Pocket Touch HD Device Interface' @@ -185,6 +190,7 @@ class POCKETBOOKHD(EB600): BCD = [0x9999] FORMATS = ['epub', 'fb2', 'prc', 'mobi', 'docx', 'doc', 'pdf', 'djvu', 'rtf', 'chm', 'txt'] + class GER2(EB600): name = 'Ganaxa GeR2 Device Interface' @@ -200,6 +206,7 @@ class GER2(EB600): WINDOWS_MAIN_MEN = 'GER2_________-FD' WINDOWS_CARD_A_MEM = 'GER2_________-SD' + class ITALICA(EB600): name = 'Italica Device Interface' @@ -233,6 +240,7 @@ class ECLICTO(EB600): EBOOK_DIR_MAIN = 'Text' EBOOK_DIR_CARD_A = '' + class DBOOK(EB600): name = 'Airis Dbook Device Interface' @@ -244,6 +252,7 @@ class DBOOK(EB600): WINDOWS_MAIN_MEM = 'AIRIS_DBOOK' WINDOWS_CARD_A_MEM = 'AIRIS_DBOOK' + class INVESBOOK(EB600): name = 'Inves Book Device Interface' @@ -256,6 +265,7 @@ class INVESBOOK(EB600): WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['00INVES_E600', 'INVES-WIBOOK', 'OK_POCKET_611_61'] + class BOOQ(EB600): name = 'Booq Device Interface' gui_name = 'bq Reader' @@ -265,6 +275,7 @@ class BOOQ(EB600): VENDOR_NAME = ['NETRONIX', '36LBOOKS'] WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['EB600', 'ELEQTOR'] + class MENTOR(EB600): name = 'Astak Mentor EB600' @@ -274,6 +285,7 @@ class MENTOR(EB600): WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'MENTOR' + class ELONEX(EB600): name = 'Elonex 600EB' @@ -289,6 +301,7 @@ class ELONEX(EB600): def can_handle(cls, dev, debug=False): return dev[3] == 'Elonex' and dev[4] == 'eBook' + class POCKETBOOK301(USBMS): name = 'PocketBook 301 Device Interface' @@ -306,6 +319,7 @@ class POCKETBOOK301(USBMS): PRODUCT_ID = [0x301] BCD = [0x132] + class POCKETBOOK602(USBMS): name = 'PocketBook Pro 602/902 Device Interface' @@ -327,6 +341,7 @@ class POCKETBOOK602(USBMS): WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['PB602', 'PB603', 'PB902', 'PB903', 'Pocket912', 'PB', 'FILE-STOR_GADGET'] + class POCKETBOOK622(POCKETBOOK602): name = 'PocketBook 622 Device Interface' @@ -340,6 +355,7 @@ class POCKETBOOK622(POCKETBOOK602): VENDOR_NAME = 'LINUX' WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'FILE-STOR_GADGET' + class POCKETBOOK360P(POCKETBOOK602): name = 'PocketBook 360+ Device Interface' @@ -350,6 +366,7 @@ class POCKETBOOK360P(POCKETBOOK602): VENDOR_NAME = '__POCKET' WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'BOOK_USB_STORAGE' + class POCKETBOOK701(USBMS): name = 'PocketBook 701 Device Interface' @@ -380,6 +397,7 @@ class POCKETBOOK701(USBMS): drives['carda'] = main return drives + class PI2(EB600): name = 'Infibeam Pi2 Device Interface' diff --git a/src/calibre/devices/edge/driver.py b/src/calibre/devices/edge/driver.py index 9491b9bc68..5fa54bf27c 100644 --- a/src/calibre/devices/edge/driver.py +++ b/src/calibre/devices/edge/driver.py @@ -11,6 +11,7 @@ Device driver for Barns and Nobel's Nook from calibre.devices.usbms.driver import USBMS + class EDGE(USBMS): name = 'Edge Device Interface' diff --git a/src/calibre/devices/errors.py b/src/calibre/devices/errors.py index 13fd9d603f..eb8c533fc8 100644 --- a/src/calibre/devices/errors.py +++ b/src/calibre/devices/errors.py @@ -6,24 +6,31 @@ Defines the errors that the device drivers generate. G{classtree ProtocolError} """ + class ProtocolError(Exception): """ The base class for all exceptions in this package """ + def __init__(self, msg): Exception.__init__(self, msg) + class TimeoutError(ProtocolError): """ There was a timeout during communication """ + def __init__(self, func_name): ProtocolError.__init__(self, "There was a timeout while communicating with the device in function: " +func_name) + class DeviceError(ProtocolError): """ Raised when device is not found """ + def __init__(self, msg=None): if msg is None: msg = "Unable to find SONY Reader. Is it connected?" ProtocolError.__init__(self, msg) + class UserFeedback(DeviceError): INFO = 0 WARN = WARNING = 1 @@ -35,6 +42,7 @@ class UserFeedback(DeviceError): self.details = details self.msg = msg + class OpenFeedback(DeviceError): def __init__(self, msg): @@ -48,10 +56,12 @@ class OpenFeedback(DeviceError): ''' raise NotImplementedError + class InitialConnectionError(OpenFeedback): """ Errors detected during connection after detection but before open, for e.g. in the is_connected() method. """ + class OpenFailed(ProtocolError): """ Raised when device cannot be opened this time. No retry is to be done. The device should continue to be polled for future opens. If the @@ -61,34 +71,45 @@ class OpenFailed(ProtocolError): ProtocolError.__init__(self, msg) self.show_me = bool(msg and msg.strip()) + class DeviceBusy(ProtocolError): """ Raised when device is busy """ + def __init__(self, uerr=""): ProtocolError.__init__(self, "Device is in use by another application:" "\nUnderlying error:" + str(uerr)) + class DeviceLocked(ProtocolError): """ Raised when device has been locked """ + def __init__(self): ProtocolError.__init__(self, "Device is locked") + class PacketError(ProtocolError): """ Errors with creating/interpreting packets """ + class FreeSpaceError(ProtocolError): """ Errors caused when trying to put files onto an overcrowded device """ + class ArgumentError(ProtocolError): """ Errors caused by invalid arguments to a public interface function """ + class PathError(ArgumentError): """ When a user supplies an incorrect/invalid path """ + def __init__(self, msg, path=None): ArgumentError.__init__(self, msg) self.path = path + class ControlError(ProtocolError): """ Errors in Command/Response pairs while communicating with the device """ + def __init__(self, query=None, response=None, desc=None): self.query = query self.response = response @@ -105,11 +126,13 @@ class ControlError(ProtocolError): return self.desc return "Unknown control error occurred" + class WrongDestinationError(PathError): ''' The user chose the wrong destination to send books to, for example by trying to send books to a non existant storage card.''' pass + class BlacklistedDevice(OpenFailed): ''' Raise this error during open() when the device being opened has been blacklisted by the user. Only used in drivers that manage device presence, diff --git a/src/calibre/devices/eslick/driver.py b/src/calibre/devices/eslick/driver.py index 6ae0afcab8..f2ac77e0cd 100644 --- a/src/calibre/devices/eslick/driver.py +++ b/src/calibre/devices/eslick/driver.py @@ -9,6 +9,7 @@ __docformat__ = 'restructuredtext en' from calibre.devices.usbms.driver import USBMS + class ESLICK(USBMS): name = 'ESlick Device Interface' diff --git a/src/calibre/devices/folder_device/driver.py b/src/calibre/devices/folder_device/driver.py index c3c417155c..d97d18bacb 100644 --- a/src/calibre/devices/folder_device/driver.py +++ b/src/calibre/devices/folder_device/driver.py @@ -11,6 +11,8 @@ from calibre.ebooks import BOOK_EXTENSIONS # This class is added to the standard device plugin chain, so that it can # be configured. It has invalid vendor_id etc, so it will never match a # device. The 'real' FOLDER_DEVICE will use the config from it. + + class FOLDER_DEVICE_FOR_CONFIG(USBMS): name = 'Folder Device Interface' gui_name = 'Folder Device' @@ -25,6 +27,7 @@ class FOLDER_DEVICE_FOR_CONFIG(USBMS): DEVICE_PLUGBOARD_NAME = 'FOLDER_DEVICE' SUPPORTS_SUB_DIRS = True + class FOLDER_DEVICE(USBMS): type = _('Device Interface') diff --git a/src/calibre/devices/hanlin/driver.py b/src/calibre/devices/hanlin/driver.py index 18b2682deb..e270c4b2f0 100644 --- a/src/calibre/devices/hanlin/driver.py +++ b/src/calibre/devices/hanlin/driver.py @@ -12,6 +12,7 @@ import re from calibre.devices.usbms.driver import USBMS + class HANLINV3(USBMS): name = 'Hanlin V3 driver' @@ -81,6 +82,7 @@ class HANLINV3(USBMS): drives['carda'] = main return drives + class SPECTRA(HANLINV3): name = 'Spectra' @@ -91,6 +93,7 @@ class SPECTRA(HANLINV3): SUPPORTS_SUB_DIRS = True + class HANLINV5(HANLINV3): name = 'Hanlin V5 driver' gui_name = 'Hanlin V5' @@ -108,6 +111,7 @@ class HANLINV5(HANLINV3): OSX_EJECT_COMMAND = ['diskutil', 'unmount', 'force'] + class BOOX(HANLINV3): name = 'BOOX driver' diff --git a/src/calibre/devices/hanvon/driver.py b/src/calibre/devices/hanvon/driver.py index bd5c54db36..90258862cc 100644 --- a/src/calibre/devices/hanvon/driver.py +++ b/src/calibre/devices/hanvon/driver.py @@ -12,10 +12,12 @@ import re, os from calibre import fsync from calibre.devices.usbms.driver import USBMS + def is_alex(device_info): return device_info[3] == u'Linux 2.6.28 with pxa3xx_u2d' and \ device_info[4] == u'Seleucia Disk' + class N516(USBMS): name = 'N516 driver' @@ -42,6 +44,7 @@ class N516(USBMS): def can_handle(self, device_info, debug=False): return not is_alex(device_info) + class KIBANO(N516): name = 'Kibano driver' @@ -56,6 +59,7 @@ class KIBANO(N516): WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['INTERNAL_SD_CARD', 'EXTERNAL_SD_CARD'] + class THEBOOK(N516): name = 'The Book driver' gui_name = 'The Book' @@ -68,6 +72,7 @@ class THEBOOK(N516): WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['_FILE-STOR_GADGE', 'FILE-STOR_GADGET'] + class LIBREAIR(N516): name = 'Libre Air Driver' gui_name = 'Libre Air' @@ -80,6 +85,7 @@ class LIBREAIR(N516): WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'FILE-STOR_GADGET' EBOOK_DIR_MAIN = 'Books' + class ALEX(N516): name = 'Alex driver' @@ -141,6 +147,7 @@ class ALEX(N516): pass self.report_progress(1.0, _('Removing books from device...')) + class AZBOOKA(ALEX): name = 'Azbooka driver' @@ -161,6 +168,7 @@ class AZBOOKA(ALEX): def upload_cover(self, path, filename, metadata, filepath): pass + class EB511(USBMS): name = 'Elonex EB 511 driver' gui_name = 'EB 511' @@ -181,6 +189,7 @@ class EB511(USBMS): OSX_MAIN_MEM_VOL_PAT = re.compile(r'/eReader') + class ODYSSEY(N516): name = 'Cybook Odyssey driver' gui_name = 'Odyssey' diff --git a/src/calibre/devices/iliad/driver.py b/src/calibre/devices/iliad/driver.py index 429e1d702a..24d8610dce 100644 --- a/src/calibre/devices/iliad/driver.py +++ b/src/calibre/devices/iliad/driver.py @@ -10,6 +10,7 @@ Device driver for IRex Iliad from calibre.devices.usbms.driver import USBMS + class ILIAD(USBMS): name = 'IRex Iliad Device Interface' diff --git a/src/calibre/devices/interface.py b/src/calibre/devices/interface.py index 1336e3c782..3a2db2ec30 100644 --- a/src/calibre/devices/interface.py +++ b/src/calibre/devices/interface.py @@ -7,6 +7,7 @@ from calibre import prints from calibre.constants import iswindows from calibre.customize import Plugin + class DevicePlugin(Plugin): """ Defines the interface that should be implemented by backends that @@ -712,6 +713,7 @@ class DevicePlugin(Plugin): ''' return (None, (None, False)) + class BookList(list): ''' A list of books. Each Book object must have the fields @@ -766,6 +768,7 @@ class BookList(list): ''' raise NotImplementedError() + class CurrentlyConnectedDevice(object): def __init__(self): diff --git a/src/calibre/devices/irexdr/driver.py b/src/calibre/devices/irexdr/driver.py index 22f1c513ec..c75e56f164 100644 --- a/src/calibre/devices/irexdr/driver.py +++ b/src/calibre/devices/irexdr/driver.py @@ -10,6 +10,7 @@ Device driver for IRex Digiatal Reader from calibre.devices.usbms.driver import USBMS + class IREXDR1000(USBMS): name = 'IRex Digital Reader 1000 Device Interface' @@ -37,6 +38,7 @@ class IREXDR1000(USBMS): DELETE_EXTS = ['.mbp'] SUPPORTS_SUB_DIRS = True + class IREXDR800(IREXDR1000): name = 'IRex Digital Reader 800 Device Interface' description = _('Communicate with the IRex Digital Reader 800') diff --git a/src/calibre/devices/iriver/driver.py b/src/calibre/devices/iriver/driver.py index df64c4f118..fe55982765 100644 --- a/src/calibre/devices/iriver/driver.py +++ b/src/calibre/devices/iriver/driver.py @@ -10,6 +10,7 @@ import re from calibre.devices.usbms.driver import USBMS + class IRIVER_STORY(USBMS): name = 'Iriver Story Device Interface' diff --git a/src/calibre/devices/jetbook/driver.py b/src/calibre/devices/jetbook/driver.py index 92da9311d9..ebae2e56a4 100644 --- a/src/calibre/devices/jetbook/driver.py +++ b/src/calibre/devices/jetbook/driver.py @@ -15,6 +15,7 @@ import sys from calibre.devices.usbms.driver import USBMS from calibre.ebooks.metadata import string_to_authors + class JETBOOK(USBMS): name = 'Ectaco JetBook Device Interface' description = _('Communicate with the JetBook eBook reader.') @@ -81,6 +82,7 @@ class JETBOOK(USBMS): return mi + class MIBUK(USBMS): name = 'MiBuk Wolder Device Interface' @@ -98,6 +100,7 @@ class MIBUK(USBMS): VENDOR_NAME = ['LINUX', 'FILE_BAC'] WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['WOLDERMIBUK', 'KED_STORAGE_GADG'] + class JETBOOK_MINI(USBMS): ''' @@ -124,6 +127,7 @@ class JETBOOK_MINI(USBMS): SUPPORTS_SUB_DIRS = True + class JETBOOK_COLOR(USBMS): ''' diff --git a/src/calibre/devices/kindle/apnx.py b/src/calibre/devices/kindle/apnx.py index a34eef1838..d66d5feea8 100644 --- a/src/calibre/devices/kindle/apnx.py +++ b/src/calibre/devices/kindle/apnx.py @@ -18,6 +18,7 @@ from calibre.utils.logging import default_log from calibre import prints, fsync from calibre.constants import DEBUG + class APNXBuilder(object): ''' Create an APNX file using a pseudo page mapping. diff --git a/src/calibre/devices/kindle/bookmark.py b/src/calibre/devices/kindle/bookmark.py index 09351918a3..b122bb7691 100644 --- a/src/calibre/devices/kindle/bookmark.py +++ b/src/calibre/devices/kindle/bookmark.py @@ -7,11 +7,13 @@ import os from cStringIO import StringIO from struct import unpack + class Bookmark(): # {{{ ''' A simple class fetching bookmark data Kindle-specific ''' + def __init__(self, path, id, book_format, bookmark_extension): self.book_format = book_format self.bookmark_extension = bookmark_extension diff --git a/src/calibre/devices/kindle/driver.py b/src/calibre/devices/kindle/driver.py index 0811ddb8f4..7f475d5b04 100644 --- a/src/calibre/devices/kindle/driver.py +++ b/src/calibre/devices/kindle/driver.py @@ -37,9 +37,11 @@ Adding a book to a collection on the Kindle does not change the book file at all file metadata. ''' + def get_kfx_path(path): return os.path.dirname(os.path.dirname(path)).rpartition('.')[0] + '.kfx' + class KINDLE(USBMS): name = 'Kindle Device Interface' @@ -327,6 +329,7 @@ class KINDLE(USBMS): mi.comments = last_update db.add_books([bm.value['path']], ['txt'], [mi]) + class KINDLE2(KINDLE): name = 'Kindle 2/3/4/Touch/PaperWhite/Voyage Device Interface' @@ -554,6 +557,7 @@ class KINDLE_DX(KINDLE2): def upload_kindle_thumbnail(self, metadata, filepath): pass + class KINDLE_FIRE(KINDLE2): name = 'Kindle Fire Device Interface' diff --git a/src/calibre/devices/kobo/books.py b/src/calibre/devices/kobo/books.py index 777c8e7a98..234e1de10d 100644 --- a/src/calibre/devices/kobo/books.py +++ b/src/calibre/devices/kobo/books.py @@ -14,6 +14,7 @@ from calibre.utils.config_base import prefs from calibre.devices.usbms.driver import debug_print from calibre.ebooks.metadata import author_to_author_sort + class Book(Book_): def __init__(self, prefix, lpath, title=None, authors=None, mime=None, date=None, ContentType=None, diff --git a/src/calibre/devices/kobo/driver.py b/src/calibre/devices/kobo/driver.py index e3c8625140..2ba1a0d68b 100644 --- a/src/calibre/devices/kobo/driver.py +++ b/src/calibre/devices/kobo/driver.py @@ -32,6 +32,8 @@ EPUB_EXT = '.epub' KEPUB_EXT = '.kepub' # Implementation of QtQHash for strings. This doesn't seem to be in the Python implementation. + + def qhash(inputstr): instr = b"" if isinstance(inputstr, bytes): @@ -900,9 +902,11 @@ class KOBO(USBMS): def collections_columns(self): opts = self.settings() return opts.extra_customization[self.OPT_COLLECTIONS] + @property def read_metadata(self): return self.settings().read_metadata + @property def show_previews(self): opts = self.settings() @@ -2879,22 +2883,31 @@ class KOBOTOUCH(KOBO): def isAura(self): return self.detected_device.idProduct in self.AURA_PRODUCT_ID + def isAuraEdition2(self): return self.detected_device.idProduct in self.AURA_EDITION2_PRODUCT_ID + def isAuraHD(self): return self.detected_device.idProduct in self.AURA_HD_PRODUCT_ID + def isAuraH2O(self): return self.detected_device.idProduct in self.AURA_H2O_PRODUCT_ID + def isAuraOne(self): return self.detected_device.idProduct in self.AURA_ONE_PRODUCT_ID + def isGlo(self): return self.detected_device.idProduct in self.GLO_PRODUCT_ID + def isGloHD(self): return self.detected_device.idProduct in self.GLO_HD_PRODUCT_ID + def isMini(self): return self.detected_device.idProduct in self.MINI_PRODUCT_ID + def isTouch(self): return self.detected_device.idProduct in self.TOUCH_PRODUCT_ID + def isTouch2(self): return self.detected_device.idProduct in self.TOUCH2_PRODUCT_ID @@ -2932,15 +2945,19 @@ class KOBOTOUCH(KOBO): @property def manage_collections(self): return self.get_pref('manage_collections') and self.supports_bookshelves + @property def create_collections(self): return self.manage_collections and self.get_pref('create_collections') and len(self.collections_columns) > 0 + @property def collections_columns(self): return self.get_pref('collections_columns') + @property def delete_empty_collections(self): return self.manage_collections and self.get_pref('delete_empty_collections') + @property def ignore_collections_names(self): # Cache the collection from the options string. @@ -2948,10 +2965,12 @@ class KOBOTOUCH(KOBO): icn = self.get_pref('ignore_collections_names') self.opts._ignore_collections_names = [x.lower().strip() for x in icn.split(',')] if icn else [] return self.opts._ignore_collections_names + @property def create_bookshelves(self): # Only for backwards compatabilty return self.manage_collections + @property def delete_empty_shelves(self): # Only for backwards compatabilty @@ -2960,9 +2979,11 @@ class KOBOTOUCH(KOBO): @property def upload_covers(self): return self.get_pref('upload_covers') + @property def keep_cover_aspect(self): return self.upload_covers and self.get_pref('keep_cover_aspect') + @property def upload_grayscale(self): return self.upload_covers and self.get_pref('upload_grayscale') @@ -2976,6 +2997,7 @@ class KOBOTOUCH(KOBO): @property def update_device_metadata(self): return self.get_pref('update_device_metadata') + @property def update_series_details(self): return self.update_device_metadata and self.get_pref('update_series') and self.supports_series() @@ -2990,12 +3012,15 @@ class KOBOTOUCH(KOBO): @property def supports_bookshelves(self): return self.dbversion >= self.min_supported_dbversion + @property def show_archived_books(self): return self.get_pref('show_archived_books') + @property def show_previews(self): return self.get_pref('show_previews') + @property def show_recommendations(self): return self.get_pref('show_recommendations') diff --git a/src/calibre/devices/kobo/kobotouch_config.py b/src/calibre/devices/kobo/kobotouch_config.py index 47f6ce358e..b7ca1bf09e 100644 --- a/src/calibre/devices/kobo/kobotouch_config.py +++ b/src/calibre/devices/kobo/kobotouch_config.py @@ -15,12 +15,15 @@ from PyQt5.Qt import (QLabel, QGridLayout, QLineEdit, QVBoxLayout, from calibre.gui2.device_drivers.tabbed_device_config import TabbedDeviceConfig, DeviceConfigTab, DeviceOptionsGroupBox from calibre.devices.usbms.driver import debug_print + def wrap_msg(msg): return textwrap.fill(msg.strip(), 100) + def setToolTipFor(widget, tt): widget.setToolTip(wrap_msg(tt)) + def create_checkbox(title, tt, state): cb = QCheckBox(title) cb.setToolTip(wrap_msg(tt)) @@ -140,6 +143,7 @@ class Tab1Config(DeviceConfigTab): # {{{ self.addDeviceWidget(self.book_uploads_options) # }}} + class Tab2Config(DeviceConfigTab): # {{{ def __init__(self, parent, device): diff --git a/src/calibre/devices/manager.py b/src/calibre/devices/manager.py index b00e944d05..ccb61d7d1e 100644 --- a/src/calibre/devices/manager.py +++ b/src/calibre/devices/manager.py @@ -18,6 +18,7 @@ class DeviceManager(object): class Job(object): count = 0 + def __init__(self, func, args): self.completed = False self.exception = None diff --git a/src/calibre/devices/mime.py b/src/calibre/devices/mime.py index e59a194366..80ee3391c2 100644 --- a/src/calibre/devices/mime.py +++ b/src/calibre/devices/mime.py @@ -5,17 +5,20 @@ __docformat__ = 'restructuredtext en' from calibre import guess_type + def _mt(path): mt = guess_type(path)[0] if not mt: mt = 'application/octet-stream' return mt + def mime_type_ext(ext): if not ext.startswith('.'): ext = '.'+ext return _mt('a'+ext) + def mime_type_path(path): return _mt(path) diff --git a/src/calibre/devices/misc.py b/src/calibre/devices/misc.py index 20eb8d3caf..c36677ddd1 100644 --- a/src/calibre/devices/misc.py +++ b/src/calibre/devices/misc.py @@ -11,6 +11,7 @@ import os from calibre.devices.usbms.driver import USBMS from calibre import fsync + class PALMPRE(USBMS): name = 'Palm Pre Device Interface' @@ -52,6 +53,7 @@ class AVANT(USBMS): EBOOK_DIR_MAIN = '' SUPPORTS_SUB_DIRS = True + class SWEEX(USBMS): # Identical to the Promedia name = 'Sweex Device Interface' @@ -101,6 +103,7 @@ class PDNOVEL(USBMS): coverfile.write(coverdata[2]) fsync(coverfile) + class PDNOVEL_KOBO(PDNOVEL): name = 'Pandigital Kobo device interface' gui_name = 'PD Novel (Kobo)' @@ -139,6 +142,7 @@ class VELOCITYMICRO(USBMS): EBOOK_DIR_MAIN = 'eBooks' SUPPORTS_SUB_DIRS = False + class GEMEI(USBMS): name = 'Gemei Device Interface' gui_name = 'GM2000' @@ -159,6 +163,7 @@ class GEMEI(USBMS): EBOOK_DIR_MAIN = 'eBooks' SUPPORTS_SUB_DIRS = True + class LUMIREAD(USBMS): name = 'Acer Lumiread Device Interface' gui_name = 'Lumiread' @@ -193,6 +198,7 @@ class LUMIREAD(USBMS): f.write(metadata.thumbnail[-1]) fsync(f) + class ALURATEK_COLOR(USBMS): name = 'Aluratek Color Device Interface' @@ -215,6 +221,7 @@ class ALURATEK_COLOR(USBMS): SCAN_FROM_ROOT = True SUPPORTS_SUB_DIRS_FOR_SCAN = True + class TREKSTOR(USBMS): name = 'Trekstor E-book player device interface' @@ -246,6 +253,7 @@ class TREKSTOR(USBMS): SUPPORTS_SUB_DIRS = True SUPPORTS_SUB_DIRS_DEFAULT = False + class EEEREADER(USBMS): name = 'Asus EEE Reader device interface' @@ -266,6 +274,7 @@ class EEEREADER(USBMS): VENDOR_NAME = ['LINUX', 'ASUS'] WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['FILE-STOR_GADGET', 'EEE_NOTE'] + class ADAM(USBMS): name = 'Notion Ink Adam device interface' @@ -288,6 +297,7 @@ class ADAM(USBMS): WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['ADAM'] SUPPORTS_SUB_DIRS = True + class NEXTBOOK(USBMS): name = 'Nextbook device interface' @@ -341,6 +351,7 @@ class NEXTBOOK(USBMS): fsync(f) ''' + class MOOVYBOOK(USBMS): name = 'Moovybook device interface' @@ -363,6 +374,7 @@ class MOOVYBOOK(USBMS): def get_main_ebook_dir(self, for_upload=False): return 'Books' if for_upload else self.EBOOK_DIR_MAIN + class COBY(USBMS): name = 'COBY MP977 device interface' @@ -389,6 +401,7 @@ class COBY(USBMS): return 'eBooks' return self.EBOOK_DIR_CARD_A + class EX124G(USBMS): name = 'Motorola Ex124G device interface' @@ -416,6 +429,7 @@ class EX124G(USBMS): return 'eBooks' return self.EBOOK_DIR_CARD_A + class WAYTEQ(USBMS): name = 'WayteQ device interface' @@ -506,6 +520,7 @@ class WOXTER(USBMS): VENDOR_NAME = ['ROCKCHIP', 'TEXET'] WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['EREADER', 'TB-146SE'] + class POCKETBOOK626(USBMS): name = 'PocketBook Touch Lux 2' @@ -526,6 +541,7 @@ class POCKETBOOK626(USBMS): VENDOR_NAME = ['USB_2.0'] WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['USB_FLASH_DRIVER'] + class SONYDPTS1(USBMS): name = 'SONY DPT-S1' @@ -547,6 +563,7 @@ class SONYDPTS1(USBMS): WINDOWS_MAIN_MEM = ['DPT-S1'] WINDOWS_CARD_A_MEM = ['DPT-S1__SD'] + class CERVANTES(USBMS): name = 'Bq Cervantes Device Interface' diff --git a/src/calibre/devices/mtp/base.py b/src/calibre/devices/mtp/base.py index 1abd4cfbc7..c60a36f199 100644 --- a/src/calibre/devices/mtp/base.py +++ b/src/calibre/devices/mtp/base.py @@ -13,10 +13,12 @@ from calibre import prints from calibre.constants import DEBUG from calibre.devices.interface import DevicePlugin + def debug(*args, **kwargs): if DEBUG: prints('MTP:', *args, **kwargs) + def synchronous(func): @wraps(func) def synchronizer(self, *args, **kwargs): @@ -24,6 +26,7 @@ def synchronous(func): return func(self, *args, **kwargs) return synchronizer + class MTPDeviceBase(DevicePlugin): name = 'MTP Device Interface' gui_name = _('MTP Device') diff --git a/src/calibre/devices/mtp/books.py b/src/calibre/devices/mtp/books.py index 0682c9ab5f..20e7b6e096 100644 --- a/src/calibre/devices/mtp/books.py +++ b/src/calibre/devices/mtp/books.py @@ -15,6 +15,7 @@ from calibre.ebooks.metadata.book.base import Metadata from calibre.ebooks.metadata.book.json_codec import JsonCodec from calibre.utils.date import utcnow + class BookList(BL): def __init__(self, storage_id): @@ -39,6 +40,7 @@ class BookList(BL): def remove_book(self, book): self.remove(book) + class Book(Metadata): def __init__(self, storage_id, lpath, other=None): @@ -70,6 +72,7 @@ class Book(Metadata): ans = '' return ans or title_sort(self.title or '') + class JSONCodec(JsonCodec): pass diff --git a/src/calibre/devices/mtp/defaults.py b/src/calibre/devices/mtp/defaults.py index a4143d0347..e01c4be8a4 100644 --- a/src/calibre/devices/mtp/defaults.py +++ b/src/calibre/devices/mtp/defaults.py @@ -11,6 +11,7 @@ import traceback, re from calibre.constants import iswindows + class DeviceDefaults(object): def __init__(self): diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py index b3b424e88a..3730ccf314 100644 --- a/src/calibre/devices/mtp/driver.py +++ b/src/calibre/devices/mtp/driver.py @@ -22,12 +22,14 @@ from calibre.utils.filenames import shorten_components_to BASE = importlib.import_module('calibre.devices.mtp.%s.driver'%( 'windows' if iswindows else 'unix')).MTP_DEVICE + class MTPInvalidSendPathError(PathError): def __init__(self, folder): PathError.__init__(self, 'Trying to send to ignored folder: %s'%folder) self.folder = folder + class MTP_DEVICE(BASE): METADATA_CACHE = 'metadata.calibre' @@ -493,6 +495,7 @@ class MTP_DEVICE(BASE): def remove_books_from_metadata(self, paths, booklists): self.report_progress(0, _('Removing books from metadata')) + class NextPath(Exception): pass @@ -531,6 +534,7 @@ class MTP_DEVICE(BASE): def settings(self): class Opts(object): + def __init__(s): s.format_map = self.get_pref('format_map') return Opts() diff --git a/src/calibre/devices/mtp/filesystem_cache.py b/src/calibre/devices/mtp/filesystem_cache.py index 352da503f8..ffc52806d6 100644 --- a/src/calibre/devices/mtp/filesystem_cache.py +++ b/src/calibre/devices/mtp/filesystem_cache.py @@ -20,6 +20,7 @@ from calibre.ebooks import BOOK_EXTENSIONS bexts = frozenset(BOOK_EXTENSIONS) - {'mbp', 'tan', 'rar', 'zip', 'xml'} + class FileOrFolder(object): def __init__(self, entry, fs_cache): @@ -180,6 +181,7 @@ class FileOrFolder(object): def mtp_id_path(self): return 'mtp:::' + json.dumps(self.object_id) + ':::' + '/'.join(self.full_path) + class FilesystemCache(object): def __init__(self, all_storage, entries): diff --git a/src/calibre/devices/mtp/test.py b/src/calibre/devices/mtp/test.py index d199cdac48..a37d6318b0 100644 --- a/src/calibre/devices/mtp/test.py +++ b/src/calibre/devices/mtp/test.py @@ -14,6 +14,7 @@ from calibre.utils.icu import lower from calibre.devices.mtp.driver import MTP_DEVICE from calibre.devices.scanner import DeviceScanner + class ProgressCallback(object): def __init__(self): @@ -25,6 +26,7 @@ class ProgressCallback(object): self.end_called = True self.count += 1 + class TestDeviceInteraction(unittest.TestCase): @classmethod @@ -256,6 +258,7 @@ def tests(): # return tl.loadTestsFromName('test.TestDeviceInteraction.test_memory_leaks') return tl.loadTestsFromTestCase(TestDeviceInteraction) + def run(): unittest.TextTestRunner(verbosity=2).run(tests()) diff --git a/src/calibre/devices/mtp/unix/driver.py b/src/calibre/devices/mtp/unix/driver.py index edab895787..3fa70720d4 100644 --- a/src/calibre/devices/mtp/unix/driver.py +++ b/src/calibre/devices/mtp/unix/driver.py @@ -22,12 +22,15 @@ MTPDevice = namedtuple('MTPDevice', 'busnum devnum vendor_id product_id ' 'bcd serial manufacturer product') null = object() + + def fingerprint(d): return MTPDevice(d.busnum, d.devnum, d.vendor_id, d.product_id, d.bcd, d.serial, d.manufacturer, d.product) APPLE = 0x05ac + class MTP_DEVICE(MTPDeviceBase): # libusb(x) does not work on OS X. So no MTP support for OS X @@ -412,6 +415,7 @@ class MTP_DEVICE(MTPDeviceBase): parent.remove_child(obj) return parent + def develop(): from calibre.devices.scanner import DeviceScanner scanner = DeviceScanner() diff --git a/src/calibre/devices/mtp/unix/sysfs.py b/src/calibre/devices/mtp/unix/sysfs.py index 5d1da333fd..30d26942cd 100644 --- a/src/calibre/devices/mtp/unix/sysfs.py +++ b/src/calibre/devices/mtp/unix/sysfs.py @@ -9,6 +9,7 @@ __docformat__ = 'restructuredtext en' import os, glob + class MTPDetect(object): SYSFS_PATH = os.environ.get('SYSFS_PATH', '/sys') diff --git a/src/calibre/devices/mtp/windows/driver.py b/src/calibre/devices/mtp/windows/driver.py index c3f3c24eeb..4722d97d44 100644 --- a/src/calibre/devices/mtp/windows/driver.py +++ b/src/calibre/devices/mtp/windows/driver.py @@ -20,6 +20,7 @@ from calibre.devices.mtp.base import MTPDeviceBase, debug null = object() + class ThreadingViolation(Exception): def __init__(self): @@ -27,6 +28,7 @@ class ThreadingViolation(Exception): 'You cannot use the MTP driver from a thread other than the ' ' thread in which startup() was called') + def same_thread(func): @wraps(func) def check_thread(self, *args, **kwargs): diff --git a/src/calibre/devices/mtp/windows/remote.py b/src/calibre/devices/mtp/windows/remote.py index ca1f96c686..37b8f08cdf 100644 --- a/src/calibre/devices/mtp/windows/remote.py +++ b/src/calibre/devices/mtp/windows/remote.py @@ -10,6 +10,7 @@ __docformat__ = 'restructuredtext en' import subprocess, sys, os, pprint, signal, time, glob, io pprint, io + def build(mod='wpd'): master = subprocess.Popen('ssh -MN getafix'.split()) master2 = subprocess.Popen('ssh -MN win64'.split()) @@ -38,6 +39,7 @@ def build(mod='wpd'): for m in (master2, master): m.wait() + def main(): fp, d = os.path.abspath(__file__), os.path.dirname if b'CALIBRE_DEVELOP_FROM' not in os.environ: diff --git a/src/calibre/devices/nokia/driver.py b/src/calibre/devices/nokia/driver.py index c5afb2d71d..8e168e0780 100644 --- a/src/calibre/devices/nokia/driver.py +++ b/src/calibre/devices/nokia/driver.py @@ -10,6 +10,7 @@ Device driver for Nokia's internet tablet devices from calibre.devices.usbms.driver import USBMS + class N770(USBMS): name = 'Nokia 770 Device Interface' @@ -34,6 +35,7 @@ class N770(USBMS): EBOOK_DIR_MAIN = 'My Ebooks' SUPPORTS_SUB_DIRS = True + class N810(N770): name = 'Nokia N800/N810/N900/N950/N9 Device Interface' gui_name = 'Nokia N800/N810/N900/N950/N9' @@ -46,6 +48,7 @@ class N810(N770): MAIN_MEMORY_VOLUME_LABEL = 'Nokia Maemo/MeeGo device Main Memory' + class E71X(USBMS): name = 'Nokia E71X device interface' @@ -66,6 +69,7 @@ class E71X(USBMS): VENDOR_NAME = 'NOKIA' WINDOWS_MAIN_MEM = 'S60' + class E52(USBMS): name = 'Nokia E52 device interface' diff --git a/src/calibre/devices/nook/driver.py b/src/calibre/devices/nook/driver.py index 5825f8d534..dfbd8e2903 100644 --- a/src/calibre/devices/nook/driver.py +++ b/src/calibre/devices/nook/driver.py @@ -16,6 +16,7 @@ from calibre import fsync from calibre.constants import isosx from calibre.devices.usbms.driver import USBMS + class NOOK(USBMS): name = 'Nook Device Interface' @@ -82,6 +83,7 @@ class NOOK(USBMS): def sanitize_path_components(self, components): return [x.replace('#', '_') for x in components] + class NOOK_COLOR(NOOK): name = 'Nook Color Device Interface' description = _('Communicate with the Nook Color, TSR, Glowlight and Tablet eBook readers.') diff --git a/src/calibre/devices/nuut2/driver.py b/src/calibre/devices/nuut2/driver.py index 0cfa73f276..05bf8b3276 100644 --- a/src/calibre/devices/nuut2/driver.py +++ b/src/calibre/devices/nuut2/driver.py @@ -10,6 +10,7 @@ Device driver for the Nuut2 from calibre.devices.usbms.driver import USBMS + class NUUT2(USBMS): name = 'Nuut2 Device Interface' diff --git a/src/calibre/devices/paladin/driver.py b/src/calibre/devices/paladin/driver.py index 37ff75f7b4..5dd100349e 100644 --- a/src/calibre/devices/paladin/driver.py +++ b/src/calibre/devices/paladin/driver.py @@ -18,10 +18,13 @@ from calibre.devices.usbms.books import CollectionsBookList, BookList DBPATH = 'paladin/database/books.db' + class ImageWrapper(object): + def __init__(self, image_path): self.image_path = image_path + class PALADIN(USBMS): name = 'Paladin Device Interface' gui_name = 'Paladin eLibrary' diff --git a/src/calibre/devices/prs505/driver.py b/src/calibre/devices/prs505/driver.py index 052127a411..0d015c9254 100644 --- a/src/calibre/devices/prs505/driver.py +++ b/src/calibre/devices/prs505/driver.py @@ -15,6 +15,7 @@ from calibre.devices.prs505 import MEDIA_XML, MEDIA_EXT, CACHE_XML, CACHE_EXT, \ from calibre import __appname__, prints from calibre.devices.usbms.books import CollectionsBookList + class PRS505(USBMS): name = 'SONY Device Interface' diff --git a/src/calibre/devices/prs505/sony_cache.py b/src/calibre/devices/prs505/sony_cache.py index 61bebf155e..132ff00b6d 100644 --- a/src/calibre/devices/prs505/sony_cache.py +++ b/src/calibre/devices/prs505/sony_cache.py @@ -60,6 +60,7 @@ MONTH_MAP = dict(Jan=1, Feb=2, Mar=3, Apr=4, May=5, Jun=6, Jul=7, Aug=8, Sep=9, INVERSE_DAY_MAP = dict(zip(DAY_MAP.values(), DAY_MAP.keys())) INVERSE_MONTH_MAP = dict(zip(MONTH_MAP.values(), MONTH_MAP.keys())) + def strptime(src): src = src.strip() src = src.split() @@ -67,6 +68,7 @@ def strptime(src): src[2] = str(MONTH_MAP[src[2]]) return time.strptime(' '.join(src), '%w, %d %m %Y %H:%M:%S %Z') + def strftime(epoch, zone=time.localtime): try: src = time.strftime("%w, %d %m %Y %H:%M:%S GMT", zone(epoch)).split() @@ -77,12 +79,14 @@ def strftime(epoch, zone=time.localtime): src[2] = INVERSE_MONTH_MAP[int(src[2])] return ' '.join(src) + def uuid(): from uuid import uuid4 return str(uuid4()).replace('-', '', 1).upper() # }}} + class XMLCache(object): def __init__(self, paths, ext_paths, prefixes, use_author_sort): diff --git a/src/calibre/devices/prst1/driver.py b/src/calibre/devices/prst1/driver.py index 29b7345f23..8693043be2 100644 --- a/src/calibre/devices/prst1/driver.py +++ b/src/calibre/devices/prst1/driver.py @@ -28,10 +28,13 @@ from calibre.constants import islinux DBPATH = 'Sony_Reader/database/books.db' THUMBPATH = 'Sony_Reader/database/cache/books/%s/thumbnail/main_thumbnail.jpg' + class ImageWrapper(object): + def __init__(self, image_path): self.image_path = image_path + class PRST1(USBMS): name = 'SONY PRST1 and newer Device Interface' gui_name = 'SONY Reader' diff --git a/src/calibre/devices/scanner.py b/src/calibre/devices/scanner.py index fc22c05c3d..ee726aec13 100644 --- a/src/calibre/devices/scanner.py +++ b/src/calibre/devices/scanner.py @@ -39,6 +39,7 @@ if iswindows: _USBDevice = namedtuple('USBDevice', 'vendor_id product_id bcd manufacturer product serial') + class USBDevice(_USBDevice): def __new__(cls, *args, **kwargs): @@ -56,6 +57,7 @@ class USBDevice(_USBDevice): __str__ = __repr__ __unicode__ = __repr__ + class LibUSBScanner(object): def __call__(self): @@ -95,6 +97,7 @@ class LibUSBScanner(object): print 'Mem consumption increased by:', memory() - start, 'MB', print 'after', num, 'repeats' + class LinuxScanner(object): SYSFS_PATH = os.environ.get('SYSFS_PATH', '/sys') @@ -166,6 +169,7 @@ class LinuxScanner(object): ans.add(dev) return ans + class FreeBSDScanner(object): def __call__(self): @@ -242,6 +246,7 @@ if isfreebsd: if isnetbsd: netbsd_scanner = None + class DeviceScanner(object): def __init__(self, *args): @@ -263,6 +268,7 @@ class DeviceScanner(object): return device.is_usb_connected(self.devices, debug=debug, only_presence=only_presence) + def test_for_mem_leak(): from calibre.utils.mem import memory, gc_histogram, diff_hists import gc @@ -289,6 +295,7 @@ def test_for_mem_leak(): diff_hists(h1, gc_histogram()) prints() + def main(args=sys.argv): test_for_mem_leak() return 0 diff --git a/src/calibre/devices/smart_device_app/driver.py b/src/calibre/devices/smart_device_app/driver.py index b8025c6bfe..deb8f531be 100644 --- a/src/calibre/devices/smart_device_app/driver.py +++ b/src/calibre/devices/smart_device_app/driver.py @@ -40,6 +40,7 @@ from calibre.utils.mdns import (publish as publish_zeroconf, unpublish as unpublish_zeroconf, get_all_ips) from calibre.utils.socket_inheritance import set_socket_inherit + def synchronous(tlockname): """A decorator to place an instance based lock around a method """ @@ -161,6 +162,7 @@ class SDBook(Book): path = getattr(self, 'path', lpath) self.path = path.replace('\\', '/') + class SMART_DEVICE_APP(DeviceConfig, DevicePlugin): name = 'SmartDevice App Interface' gui_name = _('Wireless Device') diff --git a/src/calibre/devices/sne/driver.py b/src/calibre/devices/sne/driver.py index bb8d34c59c..0195b03fe2 100644 --- a/src/calibre/devices/sne/driver.py +++ b/src/calibre/devices/sne/driver.py @@ -10,6 +10,7 @@ Device driver for Bookeen's Cybook Gen 3 from calibre.devices.usbms.driver import USBMS + class SNE(USBMS): name = 'Samsung SNE Device Interface' diff --git a/src/calibre/devices/teclast/driver.py b/src/calibre/devices/teclast/driver.py index 0e318e3949..3ecd37f356 100644 --- a/src/calibre/devices/teclast/driver.py +++ b/src/calibre/devices/teclast/driver.py @@ -4,6 +4,7 @@ __docformat__ = 'restructuredtext en' from calibre.devices.usbms.driver import USBMS + class TECLAST_K3(USBMS): name = 'Teclast K3/K5 Device Interface' @@ -44,6 +45,7 @@ class NEWSMY(TECLAST_K3): WINDOWS_MAIN_MEM = 'NEWSMY' WINDOWS_CARD_A_MEM = 'USBDISK____SD' + class ARCHOS7O(TECLAST_K3): name = 'Archos 7O device interface' gui_name = 'Archos' @@ -54,6 +56,7 @@ class ARCHOS7O(TECLAST_K3): VENDOR_NAME = 'ARCHOS' WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'USB-MSC' + class PICO(NEWSMY): name = 'Pico device interface' gui_name = 'Pico' @@ -65,6 +68,7 @@ class PICO(NEWSMY): FORMATS = ['EPUB', 'FB2', 'TXT', 'LRC', 'PDB', 'PDF', 'HTML', 'WTXT'] SCAN_FROM_ROOT = True + class IPAPYRUS(TECLAST_K3): name = 'iPapyrus device interface' @@ -76,6 +80,7 @@ class IPAPYRUS(TECLAST_K3): VENDOR_NAME = ['E_READER', 'EBOOKREA', 'ICARUS'] WINDOWS_MAIN_MEM = '' + class SOVOS(TECLAST_K3): name = 'Sovos device interface' @@ -87,6 +92,7 @@ class SOVOS(TECLAST_K3): VENDOR_NAME = 'RK28XX' WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'USB-MSC' + class SUNSTECH_EB700(TECLAST_K3): name = 'Sunstech EB700 device interface' gui_name = 'EB700' @@ -97,6 +103,7 @@ class SUNSTECH_EB700(TECLAST_K3): VENDOR_NAME = 'SUNEB700' WINDOWS_MAIN_MEM = 'USB-MSC' + class STASH(TECLAST_K3): name = 'Stash device interface' @@ -109,6 +116,7 @@ class STASH(TECLAST_K3): VENDOR_NAME = 'STASH' WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'W950' + class WEXLER(TECLAST_K3): name = 'Wexler device interface' diff --git a/src/calibre/devices/udisks.py b/src/calibre/devices/udisks.py index 63a1248b16..3781dc8b62 100644 --- a/src/calibre/devices/udisks.py +++ b/src/calibre/devices/udisks.py @@ -7,6 +7,7 @@ __docformat__ = 'restructuredtext en' import os, re + def node_mountpoint(node): def de_mangle(raw): @@ -19,9 +20,11 @@ def node_mountpoint(node): return de_mangle(line[1]) return None + class NoUDisks1(Exception): pass + class UDisks(object): def __init__(self): @@ -65,9 +68,11 @@ class UDisks(object): d = self.device(parent) d.DriveEject([]) + class NoUDisks2(Exception): pass + class UDisks2(object): BLOCK = 'org.freedesktop.UDisks2.Block' @@ -153,6 +158,7 @@ class UDisks2(object): drive.Eject({'auth.no_user_interaction':True}, dbus_interface=self.DRIVE) + def get_udisks(ver=None): if ver is None: try: @@ -162,6 +168,7 @@ def get_udisks(ver=None): return u return UDisks2() if ver == 2 else UDisks() + def get_udisks1(): u = None try: @@ -175,18 +182,22 @@ def get_udisks1(): raise EnvironmentError('UDisks not available on your system') return u + def mount(node_path): u = get_udisks1() u.mount(node_path) + def eject(node_path): u = get_udisks1() u.eject(node_path) + def umount(node_path): u = get_udisks1() u.unmount(node_path) + def test_udisks(ver=None): import sys dev = sys.argv[1] diff --git a/src/calibre/devices/usbms/books.py b/src/calibre/devices/usbms/books.py index 542116dc3c..63179faa62 100644 --- a/src/calibre/devices/usbms/books.py +++ b/src/calibre/devices/usbms/books.py @@ -15,6 +15,7 @@ from calibre import isbytestring, force_unicode from calibre.utils.config_base import tweaks from calibre.utils.icu import sort_key + class Book(Metadata): def __init__(self, prefix, lpath, size=None, other=None): @@ -46,6 +47,7 @@ class Book(Metadata): @dynamic_property def db_id(self): doc = '''The database id in the application database that this file corresponds to''' + def fget(self): match = re.search(r'_(\d+)$', self.lpath.rpartition('.')[0]) if match: @@ -56,6 +58,7 @@ class Book(Metadata): @dynamic_property def title_sorter(self): doc = '''String to sort the title. If absent, title is returned''' + def fget(self): return title_sort(self.title) return property(doc=doc, fget=fget) @@ -64,6 +67,7 @@ class Book(Metadata): def thumbnail(self): return None + class BookList(_BookList): def __init__(self, oncard, prefix, settings): @@ -99,6 +103,7 @@ class BookList(_BookList): def get_collections(self): return {} + class CollectionsBookList(BookList): def supports_collections(self): diff --git a/src/calibre/devices/usbms/cli.py b/src/calibre/devices/usbms/cli.py index 83cfc0dd91..e424787ff6 100644 --- a/src/calibre/devices/usbms/cli.py +++ b/src/calibre/devices/usbms/cli.py @@ -10,6 +10,7 @@ from calibre import fsync from calibre.devices.errors import PathError from calibre.utils.filenames import case_preserving_open_file + class File(object): def __init__(self, path): diff --git a/src/calibre/devices/usbms/device.py b/src/calibre/devices/usbms/device.py index 69a70e137a..a431cecac1 100644 --- a/src/calibre/devices/usbms/device.py +++ b/src/calibre/devices/usbms/device.py @@ -30,10 +30,12 @@ if isosx: if iswindows: usb_info_cache = {} + def eject_exe(): base = sys.extensions_location if hasattr(sys, 'new_app_layout') else os.path.dirname(sys.executable) return os.path.join(base, 'calibre-eject.exe') + class USBDevice: def __init__(self, dev): @@ -67,6 +69,7 @@ class USBDevice: p = osx_sanitize_name_pat.sub('_', (self.product or '')) return m == man and p == prod + class Device(DeviceConfig, DevicePlugin): ''' @@ -391,6 +394,7 @@ class Device(DeviceConfig, DevicePlugin): 'Could not detect BSD names for %s. Try rebooting.\nOutput from osx_get_usb_drives():\n%s' % (self.name, pformat(drives))) pat = re.compile(r'(?P\d+)([a-z]+(?P

\d+)){0,1}') + def nums(x): 'Return (disk num, partition number)' m = pat.search(x) @@ -464,6 +468,7 @@ class Device(DeviceConfig, DevicePlugin): break self._main_prefix = drives['main']+os.sep + def get_card_prefix(c): ans = drives.get(c, None) if ans is not None: @@ -580,6 +585,7 @@ class Device(DeviceConfig, DevicePlugin): mp = self.node_mountpoint(node) if mp is not None: return mp, 0 + def do_mount(node): try: from calibre.devices.udisks import mount diff --git a/src/calibre/devices/usbms/driver.py b/src/calibre/devices/usbms/driver.py index bb07f46651..bb23608e1a 100644 --- a/src/calibre/devices/usbms/driver.py +++ b/src/calibre/devices/usbms/driver.py @@ -22,6 +22,8 @@ from calibre.devices.usbms.books import BookList, Book from calibre.ebooks.metadata.book.json_codec import JsonCodec BASE_TIME = None + + def debug_print(*args): global BASE_TIME if BASE_TIME is None: @@ -29,6 +31,7 @@ def debug_print(*args): if DEBUG: prints('DEBUG: %6.1f'%(time.time()-BASE_TIME), *args) + def safe_walk(top, topdown=True, onerror=None, followlinks=False): ' A replacement for os.walk that does not die when it encounters undecodeable filenames in a linux filesystem' islink, join, isdir = os.path.islink, os.path.join, os.path.isdir diff --git a/src/calibre/devices/user_defined/driver.py b/src/calibre/devices/user_defined/driver.py index 24dac528ce..683c7f33c5 100644 --- a/src/calibre/devices/user_defined/driver.py +++ b/src/calibre/devices/user_defined/driver.py @@ -6,6 +6,7 @@ __docformat__ = 'restructuredtext en' from calibre.devices.usbms.driver import USBMS + class USER_DEFINED(USBMS): name = 'User Defined USB driver' diff --git a/src/calibre/devices/utils.py b/src/calibre/devices/utils.py index 02f5448d5f..6de17bbb9d 100644 --- a/src/calibre/devices/utils.py +++ b/src/calibre/devices/utils.py @@ -12,6 +12,7 @@ from functools import partial from calibre.devices.errors import DeviceError, WrongDestinationError, FreeSpaceError + def sanity_check(on_card, files, card_prefixes, free_space): if on_card == 'carda' and not card_prefixes[0]: raise WrongDestinationError(_( @@ -39,6 +40,7 @@ def sanity_check(on_card, files, card_prefixes, free_space): if on_card == 'cardb' and size > free_space[2] - 1024*1024: raise FreeSpaceError(_("There is insufficient free space on the storage card")) + def build_template_regexp(template): from calibre import prints @@ -62,6 +64,7 @@ def build_template_regexp(template): template = u'{title} - {authors}' return re.compile(re.sub('{([^}]*)}', f, template) + '([_\d]*$)') + def create_upload_path(mdata, fname, template, sanitize, prefix_path='', path_type=os.path, diff --git a/src/calibre/devices/winusb.py b/src/calibre/devices/winusb.py index 7940b94c06..ab6e359b56 100644 --- a/src/calibre/devices/winusb.py +++ b/src/calibre/devices/winusb.py @@ -23,6 +23,7 @@ is64bit = sys.maxsize > (1 << 32) # Data and function type definitions {{{ + class GUID(Structure): _fields_ = [ ("data1", DWORD), @@ -60,8 +61,12 @@ REG_QWORD = 11 IOCTL_STORAGE_MEDIA_REMOVAL = 0x2D4804 IOCTL_STORAGE_EJECT_MEDIA = 0x2D4808 IOCTL_STORAGE_GET_DEVICE_NUMBER = 0x2D1080 + + def CTL_CODE(DeviceType, Function, Method, Access): return (DeviceType << 16) | (Access << 14) | (Function << 2) | Method + + def USB_CTL(id): # CTL_CODE(FILE_DEVICE_USB, (id), METHOD_BUFFERED, FILE_ANY_ACCESS) return CTL_CODE(0x22, id, 0, 0) @@ -80,6 +85,7 @@ MAXIMUM_USB_STRING_LENGTH = 255 StorageDeviceNumber = namedtuple('StorageDeviceNumber', 'type number partition_number') + class STORAGE_DEVICE_NUMBER(Structure): _fields_ = [ ('DeviceType', DWORD), @@ -90,6 +96,7 @@ class STORAGE_DEVICE_NUMBER(Structure): def as_tuple(self): return StorageDeviceNumber(self.DeviceType, self.DeviceNumber, self.PartitionNumber) + class SP_DEVINFO_DATA(Structure): _fields_ = [ ('cbSize', DWORD), @@ -97,11 +104,13 @@ class SP_DEVINFO_DATA(Structure): ('DevInst', DEVINST), ('Reserved', POINTER(ULONG)), ] + def __str__(self): return "ClassGuid:%s DevInst:%s" % (self.ClassGuid, self.DevInst) PSP_DEVINFO_DATA = POINTER(SP_DEVINFO_DATA) + class SP_DEVICE_INTERFACE_DATA(Structure): _fields_ = [ ('cbSize', DWORD), @@ -109,11 +118,13 @@ class SP_DEVICE_INTERFACE_DATA(Structure): ('Flags', DWORD), ('Reserved', POINTER(ULONG)), ] + def __str__(self): return "InterfaceClassGuid:%s Flags:%s" % (self.InterfaceClassGuid, self.Flags) ANYSIZE_ARRAY = 1 + class SP_DEVICE_INTERFACE_DETAIL_DATA(Structure): _fields_ = [ ("cbSize", DWORD), @@ -122,6 +133,7 @@ class SP_DEVICE_INTERFACE_DETAIL_DATA(Structure): UCHAR = c_ubyte + class USB_DEVICE_DESCRIPTOR(Structure): _fields_ = ( ('bLength', UCHAR), @@ -146,6 +158,7 @@ class USB_DEVICE_DESCRIPTOR(Structure): self.idVendor, self.idProduct, self.bcdDevice, self.iManufacturer, self.iProduct, self.iSerialNumber) + class USB_ENDPOINT_DESCRIPTOR(Structure): _fields_ = ( ('bLength', UCHAR), @@ -156,6 +169,7 @@ class USB_ENDPOINT_DESCRIPTOR(Structure): ('bInterval', UCHAR) ) + class USB_PIPE_INFO(Structure): _fields_ = ( ('EndpointDescriptor', USB_ENDPOINT_DESCRIPTOR), @@ -176,6 +190,7 @@ class USB_NODE_CONNECTION_INFORMATION_EX(Structure): ('PipeList', USB_PIPE_INFO*ANYSIZE_ARRAY), ) + class USB_STRING_DESCRIPTOR(Structure): _fields_ = ( ('bLength', UCHAR), @@ -183,6 +198,7 @@ class USB_STRING_DESCRIPTOR(Structure): ('String', UCHAR * ANYSIZE_ARRAY), ) + class USB_DESCRIPTOR_REQUEST(Structure): class SetupPacket(Structure): @@ -343,6 +359,7 @@ setupapi = windll.setupapi cfgmgr = windll.CfgMgr32 kernel32 = windll.Kernel32 + def cwrap(name, restype, *argtypes, **kw): errcheck = kw.pop('errcheck', None) use_last_error = bool(kw.pop('use_last_error', True)) @@ -355,16 +372,19 @@ def cwrap(name, restype, *argtypes, **kw): func.errcheck = errcheck return func + def handle_err_check(result, func, args): if result == INVALID_HANDLE_VALUE: raise WinError(get_last_error()) return result + def bool_err_check(result, func, args): if not result: raise WinError(get_last_error()) return result + def config_err_check(result, func, args): if result != CR_CODES['CR_SUCCESS']: raise WindowsError(result, 'The cfgmgr32 function failed with err: %s' % CR_CODE_NAMES.get(result, result)) @@ -402,6 +422,8 @@ CM_Get_Device_ID = cwrap('CM_Get_Device_IDW', CONFIGRET, DEVINST, LPWSTR, ULONG, # Utility functions {{{ _devid_pat = None + + def devid_pat(): global _devid_pat if _devid_pat is None: @@ -469,12 +491,14 @@ def iterchildren(parent_devinst): raise yield child.value + def iterdescendants(parent_devinst): for child in iterchildren(parent_devinst): yield child for gc in iterdescendants(child): yield gc + def iterancestors(devinst): NO_MORE = CR_CODES['CR_NO_SUCH_DEVINST'] parent = DEVINST(devinst) @@ -487,6 +511,7 @@ def iterancestors(devinst): raise yield parent.value + def device_io_control(handle, which, inbuf, outbuf, initbuf): bytes_returned = DWORD(0) while True: @@ -500,6 +525,7 @@ def device_io_control(handle, which, inbuf, outbuf, initbuf): else: return outbuf, bytes_returned + def get_storage_number(devpath): sdn = STORAGE_DEVICE_NUMBER() handle = CreateFile(devpath, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, None, OPEN_EXISTING, 0, None) @@ -510,6 +536,7 @@ def get_storage_number(devpath): CloseHandle(handle) return sdn.as_tuple() + def get_device_id(devinst, buf=None): if buf is None: buf = create_unicode_buffer(512) @@ -525,6 +552,7 @@ def get_device_id(devinst, buf=None): break return wstring_at(buf), buf + def expand_environment_strings(src): sz = ExpandEnvironmentStrings(src, None, 0) while True: @@ -534,6 +562,7 @@ def expand_environment_strings(src): return buf.value sz = nsz + def convert_registry_data(raw, size, dtype): if dtype == winreg.REG_NONE: return None @@ -556,6 +585,7 @@ def convert_registry_data(raw, size, dtype): return cast(raw, POINTER(QWORD)).contents.value raise ValueError('Unsupported data type: %r' % dtype) + def get_device_registry_property(dev_list, p_devinfo, property_type=SPDRP_HARDWAREID, buf=None): if buf is None: buf = create_string_buffer(1024) @@ -575,6 +605,7 @@ def get_device_registry_property(dev_list, p_devinfo, property_type=SPDRP_HARDWA break return buf, ans + def get_device_interface_detail_data(dev_list, p_interface_data, buf=None): if buf is None: buf = create_string_buffer(512) @@ -597,6 +628,7 @@ def get_device_interface_detail_data(dev_list, p_interface_data, buf=None): break return buf, devinfo, wstring_at(addressof(buf) + sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA._fields_[0][1])) + def get_volume_information(drive_letter): if not drive_letter.endswith('\\'): drive_letter += ':\\' @@ -622,6 +654,7 @@ def get_volume_information(drive_letter): ans[name] = bool(num & flags) return ans + def get_volume_pathnames(volume_id, buf=None): if buf is None: buf = create_unicode_buffer(512) @@ -644,6 +677,7 @@ def get_volume_pathnames(volume_id, buf=None): _USBDevice = namedtuple('USBDevice', 'vendor_id product_id bcd devid devinst') + class USBDevice(_USBDevice): def __repr__(self): @@ -654,9 +688,11 @@ class USBDevice(_USBDevice): return 'USBDevice(vendor_id=%s product_id=%s bcd=%s devid=%s devinst=%s)' % ( r(self.vendor_id), r(self.product_id), r(self.bcd), self.devid, self.devinst) + def parse_hex(x): return int(x.replace(':', 'a'), 16) + def iterusbdevices(): buf = None pat = devid_pat() @@ -675,11 +711,13 @@ def iterusbdevices(): else: yield USBDevice(vid, pid, bcd, devid, devinfo.DevInst) + def scan_usb_devices(): return tuple(iterusbdevices()) # }}} + def get_drive_letters_for_device(usbdev, storage_number_map=None, debug=False): # {{{ ''' Get the drive letters for a connected device. The drive letters are sorted @@ -717,6 +755,7 @@ def get_drive_letters_for_device(usbdev, storage_number_map=None, debug=False): else: return get_drive_letters_for_device_single(usbdev, sn_map, debug=debug) + def get_drive_letters_for_device_single(usbdev, storage_number_map, debug=False): ans = {'pnp_id_map': {}, 'drive_letters':[], 'readonly_drives':set(), 'sort_map':{}} descendants = frozenset(iterdescendants(usbdev.devinst)) @@ -756,6 +795,7 @@ def get_drive_letters_for_device_single(usbdev, storage_number_map, debug=False) return ans + def get_storage_number_map(drive_types=(DRIVE_REMOVABLE, DRIVE_FIXED), debug=False): ' Get a mapping of drive letters to storage numbers for all drives on system (of the specified types) ' mask = GetLogicalDrives() @@ -774,6 +814,7 @@ def get_storage_number_map(drive_types=(DRIVE_REMOVABLE, DRIVE_FIXED), debug=Fal val.sort(key=itemgetter(0)) return dict(ans) + def get_storage_number_map_alt(debug=False): ' Alternate implementation that works without needing to call GetDriveType() (which causes floppy drives to seek) ' wbuf = create_unicode_buffer(512) @@ -815,6 +856,7 @@ def get_storage_number_map_alt(debug=False): # }}} + def is_usb_device_connected(vendor_id, product_id): # {{{ for usbdev in iterusbdevices(): if usbdev.vendor_id == vendor_id and usbdev.product_id == product_id: @@ -822,6 +864,7 @@ def is_usb_device_connected(vendor_id, product_id): # {{{ return False # }}} + def get_usb_info(usbdev, debug=False): # {{{ ''' The USB info (manufacturer/product names and serial number) Requires communication with the hub the device is connected to. @@ -870,6 +913,7 @@ def get_usb_info(usbdev, debug=False): # {{{ CloseHandle(handle) return ans + def alloc_descriptor_buf(buf): if buf is None: buf = create_string_buffer(sizeof(USB_DESCRIPTOR_REQUEST) + 700) @@ -877,6 +921,7 @@ def alloc_descriptor_buf(buf): memset(buf, 0, len(buf)) return buf + def get_device_descriptor(hub_handle, device_port, buf=None): buf = alloc_descriptor_buf(buf) @@ -886,6 +931,7 @@ def get_device_descriptor(hub_handle, device_port, buf=None): buf, bytes_returned = device_io_control(hub_handle, IOCTL_USB_GET_NODE_CONNECTION_INFORMATION_EX, buf, buf, initbuf) return buf, USB_DEVICE_DESCRIPTOR.from_buffer_copy(cast(buf, POINTER(USB_NODE_CONNECTION_INFORMATION_EX)).contents.DeviceDescriptor) + def get_device_string(hub_handle, device_port, index, buf=None, lang=0x409): buf = alloc_descriptor_buf(buf) @@ -905,6 +951,7 @@ def get_device_string(hub_handle, device_port, index, buf=None, lang=0x409): raise WindowsError('Invalid datatype for string descriptor: 0x%x' % dtype) return buf, wstring_at(addressof(data.String), sz // 2).rstrip('\0') + def get_device_languages(hub_handle, device_port, buf=None): ' Get the languages supported by the device for strings ' buf = alloc_descriptor_buf(buf) @@ -927,10 +974,12 @@ def get_device_languages(hub_handle, device_port, buf=None): # }}} + def is_readonly(drive_letter): # {{{ return get_volume_information(drive_letter)['FILE_READ_ONLY_VOLUME'] # }}} + def develop(): # {{{ from calibre.customize.ui import device_plugins usb_devices = scan_usb_devices() @@ -958,6 +1007,7 @@ def develop(): # {{{ print() print('Device USB data:', get_usb_info(usbdev, debug=True)) + def drives_for(vendor_id, product_id=None): usb_devices = scan_usb_devices() pprint(usb_devices) diff --git a/src/calibre/ebooks/__init__.py b/src/calibre/ebooks/__init__.py index 50a957f8ec..c21fec512e 100644 --- a/src/calibre/ebooks/__init__.py +++ b/src/calibre/ebooks/__init__.py @@ -10,18 +10,22 @@ from various formats. import traceback, os, re from calibre import CurrentDir, prints + class ConversionError(Exception): def __init__(self, msg, only_msg=False): Exception.__init__(self, msg) self.only_msg = only_msg + class UnknownFormatError(Exception): pass + class DRMError(ValueError): pass + class ParserError(ValueError): pass @@ -32,6 +36,7 @@ BOOK_EXTENSIONS = ['lrf', 'rar', 'zip', 'rtf', 'lit', 'txt', 'txtz', 'text', 'ht 'xps', 'oxps', 'azw4', 'book', 'zbf', 'pobi', 'docx', 'docm', 'md', 'textile', 'markdown', 'ibook', 'ibooks', 'iba', 'azw3', 'ps', 'kepub'] + class HTMLRenderer(object): def __init__(self, page, loop): @@ -74,6 +79,7 @@ def return_raster_image(path): if what(None, raw) not in (None, 'svg'): return raw + def extract_cover_from_embedded_svg(html, base, log): from lxml import etree from calibre.ebooks.oeb.base import XPath, SVG, XLINK @@ -87,6 +93,7 @@ def extract_cover_from_embedded_svg(html, base, log): path = os.path.join(base, *href.split('/')) return return_raster_image(path) + def extract_calibre_cover(raw, base, log): from calibre.ebooks.BeautifulSoup import BeautifulSoup soup = BeautifulSoup(raw) @@ -114,6 +121,7 @@ def extract_calibre_cover(raw, base, log): img = os.path.join(base, *images[0]['src'].split('/')) return return_raster_image(img) + def render_html_svg_workaround(path_to_html, log, width=590, height=750): from calibre.ebooks.oeb.base import SVG_NS raw = open(path_to_html, 'rb').read() @@ -148,10 +156,12 @@ def render_html_svg_workaround(path_to_html, log, width=590, height=750): traceback.print_exc() return data + def render_html_data(path_to_html, width, height): renderer = render_html(path_to_html, width, height) return getattr(renderer, 'data', None) + def render_html(path_to_html, width=590, height=750, as_xhtml=True): from PyQt5.QtWebKitWidgets import QWebPage from PyQt5.Qt import QEventLoop, QPalette, Qt, QUrl, QSize @@ -187,6 +197,7 @@ def render_html(path_to_html, width=590, height=750, as_xhtml=True): as_xhtml=False) return renderer + def check_ebook_format(stream, current_guess): ans = current_guess if current_guess.lower() in ('prc', 'mobi', 'azw', 'azw1', 'azw3'): @@ -196,12 +207,14 @@ def check_ebook_format(stream, current_guess): stream.seek(0) return ans + def normalize(x): if isinstance(x, unicode): import unicodedata x = unicodedata.normalize('NFC', x) return x + def calibre_cover(title, author_string, series_string=None, output_format='jpg', title_size=46, author_size=36, logo_path=None): title = normalize(title) @@ -214,6 +227,7 @@ def calibre_cover(title, author_string, series_string=None, UNIT_RE = re.compile(r'^(-*[0-9]*[.]?[0-9]*)\s*(%|em|ex|en|px|mm|cm|in|pt|pc|rem|q)$') + def unit_convert(value, base, font, dpi, body_font_size=12): ' Return value in pts' if isinstance(value, (int, long, float)): @@ -254,6 +268,7 @@ def unit_convert(value, base, font, dpi, body_font_size=12): result = value * 0.708661417325 return result + def parse_css_length(value): try: m = UNIT_RE.match(value) @@ -265,6 +280,7 @@ def parse_css_length(value): return value, unit.lower() return None, None + def generate_masthead(title, output_path=None, width=600, height=60): from calibre.ebooks.conversion.config import load_defaults recs = load_defaults('mobi_output') @@ -272,6 +288,7 @@ def generate_masthead(title, output_path=None, width=600, height=60): from calibre.ebooks.covers import generate_masthead return generate_masthead(title, output_path=output_path, width=width, height=height, font_family=masthead_font_family) + def escape_xpath_attr(value): if '"' in value: if "'" in value: diff --git a/src/calibre/ebooks/azw4/reader.py b/src/calibre/ebooks/azw4/reader.py index 60eaaef20e..a73108906c 100644 --- a/src/calibre/ebooks/azw4/reader.py +++ b/src/calibre/ebooks/azw4/reader.py @@ -16,6 +16,7 @@ import re from calibre.ebooks.pdb.formatreader import FormatReader + def unwrap(stream, output_path): raw_data = stream.read() m = re.search(br'%PDF.+%%EOF', raw_data, flags=re.DOTALL) diff --git a/src/calibre/ebooks/chardet.py b/src/calibre/ebooks/chardet.py index 22ef35eca2..fd919d3c01 100644 --- a/src/calibre/ebooks/chardet.py +++ b/src/calibre/ebooks/chardet.py @@ -19,6 +19,7 @@ ENCODING_PATS = [ ] ENTITY_PATTERN = re.compile(r'&(\S+?);') + def strip_encoding_declarations(raw, limit=50*1024): prefix = raw[:limit] suffix = raw[limit:] @@ -27,10 +28,12 @@ def strip_encoding_declarations(raw, limit=50*1024): raw = prefix + suffix return raw + def replace_encoding_declarations(raw, enc='utf-8', limit=50*1024): prefix = raw[:limit] suffix = raw[limit:] changed = [False] + def sub(m): ans = m.group() if m.group(1).lower() != enc.lower(): @@ -44,6 +47,7 @@ def replace_encoding_declarations(raw, enc='utf-8', limit=50*1024): raw = prefix + suffix return raw, changed[0] + def find_declared_encoding(raw, limit=50*1024): prefix = raw[:limit] for pat in ENCODING_PATS: @@ -51,6 +55,7 @@ def find_declared_encoding(raw, limit=50*1024): if m is not None: return m.group(1) + def substitute_entites(raw): from calibre import xml_entity_to_unicode return ENTITY_PATTERN.sub(xml_entity_to_unicode, raw) @@ -58,10 +63,12 @@ def substitute_entites(raw): _CHARSET_ALIASES = {"macintosh" : "mac-roman", "x-sjis" : "shift-jis"} + def detect(*args, **kwargs): from chardet import detect return detect(*args, **kwargs) + def force_encoding(raw, verbose, assume_utf8=False): from calibre.constants import preferred_encoding @@ -83,6 +90,7 @@ def force_encoding(raw, verbose, assume_utf8=False): encoding = 'utf-8' return encoding + def detect_xml_encoding(raw, verbose=False, assume_utf8=False): if not raw or isinstance(raw, unicode): return raw, None @@ -114,6 +122,7 @@ def detect_xml_encoding(raw, verbose=False, assume_utf8=False): return raw, encoding + def xml_to_unicode(raw, verbose=False, strip_encoding_pats=False, resolve_entities=False, assume_utf8=False): ''' diff --git a/src/calibre/ebooks/chm/metadata.py b/src/calibre/ebooks/chm/metadata.py index 3ac4374c32..b35da416f1 100644 --- a/src/calibre/ebooks/chm/metadata.py +++ b/src/calibre/ebooks/chm/metadata.py @@ -15,9 +15,11 @@ from calibre.utils.logging import default_log from calibre.ptempfile import TemporaryFile from calibre import force_unicode + def _clean(s): return s.replace(u'\u00a0', u' ') + def _detag(tag): str = u"" if tag is None: @@ -44,6 +46,7 @@ def _metadata_from_table(soup, searchfor): meta = _detag(td) return re.sub(r'^[^:]+:', '', meta).strip() + def _metadata_from_span(soup, searchfor): span = soup.find('span', {'class': re.compile(searchfor, flags=re.I)}) if span is None: @@ -51,6 +54,7 @@ def _metadata_from_span(soup, searchfor): # this metadata might need some cleaning up still :/ return _detag(span.renderContents(None).strip()) + def _get_authors(soup): aut = (_metadata_from_span(soup, r'author') or _metadata_from_table(soup, r'^\s*by\s*:?\s+')) ans = [_('Unknown')] @@ -58,12 +62,15 @@ def _get_authors(soup): ans = string_to_authors(aut) return ans + def _get_publisher(soup): return (_metadata_from_span(soup, 'imprint') or _metadata_from_table(soup, 'publisher')) + def _get_isbn(soup): return (_metadata_from_span(soup, 'isbn') or _metadata_from_table(soup, 'isbn')) + def _get_comments(soup): date = (_metadata_from_span(soup, 'cwdate') or _metadata_from_table(soup, 'pub date')) pages = (_metadata_from_span(soup, 'pages') or _metadata_from_table(soup, 'pages')) @@ -77,6 +84,7 @@ def _get_comments(soup): pass return None + def _get_cover(soup, rdr): ans = None try: @@ -159,6 +167,7 @@ def get_metadata_from_reader(rdr): return mi + def get_metadata(stream): with TemporaryFile('_chm_metadata.chm') as fname: with open(fname, 'wb') as f: diff --git a/src/calibre/ebooks/chm/reader.py b/src/calibre/ebooks/chm/reader.py index 882fb41352..a53519fc41 100644 --- a/src/calibre/ebooks/chm/reader.py +++ b/src/calibre/ebooks/chm/reader.py @@ -25,6 +25,7 @@ def match_string(s1, s2_already_lowered): return True return False + def check_all_prev_empty(tag): if tag is None: return True @@ -32,6 +33,7 @@ def check_all_prev_empty(tag): return False return check_all_prev_empty(tag.previousSibling) + def check_empty(s, rex=re.compile(r'\S')): return rex.search(s) is None @@ -39,6 +41,7 @@ def check_empty(s, rex=re.compile(r'\S')): class CHMError(Exception): pass + class CHMReader(CHMFile): def __init__(self, input, log, input_encoding=None): @@ -266,6 +269,7 @@ class CHMReader(CHMFile): if self._contents is not None: return self._contents paths = [] + def get_paths(chm, ui, ctx): # skip directories # note this path refers to the internal CHM structure diff --git a/src/calibre/ebooks/comic/__init__.py b/src/calibre/ebooks/comic/__init__.py index a5778dc3e4..f47e177c26 100644 --- a/src/calibre/ebooks/comic/__init__.py +++ b/src/calibre/ebooks/comic/__init__.py @@ -9,6 +9,7 @@ Convert CBR/CBZ files to LRF. import sys + def main(args=sys.argv): return 0 diff --git a/src/calibre/ebooks/comic/input.py b/src/calibre/ebooks/comic/input.py index 3acd830962..49ccf2ccac 100755 --- a/src/calibre/ebooks/comic/input.py +++ b/src/calibre/ebooks/comic/input.py @@ -21,6 +21,7 @@ from calibre.utils.ipc.job import ParallelJob # rescaling is done (we assume that it is a tablet output profile) MAX_SCREEN_SIZE = 3000 + def extract_comic(path_to_comic_file): ''' Un-archive the comic file. @@ -38,6 +39,7 @@ def extract_comic(path_to_comic_file): os.rename(x, os.path.join(os.path.dirname(x), nbn)) return tdir + def find_pages(dir, sort_on_mtime=False, verbose=False): ''' Find valid comic pages in a previously un-archived comic. @@ -72,6 +74,7 @@ def find_pages(dir, sort_on_mtime=False, verbose=False): prints('\t'+'\n\t'.join([os.path.relpath(p, dir) for p in pages])) return pages + class PageProcessor(list): # {{{ ''' @@ -197,6 +200,7 @@ class PageProcessor(list): # {{{ self.append(dest) # }}} + def render_pages(tasks, dest, opts, notification=lambda x, y: x): ''' Entry point for the job server. @@ -229,6 +233,7 @@ class Progress(object): # msg = msg%os.path.basename(job.args[0]) self.update(float(self.done)/self.total, msg) + def process_pages(pages, opts, update, tdir): ''' Render all identified comic pages. diff --git a/src/calibre/ebooks/compression/palmdoc.py b/src/calibre/ebooks/compression/palmdoc.py index 89a89199ac..e12978a11b 100644 --- a/src/calibre/ebooks/compression/palmdoc.py +++ b/src/calibre/ebooks/compression/palmdoc.py @@ -13,14 +13,17 @@ if not cPalmdoc: raise RuntimeError(('Failed to load required cPalmdoc module: ' '%s')%plugins['cPalmdoc'][1]) + def decompress_doc(data): return cPalmdoc.decompress(data) + def compress_doc(data): if not data: return u'' return cPalmdoc.compress(data) + def test(): TESTS = [ 'abc\x03\x04\x05\x06ms', # Test binary writing @@ -43,6 +46,7 @@ def test(): assert decompress_doc(x) == test print + def py_compress_doc(data): out = StringIO() i = 0 diff --git a/src/calibre/ebooks/compression/tcr.py b/src/calibre/ebooks/compression/tcr.py index 62482f7d3f..643f0bc311 100644 --- a/src/calibre/ebooks/compression/tcr.py +++ b/src/calibre/ebooks/compression/tcr.py @@ -6,6 +6,7 @@ __docformat__ = 'restructuredtext en' import re + class TCRCompressor(object): ''' TCR compression takes the form header+code_dict+coded_text. @@ -133,6 +134,7 @@ def decompress(stream): return ''.join(txt) + def compress(txt): t = TCRCompressor() return t.compress(txt) diff --git a/src/calibre/ebooks/conversion/cli.py b/src/calibre/ebooks/conversion/cli.py index 5091050b87..207d13a48d 100644 --- a/src/calibre/ebooks/conversion/cli.py +++ b/src/calibre/ebooks/conversion/cli.py @@ -53,9 +53,11 @@ HEURISTIC_OPTIONS = ['markup_chapter_headings', DEFAULT_TRUE_OPTIONS = HEURISTIC_OPTIONS + ['remove_fake_margins'] + def print_help(parser, log): parser.print_help() + def check_command_line_options(parser, args, log): if len(args) < 3 or args[1].startswith('-') or args[2].startswith('-'): print_help(parser, log) @@ -78,6 +80,7 @@ def check_command_line_options(parser, args, log): return input, output + def option_recommendation_to_cli_option(add_option, rec): opt = rec.option switches = ['-'+opt.short_switch] if opt.short_switch else [] @@ -111,9 +114,11 @@ def option_recommendation_to_cli_option(add_option, rec): switches = ['--disable-'+opt.long_switch] add_option(Option(*switches, **attrs)) + def group_titles(): return _('INPUT OPTIONS'), _('OUTPUT OPTIONS') + def recipe_test(option, opt_str, value, parser): assert value is None value = [] @@ -145,9 +150,11 @@ def recipe_test(option, opt_str, value, parser): setattr(parser.values, option.dest, tuple(value)) + def add_input_output_options(parser, plumber): input_options, output_options = \ plumber.input_options, plumber.output_options + def add_options(group, options): for opt in options: if plumber.input_fmt == 'recipe' and opt.option.long_switch == 'test': @@ -169,6 +176,7 @@ def add_input_output_options(parser, plumber): add_options(oo.add_option, output_options) parser.add_option_group(oo) + def add_pipeline_options(parser, plumber): groups = OrderedDict(( ('' , ('', @@ -266,6 +274,7 @@ def option_parser(): 'output.epub')) return parser + class ProgressBar(object): def __init__(self, log): @@ -276,6 +285,7 @@ class ProgressBar(object): percent = int(frac*100) self.log('%d%% %s'%(percent, msg)) + def create_option_parser(args, log): if '--version' in args: from calibre.constants import __appname__, __version__, __author__ @@ -313,11 +323,13 @@ def create_option_parser(args, log): return parser, plumber + def abspath(x): if x.startswith('http:') or x.startswith('https:'): return x return os.path.abspath(os.path.expanduser(x)) + def read_sr_patterns(path, log=None): import json, re, codecs pats = [] @@ -346,6 +358,7 @@ def read_sr_patterns(path, log=None): pat = None return json.dumps(pats) + def main(args=sys.argv): log = Log() parser, plumber = create_option_parser(args, log) @@ -391,6 +404,7 @@ def main(args=sys.argv): return 0 + def manual_index_strings(): return _('''\ The options and default values for the options change depending on both the diff --git a/src/calibre/ebooks/conversion/config.py b/src/calibre/ebooks/conversion/config.py index c0731530bd..8502b7df97 100644 --- a/src/calibre/ebooks/conversion/config.py +++ b/src/calibre/ebooks/conversion/config.py @@ -18,9 +18,11 @@ config_dir = os.path.join(config_dir, 'conversion') if not os.path.exists(config_dir): os.makedirs(config_dir) + def name_to_path(name): return os.path.join(config_dir, sanitize_file_name(name)+'.py') + def save_defaults(name, recs): path = name_to_path(name) raw = str(recs) @@ -29,6 +31,7 @@ def save_defaults(name, recs): with ExclusiveFile(path) as f: f.write(raw) + def load_defaults(name): path = name_to_path(name) if not os.path.exists(path): @@ -40,10 +43,12 @@ def load_defaults(name): r.from_string(raw) return r + def save_specifics(db, book_id, recs): raw = str(recs) db.set_conversion_options(book_id, 'PIPE', raw) + def load_specifics(db, book_id): raw = db.conversion_options(book_id, 'PIPE') r = GuiRecommendations() @@ -51,9 +56,11 @@ def load_specifics(db, book_id): r.from_string(raw) return r + def delete_specifics(db, book_id): db.delete_conversion_options(book_id, 'PIPE') + class GuiRecommendations(dict): def __new__(cls, *args): diff --git a/src/calibre/ebooks/conversion/plugins/azw4_input.py b/src/calibre/ebooks/conversion/plugins/azw4_input.py index 50329109b0..e12868c29c 100644 --- a/src/calibre/ebooks/conversion/plugins/azw4_input.py +++ b/src/calibre/ebooks/conversion/plugins/azw4_input.py @@ -8,6 +8,7 @@ import os from calibre.customize.conversion import InputFormatPlugin + class AZW4Input(InputFormatPlugin): name = 'AZW4 Input' diff --git a/src/calibre/ebooks/conversion/plugins/chm_input.py b/src/calibre/ebooks/conversion/plugins/chm_input.py index 9183c34d5f..9dd1393a13 100644 --- a/src/calibre/ebooks/conversion/plugins/chm_input.py +++ b/src/calibre/ebooks/conversion/plugins/chm_input.py @@ -9,6 +9,7 @@ from calibre.customize.conversion import InputFormatPlugin from calibre.ptempfile import TemporaryDirectory from calibre.constants import filesystem_encoding + class CHMInput(InputFormatPlugin): name = 'CHM Input' diff --git a/src/calibre/ebooks/conversion/plugins/comic_input.py b/src/calibre/ebooks/conversion/plugins/comic_input.py index e3561bbd44..882501462c 100644 --- a/src/calibre/ebooks/conversion/plugins/comic_input.py +++ b/src/calibre/ebooks/conversion/plugins/comic_input.py @@ -13,6 +13,7 @@ from calibre.customize.conversion import InputFormatPlugin, OptionRecommendation from calibre import CurrentDir from calibre.ptempfile import PersistentTemporaryDirectory + class ComicInput(InputFormatPlugin): name = 'Comic Input' diff --git a/src/calibre/ebooks/conversion/plugins/djvu_input.py b/src/calibre/ebooks/conversion/plugins/djvu_input.py index 70b79c392d..5f144286ac 100644 --- a/src/calibre/ebooks/conversion/plugins/djvu_input.py +++ b/src/calibre/ebooks/conversion/plugins/djvu_input.py @@ -12,6 +12,7 @@ from io import BytesIO from calibre.customize.conversion import InputFormatPlugin + class DJVUInput(InputFormatPlugin): name = 'DJVU Input' diff --git a/src/calibre/ebooks/conversion/plugins/docx_input.py b/src/calibre/ebooks/conversion/plugins/docx_input.py index f1c7b6a578..56d5c107ea 100644 --- a/src/calibre/ebooks/conversion/plugins/docx_input.py +++ b/src/calibre/ebooks/conversion/plugins/docx_input.py @@ -8,6 +8,7 @@ __copyright__ = '2013, Kovid Goyal ' from calibre.customize.conversion import InputFormatPlugin, OptionRecommendation + class DOCXInput(InputFormatPlugin): name = 'DOCX Input' author = 'Kovid Goyal' diff --git a/src/calibre/ebooks/conversion/plugins/docx_output.py b/src/calibre/ebooks/conversion/plugins/docx_output.py index 4c1b751196..e140b902ff 100644 --- a/src/calibre/ebooks/conversion/plugins/docx_output.py +++ b/src/calibre/ebooks/conversion/plugins/docx_output.py @@ -11,6 +11,7 @@ from calibre.customize.conversion import OutputFormatPlugin, OptionRecommendatio PAGE_SIZES = ['a0', 'a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'b0', 'b1', 'b2', 'b3', 'b4', 'b5', 'b6', 'legal', 'letter'] + class DOCXOutput(OutputFormatPlugin): name = 'DOCX Output' diff --git a/src/calibre/ebooks/conversion/plugins/epub_input.py b/src/calibre/ebooks/conversion/plugins/epub_input.py index 20a8b3ac26..c712cf9fd1 100644 --- a/src/calibre/ebooks/conversion/plugins/epub_input.py +++ b/src/calibre/ebooks/conversion/plugins/epub_input.py @@ -11,6 +11,7 @@ from calibre.customize.conversion import InputFormatPlugin, OptionRecommendation ADOBE_OBFUSCATION = 'http://ns.adobe.com/pdf/enc#RC' IDPF_OBFUSCATION = 'http://www.idpf.org/2008/embedding' + def decrypt_font_data(key, data, algorithm): is_adobe = algorithm == ADOBE_OBFUSCATION crypt_len = 1024 if is_adobe else 1040 @@ -19,11 +20,13 @@ def decrypt_font_data(key, data, algorithm): decrypt = bytes(bytearray(x^key.next() for x in crypt)) return decrypt + data[crypt_len:] + def decrypt_font(key, path, algorithm): with open(path, 'r+b') as f: data = decrypt_font_data(key, f.read(), algorithm) f.seek(0), f.truncate(), f.write(data) + class EPUBInput(InputFormatPlugin): name = 'EPUB Input' @@ -198,6 +201,7 @@ class EPUBInput(InputFormatPlugin): def find_opf(self): from lxml import etree + def attr(n, attr): for k, v in n.attrib.items(): if k.endswith(attr): diff --git a/src/calibre/ebooks/conversion/plugins/fb2_input.py b/src/calibre/ebooks/conversion/plugins/fb2_input.py index 424aa0b7e3..7ab83b4922 100644 --- a/src/calibre/ebooks/conversion/plugins/fb2_input.py +++ b/src/calibre/ebooks/conversion/plugins/fb2_input.py @@ -12,6 +12,7 @@ from calibre import guess_type FB2NS = 'http://www.gribuser.ru/xml/fictionbook/2.0' FB21NS = 'http://www.gribuser.ru/xml/fictionbook/2.1' + class FB2Input(InputFormatPlugin): name = 'FB2 Input' diff --git a/src/calibre/ebooks/conversion/plugins/fb2_output.py b/src/calibre/ebooks/conversion/plugins/fb2_output.py index f3bc939ea3..6073f87d55 100644 --- a/src/calibre/ebooks/conversion/plugins/fb2_output.py +++ b/src/calibre/ebooks/conversion/plugins/fb2_output.py @@ -8,6 +8,7 @@ import os from calibre.customize.conversion import OutputFormatPlugin, OptionRecommendation + class FB2Output(OutputFormatPlugin): name = 'FB2 Output' diff --git a/src/calibre/ebooks/conversion/plugins/html_input.py b/src/calibre/ebooks/conversion/plugins/html_input.py index 0c30005b60..34ef4c2841 100644 --- a/src/calibre/ebooks/conversion/plugins/html_input.py +++ b/src/calibre/ebooks/conversion/plugins/html_input.py @@ -18,9 +18,11 @@ from calibre.utils.localization import get_lang from calibre.utils.filenames import ascii_filename from calibre.utils.imghdr import what + def sanitize_file_name(x): return re.sub(r'[?&=;]', '_', ascii_filename(x)) + class HTMLInput(InputFormatPlugin): name = 'HTML Input' diff --git a/src/calibre/ebooks/conversion/plugins/html_output.py b/src/calibre/ebooks/conversion/plugins/html_output.py index 03d5cb78ff..aece8f36da 100644 --- a/src/calibre/ebooks/conversion/plugins/html_output.py +++ b/src/calibre/ebooks/conversion/plugins/html_output.py @@ -10,9 +10,11 @@ from calibre.customize.conversion import OutputFormatPlugin, OptionRecommendatio from calibre import CurrentDir from calibre.ptempfile import PersistentTemporaryDirectory + def relpath(*args): return _relpath(*args).replace(os.sep, '/') + class HTMLOutput(OutputFormatPlugin): name = 'HTML Output' diff --git a/src/calibre/ebooks/conversion/plugins/htmlz_input.py b/src/calibre/ebooks/conversion/plugins/htmlz_input.py index c33fa01460..9d0f09c1dd 100644 --- a/src/calibre/ebooks/conversion/plugins/htmlz_input.py +++ b/src/calibre/ebooks/conversion/plugins/htmlz_input.py @@ -11,6 +11,7 @@ import os from calibre import guess_type from calibre.customize.conversion import InputFormatPlugin + class HTMLZInput(InputFormatPlugin): name = 'HTLZ Input' diff --git a/src/calibre/ebooks/conversion/plugins/htmlz_output.py b/src/calibre/ebooks/conversion/plugins/htmlz_output.py index 9a22a2a3ec..09c669ab45 100644 --- a/src/calibre/ebooks/conversion/plugins/htmlz_output.py +++ b/src/calibre/ebooks/conversion/plugins/htmlz_output.py @@ -14,6 +14,7 @@ from calibre.customize.conversion import OutputFormatPlugin, \ OptionRecommendation from calibre.ptempfile import TemporaryDirectory + class HTMLZOutput(OutputFormatPlugin): name = 'HTMLZ Output' diff --git a/src/calibre/ebooks/conversion/plugins/lit_input.py b/src/calibre/ebooks/conversion/plugins/lit_input.py index d5643cf1a6..1a4579e200 100644 --- a/src/calibre/ebooks/conversion/plugins/lit_input.py +++ b/src/calibre/ebooks/conversion/plugins/lit_input.py @@ -8,6 +8,7 @@ __docformat__ = 'restructuredtext en' from calibre.customize.conversion import InputFormatPlugin + class LITInput(InputFormatPlugin): name = 'LIT Input' diff --git a/src/calibre/ebooks/conversion/plugins/lit_output.py b/src/calibre/ebooks/conversion/plugins/lit_output.py index 9d5c061c02..33fa1ddf7c 100644 --- a/src/calibre/ebooks/conversion/plugins/lit_output.py +++ b/src/calibre/ebooks/conversion/plugins/lit_output.py @@ -9,6 +9,7 @@ __docformat__ = 'restructuredtext en' from calibre.customize.conversion import OutputFormatPlugin + class LITOutput(OutputFormatPlugin): name = 'LIT Output' diff --git a/src/calibre/ebooks/conversion/plugins/lrf_input.py b/src/calibre/ebooks/conversion/plugins/lrf_input.py index fc4e2da26a..0c183d4a34 100644 --- a/src/calibre/ebooks/conversion/plugins/lrf_input.py +++ b/src/calibre/ebooks/conversion/plugins/lrf_input.py @@ -9,6 +9,7 @@ __docformat__ = 'restructuredtext en' import os, sys from calibre.customize.conversion import InputFormatPlugin + class LRFInput(InputFormatPlugin): name = 'LRF Input' diff --git a/src/calibre/ebooks/conversion/plugins/lrf_output.py b/src/calibre/ebooks/conversion/plugins/lrf_output.py index 3d8f6ae600..6b64eedf22 100644 --- a/src/calibre/ebooks/conversion/plugins/lrf_output.py +++ b/src/calibre/ebooks/conversion/plugins/lrf_output.py @@ -11,6 +11,7 @@ import sys, os from calibre.customize.conversion import OutputFormatPlugin from calibre.customize.conversion import OptionRecommendation + class LRFOptions(object): def __init__(self, output, opts, oeb): @@ -83,6 +84,7 @@ class LRFOptions(object): 'text_size_multiplier_for_rendered_tables'): setattr(self, x, getattr(opts, x)) + class LRFOutput(OutputFormatPlugin): name = 'LRF Output' diff --git a/src/calibre/ebooks/conversion/plugins/mobi_input.py b/src/calibre/ebooks/conversion/plugins/mobi_input.py index 3a0623da46..a0c91cd144 100644 --- a/src/calibre/ebooks/conversion/plugins/mobi_input.py +++ b/src/calibre/ebooks/conversion/plugins/mobi_input.py @@ -7,6 +7,7 @@ import os from calibre.customize.conversion import InputFormatPlugin + class MOBIInput(InputFormatPlugin): name = 'MOBI Input' diff --git a/src/calibre/ebooks/conversion/plugins/mobi_output.py b/src/calibre/ebooks/conversion/plugins/mobi_output.py index c32e64f388..f4f23b29c7 100644 --- a/src/calibre/ebooks/conversion/plugins/mobi_output.py +++ b/src/calibre/ebooks/conversion/plugins/mobi_output.py @@ -9,6 +9,7 @@ __docformat__ = 'restructuredtext en' from calibre.customize.conversion import (OutputFormatPlugin, OptionRecommendation) + def remove_html_cover(oeb, log): from calibre.ebooks.oeb.base import OEB_DOCS @@ -27,12 +28,14 @@ def remove_html_cover(oeb, log): if item.media_type in OEB_DOCS: oeb.manifest.remove(item) + def extract_mobi(output_path, opts): if opts.extract_to is not None: from calibre.ebooks.mobi.debug.main import inspect_mobi ddir = opts.extract_to inspect_mobi(output_path, ddir=ddir) + class MOBIOutput(OutputFormatPlugin): name = 'MOBI Output' @@ -261,6 +264,7 @@ class MOBIOutput(OutputFormatPlugin): for td in cols: td.tag = XHTML('span' if cols else 'div') + class AZW3Output(OutputFormatPlugin): name = 'AZW3 Output' diff --git a/src/calibre/ebooks/conversion/plugins/odt_input.py b/src/calibre/ebooks/conversion/plugins/odt_input.py index d44d3b06de..0da5226c57 100644 --- a/src/calibre/ebooks/conversion/plugins/odt_input.py +++ b/src/calibre/ebooks/conversion/plugins/odt_input.py @@ -9,6 +9,7 @@ Convert an ODT file into a Open Ebook from calibre.customize.conversion import InputFormatPlugin + class ODTInput(InputFormatPlugin): name = 'ODT Input' diff --git a/src/calibre/ebooks/conversion/plugins/oeb_output.py b/src/calibre/ebooks/conversion/plugins/oeb_output.py index 25c4bbc052..b7b50a0034 100644 --- a/src/calibre/ebooks/conversion/plugins/oeb_output.py +++ b/src/calibre/ebooks/conversion/plugins/oeb_output.py @@ -10,6 +10,7 @@ from calibre.customize.conversion import (OutputFormatPlugin, OptionRecommendation) from calibre import CurrentDir + class OEBOutput(OutputFormatPlugin): name = 'OEB Output' diff --git a/src/calibre/ebooks/conversion/plugins/pdb_input.py b/src/calibre/ebooks/conversion/plugins/pdb_input.py index cc57974fdc..ea4910a366 100644 --- a/src/calibre/ebooks/conversion/plugins/pdb_input.py +++ b/src/calibre/ebooks/conversion/plugins/pdb_input.py @@ -8,6 +8,7 @@ import os from calibre.customize.conversion import InputFormatPlugin + class PDBInput(InputFormatPlugin): name = 'PDB Input' diff --git a/src/calibre/ebooks/conversion/plugins/pdb_output.py b/src/calibre/ebooks/conversion/plugins/pdb_output.py index af0fb95801..b277ab353d 100644 --- a/src/calibre/ebooks/conversion/plugins/pdb_output.py +++ b/src/calibre/ebooks/conversion/plugins/pdb_output.py @@ -10,6 +10,7 @@ from calibre.customize.conversion import OutputFormatPlugin, \ OptionRecommendation from calibre.ebooks.pdb import PDBError, get_writer, ALL_FORMAT_WRITERS + class PDBOutput(OutputFormatPlugin): name = 'PDB Output' diff --git a/src/calibre/ebooks/conversion/plugins/pdf_input.py b/src/calibre/ebooks/conversion/plugins/pdf_input.py index 0342b3b218..3b4f352fb1 100644 --- a/src/calibre/ebooks/conversion/plugins/pdf_input.py +++ b/src/calibre/ebooks/conversion/plugins/pdf_input.py @@ -8,6 +8,7 @@ import os from calibre.customize.conversion import InputFormatPlugin, OptionRecommendation + class PDFInput(InputFormatPlugin): name = 'PDF Input' diff --git a/src/calibre/ebooks/conversion/plugins/pdf_output.py b/src/calibre/ebooks/conversion/plugins/pdf_output.py index ad97995c37..c0b54f3879 100644 --- a/src/calibre/ebooks/conversion/plugins/pdf_output.py +++ b/src/calibre/ebooks/conversion/plugins/pdf_output.py @@ -21,7 +21,9 @@ UNITS = ['millimeter', 'centimeter', 'point', 'inch' , 'pica' , 'didot', PAPER_SIZES = [u'a0', u'a1', u'a2', u'a3', u'a4', u'a5', u'a6', u'b0', u'b1', u'b2', u'b3', u'b4', u'b5', u'b6', u'legal', u'letter'] + class PDFMetadata(object): # {{{ + def __init__(self, mi=None): from calibre import force_unicode from calibre.ebooks.metadata import authors_to_string @@ -42,6 +44,7 @@ class PDFMetadata(object): # {{{ self.author = force_unicode(self.author) # }}} + class PDFOutput(OutputFormatPlugin): name = 'PDF Output' diff --git a/src/calibre/ebooks/conversion/plugins/pml_input.py b/src/calibre/ebooks/conversion/plugins/pml_input.py index b61ac2116c..9c8e7a9a62 100644 --- a/src/calibre/ebooks/conversion/plugins/pml_input.py +++ b/src/calibre/ebooks/conversion/plugins/pml_input.py @@ -11,6 +11,7 @@ import shutil from calibre.customize.conversion import InputFormatPlugin from calibre.ptempfile import TemporaryDirectory + class PMLInput(InputFormatPlugin): name = 'PML Input' diff --git a/src/calibre/ebooks/conversion/plugins/pml_output.py b/src/calibre/ebooks/conversion/plugins/pml_output.py index 2df1a936a8..939d1653dc 100644 --- a/src/calibre/ebooks/conversion/plugins/pml_output.py +++ b/src/calibre/ebooks/conversion/plugins/pml_output.py @@ -10,6 +10,7 @@ from calibre.customize.conversion import (OutputFormatPlugin, OptionRecommendation) from calibre.ptempfile import TemporaryDirectory + class PMLOutput(OutputFormatPlugin): name = 'PML Output' diff --git a/src/calibre/ebooks/conversion/plugins/rb_input.py b/src/calibre/ebooks/conversion/plugins/rb_input.py index 8641f6d91a..eb61e062cb 100644 --- a/src/calibre/ebooks/conversion/plugins/rb_input.py +++ b/src/calibre/ebooks/conversion/plugins/rb_input.py @@ -8,6 +8,7 @@ import os from calibre.customize.conversion import InputFormatPlugin + class RBInput(InputFormatPlugin): name = 'RB Input' diff --git a/src/calibre/ebooks/conversion/plugins/rb_output.py b/src/calibre/ebooks/conversion/plugins/rb_output.py index 992843719c..5e314a557f 100644 --- a/src/calibre/ebooks/conversion/plugins/rb_output.py +++ b/src/calibre/ebooks/conversion/plugins/rb_output.py @@ -8,6 +8,7 @@ import os from calibre.customize.conversion import OutputFormatPlugin, OptionRecommendation + class RBOutput(OutputFormatPlugin): name = 'RB Output' diff --git a/src/calibre/ebooks/conversion/plugins/recipe_input.py b/src/calibre/ebooks/conversion/plugins/recipe_input.py index 27f4286cc2..e16b8f7bee 100644 --- a/src/calibre/ebooks/conversion/plugins/recipe_input.py +++ b/src/calibre/ebooks/conversion/plugins/recipe_input.py @@ -12,9 +12,11 @@ from calibre.customize.conversion import InputFormatPlugin, OptionRecommendation from calibre.constants import numeric_version from calibre import walk + class RecipeDisabled(Exception): pass + class RecipeInput(InputFormatPlugin): name = 'Recipe Input' diff --git a/src/calibre/ebooks/conversion/plugins/rtf_output.py b/src/calibre/ebooks/conversion/plugins/rtf_output.py index ae9e1ea566..b350242271 100644 --- a/src/calibre/ebooks/conversion/plugins/rtf_output.py +++ b/src/calibre/ebooks/conversion/plugins/rtf_output.py @@ -8,6 +8,7 @@ import os from calibre.customize.conversion import OutputFormatPlugin + class RTFOutput(OutputFormatPlugin): name = 'RTF Output' diff --git a/src/calibre/ebooks/conversion/plugins/snb_input.py b/src/calibre/ebooks/conversion/plugins/snb_input.py index 4b37187cce..4fc4b6c95d 100755 --- a/src/calibre/ebooks/conversion/plugins/snb_input.py +++ b/src/calibre/ebooks/conversion/plugins/snb_input.py @@ -12,9 +12,11 @@ from calibre.utils.filenames import ascii_filename HTML_TEMPLATE = u'%s\n%s\n' + def html_encode(s): return s.replace(u'&', u'&').replace(u'<', u'<').replace(u'>', u'>').replace(u'"', u'"').replace(u"'", u''').replace(u'\n', u'
').replace(u' ', u' ') # noqa + class SNBInput(InputFormatPlugin): name = 'SNB Input' diff --git a/src/calibre/ebooks/conversion/plugins/snb_output.py b/src/calibre/ebooks/conversion/plugins/snb_output.py index ccf9affdde..c4900b0621 100644 --- a/src/calibre/ebooks/conversion/plugins/snb_output.py +++ b/src/calibre/ebooks/conversion/plugins/snb_output.py @@ -10,6 +10,7 @@ from calibre.customize.conversion import OutputFormatPlugin, OptionRecommendatio from calibre.ptempfile import TemporaryDirectory from calibre.constants import __appname__, __version__ + class SNBOutput(OutputFormatPlugin): name = 'SNB Output' @@ -254,6 +255,7 @@ if __name__ == '__main__': from calibre.ebooks.oeb.base import OEBBook from calibre.ebooks.conversion.preprocess import HTMLPreProcessor from calibre.customize.profiles import HanlinV3Output + class OptionValues(object): pass diff --git a/src/calibre/ebooks/conversion/plugins/tcr_input.py b/src/calibre/ebooks/conversion/plugins/tcr_input.py index 5ee34285bd..13fc22b87b 100644 --- a/src/calibre/ebooks/conversion/plugins/tcr_input.py +++ b/src/calibre/ebooks/conversion/plugins/tcr_input.py @@ -8,6 +8,7 @@ from cStringIO import StringIO from calibre.customize.conversion import InputFormatPlugin + class TCRInput(InputFormatPlugin): name = 'TCR Input' diff --git a/src/calibre/ebooks/conversion/plugins/tcr_output.py b/src/calibre/ebooks/conversion/plugins/tcr_output.py index 5e81b4a0e5..fa4ba7b25d 100644 --- a/src/calibre/ebooks/conversion/plugins/tcr_output.py +++ b/src/calibre/ebooks/conversion/plugins/tcr_output.py @@ -9,6 +9,7 @@ import os from calibre.customize.conversion import OutputFormatPlugin, \ OptionRecommendation + class TCROutput(OutputFormatPlugin): name = 'TCR Output' diff --git a/src/calibre/ebooks/conversion/plugins/txt_input.py b/src/calibre/ebooks/conversion/plugins/txt_input.py index 1fdc05d729..d7990cb8f4 100644 --- a/src/calibre/ebooks/conversion/plugins/txt_input.py +++ b/src/calibre/ebooks/conversion/plugins/txt_input.py @@ -24,6 +24,7 @@ MD_EXTENSIONS = { 'wikilinks': _('Wiki style links'), } + class TXTInput(InputFormatPlugin): name = 'TXT Input' diff --git a/src/calibre/ebooks/conversion/plugins/txt_output.py b/src/calibre/ebooks/conversion/plugins/txt_output.py index 99bc2cbc92..a5ca0fd88a 100644 --- a/src/calibre/ebooks/conversion/plugins/txt_output.py +++ b/src/calibre/ebooks/conversion/plugins/txt_output.py @@ -14,6 +14,7 @@ from calibre.ptempfile import TemporaryDirectory, TemporaryFile NEWLINE_TYPES = ['system', 'unix', 'old_mac', 'windows'] + class TXTOutput(OutputFormatPlugin): name = 'TXT Output' diff --git a/src/calibre/ebooks/conversion/plumber.py b/src/calibre/ebooks/conversion/plumber.py index 04ce4ecd27..1a3e6e5503 100644 --- a/src/calibre/ebooks/conversion/plumber.py +++ b/src/calibre/ebooks/conversion/plumber.py @@ -40,15 +40,18 @@ various stages of conversion. The stages are: ''' + def supported_input_formats(): fmts = available_input_formats() for x in ('zip', 'rar', 'oebzip'): fmts.add(x) return fmts + class OptionValues(object): pass + class CompositeProgressReporter(object): def __init__(self, global_min, global_max, global_reporter): @@ -62,6 +65,7 @@ class CompositeProgressReporter(object): ARCHIVE_FMTS = ('zip', 'rar', 'oebzip') + class Plumber(object): ''' @@ -1230,10 +1234,13 @@ OptionRecommendation(name='search_replace', # This has to be global as create_oebbook can be called from other locations # (for example in the html input plugin) regex_wizard_callback = None + + def set_regex_wizard_callback(f): global regex_wizard_callback regex_wizard_callback = f + def create_oebbook(log, path_or_stream, opts, reader=None, encoding='utf-8', populate=True, for_regex_wizard=False, specialize=None): ''' diff --git a/src/calibre/ebooks/conversion/preprocess.py b/src/calibre/ebooks/conversion/preprocess.py index 182aee4567..eba44661eb 100644 --- a/src/calibre/ebooks/conversion/preprocess.py +++ b/src/calibre/ebooks/conversion/preprocess.py @@ -42,11 +42,13 @@ LIGATURES = { _ligpat = re.compile(u'|'.join(LIGATURES)) + def sanitize_head(match): x = match.group(1) x = _span_pat.sub('', x) return '\n%s\n' % x + def chap_head(match): chap = match.group('chap') title = match.group('title') @@ -55,6 +57,7 @@ def chap_head(match): else: return '

'+chap+'

\n

'+title+'

\n' + def wrap_lines(match): ital = match.group('ital') if not ital: @@ -62,6 +65,7 @@ def wrap_lines(match): else: return ital+' ' + def smarten_punctuation(html, log=None): from calibre.utils.smartypants import smartyPants from calibre.ebooks.chardet import substitute_entites @@ -78,6 +82,7 @@ def smarten_punctuation(html, log=None): html = html.replace(stop, '-->') return substitute_entites(html) + class DocAnalysis(object): ''' Provides various text analysis functions to determine how the document is structured. @@ -182,6 +187,7 @@ class DocAnalysis(object): # print str(maxValue)+" of the lines were in one bucket" return True + class Dehyphenator(object): ''' Analyzes words to determine whether hyphens should be retained/removed. Uses the document @@ -276,6 +282,7 @@ class Dehyphenator(object): html = intextmatch.sub(self.dehyphenate, html) return html + class CSSPreProcessor(object): # Remove some of the broken CSS Microsoft products @@ -320,6 +327,7 @@ class CSSPreProcessor(object): return u'\n'.join(ans) + class HTMLPreProcessor(object): PREPROCESS = [ @@ -489,6 +497,7 @@ class HTMLPreProcessor(object): (re.compile('<]*?id=subtitle[^><]*?>(.*?)', re.IGNORECASE|re.DOTALL), lambda match : '

%s

'%(match.group(1),)), ] + def __init__(self, log=None, extra_opts=None, regex_wizard_callback=None): self.log = log self.extra_opts = extra_opts @@ -530,6 +539,7 @@ class HTMLPreProcessor(object): user_sr_rules = {} # Function for processing search and replace + def do_search_replace(search_pattern, replace_txt): try: search_re = re.compile(search_pattern) diff --git a/src/calibre/ebooks/covers.py b/src/calibre/ebooks/covers.py index b92e9625aa..95bbf86135 100644 --- a/src/calibre/ebooks/covers.py +++ b/src/calibre/ebooks/covers.py @@ -55,12 +55,15 @@ re(authors, '&&', '&') Prefs = namedtuple('Prefs', ' '.join(sorted(cprefs.defaults))) _use_roman = None + + def get_use_roman(): global _use_roman if _use_roman is None: return config['use_roman_numerals_for_series_number'] return _use_roman + def set_use_roman(val): global _use_roman _use_roman = bool(val) @@ -71,6 +74,7 @@ def set_use_roman(val): Point = namedtuple('Point', 'x y') + def parse_text_formatting(text): pos = 0 tokens = [] @@ -122,6 +126,7 @@ def parse_text_formatting(text): formats.append(r) return text, formats + class Block(object): def __init__(self, text='', width=0, font=None, img=None, max_height=100, align=Qt.AlignCenter): @@ -168,6 +173,7 @@ class Block(object): def position(self): def fget(self): return self._position + def fset(self, (x, y)): self._position = Point(x, y) if self.layouts: @@ -194,6 +200,7 @@ class Block(object): l.draw(painter, QPointF()) painter.restore() + def layout_text(prefs, img, title, subtitle, footer, max_height, style): width = img.width() - 2 * style.hmargin title, subtitle, footer = title, subtitle, footer @@ -223,38 +230,47 @@ def layout_text(prefs, img, title, subtitle, footer, max_height, style): # }}} # Format text using templates {{{ + + def sanitize(s): return unicodedata.normalize('NFC', clean_xml_chars(clean_ascii_chars(force_unicode(s or '')))) _formatter = None _template_cache = {} + def escape_formatting(val): return val.replace('&', '&').replace('<', '<').replace('>', '>') + def unescape_formatting(val): return val.replace('<', '<').replace('>', '>').replace('&', '&') + class Formatter(SafeFormat): def get_value(self, orig_key, args, kwargs): ans = SafeFormat.get_value(self, orig_key, args, kwargs) return escape_formatting(ans) + def formatter(): global _formatter if _formatter is None: _formatter = Formatter() return _formatter + def format_fields(mi, prefs): f = formatter() + def safe_format(field): return f.safe_format( getattr(prefs, field), mi, _('Template error'), mi, template_cache=_template_cache ) return map(safe_format, ('title_template', 'subtitle_template', 'footer_template')) + @contextmanager def preserve_fields(obj, fields): if isinstance(fields, basestring): @@ -270,6 +286,7 @@ def preserve_fields(obj, fields): else: setattr(obj, f, val) + def format_text(mi, prefs): with preserve_fields(mi, 'authors formatted_series_index'): mi.authors = [a for a in mi.authors if a != _('Unknown')] @@ -280,6 +297,7 @@ def format_text(mi, prefs): # Colors {{{ ColorTheme = namedtuple('ColorTheme', 'color1 color2 contrast_color1 contrast_color2') + def to_theme(x): return {k:v for k, v in zip(ColorTheme._fields[:4], x.split())} @@ -297,6 +315,7 @@ def theme_to_colors(theme): colors = {k:QColor('#' + theme[k]) for k in ColorTheme._fields} return ColorTheme(**colors) + def load_color_themes(prefs): t = default_color_themes.copy() t.update(prefs.color_themes) @@ -307,6 +326,7 @@ def load_color_themes(prefs): ans = [theme_to_colors(v) for k, v in default_color_themes.iteritems()] return ans + def color(color_theme, name): ans = getattr(color_theme, name) if not ans.isValid(): @@ -316,6 +336,8 @@ def color(color_theme, name): # }}} # Styles {{{ + + class Style(object): TITLE_ALIGN = SUBTITLE_ALIGN = FOOTER_ALIGN = Qt.AlignHCenter | Qt.AlignTop @@ -334,6 +356,7 @@ class Style(object): self.ccolor1 = color(color_theme, 'contrast_color1') self.ccolor2 = color(color_theme, 'contrast_color2') + class Cross(Style): NAME = 'The Cross' @@ -354,6 +377,7 @@ class Cross(Style): painter.fillRect(r, self.color2) return self.ccolor2, self.ccolor2, self.ccolor1 + class Half(Style): NAME = 'Half and Half' @@ -365,9 +389,11 @@ class Half(Style): painter.fillRect(rect, QBrush(g)) return self.ccolor1, self.ccolor1, self.ccolor1 + def rotate_vector(angle, x, y): return x * cos(angle) - y * sin(angle), x * sin(angle) + y * cos(angle) + def draw_curved_line(painter_path, dx, dy, c1_frac, c1_amp, c2_frac, c2_amp): length = sqrt(dx * dx + dy * dy) angle = atan2(dy, dx) @@ -376,6 +402,7 @@ def draw_curved_line(painter_path, dx, dy, c1_frac, c1_amp, c2_frac, c2_amp): pos = painter_path.currentPosition() painter_path.cubicTo(pos + c1, pos + c2, pos + QPointF(dx, dy)) + class Banner(Style): NAME = 'Banner' @@ -445,6 +472,7 @@ class Banner(Style): painter.restore() return self.ccolor2, self.ccolor2, self.ccolor1 + class Ornamental(Style): NAME = 'Ornamental' @@ -475,6 +503,7 @@ class Ornamental(Style): pen = p.pen() pen.setColor(self.ccolor1) p.setPen(pen) + def corner(): b = QBrush(self.ccolor1) p.fillPath(path, b) @@ -503,6 +532,7 @@ class Ornamental(Style): return self.ccolor2, self.ccolor2, self.ccolor1 + class Blocks(Style): NAME = 'Blocks' @@ -520,12 +550,14 @@ class Blocks(Style): painter.fillRect(r, self.color2) return self.ccolor1, self.ccolor1, self.ccolor2 + def all_styles(): return set( x.NAME for x in globals().itervalues() if isinstance(x, type) and issubclass(x, Style) and x is not Style ) + def load_styles(prefs, respect_disabled=True): disabled = frozenset(prefs.disabled_styles) if respect_disabled else () ans = tuple(x for x in globals().itervalues() if @@ -538,10 +570,12 @@ def load_styles(prefs, respect_disabled=True): # }}} + def init_environment(): ensure_app() load_builtin_fonts() + def generate_cover(mi, prefs=None, as_qimage=False): init_environment() prefs = prefs or cprefs @@ -565,6 +599,7 @@ def generate_cover(mi, prefs=None, as_qimage=False): return img return pixmap_to_data(img) + def override_prefs(base_prefs, **overrides): ans = {k:overrides.get(k, base_prefs[k]) for k in cprefs.defaults} override_color_theme = overrides.get('override_color_theme') @@ -582,6 +617,7 @@ def override_prefs(base_prefs, **overrides): return ans + def create_cover(title, authors, series=None, series_index=1, prefs=None, as_qimage=False): ' Create a cover from the specified title, author and series. Any user set' ' templates are ignored, to ensure that the specified metadata is used. ' @@ -593,6 +629,7 @@ def create_cover(title, authors, series=None, series_index=1, prefs=None, as_qim prefs or cprefs, title_template=d['title_template'], subtitle_template=d['subtitle_template'], footer_template=d['footer_template']) return generate_cover(mi, prefs=prefs, as_qimage=as_qimage) + def calibre_cover2(title, author_string='', series_string='', prefs=None, as_qimage=False, logo_path=None): init_environment() title, subtitle, footer = '' + escape_formatting(title), '' + escape_formatting(series_string), '' + escape_formatting(author_string) @@ -605,8 +642,10 @@ def calibre_cover2(title, author_string='', series_string='', prefs=None, as_qim img.fill(Qt.white) # colors = to_theme('ffffff ffffff 000000 000000') color_theme = theme_to_colors(fallback_colors) + class CalibeLogoStyle(Style): NAME = GUI_NAME = 'calibre' + def __call__(self, painter, rect, color_theme, title_block, subtitle_block, footer_block): top = title_block.position.y + 10 extra_spacing = subtitle_block.line_spacing // 2 if subtitle_block.line_spacing else title_block.line_spacing // 3 @@ -636,6 +675,7 @@ def calibre_cover2(title, author_string='', series_string='', prefs=None, as_qim return img return pixmap_to_data(img) + def message_image(text, width=500, height=400, font_size=20): init_environment() img = QImage(width, height, QImage.Format_ARGB32) @@ -649,10 +689,12 @@ def message_image(text, width=500, height=400, font_size=20): p.end() return pixmap_to_data(img) + def scale_cover(prefs, scale): for x in ('cover_width', 'cover_height', 'title_font_size', 'subtitle_font_size', 'footer_font_size'): prefs[x] = int(scale * prefs[x]) + def generate_masthead(title, output_path=None, width=600, height=60, as_qimage=False, font_family=None): init_environment() font_family = font_family or cprefs['title_font_family'] or 'Liberation Serif' @@ -674,6 +716,7 @@ def generate_masthead(title, output_path=None, width=600, height=60, as_qimage=F with open(output_path, 'wb') as f: f.write(data) + def test(scale=0.25): from PyQt5.Qt import QLabel, QApplication, QPixmap, QMainWindow, QWidget, QScrollArea, QGridLayout app = QApplication([]) diff --git a/src/calibre/ebooks/css_transform_rules.py b/src/calibre/ebooks/css_transform_rules.py index e1c3c12077..577bee9cb0 100644 --- a/src/calibre/ebooks/css_transform_rules.py +++ b/src/calibre/ebooks/css_transform_rules.py @@ -20,6 +20,7 @@ def compile_pat(pat): REGEX_FLAGS = regex.VERSION1 | regex.UNICODE | regex.IGNORECASE return regex.compile(pat, flags=REGEX_FLAGS) + def all_properties(decl): ' This is needed because CSSStyleDeclaration.getProperties(None, all=True) does not work and is slower than it needs to be. ' for item in decl.seq: @@ -27,6 +28,7 @@ def all_properties(decl): if isinstance(p, Property): yield p + class StyleDeclaration(object): def __init__(self, css_declaration): @@ -97,6 +99,7 @@ class StyleDeclaration(object): operator_map = {'==':'eq', '!=': 'ne', '<=':'le', '<':'lt', '>=':'ge', '>':'gt', '-':'sub', '+': 'add', '*':'mul', '/':'truediv'} + def unit_convert(value, unit, dpi=96.0, body_font_size=12): result = None if unit == 'px': @@ -117,6 +120,7 @@ def unit_convert(value, unit, dpi=96.0, body_font_size=12): result = value * 0.708661417325 return result + def parse_css_length_or_number(raw, default_unit=None): if isinstance(raw, (int, long, float)): return raw, default_unit @@ -125,6 +129,7 @@ def parse_css_length_or_number(raw, default_unit=None): except Exception: return parse_css_length(raw) + def numeric_match(value, unit, pts, op, raw): try: v, u = parse_css_length_or_number(raw) @@ -141,6 +146,7 @@ def numeric_match(value, unit, pts, op, raw): return False return op(p, pts) + def transform_number(val, op, raw): try: v, u = parse_css_length_or_number(raw, default_unit='') @@ -153,6 +159,7 @@ def transform_number(val, op, raw): v = int(v) return str(v) + u + class Rule(object): def __init__(self, property='color', match_type='*', query='', action='remove', action_data=''): @@ -226,6 +233,7 @@ MATCH_TYPE_MAP = OrderedDict(( allowed_keys = frozenset('property match_type query action action_data'.split()) + def validate_rule(rule): keys = frozenset(rule) extra = keys - allowed_keys @@ -281,9 +289,11 @@ def validate_rule(rule): return _('Invalid number'), _('%s is not a number') % ad return None, None + def compile_rules(serialized_rules): return [Rule(**r) for r in serialized_rules] + def transform_declaration(compiled_rules, decl): decl = StyleDeclaration(decl) changed = False @@ -292,6 +302,7 @@ def transform_declaration(compiled_rules, decl): changed = True return changed + def transform_sheet(compiled_rules, sheet): changed = False for rule in sheet.cssRules.rulesOfType(CSSRule.STYLE_RULE): @@ -299,6 +310,7 @@ def transform_sheet(compiled_rules, sheet): changed = True return changed + def transform_container(container, serialized_rules, names=()): from calibre.ebooks.oeb.polish.css import transform_css rules = compile_rules(serialized_rules) @@ -307,6 +319,7 @@ def transform_container(container, serialized_rules, names=()): transform_style=partial(transform_declaration, rules), names=names ) + def rule_to_text(rule): def get(prop): return rule.get(prop) or '' @@ -318,6 +331,7 @@ def rule_to_text(rule): text += get('action_data') return text + def export_rules(serialized_rules): lines = [] for rule in serialized_rules: @@ -326,6 +340,7 @@ def export_rules(serialized_rules): lines.append('') return '\n'.join(lines).encode('utf-8') + def import_rules(raw_data): import regex pat = regex.compile('\s*(\S+)\s*:\s*(.+)', flags=regex.VERSION1) @@ -350,6 +365,7 @@ def import_rules(raw_data): if current_rule: yield sanitize(current_rule) + def test(return_tests=False): # {{{ import unittest diff --git a/src/calibre/ebooks/djvu/djvu.py b/src/calibre/ebooks/djvu/djvu.py index 40dfc91edf..03de80acde 100644 --- a/src/calibre/ebooks/djvu/djvu.py +++ b/src/calibre/ebooks/djvu/djvu.py @@ -17,6 +17,7 @@ import struct from calibre.ebooks.djvu.djvubzzdec import BZZDecoder from calibre.constants import plugins + class DjvuChunk(object): def __init__(self, buf, start, end, align=True, bigendian=True, @@ -106,6 +107,7 @@ class DjvuChunk(object): for schunk in self._subchunks: schunk.dump(verbose=verbose, indent=indent+1, out=out, txtout=txtout) + class DJVUFile(object): def __init__(self, instream, verbose=0): @@ -121,6 +123,7 @@ class DJVUFile(object): def dump(self, outfile=None, maxlevel=0): self.dc.dump(out=outfile, maxlevel=maxlevel) + def main(): f = DJVUFile(open(sys.argv[-1], 'rb')) print (f.get_text(sys.stdout)) diff --git a/src/calibre/ebooks/djvu/djvubzzdec.py b/src/calibre/ebooks/djvu/djvubzzdec.py index 025ec7b606..407794520c 100644 --- a/src/calibre/ebooks/djvu/djvubzzdec.py +++ b/src/calibre/ebooks/djvu/djvubzzdec.py @@ -78,12 +78,16 @@ CTXIDS = 3 MAXLEN = 1024 ** 2 # Exception classes used by this module. + + class BZZDecoderError(Exception): """This exception is raised when BZZDecode runs into trouble """ + def __init__(self, msg): self.msg = msg + def __str__(self): return "BZZDecoderError: %s" % (self.msg) @@ -387,6 +391,7 @@ xmtf = ( ) # }}} + class BZZDecoder(): def __init__(self, infile, outfile): diff --git a/src/calibre/ebooks/docx/__init__.py b/src/calibre/ebooks/docx/__init__.py index a9db462305..35d65909c2 100644 --- a/src/calibre/ebooks/docx/__init__.py +++ b/src/calibre/ebooks/docx/__init__.py @@ -6,6 +6,7 @@ from __future__ import (unicode_literals, division, absolute_import, __license__ = 'GPL v3' __copyright__ = '2013, Kovid Goyal ' + class InvalidDOCX(ValueError): pass diff --git a/src/calibre/ebooks/docx/block_styles.py b/src/calibre/ebooks/docx/block_styles.py index fe62ea7641..23fdc6e252 100644 --- a/src/calibre/ebooks/docx/block_styles.py +++ b/src/calibre/ebooks/docx/block_styles.py @@ -8,10 +8,12 @@ __copyright__ = '2013, Kovid Goyal ' from collections import OrderedDict + class Inherit: pass inherit = Inherit() + def binary_property(parent, name, XPath, get): vals = XPath('./w:%s' % name)(parent) if not vals: @@ -19,17 +21,20 @@ def binary_property(parent, name, XPath, get): val = get(vals[0], 'w:val', 'on') return True if val in {'on', '1', 'true'} else False + def simple_color(col, auto='black'): if not col or col == 'auto' or len(col) != 6: return auto return '#'+col + def simple_float(val, mult=1.0): try: return float(val) * mult except (ValueError, TypeError, AttributeError, KeyError): pass + def twips(val, mult=0.05): ''' Parse val as either a pure number representing twentieths of a point or a number followed by the suffix pt, representing pts.''' try: @@ -76,6 +81,7 @@ LINE_STYLES = { # {{{ border_props = ('padding_%s', 'border_%s_width', 'border_%s_style', 'border_%s_color') border_edges = ('left', 'top', 'right', 'bottom', 'between') + def read_single_border(parent, edge, XPath, get): color = style = width = padding = None for elem in XPath('./w:%s' % edge)(parent): @@ -100,6 +106,7 @@ def read_single_border(parent, edge, XPath, get): pass return {p:v for p, v in zip(border_props, (padding, width, style, color))} + def read_border(parent, dest, XPath, get, border_edges=border_edges, name='pBdr'): vals = {k % edge:inherit for edge in border_edges for k in border_props} @@ -112,6 +119,7 @@ def read_border(parent, dest, XPath, get, border_edges=border_edges, name='pBdr' for key, val in vals.iteritems(): setattr(dest, key, val) + def border_to_css(edge, style, css): bs = getattr(style, 'border_%s_style' % edge) bc = getattr(style, 'border_%s_color' % edge) @@ -128,6 +136,7 @@ def border_to_css(edge, style, css): bw = '%.3gpt' % bw css['border-%s-width' % edge] = bw + def read_indent(parent, dest, XPath, get): padding_left = padding_right = text_indent = inherit for indent in XPath('./w:ind')(parent): @@ -154,6 +163,7 @@ def read_indent(parent, dest, XPath, get): setattr(dest, 'margin_right', padding_right) setattr(dest, 'text_indent', text_indent) + def read_justification(parent, dest, XPath, get): ans = inherit for jc in XPath('./w:jc[@w:val]')(parent): @@ -168,6 +178,7 @@ def read_justification(parent, dest, XPath, get): ans = {'start':'left'}.get(val, 'right') setattr(dest, 'text_align', ans) + def read_spacing(parent, dest, XPath, get): padding_top = padding_bottom = line_height = inherit for s in XPath('./w:spacing')(parent): @@ -191,6 +202,7 @@ def read_spacing(parent, dest, XPath, get): setattr(dest, 'margin_bottom', padding_bottom) setattr(dest, 'line_height', line_height) + def read_shd(parent, dest, XPath, get): ans = inherit for shd in XPath('./w:shd[@w:fill]')(parent): @@ -199,6 +211,7 @@ def read_shd(parent, dest, XPath, get): ans = simple_color(val, auto='transparent') setattr(dest, 'background_color', ans) + def read_numbering(parent, dest, XPath, get): lvl = num_id = None for np in XPath('./w:numPr')(parent): @@ -212,6 +225,7 @@ def read_numbering(parent, dest, XPath, get): val = (num_id, lvl) if num_id is not None or lvl is not None else inherit setattr(dest, 'numbering', val) + class Frame(object): all_attributes = ('drop_cap', 'h', 'w', 'h_anchor', 'h_rule', 'v_anchor', 'wrap', @@ -289,6 +303,7 @@ class Frame(object): def __ne__(self, other): return not self.__eq__(other) + def read_frame(parent, dest, XPath, get): ans = inherit for fp in XPath('./w:framePr')(parent): @@ -297,6 +312,7 @@ def read_frame(parent, dest, XPath, get): # }}} + class ParagraphStyle(object): all_properties = ( diff --git a/src/calibre/ebooks/docx/char_styles.py b/src/calibre/ebooks/docx/char_styles.py index 51f534da7e..df35af6039 100644 --- a/src/calibre/ebooks/docx/char_styles.py +++ b/src/calibre/ebooks/docx/char_styles.py @@ -11,6 +11,8 @@ from calibre.ebooks.docx.block_styles import ( # noqa inherit, simple_color, LINE_STYLES, simple_float, binary_property, read_shd) # Read from XML {{{ + + def read_text_border(parent, dest, XPath, get): border_color = border_style = border_width = padding = inherit elems = XPath('./w:bdr')(parent) @@ -45,6 +47,7 @@ def read_text_border(parent, dest, XPath, get): setattr(dest, 'border_width', border_width) setattr(dest, 'padding', padding) + def read_color(parent, dest, XPath, get): ans = inherit for col in XPath('./w:color[@w:val]')(parent): @@ -54,12 +57,14 @@ def read_color(parent, dest, XPath, get): ans = simple_color(val) setattr(dest, 'color', ans) + def convert_highlight_color(val): return { 'darkBlue': '#000080', 'darkCyan': '#008080', 'darkGray': '#808080', 'darkGreen': '#008000', 'darkMagenta': '#800080', 'darkRed': '#800000', 'darkYellow': '#808000', 'lightGray': '#c0c0c0'}.get(val, val) + def read_highlight(parent, dest, XPath, get): ans = inherit for col in XPath('./w:highlight[@w:val]')(parent): @@ -73,6 +78,7 @@ def read_highlight(parent, dest, XPath, get): ans = val setattr(dest, 'highlight', ans) + def read_lang(parent, dest, XPath, get): ans = inherit for col in XPath('./w:lang[@w:val]')(parent): @@ -90,6 +96,7 @@ def read_lang(parent, dest, XPath, get): ans = val setattr(dest, 'lang', ans) + def read_letter_spacing(parent, dest, XPath, get): ans = inherit for col in XPath('./w:spacing[@w:val]')(parent): @@ -98,6 +105,7 @@ def read_letter_spacing(parent, dest, XPath, get): ans = val setattr(dest, 'letter_spacing', ans) + def read_sz(parent, dest, XPath, get): ans = inherit for col in XPath('./w:sz[@w:val]')(parent): @@ -106,6 +114,7 @@ def read_sz(parent, dest, XPath, get): ans = val setattr(dest, 'font_size', ans) + def read_underline(parent, dest, XPath, get): ans = inherit for col in XPath('./w:u[@w:val]')(parent): @@ -114,6 +123,7 @@ def read_underline(parent, dest, XPath, get): ans = val if val == 'none' else 'underline' setattr(dest, 'text_decoration', ans) + def read_vert_align(parent, dest, XPath, get): ans = inherit for col in XPath('./w:vertAlign[@w:val]')(parent): @@ -122,6 +132,7 @@ def read_vert_align(parent, dest, XPath, get): ans = val setattr(dest, 'vert_align', ans) + def read_position(parent, dest, XPath, get): ans = inherit for col in XPath('./w:position[@w:val]')(parent): @@ -132,6 +143,7 @@ def read_position(parent, dest, XPath, get): pass setattr(dest, 'position', ans) + def read_font_family(parent, dest, XPath, get): ans = inherit for col in XPath('./w:rFonts')(parent): @@ -145,6 +157,7 @@ def read_font_family(parent, dest, XPath, get): setattr(dest, 'font_family', ans) # }}} + class RunStyle(object): all_properties = { diff --git a/src/calibre/ebooks/docx/cleanup.py b/src/calibre/ebooks/docx/cleanup.py index 2a2dd5b12a..04bf96e441 100644 --- a/src/calibre/ebooks/docx/cleanup.py +++ b/src/calibre/ebooks/docx/cleanup.py @@ -10,6 +10,7 @@ import os NBSP = '\xa0' + def mergeable(previous, current): if previous.tail or current.tail: return False @@ -25,6 +26,7 @@ def mergeable(previous, current): except StopIteration: return False + def append_text(parent, text): if len(parent) > 0: parent[-1].tail = (parent[-1].tail or '') + text @@ -88,6 +90,7 @@ def lift(span): else: add_text(last_child, 'tail', span.tail) + def before_count(root, tag, limit=10): body = root.xpath('//body[1]') if not body: @@ -100,6 +103,7 @@ def before_count(root, tag, limit=10): if ans > limit: return limit + def cleanup_markup(log, root, styles, dest_dir, detect_cover, XPath): # Move
s outside paragraphs, if possible. pancestor = XPath('|'.join('ancestor::%s[1]' % x for x in ('p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'))) diff --git a/src/calibre/ebooks/docx/container.py b/src/calibre/ebooks/docx/container.py index 024c55a0a7..448ceb97c7 100644 --- a/src/calibre/ebooks/docx/container.py +++ b/src/calibre/ebooks/docx/container.py @@ -21,10 +21,13 @@ from calibre.utils.logging import default_log from calibre.utils.zipfile import ZipFile from calibre.ebooks.oeb.parse_utils import RECOVER_PARSER + def fromstring(raw, parser=RECOVER_PARSER): return etree.fromstring(raw, parser=parser) # Read metadata {{{ + + def read_doc_props(raw, mi, XPath): root = fromstring(raw) titles = XPath('//dc:title')(root) @@ -66,12 +69,14 @@ def read_doc_props(raw, mi, XPath): if langs: mi.languages = langs + def read_app_props(raw, mi): root = fromstring(raw) company = root.xpath('//*[local-name()="Company"]') if company and company[0].text and company[0].text.strip(): mi.publisher = company[0].text.strip() + def read_default_style_language(raw, mi, XPath): root = fromstring(raw) for lang in XPath('/w:styles/w:docDefaults/w:rPrDefault/w:rPr/w:lang/@w:val')(root): @@ -81,6 +86,7 @@ def read_default_style_language(raw, mi, XPath): break # }}} + class DOCX(object): def __init__(self, path_or_stream, log=None, extract=True): diff --git a/src/calibre/ebooks/docx/dump.py b/src/calibre/ebooks/docx/dump.py index f0699ef223..c1edfeccf4 100644 --- a/src/calibre/ebooks/docx/dump.py +++ b/src/calibre/ebooks/docx/dump.py @@ -13,6 +13,7 @@ from lxml import etree from calibre import walk from calibre.utils.zipfile import ZipFile + def pretty_all_xml_in_dir(path): for f in walk(path): if f.endswith('.xml') or f.endswith('.rels'): @@ -24,6 +25,7 @@ def pretty_all_xml_in_dir(path): stream.truncate() stream.write(etree.tostring(root, pretty_print=True, encoding='utf-8', xml_declaration=True)) + def do_dump(path, dest): if os.path.exists(dest): shutil.rmtree(dest) @@ -31,6 +33,7 @@ def do_dump(path, dest): zf.extractall(dest) pretty_all_xml_in_dir(dest) + def dump(path): dest = os.path.splitext(os.path.basename(path))[0] dest += '-dumped' diff --git a/src/calibre/ebooks/docx/fields.py b/src/calibre/ebooks/docx/fields.py index 9c774a0791..3d7e457019 100644 --- a/src/calibre/ebooks/docx/fields.py +++ b/src/calibre/ebooks/docx/fields.py @@ -10,6 +10,7 @@ import re from calibre.ebooks.docx.index import process_index, polish_index_markup + class Field(object): def __init__(self, start): @@ -47,6 +48,7 @@ scanner = re.Scanner([ null = object() + def parser(name, field_map, default_field_name=None): field_map = dict((x.split(':') for x in field_map.split())) @@ -224,6 +226,7 @@ class Fields(object): for idx, blocks in self.index_fields: polish_index_markup(idx, [rmap[b] for b in blocks]) + def test_parse_fields(return_tests=False): import unittest diff --git a/src/calibre/ebooks/docx/fonts.py b/src/calibre/ebooks/docx/fonts.py index cc6f8001e8..9d3e531a3c 100644 --- a/src/calibre/ebooks/docx/fonts.py +++ b/src/calibre/ebooks/docx/fonts.py @@ -16,16 +16,19 @@ from calibre.utils.fonts.utils import panose_to_css_generic_family, is_truetype_ Embed = namedtuple('Embed', 'name key subsetted') + def has_system_fonts(name): try: return bool(font_scanner.fonts_for_family(name)) except NoFonts: return False + def get_variant(bold=False, italic=False): return {(False, False):'Regular', (False, True):'Italic', (True, False):'Bold', (True, True):'BoldItalic'}[(bold, italic)] + class Family(object): def __init__(self, elem, embed_relationships, XPath, get): diff --git a/src/calibre/ebooks/docx/footnotes.py b/src/calibre/ebooks/docx/footnotes.py index 6acc506caf..a078b9f57c 100644 --- a/src/calibre/ebooks/docx/footnotes.py +++ b/src/calibre/ebooks/docx/footnotes.py @@ -8,6 +8,7 @@ __copyright__ = '2013, Kovid Goyal ' from collections import OrderedDict + class Note(object): def __init__(self, namespace, parent, rels): @@ -20,6 +21,7 @@ class Note(object): for p in self.namespace.descendants(self.parent, 'w:p', 'w:tbl'): yield p + class Footnotes(object): def __init__(self, namespace): diff --git a/src/calibre/ebooks/docx/images.py b/src/calibre/ebooks/docx/images.py index 10e64545c5..81103407c7 100644 --- a/src/calibre/ebooks/docx/images.py +++ b/src/calibre/ebooks/docx/images.py @@ -16,21 +16,26 @@ from calibre.utils.filenames import ascii_filename from calibre.utils.img import resize_to_fit, image_to_data from calibre.utils.imghdr import what + class LinkedImageNotFound(ValueError): def __init__(self, fname): ValueError.__init__(self, fname) self.fname = fname + def image_filename(x): return ascii_filename(x).replace(' ', '_').replace('#', '_') + def emu_to_pt(x): return x / 12700 + def pt_to_emu(x): return int(x * 12700) + def get_image_properties(parent, XPath, get): width = height = None for extent in XPath('./wp:extent')(parent): @@ -71,6 +76,7 @@ def get_image_margins(elem): ans['padding-%s' % css] = '%.3gpt' % val return ans + def get_hpos(anchor, page_width, XPath, get): for ph in XPath('./wp:positionH')(anchor): rp = ph.get('relativeFrom', None) diff --git a/src/calibre/ebooks/docx/index.py b/src/calibre/ebooks/docx/index.py index 13bc9242cb..d7631bc4a0 100644 --- a/src/calibre/ebooks/docx/index.py +++ b/src/calibre/ebooks/docx/index.py @@ -12,6 +12,7 @@ from lxml import etree from calibre.utils.icu import partition_by_first_letter, sort_key + def get_applicable_xe_fields(index, xe_fields, XPath, expand): iet = index.get('entry-type', None) xe_fields = [xe for xe in xe_fields if xe.get('entry-type', None) == iet] @@ -39,6 +40,7 @@ def get_applicable_xe_fields(index, xe_fields, XPath, expand): return [xe for xe in xe_fields if contained(xe)] + def make_block(expand, style, parent, pos): p = parent.makeelement(expand('w:p')) parent.insert(pos, p) @@ -55,6 +57,7 @@ def make_block(expand, style, parent, pos): r.append(t) return p, t + def add_xe(xe, t, expand): text = xe.get('text', '') pt = xe.get('page-number-text', None) @@ -69,6 +72,7 @@ def add_xe(xe, t, expand): r.append(t2) return xe['anchor'], t.getparent() + def process_index(field, index, xe_fields, log, XPath, expand): ''' We remove all the word generated index markup and replace it with our own @@ -118,6 +122,7 @@ def process_index(field, index, xe_fields, log, XPath, expand): return hyperlinks, blocks + def split_up_block(block, a, text, parts, ldict): prefix = parts[:-1] a.text = parts[-1] @@ -168,6 +173,7 @@ If there is no matching entry, then because of the original reversed order we wa to insert nk+1 and all following entries from n into p immediately following pk. """ + def find_match(prev_block, pind, nextent, ldict): curlevel = ldict.get(prev_block[pind], -1) if curlevel < 0: @@ -182,6 +188,7 @@ def find_match(prev_block, pind, nextent, ldict): return p return -1 + def add_link(pent, nent, ldict): na = nent.xpath('descendant::a[1]') # If there is no link, leave it as text @@ -200,6 +207,7 @@ def add_link(pent, nent, ldict): pent.text = "" pent.append(na) + def merge_blocks(prev_block, next_block, pind, nind, next_path, ldict): # First elements match. Any more in next? if len(next_path) == (nind + 1): @@ -222,6 +230,7 @@ def merge_blocks(prev_block, next_block, pind, nind, next_path, ldict): next_block.getparent().remove(next_block) + def polish_index_markup(index, blocks): # Blocks are in reverse order at this point path_map = {} diff --git a/src/calibre/ebooks/docx/names.py b/src/calibre/ebooks/docx/names.py index 6f9c4247c0..f7e72e1a3f 100644 --- a/src/calibre/ebooks/docx/names.py +++ b/src/calibre/ebooks/docx/names.py @@ -75,12 +75,15 @@ STRICT_NAMESPACES = { } # }}} + def barename(x): return x.rpartition('}')[-1] + def XML(x): return '{%s}%s' % (TRANSITIONAL_NAMESPACES['xml'], x) + def generate_anchor(name, existing): x = y = 'id_' + re.sub(r'[^0-9a-zA-Z_]', '', ascii_text(name)).lstrip('_') c = 1 @@ -89,6 +92,7 @@ def generate_anchor(name, existing): c += 1 return y + class DOCXNamespace(object): def __init__(self, transitional=True): diff --git a/src/calibre/ebooks/docx/numbering.py b/src/calibre/ebooks/docx/numbering.py index 20bb396aee..a132cf2233 100644 --- a/src/calibre/ebooks/docx/numbering.py +++ b/src/calibre/ebooks/docx/numbering.py @@ -31,6 +31,7 @@ STYLE_MAP = { 'decimalZero': 'decimal-leading-zero', } + def alphabet(val, lower=True): x = string.ascii_lowercase if lower else string.ascii_uppercase return x[(abs(val - 1)) % len(x)] @@ -41,6 +42,7 @@ alphabet_map = { 'decimal-leading-zero': lambda x: '0%d' % x } + class Level(object): def __init__(self, namespace, lvl=None): @@ -148,6 +150,7 @@ class Level(object): css.pop('font-family', None) return css + class NumberingDefinition(object): def __init__(self, namespace, parent=None, an_id=None): @@ -169,6 +172,7 @@ class NumberingDefinition(object): ans.levels[l] = lvl.copy() return ans + class Numbering(object): def __init__(self, namespace): diff --git a/src/calibre/ebooks/docx/settings.py b/src/calibre/ebooks/docx/settings.py index dbd76b57e9..20d5b19eb2 100644 --- a/src/calibre/ebooks/docx/settings.py +++ b/src/calibre/ebooks/docx/settings.py @@ -6,6 +6,7 @@ from __future__ import (unicode_literals, division, absolute_import, __license__ = 'GPL v3' __copyright__ = '2013, Kovid Goyal ' + class Settings(object): def __init__(self, namespace): diff --git a/src/calibre/ebooks/docx/styles.py b/src/calibre/ebooks/docx/styles.py index 9b3dc06958..9eb7b485d0 100644 --- a/src/calibre/ebooks/docx/styles.py +++ b/src/calibre/ebooks/docx/styles.py @@ -13,6 +13,7 @@ from calibre.ebooks.docx.block_styles import ParagraphStyle, inherit, twips from calibre.ebooks.docx.char_styles import RunStyle from calibre.ebooks.docx.tables import TableStyle + class PageProperties(object): ''' diff --git a/src/calibre/ebooks/docx/tables.py b/src/calibre/ebooks/docx/tables.py index 6c3146cd33..cd73562630 100644 --- a/src/calibre/ebooks/docx/tables.py +++ b/src/calibre/ebooks/docx/tables.py @@ -15,6 +15,7 @@ from calibre.ebooks.docx.char_styles import RunStyle read_shd = rs edges = ('left', 'top', 'right', 'bottom') + def _read_width(elem, get): ans = inherit try: @@ -32,18 +33,21 @@ def _read_width(elem, get): ans = '%.3g%%' % (w/50) return ans + def read_width(parent, dest, XPath, get): ans = inherit for tblW in XPath('./w:tblW')(parent): ans = _read_width(tblW, get) setattr(dest, 'width', ans) + def read_cell_width(parent, dest, XPath, get): ans = inherit for tblW in XPath('./w:tcW')(parent): ans = _read_width(tblW, get) setattr(dest, 'width', ans) + def read_padding(parent, dest, XPath, get): name = 'tblCellMar' if parent.tag.endswith('}tblPr') else 'tcMar' ans = {x:inherit for x in edges} @@ -54,6 +58,7 @@ def read_padding(parent, dest, XPath, get): for x in edges: setattr(dest, 'cell_padding_%s' % x, ans[x]) + def read_justification(parent, dest, XPath, get): left = right = inherit for jc in XPath('./w:jc[@w:val]')(parent): @@ -69,18 +74,21 @@ def read_justification(parent, dest, XPath, get): setattr(dest, 'margin_left', left) setattr(dest, 'margin_right', right) + def read_spacing(parent, dest, XPath, get): ans = inherit for cs in XPath('./w:tblCellSpacing')(parent): ans = _read_width(cs, get) setattr(dest, 'spacing', ans) + def read_float(parent, dest, XPath, get): ans = inherit for x in XPath('./w:tblpPr')(parent): ans = {k.rpartition('}')[-1]: v for k, v in x.attrib.iteritems()} setattr(dest, 'float', ans) + def read_indent(parent, dest, XPath, get): ans = inherit for cs in XPath('./w:tblInd')(parent): @@ -89,10 +97,12 @@ def read_indent(parent, dest, XPath, get): border_edges = ('left', 'top', 'right', 'bottom', 'insideH', 'insideV') + def read_borders(parent, dest, XPath, get): name = 'tblBorders' if parent.tag.endswith('}tblPr') else 'tcBorders' read_border(parent, dest, XPath, get, border_edges, name) + def read_height(parent, dest, XPath, get): ans = inherit for rh in XPath('./w:trHeight')(parent): @@ -102,6 +112,7 @@ def read_height(parent, dest, XPath, get): ans = (rule, val) setattr(dest, 'height', ans) + def read_vertical_align(parent, dest, XPath, get): ans = inherit for va in XPath('./w:vAlign')(parent): @@ -109,6 +120,7 @@ def read_vertical_align(parent, dest, XPath, get): ans = {'center': 'middle', 'top': 'top', 'bottom': 'bottom'}.get(val, 'middle') setattr(dest, 'vertical_align', ans) + def read_col_span(parent, dest, XPath, get): ans = inherit for gs in XPath('./w:gridSpan')(parent): @@ -118,6 +130,7 @@ def read_col_span(parent, dest, XPath, get): continue setattr(dest, 'col_span', ans) + def read_merge(parent, dest, XPath, get): for x in ('hMerge', 'vMerge'): ans = inherit @@ -125,6 +138,7 @@ def read_merge(parent, dest, XPath, get): ans = get(m, 'w:val', 'continue') setattr(dest, x, ans) + def read_band_size(parent, dest, XPath, get): for x in ('Col', 'Row'): ans = 1 @@ -135,6 +149,7 @@ def read_band_size(parent, dest, XPath, get): continue setattr(dest, '%s_band_size' % x.lower(), ans) + def read_look(parent, dest, XPath, get): ans = 0 for x in XPath('./w:tblLook')(parent): @@ -146,6 +161,7 @@ def read_look(parent, dest, XPath, get): # }}} + def clone(style): if style is None: return None @@ -156,6 +172,7 @@ def clone(style): ans.update(style) return ans + class Style(object): is_bidi = False @@ -195,6 +212,7 @@ class Style(object): c[a % 'left'] = r return c + class RowStyle(Style): all_properties = ('height', 'cantSplit', 'hidden', 'spacing',) @@ -230,6 +248,7 @@ class RowStyle(Style): c.update(self.convert_spacing()) return self._css + class CellStyle(Style): all_properties = ('background_color', 'cell_padding_left', 'cell_padding_right', 'cell_padding_top', @@ -273,6 +292,7 @@ class CellStyle(Style): return self._css + class TableStyle(Style): all_properties = ( @@ -434,6 +454,7 @@ class Table(object): def get_overrides(self, r, c, num_of_rows, num_of_cols_in_row): 'List of possible overrides for the given para' overrides = ['wholeTable'] + def divisor(m, n): return (m - (m % n)) // n if c is not None: @@ -649,6 +670,7 @@ class Table(object): if css: elem.set('class', self.styles.register(css, elem.tag)) + class Tables(object): def __init__(self, namespace): diff --git a/src/calibre/ebooks/docx/to_html.py b/src/calibre/ebooks/docx/to_html.py index 9919950aa5..5ac289e057 100644 --- a/src/calibre/ebooks/docx/to_html.py +++ b/src/calibre/ebooks/docx/to_html.py @@ -32,6 +32,7 @@ from calibre.utils.localization import canonicalize_lang, lang_as_iso639_1 NBSP = '\xa0' + class Text: def __init__(self, elem, attr, buf): @@ -41,6 +42,7 @@ class Text: setattr(self.elem, self.attr, ''.join(self.buf)) self.elem, self.attr, self.buf = elem, 'tail', [] + def html_lang(docx_lang): lang = canonicalize_lang(docx_lang) if lang and lang != 'und': @@ -48,6 +50,7 @@ def html_lang(docx_lang): if lang: return lang + class Convert(object): def __init__(self, path_or_stream, dest_dir=None, log=None, detect_cover=True, notes_text=None, notes_nopb=False, nosupsub=False): @@ -365,6 +368,7 @@ class Convert(object): opf.create_spine(['index.html']) if self.cover_image is not None: opf.guide.set_cover(self.cover_image) + def process_guide(E, guide): if self.toc_anchor is not None: guide.append(E.reference( diff --git a/src/calibre/ebooks/docx/toc.py b/src/calibre/ebooks/docx/toc.py index f099c816e3..1b05f2c91f 100644 --- a/src/calibre/ebooks/docx/toc.py +++ b/src/calibre/ebooks/docx/toc.py @@ -13,6 +13,7 @@ from lxml.etree import tostring from calibre.ebooks.metadata.toc import TOC from calibre.ebooks.oeb.polish.toc import elem_to_toc_text + class Count(object): __slots__ = ('val',) @@ -20,6 +21,7 @@ class Count(object): def __init__(self): self.val = 0 + def from_headings(body, log, namespace): ' Create a TOC from headings in the document ' XPath, descendants = namespace.XPath, namespace.descendants @@ -61,6 +63,7 @@ def from_headings(body, log, namespace): log('Generating Table of Contents from headings') return tocroot + def structure_toc(entries): indent_vals = sorted({x.indent for x in entries}) last_found = [None for i in indent_vals] @@ -88,6 +91,7 @@ def structure_toc(entries): return newtoc + def link_to_txt(a, styles, object_map): if len(a) > 1: for child in a: @@ -99,6 +103,7 @@ def link_to_txt(a, styles, object_map): return tostring(a, method='text', with_tail=False, encoding=unicode).strip() + def from_toc(docx, link_map, styles, object_map, log, namespace): XPath, get, ancestor = namespace.XPath, namespace.get, namespace.ancestor toc_level = None @@ -137,5 +142,6 @@ def from_toc(docx, link_map, styles, object_map, log, namespace): log('Found Word Table of Contents, using it to generate the Table of Contents') return structure_toc(toc) + def create_toc(docx, body, link_map, styles, object_map, log, namespace): return from_toc(docx, link_map, styles, object_map, log, namespace) or from_headings(body, log, namespace) diff --git a/src/calibre/ebooks/docx/writer/container.py b/src/calibre/ebooks/docx/writer/container.py index 9c6b460464..841a67a7b6 100644 --- a/src/calibre/ebooks/docx/writer/container.py +++ b/src/calibre/ebooks/docx/writer/container.py @@ -20,6 +20,7 @@ from calibre.utils.localization import canonicalize_lang, lang_as_iso639_1 from calibre.utils.zipfile import ZipFile from calibre.ebooks.pdf.render.common import PAPER_SIZES + def xml2str(root, pretty_print=False, with_tail=False): if hasattr(etree, 'cleanup_namespaces'): etree.cleanup_namespaces(root) @@ -27,14 +28,17 @@ def xml2str(root, pretty_print=False, with_tail=False): pretty_print=pretty_print, with_tail=with_tail) return ans + def page_size(opts): width, height = PAPER_SIZES[opts.docx_page_size] if opts.docx_custom_page_size is not None: width, height = map(float, opts.docx_custom_page_size.partition('x')[0::2]) return width, height + def create_skeleton(opts, namespaces=None): namespaces = namespaces or DOCXNamespace().namespaces + def w(x): return '{%s}%s' % (namespaces['w'], x) dn = {k:v for k, v in namespaces.iteritems() if k in {'w', 'r', 'm', 've', 'o', 'wp', 'w10', 'wne', 'a', 'pic'}} @@ -44,6 +48,7 @@ def create_skeleton(opts, namespaces=None): doc.append(body) width, height = page_size(opts) width, height = int(20 * width), int(20 * height) + def margin(which): return w(which), str(int(getattr(opts, 'margin_'+which) * 20)) body.append(E.sectPr( @@ -132,6 +137,7 @@ class DocumentRelationships(object): relationships.append(r) return xml2str(relationships) + class DOCX(object): def __init__(self, opts, log): diff --git a/src/calibre/ebooks/docx/writer/fonts.py b/src/calibre/ebooks/docx/writer/fonts.py index 93fd880c2c..dccb8c22b7 100644 --- a/src/calibre/ebooks/docx/writer/fonts.py +++ b/src/calibre/ebooks/docx/writer/fonts.py @@ -12,12 +12,14 @@ from uuid import uuid4 from calibre.ebooks.oeb.base import OEB_STYLES from calibre.ebooks.oeb.transforms.subset import find_font_face_rules + def obfuscate_font_data(data, key): prefix = bytearray(data[:32]) key = bytearray(reversed(key.bytes)) prefix = bytes(bytearray(prefix[i]^key[i % len(key)] for i in xrange(len(prefix)))) return prefix + data[32:] + class FontsManager(object): def __init__(self, namespace, oeb, opts): diff --git a/src/calibre/ebooks/docx/writer/from_html.py b/src/calibre/ebooks/docx/writer/from_html.py index a1f93d87d1..08351e85b2 100644 --- a/src/calibre/ebooks/docx/writer/from_html.py +++ b/src/calibre/ebooks/docx/writer/from_html.py @@ -20,12 +20,14 @@ from calibre.ebooks.oeb.stylizer import Stylizer as Sz, Style as St from calibre.ebooks.oeb.base import XPath, barename from calibre.utils.localization import lang_as_iso639_1 + def lang_for_tag(tag): for attr in ('lang', '{http://www.w3.org/XML/1998/namespace}lang'): val = lang_as_iso639_1(tag.get(attr)) if val: return val + class Style(St): def __init__(self, *args, **kwargs): @@ -42,6 +44,7 @@ class Style(St): self._letterSpacing = self._unit_convert(val) return self._letterSpacing + class Stylizer(Sz): def style(self, element): @@ -50,6 +53,7 @@ class Stylizer(Sz): except KeyError: return Style(element, self) + class TextRun(object): ws_pat = None @@ -128,6 +132,7 @@ class TextRun(object): ans += len(text) return ans + class Block(object): def __init__(self, namespace, styles_manager, links_manager, html_block, style, is_table_cell=False, float_spec=None, is_list_item=False): @@ -242,6 +247,7 @@ class Block(object): return False return True + class Blocks(object): def __init__(self, namespace, styles_manager, links_manager): @@ -383,6 +389,7 @@ class Blocks(object): def __repr__(self): return 'Block(%r)' % self.runs + class Convert(object): # Word does not apply default styling to hyperlinks, so we ensure they get diff --git a/src/calibre/ebooks/docx/writer/images.py b/src/calibre/ebooks/docx/writer/images.py index c81bec226f..cd71d4e88f 100644 --- a/src/calibre/ebooks/docx/writer/images.py +++ b/src/calibre/ebooks/docx/writer/images.py @@ -21,6 +21,7 @@ from calibre.utils.imghdr import identify Image = namedtuple('Image', 'rid fname width height fmt item') + def as_num(x): try: return float(x) @@ -28,6 +29,7 @@ def as_num(x): pass return 0 + def get_image_margins(style): ans = {} for edge in 'Left Right Top Bottom'.split(): @@ -35,6 +37,7 @@ def get_image_margins(style): ans['dist' + edge[0]] = str(pt_to_emu(val)) return ans + class ImagesManager(object): def __init__(self, oeb, document_relationships): diff --git a/src/calibre/ebooks/docx/writer/links.py b/src/calibre/ebooks/docx/writer/links.py index afaa15b8ee..0089b2653a 100644 --- a/src/calibre/ebooks/docx/writer/links.py +++ b/src/calibre/ebooks/docx/writer/links.py @@ -12,6 +12,7 @@ from urlparse import urlparse from calibre.utils.filenames import ascii_text + def start_text(tag, prefix_len=0, top_level=True): ans = tag.text or '' limit = 50 - prefix_len @@ -24,6 +25,7 @@ def start_text(tag, prefix_len=0, top_level=True): ans = ans[:limit] + '...' return ans + class TOCItem(object): def __init__(self, title, bmark, level): @@ -55,6 +57,7 @@ class TOCItem(object): makeelement(r, 'w:fldChar', w_fldCharType='end') body.insert(0, p) + def sanitize_bookmark_name(base): return re.sub(r'[^0-9a-zA-Z]', '_', ascii_text(base)) diff --git a/src/calibre/ebooks/docx/writer/lists.py b/src/calibre/ebooks/docx/writer/lists.py index b9dfc15ca4..c9b0d930b4 100644 --- a/src/calibre/ebooks/docx/writer/lists.py +++ b/src/calibre/ebooks/docx/writer/lists.py @@ -48,6 +48,7 @@ def find_list_containers(list_tag, tag_style): ans.append(node) return ans + class NumberingDefinition(object): def __init__(self, top_most, stylizer, namespace): @@ -87,6 +88,7 @@ class NumberingDefinition(object): for level in self.levels: level.serialize(an, makeelement) + class Level(object): def __init__(self, list_type, container, items, ilvl=0): @@ -121,6 +123,7 @@ class Level(object): ff = {'\uf0b7':'Symbol', '\uf0a7':'Wingdings'}.get(self.lvl_text, 'Courier New') makeelement(makeelement(lvl, 'w:rPr'), 'w:rFonts', w_ascii=ff, w_hAnsi=ff, w_hint="default") + class ListsManager(object): def __init__(self, docx): diff --git a/src/calibre/ebooks/docx/writer/styles.py b/src/calibre/ebooks/docx/writer/styles.py index deeb0fa46a..fc8a00bd91 100644 --- a/src/calibre/ebooks/docx/writer/styles.py +++ b/src/calibre/ebooks/docx/writer/styles.py @@ -22,6 +22,7 @@ border_edges = ('left', 'top', 'right', 'bottom') border_props = ('padding_%s', 'border_%s_width', 'border_%s_style', 'border_%s_color') ignore = object() + def parse_css_font_family(raw): decl, errs = css_parser.parse_style_attr('font-family:' + raw) if decl: @@ -32,17 +33,21 @@ def parse_css_font_family(raw): break yield val + def css_font_family_to_docx(raw): generic = {'serif':'Cambria', 'sansserif':'Candara', 'sans-serif':'Candara', 'fantasy':'Comic Sans', 'cursive':'Segoe Script'} for ff in parse_css_font_family(raw): return generic.get(ff.lower(), ff) + def bmap(x): return 'on' if x else 'off' + def is_dropcaps(html_tag, tag_style): return len(html_tag) < 2 and len(etree.tostring(html_tag, method='text', encoding=unicode, with_tail=False)) < 5 and tag_style['float'] == 'left' + class CombinedStyle(object): def __init__(self, bs, rs, blocks, namespace): @@ -74,6 +79,7 @@ class CombinedStyle(object): rPr = makeelement(block, 'w:rPr') self.rs.serialize_properties(rPr, normal_style.rs) + class FloatSpec(object): def __init__(self, namespace, html_tag, tag_style): @@ -132,6 +138,7 @@ class FloatSpec(object): bstyle = getattr(self, 'border_%s_style' % edge) self.makeelement(bdr, 'w:'+edge, w_space=str(padding), w_val=bstyle, w_sz=str(width), w_color=getattr(self, 'border_%s_color' % edge)) + class DOCXStyle(object): ALL_PROPS = () @@ -186,6 +193,7 @@ LINE_STYLES = { 'outset': 'outset', } + class TextStyle(DOCXStyle): ALL_PROPS = ('font_family', 'font_size', 'bold', 'italic', 'color', @@ -341,6 +349,7 @@ class TextStyle(DOCXStyle): if bdr.attrib: rPr.append(bdr) + class DescendantTextStyle(object): def __init__(self, parent_style, child_style): @@ -348,6 +357,7 @@ class DescendantTextStyle(object): self.makeelement = child_style.makeelement p = [] + def add(name, **props): p.append((name, frozenset(props.iteritems()))) @@ -461,6 +471,7 @@ def read_css_block_borders(self, css, store_css_style=False): if store_css_style: setattr(self, 'border_%s_css_style' % edge, css['border-%s-style' % edge].lower()) + class BlockStyle(DOCXStyle): ALL_PROPS = tuple( diff --git a/src/calibre/ebooks/docx/writer/tables.py b/src/calibre/ebooks/docx/writer/tables.py index 6a2f7e4c4d..7865f958b8 100644 --- a/src/calibre/ebooks/docx/writer/tables.py +++ b/src/calibre/ebooks/docx/writer/tables.py @@ -11,6 +11,7 @@ from collections import namedtuple from calibre.ebooks.docx.writer.utils import convert_color from calibre.ebooks.docx.writer.styles import read_css_block_borders as rcbb, border_edges + class Dummy(object): pass @@ -18,6 +19,7 @@ Border = namedtuple('Border', 'css_style style width color level') border_style_weight = { x:100-i for i, x in enumerate(('double', 'solid', 'dashed', 'dotted', 'ridge', 'outset', 'groove', 'inset'))} + class SpannedCell(object): def __init__(self, spanning_cell, horizontal=True): @@ -34,6 +36,7 @@ class SpannedCell(object): makeelement(tcPr, 'w:%sMerge' % ('h' if self.horizontal else 'v'), w_val='continue') makeelement(tc, 'w:p') + def read_css_block_borders(self, css): obj = Dummy() rcbb(obj, css, store_css_style=True) @@ -47,6 +50,7 @@ def read_css_block_borders(self, css): )) setattr(self, 'padding_' + edge, getattr(obj, 'padding_' + edge)) + def as_percent(x): if x and x.endswith('%'): try: @@ -54,6 +58,7 @@ def as_percent(x): except Exception: pass + def convert_width(tag_style): if tag_style is not None: w = tag_style._get('width') @@ -69,6 +74,7 @@ def convert_width(tag_style): pass return ('auto', 0) + class Cell(object): BLEVEL = 2 @@ -200,6 +206,7 @@ class Cell(object): ans = self.table.rows[ridx+1].cells[idx] return getattr(ans, 'spanning_cell', ans) + class Row(object): BLEVEL = 1 @@ -243,6 +250,7 @@ class Row(object): for cell in self.cells: cell.serialize(tr, makeelement) + class Table(object): BLEVEL = 0 diff --git a/src/calibre/ebooks/docx/writer/utils.py b/src/calibre/ebooks/docx/writer/utils.py index 8b6b50cfb6..b63f941c66 100644 --- a/src/calibre/ebooks/docx/writer/utils.py +++ b/src/calibre/ebooks/docx/writer/utils.py @@ -8,6 +8,7 @@ __copyright__ = '2013, Kovid Goyal ' from tinycss.color3 import parse_color_string + def int_or_zero(raw): try: return int(raw) @@ -15,6 +16,8 @@ def int_or_zero(raw): return 0 # convert_color() {{{ + + def convert_color(value): if not value: return @@ -27,8 +30,10 @@ def convert_color(value): return return '%02X%02X%02X' % (int(val.red * 255), int(val.green * 255), int(val.blue * 255)) + def test_convert_color(return_tests=False): import unittest + class TestColors(unittest.TestCase): def test_color_conversion(self): diff --git a/src/calibre/ebooks/epub/__init__.py b/src/calibre/ebooks/epub/__init__.py index 3c70625e91..1632ad576c 100644 --- a/src/calibre/ebooks/epub/__init__.py +++ b/src/calibre/ebooks/epub/__init__.py @@ -8,6 +8,7 @@ Conversion to EPUB. ''' from calibre.utils.zipfile import ZipFile, ZIP_STORED + def rules(stylesheets): for s in stylesheets: if hasattr(s, 'cssText'): @@ -15,6 +16,7 @@ def rules(stylesheets): if r.type == r.STYLE_RULE: yield r + def initialize_container(path_to_container, opf_name='metadata.opf', extra_entries=[]): ''' diff --git a/src/calibre/ebooks/epub/cfi/parse.py b/src/calibre/ebooks/epub/cfi/parse.py index 71188e10de..0637295db9 100644 --- a/src/calibre/ebooks/epub/cfi/parse.py +++ b/src/calibre/ebooks/epub/cfi/parse.py @@ -11,6 +11,7 @@ from future_builtins import map, zip is_narrow_build = sys.maxunicode < 0x10ffff + class Parser(object): ''' See epubcfi.ebnf for the specification that this parser tries to @@ -166,18 +167,21 @@ class Parser(object): _parser = None + def parser(): global _parser if _parser is None: _parser = Parser() return _parser + def get_steps(pcfi): ans = tuple(pcfi['steps']) if 'redirect' in pcfi: ans += get_steps(pcfi['redirect']) return ans + def cfi_sort_key(cfi, only_path=True): p = parser() try: diff --git a/src/calibre/ebooks/epub/cfi/tests.py b/src/calibre/ebooks/epub/cfi/tests.py index 2e3bce5caf..ba7be7622c 100644 --- a/src/calibre/ebooks/epub/cfi/tests.py +++ b/src/calibre/ebooks/epub/cfi/tests.py @@ -11,6 +11,7 @@ from future_builtins import map from calibre.ebooks.epub.cfi.parse import parser, cfi_sort_key + class Tests(unittest.TestCase): def test_sorting(self): @@ -25,17 +26,21 @@ class Tests(unittest.TestCase): def test_parsing(self): p = parser() + def step(x): if isinstance(x, int): return {'num': x} return {'num':x[0], 'id':x[1]} + def s(*args): return {'steps':list(map(step, args))} + def r(*args): idx = args.index('!') ans = s(*args[:idx]) ans['redirect'] = s(*args[idx+1:]) return ans + def o(*args): ans = s(1) step = ans['steps'][-1] @@ -45,6 +50,7 @@ class Tests(unittest.TestCase): typ, val = args[2:] step[{'@':'spatial_offset', '~':'temporal_offset'}[typ]] = val return ans + def a(before=None, after=None, **params): ans = o(':', 3) step = ans['steps'][-1] @@ -90,6 +96,7 @@ class Tests(unittest.TestCase): ]: self.assertEqual(p.parse_path(raw), (path, leftover)) + def find_tests(): return unittest.TestLoader().loadTestsFromTestCase(Tests) diff --git a/src/calibre/ebooks/epub/pages.py b/src/calibre/ebooks/epub/pages.py index c8d146107d..d76a4a8c12 100644 --- a/src/calibre/ebooks/epub/pages.py +++ b/src/calibre/ebooks/epub/pages.py @@ -18,6 +18,7 @@ NSMAP = {'h': XHTML_NS, 'html': XHTML_NS, 'xhtml': XHTML_NS} PAGE_RE = re.compile(r'page', re.IGNORECASE) ROMAN_RE = re.compile(r'^[ivxlcdm]+$', re.IGNORECASE) + def filter_name(name): name = name.strip() name = PAGE_RE.sub('', name) @@ -27,11 +28,13 @@ def filter_name(name): break return name + def build_name_for(expr): if not expr: counter = count(1) return lambda elem: str(counter.next()) selector = XPath(expr, namespaces=NSMAP) + def name_for(elem): results = selector(elem) if not results: @@ -40,6 +43,7 @@ def build_name_for(expr): return filter_name(name) return name_for + def add_page_map(opfpath, opts): oeb = OEBBook(opfpath) selector = XPath(opts.page, namespaces=NSMAP) diff --git a/src/calibre/ebooks/epub/periodical.py b/src/calibre/ebooks/epub/periodical.py index ab43928c0a..3422fd6e60 100644 --- a/src/calibre/ebooks/epub/periodical.py +++ b/src/calibre/ebooks/epub/periodical.py @@ -78,6 +78,7 @@ SONY_ATOM_ENTRY = u'''\ ''' + def sony_metadata(oeb): m = oeb.metadata title = short_title = unicode(m.title[0]) diff --git a/src/calibre/ebooks/fb2/__init__.py b/src/calibre/ebooks/fb2/__init__.py index e1f3863f29..4d9887e6a5 100644 --- a/src/calibre/ebooks/fb2/__init__.py +++ b/src/calibre/ebooks/fb2/__init__.py @@ -7,6 +7,7 @@ __license__ = 'GPL v3' __copyright__ = '2012, Kovid Goyal ' __docformat__ = 'restructuredtext en' + def base64_decode(raw): from io import BytesIO from base64 import b64decode diff --git a/src/calibre/ebooks/fb2/fb2ml.py b/src/calibre/ebooks/fb2/fb2ml.py index de695154bb..a1981e01ee 100644 --- a/src/calibre/ebooks/fb2/fb2ml.py +++ b/src/calibre/ebooks/fb2/fb2ml.py @@ -20,6 +20,7 @@ from calibre.utils.localization import lang_as_iso639_1 from calibre.utils.img import save_cover_data_to from calibre.ebooks.oeb.base import urlnormalize + class FB2MLizer(object): ''' Todo: * Include more FB2 specific tags in the conversion. diff --git a/src/calibre/ebooks/html/input.py b/src/calibre/ebooks/html/input.py index 24efdfccf3..fe200ec01b 100644 --- a/src/calibre/ebooks/html/input.py +++ b/src/calibre/ebooks/html/input.py @@ -19,6 +19,7 @@ from calibre.ebooks.chardet import detect_xml_encoding from calibre.constants import iswindows from calibre import unicode_path, as_unicode, replace_entities + class Link(object): ''' @@ -73,6 +74,7 @@ class IgnoreFile(Exception): self.doesnt_exist = errno == gerrno.ENOENT self.errno = errno + class HTMLFile(object): ''' diff --git a/src/calibre/ebooks/html/to_zip.py b/src/calibre/ebooks/html/to_zip.py index 545432d1d9..2c21a9572f 100644 --- a/src/calibre/ebooks/html/to_zip.py +++ b/src/calibre/ebooks/html/to_zip.py @@ -12,6 +12,7 @@ import textwrap, os, glob from calibre.customize import FileTypePlugin from calibre.constants import numeric_version + class HTML2ZIP(FileTypePlugin): name = 'HTML to ZIP' author = 'Kovid Goyal' diff --git a/src/calibre/ebooks/htmlz/oeb2html.py b/src/calibre/ebooks/htmlz/oeb2html.py index 12a4271290..cacaa6cc07 100644 --- a/src/calibre/ebooks/htmlz/oeb2html.py +++ b/src/calibre/ebooks/htmlz/oeb2html.py @@ -25,6 +25,7 @@ from calibre.utils.logging import default_log SELF_CLOSING_TAGS = {'area', 'base', 'basefont', 'br', 'hr', 'input', 'img', 'link', 'meta'} + class OEB2HTML(object): ''' Base class. All subclasses should implement dump_text to actually transform @@ -410,12 +411,14 @@ def oeb2html_no_css(oeb_book, log, opts): images = izer.images return (html, images) + def oeb2html_inline_css(oeb_book, log, opts): izer = OEB2HTMLInlineCSSizer(log) html = izer.oeb2html(oeb_book, opts) images = izer.images return (html, images) + def oeb2html_class_css(oeb_book, log, opts): izer = OEB2HTMLClassCSSizer(log) setattr(opts, 'class_style', 'inline') diff --git a/src/calibre/ebooks/hyphenate.py b/src/calibre/ebooks/hyphenate.py index 439daaef15..5c3fb8191e 100644 --- a/src/calibre/ebooks/hyphenate.py +++ b/src/calibre/ebooks/hyphenate.py @@ -18,6 +18,7 @@ import re __version__ = '1.0.20070709' + class Hyphenator: def __init__(self, patterns, exceptions=''): diff --git a/src/calibre/ebooks/lit/__init__.py b/src/calibre/ebooks/lit/__init__.py index 412a52ab05..5be3cb9b6a 100644 --- a/src/calibre/ebooks/lit/__init__.py +++ b/src/calibre/ebooks/lit/__init__.py @@ -1,5 +1,6 @@ __license__ = 'GPL v3' __copyright__ = '2008, Marshall T. Vandegrift ' + class LitError(Exception): pass diff --git a/src/calibre/ebooks/lit/from_any.py b/src/calibre/ebooks/lit/from_any.py index 85f9a6e3e2..93eb4e75ff 100644 --- a/src/calibre/ebooks/lit/from_any.py +++ b/src/calibre/ebooks/lit/from_any.py @@ -14,13 +14,16 @@ from calibre.ebooks.epub import config as common_config from calibre.ptempfile import TemporaryDirectory from calibre.ebooks.lit.writer import oeb2lit + def config(defaults=None): c = common_config(defaults=defaults, name='lit') return c + def option_parser(usage=USAGE): return config().option_parser(usage=usage%('LIT', formats())) + def any2lit(opts, path): ext = os.path.splitext(path)[1] if not ext: diff --git a/src/calibre/ebooks/lit/lzx.py b/src/calibre/ebooks/lit/lzx.py index 3f324a65a6..a1e447d797 100644 --- a/src/calibre/ebooks/lit/lzx.py +++ b/src/calibre/ebooks/lit/lzx.py @@ -17,7 +17,9 @@ __all__ = ['Compressor', 'Decompressor', 'LZXError'] LZXError = _lzx.LZXError Compressor = _lzx.Compressor + class Decompressor(object): + def __init__(self, wbits): self.wbits = wbits self.blocksize = 1 << wbits diff --git a/src/calibre/ebooks/lit/mssha1.py b/src/calibre/ebooks/lit/mssha1.py index 1bdef12950..3ccf2b10e8 100644 --- a/src/calibre/ebooks/lit/mssha1.py +++ b/src/calibre/ebooks/lit/mssha1.py @@ -16,6 +16,7 @@ import struct, copy # and is reused here with tiny modifications. # ====================================================================== + def _long2bytesBigEndian(n, blocksize=0): """Convert a long integer to a byte string. @@ -84,16 +85,21 @@ def _rotateLeft(x, n): def f0_19(B, C, D): return (B & (C ^ D)) ^ D + def f20_39(B, C, D): return B ^ C ^ D + def f40_59(B, C, D): return ((B | C) & D) | (B & C) + def f60_79(B, C, D): return B ^ C ^ D # Microsoft's lovely addition... + + def f6_42(B, C, D): return (B + C) ^ C @@ -119,6 +125,7 @@ K = [ 0xCA62C1D6L # (60 <= t <= 79) ] + class mssha1(object): "An implementation of the MD5 hash function in pure Python." @@ -300,6 +307,7 @@ class mssha1(object): digest_size = digestsize = 20 blocksize = 1 + def new(arg=None): """Return a new mssha1 crypto object. diff --git a/src/calibre/ebooks/lit/reader.py b/src/calibre/ebooks/lit/reader.py index 02d13a05aa..17e3f2ddbf 100644 --- a/src/calibre/ebooks/lit/reader.py +++ b/src/calibre/ebooks/lit/reader.py @@ -56,15 +56,19 @@ FLAG_BLOCK = (1 << 2) FLAG_HEAD = (1 << 3) FLAG_ATOM = (1 << 4) + def u32(bytes): return struct.unpack(' 0: @@ -77,10 +81,12 @@ def encint(bytes, remaining): break return val, bytes[pos:], remaining + def msguid(bytes): values = struct.unpack("]*?>|<\s*%(t)s\s*>)'%dict(t=tagname), close=r''%dict(t=tagname)) + class HTMLConverter(object): SELECTOR_PAT = re.compile(r"([A-Za-z0-9\-\_\:\.]+[A-Za-z0-9\-\_\:\.\s\,]*)\s*\{([^\}]*)\}") PAGE_BREAK_PAT = re.compile(r'page-break-(?:after|before)\s*:\s*(\w+)', re.IGNORECASE) @@ -1802,6 +1807,7 @@ class HTMLConverter(object): for _file in self.scaled_images.values() + self.rotated_images.values(): _file.__del__() + def process_file(path, options, logger): if not isinstance(path, unicode): path = path.decode(sys.getfilesystemencoding()) @@ -1920,6 +1926,7 @@ def process_file(path, options, logger): conv.cleanup() return oname + def try_opf(path, options, logger): if hasattr(options, 'opf'): opf = options.opf diff --git a/src/calibre/ebooks/lrf/html/convert_to.py b/src/calibre/ebooks/lrf/html/convert_to.py index 4aee876452..ad780b90bf 100644 --- a/src/calibre/ebooks/lrf/html/convert_to.py +++ b/src/calibre/ebooks/lrf/html/convert_to.py @@ -12,6 +12,7 @@ from calibre.ebooks.metadata.opf import OPFCreator from calibre.ebooks.lrf.objects import PageAttr, BlockAttr, TextAttr from calibre.ebooks.lrf.pylrs.pylrs import TextStyle + class BlockStyle(object): def __init__(self, ba): @@ -84,6 +85,7 @@ class LRFConverter(object): self.create_page_styles() self.create_block_styles() + def option_parser(): parser = OptionParser(usage='%prog book.lrf') parser.add_option('--output-dir', '-o', default=None, help=( @@ -91,6 +93,7 @@ def option_parser(): parser.add_option('--verbose', default=False, action='store_true', dest='verbose') return parser + def process_file(lrfpath, opts, logger=None): if logger is None: level = logging.DEBUG if opts.verbose else logging.INFO diff --git a/src/calibre/ebooks/lrf/html/table.py b/src/calibre/ebooks/lrf/html/table.py index df9bb8bead..ac0b15d372 100644 --- a/src/calibre/ebooks/lrf/html/table.py +++ b/src/calibre/ebooks/lrf/html/table.py @@ -7,9 +7,11 @@ from calibre.ebooks.lrf.pylrs.pylrs import TextBlock, Text, CR, Span, \ CharButton, Plot, Paragraph, \ LrsTextTag + def ceil(num): return int(math.ceil(num)) + def print_xml(elem): from calibre.ebooks.lrf.pylrs.pylrs import ElementWriter elem = elem.toElement('utf8') @@ -17,11 +19,13 @@ def print_xml(elem): ew.write(sys.stdout) print + def cattrs(base, extra): new = base.copy() new.update(extra) return new + def tokens(tb): ''' Return the next token. A token is : diff --git a/src/calibre/ebooks/lrf/html/table_as_image.py b/src/calibre/ebooks/lrf/html/table_as_image.py index ade08b1088..7a22743e91 100644 --- a/src/calibre/ebooks/lrf/html/table_as_image.py +++ b/src/calibre/ebooks/lrf/html/table_as_image.py @@ -11,6 +11,7 @@ from PyQt5.Qt import QUrl, QApplication, QSize, QEventLoop, \ QPainter, QImage, QObject, Qt from PyQt5.QtWebKitWidgets import QWebPage + class HTMLTableRenderer(QObject): def __init__(self, html, base_dir, width, height, dpi, factor): @@ -60,6 +61,7 @@ class HTMLTableRenderer(QObject): finally: QApplication.quit() + def render_table(soup, table, css, base_dir, width, height, dpi, factor=1.0): head = '' for e in soup.findAll(['link', 'style']): @@ -84,6 +86,7 @@ def render_table(soup, table, css, base_dir, width, height, dpi, factor=1.0): atexit.register(shutil.rmtree, tdir) return images + def do_render(html, base_dir, width, height, dpi, factor): from calibre.gui2 import is_ok_to_use_qt if not is_ok_to_use_qt(): diff --git a/src/calibre/ebooks/lrf/input.py b/src/calibre/ebooks/lrf/input.py index 431bb86c9c..b678f38cec 100644 --- a/src/calibre/ebooks/lrf/input.py +++ b/src/calibre/ebooks/lrf/input.py @@ -13,6 +13,7 @@ from lxml import etree from calibre import guess_type + class Canvas(etree.XSLTExtension): def __init__(self, doc, styles, text_block, log): @@ -98,6 +99,7 @@ class MediaType(etree.XSLTExtension): typ = 'application/octet-stream' output_parent.text = typ + class ImageBlock(etree.XSLTExtension): def __init__(self, canvas): diff --git a/src/calibre/ebooks/lrf/lrfparser.py b/src/calibre/ebooks/lrf/lrfparser.py index b36d4a2130..7315316eeb 100644 --- a/src/calibre/ebooks/lrf/lrfparser.py +++ b/src/calibre/ebooks/lrf/lrfparser.py @@ -134,6 +134,7 @@ class LRFDocument(LRFMetaFile): self.write_files() return '\n' + bookinfo + pages + styles + objects + '' + def option_parser(): parser = OptionParser(usage=_('%prog book.lrf\nConvert an LRF file into an LRS (XML UTF-8 encoded) file')) parser.add_option('--output', '-o', default=None, help=_('Output LRS file'), dest='out') @@ -143,6 +144,7 @@ def option_parser(): parser.add_option('--verbose', default=False, action='store_true', dest='verbose', help=_('Be more verbose')) return parser + def main(args=sys.argv, logger=None): parser = option_parser() opts, args = parser.parse_args(args) diff --git a/src/calibre/ebooks/lrf/lrs/convert_from.py b/src/calibre/ebooks/lrf/lrs/convert_from.py index 88d21a74a5..fb8f289209 100644 --- a/src/calibre/ebooks/lrf/lrs/convert_from.py +++ b/src/calibre/ebooks/lrf/lrs/convert_from.py @@ -17,6 +17,7 @@ from calibre.ebooks.lrf.pylrs.pylrs import Book, PageStyle, TextStyle, \ DropCaps, Footer, RuledLine from calibre.ebooks.chardet import xml_to_unicode + class LrsParser(object): SELF_CLOSING_TAGS = [i.lower() for i in ['CR', 'Plot', 'NoBR', 'Space', diff --git a/src/calibre/ebooks/lrf/meta.py b/src/calibre/ebooks/lrf/meta.py index 0df17119a8..05912604d3 100644 --- a/src/calibre/ebooks/lrf/meta.py +++ b/src/calibre/ebooks/lrf/meta.py @@ -25,10 +25,12 @@ WORD = "}, that implements access to protocol packets in a human readable way. """ + def __init__(self, start=16, fmt=DWORD): """ @param start: The byte at which this field is stored in the buffer @@ -75,11 +77,14 @@ class versioned_field(field): else: field.__set__(self, obj, val) + class LRFException(Exception): pass + class fixed_stringfield(object): """ A field storing a variable length string. """ + def __init__(self, length=8, start=0): """ @param length: Size of this string @@ -104,6 +109,7 @@ class fixed_stringfield(object): return "A string of length " + str(self._length) + \ " starting at byte " + str(self._start) + class xml_attr_field(object): def __init__(self, tag_name, attr, parent='BookInfo'): @@ -144,11 +150,13 @@ class xml_attr_field(object): def __str__(self): return self.tag_name+'.'+self.attr + class xml_field(object): """ Descriptor that gets and sets XML based meta information from an LRF file. Works for simple XML fields of the form data """ + def __init__(self, tag_name, parent="BookInfo"): """ @param tag_name: The XML tag whose data we operate on @@ -213,6 +221,7 @@ class xml_field(object): def __repr__(self): return "XML Field: " + self.tag_name + " in " + self.parent + def insert_into_file(fileobj, data, start, end): """ Insert data into fileobj at position C{start}. @@ -288,6 +297,7 @@ def get_metadata(stream): return mi + class LRFMetaFile(object): """ Has properties to read and write all Meta information in a LRF file. """ #: The first 6 bytes of all valid LRF files @@ -376,6 +386,7 @@ class LRFMetaFile(object): Document meta information as a minidom Document object. To set use a minidom document object. """ + def fget(self): if self.compressed_info_size == 0: raise LRFException("This document has no meta info") @@ -416,6 +427,7 @@ class LRFMetaFile(object): @safe_property def thumbnail_pos(): doc = """ The position of the thumbnail in the LRF file """ + def fget(self): return self.info_start + self.compressed_info_size-4 return {"fget":fget, "doc":doc} @@ -440,6 +452,7 @@ class LRFMetaFile(object): Represented as a string. The string you would get from the file read function. """ + def fget(self): size = self.thumbnail_size if size: @@ -648,6 +661,7 @@ Show/edit the metadata in an LRF file.\n\n'''), return parser + def set_metadata(stream, mi): lrf = LRFMetaFile(stream) if mi.title: diff --git a/src/calibre/ebooks/lrf/objects.py b/src/calibre/ebooks/lrf/objects.py index f087840271..f3cfc88ab7 100644 --- a/src/calibre/ebooks/lrf/objects.py +++ b/src/calibre/ebooks/lrf/objects.py @@ -15,6 +15,7 @@ ruby_tags = { 0xF57A: ['emplinetype', 'W', {0: 'none', 0x10: 'solid', 0x20: 'dashed', 0x30: 'double', 0x40: 'dotted'}] } + class LRFObject(object): tag_map = { @@ -91,6 +92,7 @@ class LRFObject(object): def __str__(self): return unicode(self).encode('utf-8') + class LRFContentObject(LRFObject): tag_map = {} @@ -189,6 +191,7 @@ class PageTree(LRFObject): for id in getattr(self, '_contents', []): yield self._document.objects[id] + class StyleObject(object): def _tags_to_xml(self): @@ -213,6 +216,7 @@ class StyleObject(object): d[attr] = getattr(self, attr) return d + class PageAttr(StyleObject, LRFObject): tag_map = { 0xF507: ['oddheaderid', 'D'], @@ -271,6 +275,7 @@ class EmptyPageElement(object): def __str__(self): return unicode(self) + class PageDiv(EmptyPageElement): def __init__(self, pain, spacesize, linewidth, linecolor): @@ -296,6 +301,7 @@ class RuledLine(EmptyPageElement): return u'\n\n'%\ (self.linelength, self.linetype, self.linewidth, self.linecolor) + class Wait(EmptyPageElement): def __init__(self, time): @@ -304,6 +310,7 @@ class Wait(EmptyPageElement): def __unicode__(self): return u'\n\n'%(self.time) + class Locate(EmptyPageElement): pos_map = {1:'bottomleft', 2:'bottomright',3:'topright',4:'topleft', 5:'base'} @@ -314,6 +321,7 @@ class Locate(EmptyPageElement): def __unicode__(self): return u'\n\n'%(self.pos) + class BlockSpace(EmptyPageElement): def __init__(self, xspace, yspace): @@ -323,6 +331,7 @@ class BlockSpace(EmptyPageElement): return u'\n\n'%\ (self.xspace, self.yspace) + class Page(LRFStream): tag_map = { 0xF503: ['style_id', 'D'], @@ -937,6 +946,7 @@ class Image(LRFObject): return u'\n'%\ (self.id, self.x0, self.y0, self.x1, self.y1, self.xsize, self.ysize, self.refstream) + class PutObj(EmptyPageElement): def __init__(self, objects, x1, y1, refobj): @@ -946,6 +956,7 @@ class PutObj(EmptyPageElement): def __unicode__(self): return u''%(self.x1, self.y1, self.refobj) + class Canvas(LRFStream): tag_map = { 0xF551: ['canvaswidth', 'W'], @@ -997,9 +1008,11 @@ class Canvas(LRFStream): for i in self._contents: yield i + class Header(Canvas): pass + class Footer(Canvas): pass @@ -1007,6 +1020,7 @@ class Footer(Canvas): class ESound(LRFObject): pass + class ImageStream(LRFStream): tag_map = { 0xF555: ['comment', 'P'], @@ -1027,9 +1041,11 @@ class ImageStream(LRFStream): return u'\n'%\ (self.id, self.encoding, self.file) + class Import(LRFStream): pass + class Button(LRFObject): tag_map = { 0xF503: ['', 'do_ref_image'], @@ -1121,15 +1137,19 @@ class Button(LRFObject): class Window(LRFObject): pass + class PopUpWin(LRFObject): pass + class Sound(LRFObject): pass + class SoundStream(LRFObject): pass + class Font(LRFStream): tag_map = { 0xF559: ['fontfilename', 'P'], @@ -1148,6 +1168,7 @@ class Font(LRFStream): (self.id, self.fontfilename, self.fontfacename, self.file) return s + class ObjectInfo(LRFStream): pass @@ -1180,9 +1201,11 @@ class BookAttr(StyleObject, LRFObject): s += '\n' return s + class SimpleText(Text): pass + class TocLabel(object): def __init__(self, refpage, refobject, label): @@ -1191,6 +1214,7 @@ class TocLabel(object): def __unicode__(self): return u'%s\n'%(self.refpage, self.refobject, self.label) + class TOCObject(LRFStream): def initialize(self): diff --git a/src/calibre/ebooks/lrf/pylrs/elements.py b/src/calibre/ebooks/lrf/pylrs/elements.py index 038172e52b..b727b9dd5b 100644 --- a/src/calibre/ebooks/lrf/pylrs/elements.py +++ b/src/calibre/ebooks/lrf/pylrs/elements.py @@ -1,5 +1,6 @@ """ elements.py -- replacements and helpers for ElementTree """ + class ElementWriter(object): def __init__(self, e, header=False, sourceEncoding="ascii", diff --git a/src/calibre/ebooks/lrf/pylrs/pylrf.py b/src/calibre/ebooks/lrf/pylrs/pylrf.py index 6cc4621baa..767494fefd 100644 --- a/src/calibre/ebooks/lrf/pylrs/pylrf.py +++ b/src/calibre/ebooks/lrf/pylrs/pylrf.py @@ -69,12 +69,15 @@ PYLRF_VERSION = "1.0" # anyway. # + class LrfError(Exception): pass + def writeByte(f, byte): f.write(struct.pack(" 65535: raise LrfError('Cannot encode a number greater than 65535 in a word.') @@ -86,35 +89,45 @@ def writeWord(f, word): def writeSignedWord(f, sword): f.write(struct.pack("I", int(color, 0))) + def writeLineWidth(f, width): writeWord(f, int(width)) + def writeUnicode(f, string, encoding): if isinstance(string, str): string = string.decode(encoding) @@ -125,6 +138,7 @@ def writeUnicode(f, string, encoding): writeWord(f, length) writeString(f, string) + def writeRaw(f, string, encoding): if isinstance(string, str): string = string.decode(encoding) @@ -132,24 +146,28 @@ def writeRaw(f, string, encoding): string = string.encode("utf-16-le") writeString(f, string) + def writeRubyAA(f, rubyAA): ralign, radjust = rubyAA radjust = {"line-edge":0x10, "none":0}[radjust] ralign = {"start":1, "center":2}[ralign] writeWord(f, ralign | radjust) + def writeBgImage(f, bgInfo): imode, iid = bgInfo imode = {"pfix": 0, "fix":1, "tile":2, "centering":3}[imode] writeWord(f, imode) writeDWord(f, iid) + def writeEmpDots(f, dotsInfo, encoding): refDotsFont, dotsFontName, dotsCode = dotsInfo writeDWord(f, refDotsFont) LrfTag("fontfacename", dotsFontName).write(f, encoding) writeWord(f, int(dotsCode, 0)) + def writeRuledLine(f, lineInfo): lineLength, lineType, lineWidth, lineColor = lineInfo writeWord(f, lineLength) @@ -398,6 +416,7 @@ STREAM_COMPRESSED = 0x100 STREAM_FORCE_COMPRESSED = 0x8100 STREAM_TOC = 0x0051 + class LrfStreamBase(object): def __init__(self, streamFlags, streamData=None): @@ -557,6 +576,7 @@ class LrfToc(LrfObject): Table of contents. Format of toc is: [ (pageid, objid, string)...] """ + def __init__(self, objId, toc, se): LrfObject.__init__(self, "TOC", objId) streamData = self._makeTocStream(toc, se) diff --git a/src/calibre/ebooks/lrf/pylrs/pylrs.py b/src/calibre/ebooks/lrf/pylrs/pylrs.py index 631c9f44c3..721621e7f1 100644 --- a/src/calibre/ebooks/lrf/pylrs/pylrs.py +++ b/src/calibre/ebooks/lrf/pylrs/pylrs.py @@ -58,12 +58,15 @@ DEFAULT_GENREADING = "fs" # default is yes to both lrf and lrs from calibre import __appname__, __version__ from calibre import entity_to_unicode + class LrsError(Exception): pass + class ContentError(Exception): pass + def _checkExists(filename): if not os.path.exists(filename): raise LrsError("file '%s' not found" % filename) @@ -136,6 +139,7 @@ def appendTextElements(e, contentsList, se): class Delegator(object): """ A mixin class to create delegated methods that create elements. """ + def __init__(self, delegates): self.delegates = delegates self.delegatedMethods = [] @@ -218,6 +222,7 @@ class Delegator(object): class LrsAttributes(object): """ A mixin class to handle default and user supplied attributes. """ + def __init__(self, defaults, alsoAllow=None, **settings): if alsoAllow is None: alsoAllow = [] @@ -235,6 +240,7 @@ class LrsContainer(object): """ This class is a mixin class for elements that are contained in or contain an unknown number of other elements. """ + def __init__(self, validChildren): self.parent = None self.contents = [] @@ -560,6 +566,7 @@ class Book(Delegator): old_base_font_size = float(max(fonts.items(), key=operator.itemgetter(1))[0]) factor = base_font_size / old_base_font_size + def rescale(old): return str(int(int(old) * factor)) @@ -625,6 +632,7 @@ class Book(Delegator): class BookInformation(Delegator): """ Just a container for the Info and TableOfContents elements. """ + def __init__(self): Delegator.__init__(self, [Info(), TableOfContents()]) @@ -636,6 +644,7 @@ class BookInformation(Delegator): class Info(Delegator): """ Just a container for the BookInfo and DocInfo elements. """ + def __init__(self): self.genreading = DEFAULT_GENREADING Delegator.__init__(self, [BookInfo(), DocInfo()]) @@ -940,6 +949,7 @@ class Template(object): # does nothing pass + class StyleDefault(LrsAttributes): """ Supply some defaults for all TextBlocks. @@ -1079,6 +1089,7 @@ class BookSetting(LrsAttributes): class LrsStyle(LrsObject, LrsAttributes, LrsContainer): """ A mixin class for styles. """ + def __init__(self, elementName, defaults=None, alsoAllow=None, **overrides): if defaults is None: defaults = {} @@ -1117,6 +1128,7 @@ class LrsStyle(LrsObject, LrsAttributes, LrsContainer): return self.__class__ == other.__class__ and self.attrs == other.attrs return False + class TextStyle(LrsStyle): """ The text style of a TextBlock. Default is 10 pt. Times Roman. @@ -1477,6 +1489,7 @@ class Paragraph(LrsContainer): the things that can go in it.) It's less confusing (to me) to use explicit .append methods to build up the text stream. """ + def __init__(self, text=None): LrsContainer.__init__(self, [Text, CR, DropCaps, CharButton, LrsSimpleChar1, basestring]) @@ -1620,9 +1633,11 @@ class Button(LrsObject, LrsContainer): return b + class ButtonBlock(Button): pass + class PushButton(LrsContainer): def __init__(self, **settings): @@ -1636,6 +1651,7 @@ class PushButton(LrsContainer): return b + class JumpTo(LrsContainer): def __init__(self, textBlock): @@ -1690,8 +1706,10 @@ class Plot(LrsSimpleChar1, LrsContainer): Plot.ADJUSTMENT_VALUES[adj]) parent.appendLrfTag(LrfTag("Plot", params)) + class Text(LrsContainer): """ A object that represents raw text. Does not have a toElement. """ + def __init__(self, text): LrsContainer.__init__(self, []) self.text = text @@ -1712,6 +1730,7 @@ class CR(LrsSimpleChar1, LrsContainer): A line break (when appended to a Paragraph) or a paragraph break (when appended to a TextBlock). """ + def __init__(self): LrsContainer.__init__(self, []) @@ -1727,6 +1746,7 @@ class Italic(LrsSimpleChar1, LrsTextTag): def __init__(self, text=None): LrsTextTag.__init__(self, text, [LrsSimpleChar1]) + class Sub(LrsSimpleChar1, LrsTextTag): def __init__(self, text=None): @@ -1769,6 +1789,7 @@ class Box(LrsSimpleChar1, LrsContainer): Draw a box around text. Unfortunately, does not seem to do anything on the PRS-500. """ + def __init__(self, linetype="solid"): LrsContainer.__init__(self, [Text, basestring]) if linetype not in LINE_TYPE_ENCODING: @@ -1848,6 +1869,7 @@ class Span(LrsSimpleChar1, LrsContainer): appendTextElements(element, self.contents, se) return element + class EmpLine(LrsTextTag, LrsSimpleChar1): emplinetypes = ['none', 'solid', 'dotted', 'dashed', 'double'] emplinepositions = ['before', 'after'] @@ -1879,11 +1901,13 @@ class EmpLine(LrsTextTag, LrsSimpleChar1): appendTextElements(element, self.contents, se) return element + class Bold(Span): """ There is no known "bold" lrf tag. Use Span with a fontweight in LRF, but use the word Bold in the LRS. """ + def __init__(self, text=None): Span.__init__(self, text, fontweight=800) @@ -1895,6 +1919,7 @@ class Bold(Span): class BlockSpace(LrsContainer): """ Can be appended to a page to move the text point. """ + def __init__(self, xspace=0, yspace=0, x=0, y=0): LrsContainer.__init__(self, []) if xspace == 0 and x != 0: @@ -1928,6 +1953,7 @@ class CharButton(LrsSimpleChar1, LrsContainer): Only text or SimpleChars can be appended to the CharButton. """ + def __init__(self, button, text=None): LrsContainer.__init__(self, [basestring, Text, LrsSimpleChar1]) self.button = None @@ -2037,6 +2063,7 @@ class JumpButton(LrsObject, LrsContainer): Actually creates several elements in the XML. JumpButtons must be eventually appended to a Book (actually, an Object.) """ + def __init__(self, textBlock): LrsObject.__init__(self) LrsContainer.__init__(self, []) @@ -2140,6 +2167,7 @@ class Header(HeaderOrFooter): class Footer(HeaderOrFooter): pass + class Canvas(LrsObject, LrsContainer, LrsAttributes): defaults = dict(framemode="square", layout="LrTb", framewidth="0", framecolor="0x00000000", bgcolor="0xFF000000", @@ -2266,6 +2294,7 @@ class ImageStream(LrsObject, LrsContainer): element.text = self.comment return element + class Image(LrsObject, LrsContainer, LrsAttributes): defaults = dict() @@ -2383,6 +2412,7 @@ class ImageBlock(LrsObject, LrsContainer, LrsAttributes): class Font(LrsContainer): """ Allows a TrueType file to be embedded in an Lrf. """ + def __init__(self, file=None, fontname=None, fontfilename=None, encoding=None): LrsContainer.__init__(self, []) try: diff --git a/src/calibre/ebooks/lrf/tags.py b/src/calibre/ebooks/lrf/tags.py index 79de3ac982..1767f1888a 100644 --- a/src/calibre/ebooks/lrf/tags.py +++ b/src/calibre/ebooks/lrf/tags.py @@ -6,6 +6,7 @@ import struct from calibre.ebooks.lrf import LRFParseError + class Tag(object): tags = { diff --git a/src/calibre/ebooks/metadata/__init__.py b/src/calibre/ebooks/metadata/__init__.py index 67ad6c5c92..e5cb56440a 100644 --- a/src/calibre/ebooks/metadata/__init__.py +++ b/src/calibre/ebooks/metadata/__init__.py @@ -22,6 +22,7 @@ except: 'is invalid, using default') _author_pat = re.compile(r'(?i),?\s+(and|with)\s+') + def string_to_authors(raw): if not raw: return [] @@ -30,12 +31,14 @@ def string_to_authors(raw): authors = [a.strip().replace(u'\uffff', '&') for a in raw.split('&')] return [a for a in authors if a] + def authors_to_string(authors): if authors is not None: return ' & '.join([a.replace('&', '&&') for a in authors if a]) else: return '' + def author_to_author_sort(author, method=None): if not author: return u'' @@ -93,10 +96,13 @@ def author_to_author_sort(author, method=None): return u' '.join(atokens) + def authors_to_sort_string(authors): return ' & '.join(map(author_to_author_sort, authors)) _title_pats = {} + + def get_title_sort_pat(lang=None): ans = _title_pats.get(lang, None) if ans is not None: @@ -129,6 +135,7 @@ def get_title_sort_pat(lang=None): _ignore_starts = u'\'"'+u''.join(unichr(x) for x in range(0x2018, 0x201e)+[0x2032, 0x2033]) + def title_sort(title, order=None, lang=None): if order is None: order = tweaks['title_series_sorting'] @@ -177,6 +184,7 @@ def fmt_sidx(i, fmt='%.2f', use_roman=False): return roman(int(i)) if use_roman else '%d'%int(i) return fmt%i + class Resource(object): ''' @@ -325,6 +333,7 @@ def MetaInformation(title, authors=(_('Unknown'),)): authors = mi.authors return Metadata(title, authors, other=mi) + def check_isbn10(isbn): try: digits = map(int, isbn[:9]) @@ -336,6 +345,7 @@ def check_isbn10(isbn): pass return None + def check_isbn13(isbn): try: digits = map(int, isbn[:12]) @@ -349,6 +359,7 @@ def check_isbn13(isbn): pass return None + def check_isbn(isbn): if not isbn: return None @@ -362,6 +373,7 @@ def check_isbn(isbn): return check_isbn13(isbn) return None + def check_issn(issn): if not issn: return None @@ -376,6 +388,7 @@ def check_issn(issn): pass return None + def format_isbn(isbn): cisbn = check_isbn(isbn) if not cisbn: @@ -385,6 +398,7 @@ def format_isbn(isbn): return '-'.join((i[:2], i[2:6], i[6:9], i[9])) return '-'.join((i[:3], i[3:5], i[5:9], i[9:12], i[12])) + def check_doi(doi): 'Check if something that looks like a DOI is present anywhere in the string' if not doi: @@ -394,6 +408,7 @@ def check_doi(doi): return doi_check.group() return None + def rating_to_stars(value, allow_half_stars=False, star=u'★', half=u'½'): r = max(0, min(int(value or 0), 10)) if allow_half_stars: diff --git a/src/calibre/ebooks/metadata/archive.py b/src/calibre/ebooks/metadata/archive.py index 96fc0de7bb..938ab24bc5 100644 --- a/src/calibre/ebooks/metadata/archive.py +++ b/src/calibre/ebooks/metadata/archive.py @@ -11,12 +11,14 @@ from contextlib import closing from calibre.customize import FileTypePlugin + def is_comic(list_of_names): extensions = set([x.rpartition('.')[-1].lower() for x in list_of_names if '.' in x and x.lower().rpartition('/')[-1] != 'thumbs.db']) comic_extensions = set(['jpg', 'jpeg', 'png']) return len(extensions - comic_extensions) == 0 + def archive_type(stream): from calibre.utils.zipfile import stringFileHeader try: @@ -99,6 +101,7 @@ class ArchiveExtract(FileTypePlugin): of.write(zf.read(fname)) return of.name + def get_comic_book_info(d, mi, series_index='volume'): # See http://code.google.com/p/comicbookinfo/wiki/Example series = d.get('series', '') @@ -145,6 +148,7 @@ def get_comic_book_info(d, mi, series_index='volume'): except: pass + def get_comic_metadata(stream, stream_type, series_index='volume'): # See http://code.google.com/p/comicbookinfo/wiki/Example from calibre.ebooks.metadata import MetaInformation diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py index 0b9317a190..0dc7575141 100644 --- a/src/calibre/ebooks/metadata/book/base.py +++ b/src/calibre/ebooks/metadata/book/base.py @@ -20,6 +20,7 @@ from calibre.utils.icu import sort_key SIMPLE_GET = frozenset(STANDARD_METADATA_FIELDS - TOP_LEVEL_IDENTIFIERS) SIMPLE_SET = frozenset(SIMPLE_GET - {'identifiers'}) + def human_readable(size, precision=2): """ Convert a size in bytes into megabytes """ return ('%.'+str(precision)+'f'+ 'MB') % ((size/(1024.*1024.)),) @@ -42,6 +43,7 @@ NULL_VALUES = { field_metadata = FieldMetadata() + def reset_field_metadata(): global field_metadata field_metadata = FieldMetadata() @@ -49,6 +51,7 @@ def reset_field_metadata(): ck = lambda typ: icu_lower(typ).strip().replace(':', '').replace(',', '') cv = lambda val: val.strip().replace(',', '|') + class Metadata(object): ''' @@ -706,6 +709,7 @@ class Metadata(object): from calibre.utils.date import isoformat from calibre.ebooks.metadata import authors_to_string ans = [] + def fmt(x, y): ans.append(u'%-20s: %s'%(unicode(x), unicode(y))) @@ -787,6 +791,7 @@ class Metadata(object): # }}} + def field_from_string(field, raw, field_metadata): ''' Parse the string raw to return an object that is suitable for calling set() on a Metadata object. ''' diff --git a/src/calibre/ebooks/metadata/book/formatter.py b/src/calibre/ebooks/metadata/book/formatter.py index c60bc95053..8721ed7eb0 100644 --- a/src/calibre/ebooks/metadata/book/formatter.py +++ b/src/calibre/ebooks/metadata/book/formatter.py @@ -10,6 +10,7 @@ from calibre.ebooks.metadata.book import TOP_LEVEL_IDENTIFIERS, ALL_METADATA_FIE from calibre.utils.formatter import TemplateFormatter + class SafeFormat(TemplateFormatter): def __init__(self): diff --git a/src/calibre/ebooks/metadata/book/json_codec.py b/src/calibre/ebooks/metadata/book/json_codec.py index c01a36d5ac..c730dd71ef 100644 --- a/src/calibre/ebooks/metadata/book/json_codec.py +++ b/src/calibre/ebooks/metadata/book/json_codec.py @@ -15,6 +15,8 @@ from calibre import isbytestring # Translate datetimes to and from strings. The string form is the datetime in # UTC. The returned date is also UTC + + def string_to_datetime(src): from calibre.utils.iso8601 import parse_iso8601 if src != "None": @@ -24,6 +26,7 @@ def string_to_datetime(src): pass return None + def datetime_to_string(dateval): from calibre.utils.date import isoformat, UNDEFINED_DATE, local_tz if dateval is None: @@ -36,6 +39,7 @@ def datetime_to_string(dateval): return "None" return isoformat(dateval) + def encode_thumbnail(thumbnail): ''' Encode the image part of a thumbnail, then return the 3 part tuple @@ -53,6 +57,7 @@ def encode_thumbnail(thumbnail): return None return (thumbnail[0], thumbnail[1], b64encode(str(thumbnail[2]))) + def decode_thumbnail(tup): ''' Decode an encoded thumbnail into its 3 component parts @@ -61,6 +66,7 @@ def decode_thumbnail(tup): return None return (tup[0], tup[1], b64decode(tup[2])) + def object_to_unicode(obj, enc=preferred_encoding): def dec(x): @@ -79,6 +85,7 @@ def object_to_unicode(obj, enc=preferred_encoding): return ans return obj + def encode_is_multiple(fm): if fm.get('is_multiple', None): # migrate is_multiple back to a character @@ -92,6 +99,7 @@ def encode_is_multiple(fm): fm['is_multiple'] = None fm['is_multiple2'] = {} + def decode_is_multiple(fm): im = fm.get('is_multiple2', None) if im: @@ -115,6 +123,7 @@ def decode_is_multiple(fm): im = {} fm['is_multiple'] = im + class JsonCodec(object): def __init__(self, field_metadata=None): diff --git a/src/calibre/ebooks/metadata/book/render.py b/src/calibre/ebooks/metadata/book/render.py index b930b080a2..19b662c072 100644 --- a/src/calibre/ebooks/metadata/book/render.py +++ b/src/calibre/ebooks/metadata/book/render.py @@ -22,6 +22,7 @@ from calibre.utils.localization import calibre_langcode_to_name default_sort = ('title', 'title_sort', 'authors', 'author_sort', 'series', 'rating', 'pubdate', 'tags', 'publisher', 'identifiers') + def field_sort(mi, name): try: title = mi.metadata_for_field(name)['name'] @@ -29,6 +30,7 @@ def field_sort(mi, name): title = 'zzz' return {x:(i, None) for i, x in enumerate(default_sort)}.get(name, (10000, sort_key(title))) + def displayable_field_keys(mi): for k in mi.all_field_keys(): try: @@ -42,17 +44,21 @@ def displayable_field_keys(mi): ): yield k + def get_field_list(mi): for field in sorted(displayable_field_keys(mi), key=partial(field_sort, mi)): yield field, True + def search_href(search_term, value): search = '%s:"=%s"' % (search_term, value.replace('"', '\\"')) return prepare_string_for_xml('search:' + hexlify(search.encode('utf-8')), True) + def item_data(field_name, value, book_id): return hexlify(cPickle.dumps((field_name, value, book_id), -1)) + def mi_to_html(mi, field_list=None, default_author_link=None, use_roman_numbers=True, rating_font='Liberation Serif', rtl=False): if field_list is None: field_list = get_field_list(mi) diff --git a/src/calibre/ebooks/metadata/cli.py b/src/calibre/ebooks/metadata/cli.py index 886bd1c453..762617c885 100644 --- a/src/calibre/ebooks/metadata/cli.py +++ b/src/calibre/ebooks/metadata/cli.py @@ -92,12 +92,14 @@ def config(): help=_('Set the BookID in LRF files')) return c + def filetypes(): readers = set([]) for r in metadata_readers(): readers = readers.union(set(r.file_types)) return readers + def option_parser(): writers = set([]) for w in metadata_writers(): @@ -105,6 +107,7 @@ def option_parser(): ft, w = ', '.join(sorted(filetypes())), ', '.join(sorted(writers)) return config().option_parser(USAGE.format(ft, w)) + def do_set_metadata(opts, mi, stream, stream_type): mi = MetaInformation(mi) for x in ('guide', 'toc', 'manifest', 'spine'): diff --git a/src/calibre/ebooks/metadata/docx.py b/src/calibre/ebooks/metadata/docx.py index 1eaac21c5b..ceba4b7708 100644 --- a/src/calibre/ebooks/metadata/docx.py +++ b/src/calibre/ebooks/metadata/docx.py @@ -15,6 +15,7 @@ from calibre.ebooks.docx.container import DOCX from calibre.ebooks.docx.writer.container import update_doc_props, xml2str from calibre.utils.imghdr import identify + def get_cover(docx): doc = docx.document get = docx.namespace.get @@ -34,6 +35,7 @@ def get_cover(docx): if 0.8 <= height/width <= 1.8 and height*width >= 160000: return (fmt, raw) + def get_metadata(stream): c = DOCX(stream, extract=False) mi = c.metadata @@ -50,6 +52,7 @@ def get_metadata(stream): return mi + def set_metadata(stream, mi): from calibre.utils.zipfile import safe_replace c = DOCX(stream, extract=False) diff --git a/src/calibre/ebooks/metadata/epub.py b/src/calibre/ebooks/metadata/epub.py index f36a8c6255..245ee2cef3 100644 --- a/src/calibre/ebooks/metadata/epub.py +++ b/src/calibre/ebooks/metadata/epub.py @@ -18,16 +18,21 @@ from calibre.ptempfile import TemporaryDirectory from calibre import CurrentDir, walk from calibre.constants import isosx + class EPubException(Exception): pass + class OCFException(EPubException): pass + class ContainerException(OCFException): pass + class Container(dict): + def __init__(self, stream=None): if not stream: return @@ -46,6 +51,7 @@ class Container(dict): except KeyError: raise EPubException(" element malformed") + class OCF(object): MIMETYPE = 'application/epub+zip' CONTAINER_PATH = 'META-INF/container.xml' @@ -54,6 +60,7 @@ class OCF(object): def __init__(self): raise NotImplementedError('Abstract base class') + class Encryption(object): OBFUSCATION_ALGORITHMS = frozenset(['http://ns.adobe.com/pdf/enc#RC', @@ -78,6 +85,7 @@ class Encryption(object): class OCFReader(OCF): + def __init__(self): try: mimetype = self.open('mimetype').read().rstrip() @@ -121,6 +129,7 @@ class OCFReader(OCF): class OCFZipReader(OCFReader): + def __init__(self, stream, mode='r', root=None): if isinstance(stream, (LocalZipFile, ZipFile)): self.archive = stream @@ -146,6 +155,7 @@ class OCFZipReader(OCFReader): def read_bytes(self, name): return self.archive.read(name) + def get_zip_reader(stream, root=None): try: zf = ZipFile(stream, mode='r') @@ -154,7 +164,9 @@ def get_zip_reader(stream, root=None): zf = LocalZipFile(stream) return OCFZipReader(zf, root=root) + class OCFDirReader(OCFReader): + def __init__(self, path): self.root = path super(OCFDirReader, self).__init__() @@ -162,6 +174,7 @@ class OCFDirReader(OCFReader): def open(self, path, *args, **kwargs): return open(os.path.join(self.root, path), *args, **kwargs) + def render_cover(cpage, zf, reader=None): from calibre.ebooks import render_html_svg_workaround from calibre.utils.logging import default_log @@ -212,6 +225,7 @@ def render_cover(cpage, zf, reader=None): return render_html_svg_workaround(cpage, default_log) + def get_cover(raster_cover, first_spine_item, reader): zf = reader.archive @@ -231,6 +245,7 @@ def get_cover(raster_cover, first_spine_item, reader): return render_cover(first_spine_item, zf, reader=reader) + def get_metadata(stream, extract_cover=True): """ Return metadata as a :class:`Metadata` object """ stream.seek(0) @@ -253,13 +268,16 @@ def get_metadata(stream, extract_cover=True): mi.timestamp = None return mi + def get_quick_metadata(stream): return get_metadata(stream, False) + def serialize_cover_data(new_cdata, cpath): from calibre.utils.img import save_cover_data_to return save_cover_data_to(new_cdata, data_fmt=os.path.splitext(cpath)[1][1:]) + def set_metadata(stream, mi, apply_null=False, update_timestamp=False, force_identifiers=False, add_missing_cover=True): stream.seek(0) reader = get_zip_reader(stream, root=os.getcwdu()) diff --git a/src/calibre/ebooks/metadata/ereader.py b/src/calibre/ebooks/metadata/ereader.py index b85d373aef..79119e759e 100644 --- a/src/calibre/ebooks/metadata/ereader.py +++ b/src/calibre/ebooks/metadata/ereader.py @@ -17,6 +17,7 @@ from calibre.ebooks.pdb.ereader.reader132 import HeaderRecord from calibre.ebooks.pdb.header import PdbHeaderBuilder from calibre.ebooks.pdb.header import PdbHeaderReader + def get_cover(pheader, eheader): cover_data = None @@ -29,6 +30,7 @@ def get_cover(pheader, eheader): return ('png', cover_data) + def get_metadata(stream, extract_cover=True): """ Return metadata as a L{MetaInfo} object @@ -62,6 +64,7 @@ def get_metadata(stream, extract_cover=True): return mi + def set_metadata(stream, mi): pheader = PdbHeaderReader(stream) diff --git a/src/calibre/ebooks/metadata/extz.py b/src/calibre/ebooks/metadata/extz.py index d4a0400764..1d34bb0ad8 100644 --- a/src/calibre/ebooks/metadata/extz.py +++ b/src/calibre/ebooks/metadata/extz.py @@ -16,6 +16,7 @@ from calibre.ebooks.metadata.opf2 import OPF from calibre.ptempfile import PersistentTemporaryFile from calibre.utils.zipfile import ZipFile, safe_replace + def get_metadata(stream, extract_cover=True): ''' Return metadata as a L{MetaInfo} object @@ -45,6 +46,7 @@ def get_metadata(stream, extract_cover=True): return mi return mi + def set_metadata(stream, mi): replacements = {} @@ -86,6 +88,7 @@ def set_metadata(stream, mi): except: pass + def get_first_opf_name(zf): names = zf.namelist() opfs = [] @@ -97,6 +100,7 @@ def get_first_opf_name(zf): opfs.sort() return opfs[0] + def _write_new_cover(new_cdata, cpath): from calibre.utils.img import save_cover_data_to new_cover = PersistentTemporaryFile(suffix=os.path.splitext(cpath)[1]) diff --git a/src/calibre/ebooks/metadata/fb2.py b/src/calibre/ebooks/metadata/fb2.py index c7014af31a..b77c5e9a24 100644 --- a/src/calibre/ebooks/metadata/fb2.py +++ b/src/calibre/ebooks/metadata/fb2.py @@ -28,9 +28,11 @@ NAMESPACES = { tostring = partial(etree.tostring, method='text', encoding=unicode) + def XLINK(tag): return '{%s}%s'%(NAMESPACES['xlink'], tag) + class Context(object): def __init__(self, root): @@ -82,6 +84,7 @@ class Context(object): else: self.create_tag(parent, 'empty-line', at_start=False) + def get_metadata(stream): ''' Return fb2 metadata as a L{MetaInformation} object ''' @@ -135,6 +138,7 @@ def get_metadata(stream): return mi + def _parse_authors(root, ctx): authors = [] # pick up authors but only from 1 secrion ; otherwise it is not consistent! @@ -154,6 +158,7 @@ def _parse_authors(root, ctx): return authors + def _parse_author(elm_author, ctx): """ Returns a list of display author and sortable author""" @@ -187,6 +192,7 @@ def _parse_book_title(root, ctx): return book_title + def _parse_cover(root, mi, ctx): # pickup from , if not exists it fallbacks to imgid = ctx.XPath('substring-after(string(//fb:coverpage/fb:image/@xlink:href), "#")')(root) @@ -196,6 +202,7 @@ def _parse_cover(root, mi, ctx): except: pass + def _parse_cover_data(root, imgid, mi, ctx): from calibre.ebooks.fb2 import base64_decode elm_binary = ctx.XPath('//fb:binary[@id="%s"]'%imgid)(root) @@ -217,6 +224,7 @@ def _parse_cover_data(root, imgid, mi, ctx): else: prints("WARNING: Unsupported coverpage mime-type '%s' (id=#%s)" % (mimetype, imgid)) + def _parse_tags(root, mi, ctx): # pick up genre but only from 1 secrion ; otherwise it is not consistent! # Those are fallbacks: @@ -227,6 +235,7 @@ def _parse_tags(root, mi, ctx): mi.tags = list(map(unicode, tags)) break + def _parse_series(root, mi, ctx): # calibri supports only 1 series: use the 1-st one # pick up sequence but only from 1 secrion in preferred order @@ -243,6 +252,7 @@ def _parse_series(root, mi, ctx): except Exception: pass + def _parse_isbn(root, mi, ctx): # some people try to put several isbn in this field, but it is not allowed. try to stick to the 1-st one in this case isbn = ctx.XPath('normalize-space(//fb:publish-info/fb:isbn/text())')(root) @@ -253,6 +263,7 @@ def _parse_isbn(root, mi, ctx): if check_isbn(isbn): mi.isbn = isbn + def _parse_comments(root, mi, ctx): # pick up annotation but only from 1 secrion ; fallback: for annotation_sec in ['title-info', 'src-title-info']: @@ -262,23 +273,27 @@ def _parse_comments(root, mi, ctx): # TODO: tags i18n, xslt? break + def _parse_publisher(root, mi, ctx): publisher = ctx.XPath('string(//fb:publish-info/fb:publisher/text())')(root) if publisher: mi.publisher = publisher + def _parse_pubdate(root, mi, ctx): year = ctx.XPath('number(//fb:publish-info/fb:year/text())')(root) if float.is_integer(year): # only year is available, so use 2nd of June mi.pubdate = parse_only_date(type(u'')(int(year))) + def _parse_language(root, mi, ctx): language = ctx.XPath('string(//fb:title-info/fb:lang/text())')(root) if language: mi.language = language mi.languages = [language] + def _get_fbroot(stream): parser = etree.XMLParser(recover=True, no_network=True) raw = stream.read() @@ -286,12 +301,14 @@ def _get_fbroot(stream): root = etree.fromstring(raw, parser=parser) return ensure_namespace(root) + def _set_title(title_info, mi, ctx): if not mi.is_null('title'): ctx.clear_meta_tags(title_info, 'book-title') title = ctx.get_or_create(title_info, 'book-title') title.text = mi.title + def _set_comments(title_info, mi, ctx): if not mi.is_null('comments'): from calibre.utils.html2text import html2text @@ -319,6 +336,7 @@ def _set_authors(title_info, mi, ctx): if author_parts: ctx.create_tag(atag, 'last-name', at_start=False).text = ' '.join(author_parts) + def _set_tags(title_info, mi, ctx): if not mi.is_null('tags'): ctx.clear_meta_tags(title_info, 'genre') @@ -326,6 +344,7 @@ def _set_tags(title_info, mi, ctx): tag = ctx.create_tag(title_info, 'genre') tag.text = t + def _set_series(title_info, mi, ctx): if not mi.is_null('series'): ctx.clear_meta_tags(title_info, 'sequence') @@ -336,16 +355,20 @@ def _set_series(title_info, mi, ctx): except: seq.set('number', '1') + def _rnd_name(size=8, chars=ascii_letters + digits): return ''.join(random.choice(chars) for x in range(size)) + def _rnd_pic_file_name(prefix='calibre_cover_', size=32, ext='jpg'): return prefix + _rnd_name(size=size) + '.' + ext + def _encode_into_jpeg(data): data = save_cover_data_to(data) return b64encode(data) + def _set_cover(title_info, mi, ctx): if not mi.is_null('cover_data') and mi.cover_data[1]: coverpage = ctx.get_or_create(title_info, 'coverpage') @@ -360,6 +383,7 @@ def _set_cover(title_info, mi, ctx): cim_binary.attrib['content-type'] = 'image/jpeg' cim_binary.text = _encode_into_jpeg(mi.cover_data[1]) + def set_metadata(stream, mi, apply_null=False, update_timestamp=False): stream.seek(0) root = _get_fbroot(stream) @@ -388,6 +412,7 @@ def set_metadata(stream, mi, apply_null=False, update_timestamp=False): stream.write(etree.tostring(root, method='xml', encoding='utf-8', xml_declaration=False)) + def ensure_namespace(doc): # Workaround for broken FB2 files produced by convertonlinefree.com. See # https://bugs.launchpad.net/bugs/1404701 diff --git a/src/calibre/ebooks/metadata/haodoo.py b/src/calibre/ebooks/metadata/haodoo.py index a32f7a2268..1d17980cd1 100644 --- a/src/calibre/ebooks/metadata/haodoo.py +++ b/src/calibre/ebooks/metadata/haodoo.py @@ -11,6 +11,7 @@ __docformat__ = 'restructuredtext en' from calibre.ebooks.pdb.header import PdbHeaderReader from calibre.ebooks.pdb.haodoo.reader import Reader + def get_metadata(stream, extract_cover=True): ''' Return metadata as a L{MetaInfo} object diff --git a/src/calibre/ebooks/metadata/html.py b/src/calibre/ebooks/metadata/html.py index 13100db191..609a0dee1c 100644 --- a/src/calibre/ebooks/metadata/html.py +++ b/src/calibre/ebooks/metadata/html.py @@ -17,6 +17,7 @@ from calibre.ebooks.chardet import xml_to_unicode from calibre import replace_entities, isbytestring from calibre.utils.date import parse_date, is_date_undefined + def get_metadata(stream): src = stream.read() return get_metadata_(src) @@ -55,6 +56,7 @@ META_NAMES = { # single quotes inside double quotes and vice versa. attr_pat = r'''(?:(?P')|(?P"))(?P(?(sq)[^']+|[^"]+))(?(sq)'|")''' + def parse_meta_tags(src): rmap = {} for field, names in META_NAMES.iteritems(): @@ -84,6 +86,7 @@ def parse_meta_tags(src): return ans return ans + def parse_comment_tags(src): all_names = '|'.join(COMMENT_NAMES.itervalues()) rmap = {v:k for k, v in COMMENT_NAMES.iteritems()} @@ -96,6 +99,7 @@ def parse_comment_tags(src): break return ans + def get_metadata_(src, encoding=None): # Meta data definitions as in # http://www.mobileread.com/forums/showpost.php?p=712544&postcount=9 diff --git a/src/calibre/ebooks/metadata/imp.py b/src/calibre/ebooks/metadata/imp.py index 139067365d..c02deff161 100644 --- a/src/calibre/ebooks/metadata/imp.py +++ b/src/calibre/ebooks/metadata/imp.py @@ -8,6 +8,7 @@ from calibre.ebooks.metadata import MetaInformation, string_to_authors MAGIC = ['\x00\x01BOOKDOUG', '\x00\x02BOOKDOUG'] + def get_metadata(stream): """ Return metadata as a L{MetaInfo} object """ title = 'Unknown' diff --git a/src/calibre/ebooks/metadata/kdl.py b/src/calibre/ebooks/metadata/kdl.py index 13d86ebb14..1567f53b73 100644 --- a/src/calibre/ebooks/metadata/kdl.py +++ b/src/calibre/ebooks/metadata/kdl.py @@ -19,6 +19,7 @@ URL = \ _ignore_starts = u'\'"'+u''.join(unichr(x) for x in range(0x2018, 0x201e)+[0x2032, 0x2033]) + def get_series(title, authors, timeout=60): mi = Metadata(title, authors) if title and title[0] in _ignore_starts: diff --git a/src/calibre/ebooks/metadata/kfx.py b/src/calibre/ebooks/metadata/kfx.py index 7acc32716e..fedb5a9a5e 100644 --- a/src/calibre/ebooks/metadata/kfx.py +++ b/src/calibre/ebooks/metadata/kfx.py @@ -19,6 +19,7 @@ from calibre.utils.date import parse_only_date from calibre.utils.localization import canonicalize_lang from calibre.utils.imghdr import identify + class InvalidKFX(ValueError): pass @@ -60,6 +61,7 @@ COVER_KEY = "cover_image_base64" def hexs(string, sep=' '): return sep.join('%02x' % ord(b) for b in string) + class PackedData(object): ''' @@ -236,6 +238,7 @@ def property_name(property_number): # strings using a symbol table return b"P%d" % property_number + def extract_metadata(container_data): metadata = defaultdict(list) @@ -258,12 +261,14 @@ def extract_metadata(container_data): return metadata + def dump_metadata(m): d = dict(m) d[COVER_KEY] = bool(d.get(COVER_KEY)) from pprint import pprint pprint(d) + def read_metadata_kfx(stream, read_cover=True): ' Read the metadata.kfx file that is found in the sdr book folder for KFX files ' c = Container(stream.read()) diff --git a/src/calibre/ebooks/metadata/library_thing.py b/src/calibre/ebooks/metadata/library_thing.py index d0094a8f92..a829338313 100644 --- a/src/calibre/ebooks/metadata/library_thing.py +++ b/src/calibre/ebooks/metadata/library_thing.py @@ -17,17 +17,21 @@ OPENLIBRARY = 'http://covers.openlibrary.org/b/isbn/%s-L.jpg?default=false' _lt_br = None + + def get_browser(): global _lt_br if _lt_br is None: _lt_br = browser(user_agent=random_user_agent()) return _lt_br.clone_browser() + class HeadRequest(mechanize.Request): def get_method(self): return 'HEAD' + def check_for_cover(isbn, timeout=5.): br = get_browser() br.set_handle_redirect(False) @@ -39,15 +43,19 @@ def check_for_cover(isbn, timeout=5.): return True return False + class LibraryThingError(Exception): pass + class ISBNNotFound(LibraryThingError): pass + class ServerBusy(LibraryThingError): pass + def login(br, username, password): raw = br.open('http://www.librarything.com').read() if '>Sign out' in raw: @@ -59,6 +67,7 @@ def login(br, username, password): if '>Sign out' not in raw: raise ValueError('Failed to login as %r:%r'%(username, password)) + def option_parser(): parser = OptionParser(usage=_(''' %prog [options] ISBN @@ -71,6 +80,7 @@ Fetch a cover image/social metadata for the book identified by ISBN from Library help='Password for LibraryThing.com') return parser + def get_social_metadata(title, authors, publisher, isbn, username=None, password=None): from calibre.ebooks.metadata import MetaInformation diff --git a/src/calibre/ebooks/metadata/lit.py b/src/calibre/ebooks/metadata/lit.py index 532f61036c..9f39fe594f 100644 --- a/src/calibre/ebooks/metadata/lit.py +++ b/src/calibre/ebooks/metadata/lit.py @@ -8,6 +8,7 @@ import cStringIO, os from calibre.ebooks.metadata.opf2 import OPF + def get_metadata(stream): from calibre.ebooks.lit.reader import LitContainer from calibre.utils.logging import Log diff --git a/src/calibre/ebooks/metadata/lrx.py b/src/calibre/ebooks/metadata/lrx.py index df548d9574..bfcb2a4c81 100644 --- a/src/calibre/ebooks/metadata/lrx.py +++ b/src/calibre/ebooks/metadata/lrx.py @@ -13,19 +13,24 @@ from lxml import etree from calibre.ebooks.metadata import MetaInformation, string_to_authors + def _read(f, at, amount): f.seek(at) return f.read(amount) + def word_be(buf): return struct.unpack('>L', buf)[0] + def word_le(buf): return struct.unpack('H', buf)[0] diff --git a/src/calibre/ebooks/metadata/meta.py b/src/calibre/ebooks/metadata/meta.py index 4336fa7435..de773ec8c6 100644 --- a/src/calibre/ebooks/metadata/meta.py +++ b/src/calibre/ebooks/metadata/meta.py @@ -24,9 +24,11 @@ METADATA_PRIORITIES = collections.defaultdict(lambda:0) for i, ext in enumerate(_METADATA_PRIORITIES): METADATA_PRIORITIES[ext] = i + def path_to_ext(path): return os.path.splitext(path)[1][1:].lower() + def metadata_from_formats(formats, force_read_metadata=False, pattern=None): try: return _metadata_from_formats(formats, force_read_metadata, pattern) @@ -36,6 +38,7 @@ def metadata_from_formats(formats, force_read_metadata=False, pattern=None): mi.authors = [_('Unknown')] return mi + def _metadata_from_formats(formats, force_read_metadata=False, pattern=None): mi = MetaInformation(None, None) formats.sort(cmp=lambda x,y: cmp(METADATA_PRIORITIES[path_to_ext(x)], @@ -67,6 +70,7 @@ def _metadata_from_formats(formats, force_read_metadata=False, pattern=None): return mi + def get_metadata(stream, stream_type='lrf', use_libprs_metadata=False, force_read_metadata=False, pattern=None): pos = 0 @@ -117,6 +121,7 @@ def _get_metadata(stream, stream_type, use_libprs_metadata, return base + def set_metadata(stream, mi, stream_type='lrf', report_error=None): if stream_type: stream_type = stream_type.lower() @@ -194,6 +199,7 @@ def metadata_from_filename(name, pat=None, fallback_pat=None): mi.title = name return mi + def opf_metadata(opfpath): if hasattr(opfpath, 'read'): f = opfpath @@ -216,6 +222,7 @@ def opf_metadata(opfpath): traceback.print_exc() pass + def forked_read_metadata(path, tdir): from calibre.ebooks.metadata.opf2 import metadata_to_opf with lopen(path, 'rb') as f: diff --git a/src/calibre/ebooks/metadata/mobi.py b/src/calibre/ebooks/metadata/mobi.py index 4256062a36..7def78a886 100644 --- a/src/calibre/ebooks/metadata/mobi.py +++ b/src/calibre/ebooks/metadata/mobi.py @@ -21,11 +21,13 @@ from calibre.utils.date import now as nowf from calibre.utils.imghdr import what from calibre.utils.localization import canonicalize_lang, lang_as_iso639_1 + def is_image(ss): if ss is None: return False return what(None, ss[:200]) is not None + class StreamSlicer(object): def __init__(self, stream, start=0, stop=None): @@ -93,6 +95,7 @@ class StreamSlicer(object): def truncate(self, value): self._stream.truncate(value) + class MetadataUpdater(object): DRM_KEY_SIZE = 48 @@ -323,6 +326,7 @@ class MetadataUpdater(object): def update(self, mi): mi.title = normalize(mi.title) + def update_exth_record(rec): recs.append(rec) if rec[0] in self.original_exth_records: @@ -457,11 +461,13 @@ class MetadataUpdater(object): self.thumbnail_record[:] = thumbnail return + def set_metadata(stream, mi): mu = MetadataUpdater(stream) mu.update(mi) return + def get_metadata(stream): from calibre.ebooks.metadata import MetaInformation from calibre.ptempfile import TemporaryDirectory diff --git a/src/calibre/ebooks/metadata/odt.py b/src/calibre/ebooks/metadata/odt.py index 71246f321f..d520573021 100644 --- a/src/calibre/ebooks/metadata/odt.py +++ b/src/calibre/ebooks/metadata/odt.py @@ -55,6 +55,7 @@ fields = { # 'template': (METANS,u'template'), } + def normalize(str): """ The normalize-space function returns the argument string with whitespace @@ -63,11 +64,13 @@ def normalize(str): """ return whitespace.sub(' ', str).strip() + class MetaCollector: """ The MetaCollector is a pseudo file object, that can temporarily ignore write-calls It could probably be replaced with a StringIO object. """ + def __init__(self): self._content = [] self.dowrite = True @@ -155,6 +158,7 @@ class odfmetaparser(xml.sax.saxutils.XMLGenerator): def data(self): return normalize(''.join(self._data)) + def get_metadata(stream, extract_cover=True): zin = zipfile.ZipFile(stream, 'r') odfs = odfmetaparser() @@ -217,6 +221,7 @@ def get_metadata(stream, extract_cover=True): return mi + def read_cover(stream, zin, mi, opfmeta, extract_cover): # search for an draw:image in a draw:frame with the name 'opf.cover' # if opf.metadata prop is false, just use the first image that diff --git a/src/calibre/ebooks/metadata/opf.py b/src/calibre/ebooks/metadata/opf.py index 09b4d41660..90b94e727e 100644 --- a/src/calibre/ebooks/metadata/opf.py +++ b/src/calibre/ebooks/metadata/opf.py @@ -12,6 +12,7 @@ from calibre.ebooks.metadata.opf3 import apply_metadata, read_metadata from calibre.ebooks.metadata.utils import parse_opf, normalize_languages, create_manifest_item, parse_opf_version from calibre.ebooks.metadata import MetaInformation + class DummyFile(object): def __init__(self, raw): @@ -20,24 +21,29 @@ class DummyFile(object): def read(self): return self.raw + def get_metadata2(root, ver): opf = OPF(None, preparsed_opf=root, read_toc=False) return opf.to_book_metadata(), ver, opf.raster_cover, opf.first_spine_item() + def get_metadata3(root, ver): return read_metadata(root, ver=ver, return_extra_data=True) + def get_metadata_from_parsed(root): ver = parse_opf_version(root.get('version')) f = get_metadata2 if ver.major < 3 else get_metadata3 return f(root, ver) + def get_metadata(stream): if isinstance(stream, bytes): stream = DummyFile(stream) root = parse_opf(stream) return get_metadata_from_parsed(root) + def set_metadata_opf2(root, cover_prefix, mi, opf_version, cover_data=None, apply_null=False, update_timestamp=False, force_identifiers=False, add_missing_cover=True): mi = MetaInformation(mi) @@ -85,6 +91,7 @@ def set_metadata_opf2(root, cover_prefix, mi, opf_version, with pretty_print: return opf.render(), raster_cover + def set_metadata_opf3(root, cover_prefix, mi, opf_version, cover_data=None, apply_null=False, update_timestamp=False, force_identifiers=False, add_missing_cover=True): raster_cover = apply_metadata( @@ -93,6 +100,7 @@ def set_metadata_opf3(root, cover_prefix, mi, opf_version, force_identifiers=force_identifiers, add_missing_cover=add_missing_cover) return etree.tostring(root, encoding='utf-8'), raster_cover + def set_metadata(stream, mi, cover_prefix='', cover_data=None, apply_null=False, update_timestamp=False, force_identifiers=False, add_missing_cover=True): if isinstance(stream, bytes): stream = DummyFile(stream) diff --git a/src/calibre/ebooks/metadata/opf2.py b/src/calibre/ebooks/metadata/opf2.py index fa45b516a4..e8fc502622 100644 --- a/src/calibre/ebooks/metadata/opf2.py +++ b/src/calibre/ebooks/metadata/opf2.py @@ -27,6 +27,7 @@ from calibre.utils.config import tweaks pretty_print_opf = False + class PrettyPrint(object): def __enter__(self): @@ -38,6 +39,7 @@ class PrettyPrint(object): pretty_print_opf = False pretty_print = PrettyPrint() + class Resource(object): # {{{ ''' @@ -121,6 +123,7 @@ class Resource(object): # {{{ # }}} + class ResourceCollection(object): # {{{ def __init__(self): @@ -174,6 +177,7 @@ class ResourceCollection(object): # {{{ # }}} + class ManifestItem(Resource): # {{{ @staticmethod @@ -190,6 +194,7 @@ class ManifestItem(Resource): # {{{ def media_type(self): def fget(self): return self.mime_type + def fset(self, val): self.mime_type = val return property(fget=fget, fset=fset) @@ -212,6 +217,7 @@ class ManifestItem(Resource): # {{{ # }}} + class Manifest(ResourceCollection): # {{{ def append_from_opf_manifest_item(self, item, dir): @@ -283,6 +289,7 @@ class Manifest(ResourceCollection): # {{{ # }}} + class Spine(ResourceCollection): # {{{ class Item(Resource): @@ -357,6 +364,7 @@ class Spine(ResourceCollection): # {{{ # }}} + class Guide(ResourceCollection): # {{{ class Reference(Resource): @@ -395,6 +403,7 @@ class Guide(ResourceCollection): # {{{ # }}} + class MetadataField(object): def __init__(self, name, is_dc=True, formatter=None, none_is=None, @@ -437,6 +446,7 @@ class MetadataField(object): elem = obj.create_metadata_element(self.name, is_dc=self.is_dc) obj.set_text(elem, self.renderer(val)) + class TitleSortField(MetadataField): def __get__(self, obj, type=None): @@ -465,6 +475,7 @@ class TitleSortField(MetadataField): if attr.endswith('file-as'): del match.attrib[attr] + def serialize_user_metadata(metadata_elem, all_user_metadata, tail='\n'+(' '*8)): from calibre.utils.config import to_json from calibre.ebooks.metadata.book.json_codec import (object_to_unicode, @@ -495,6 +506,7 @@ def dump_dict(cats): return json.dumps(object_to_unicode(cats), ensure_ascii=False, skipkeys=True) + class OPF(object): # {{{ MIMETYPE = 'application/oebps-package+xml' @@ -1283,6 +1295,7 @@ class OPF(object): # {{{ smap[child.get('name')] = (child, self.metadata.index(child)) if len(smap) == 2 and smap['calibre:series'][1] > smap['calibre:series_index'][1]: s, si = smap['calibre:series'][0], smap['calibre:series_index'][0] + def swap(attr): t = s.get(attr, '') s.set(attr, si.get(attr, '')), si.set(attr, t) @@ -1342,6 +1355,7 @@ class OPF(object): # {{{ # }}} + class OPFCreator(Metadata): def __init__(self, base_path, other): @@ -1599,6 +1613,7 @@ def metadata_to_opf(mi, as_string=True, default_lang=None): metadata = root[0] guide = root[1] metadata[0].tail = '\n'+(' '*8) + def factory(tag, text=None, sort=None, role=None, scheme=None, name=None, content=None): attrib = {} @@ -1786,12 +1801,15 @@ class OPFTest(unittest.TestCase): self.opf.smart_update(MetaInformation(self.opf)) self.testReading() + def suite(): return unittest.TestLoader().loadTestsFromTestCase(OPFTest) + def test(): unittest.TextTestRunner(verbosity=2).run(suite()) + def test_user_metadata(): from cStringIO import StringIO mi = Metadata('Test title', ['test author1', 'test author2']) diff --git a/src/calibre/ebooks/metadata/opf3.py b/src/calibre/ebooks/metadata/opf3.py index 6f41a58119..541f0f03c2 100644 --- a/src/calibre/ebooks/metadata/opf3.py +++ b/src/calibre/ebooks/metadata/opf3.py @@ -26,6 +26,7 @@ from calibre.utils.localization import canonicalize_lang _xpath_cache = {} _re_cache = {} + def uniq(vals): ''' Remove all duplicates from vals, while preserving order. ''' vals = vals or () @@ -33,9 +34,11 @@ def uniq(vals): seen_add = seen.add return list(x for x in vals if x not in seen and not seen_add(x)) + def dump_dict(cats): return json.dumps(object_to_unicode(cats or {}), ensure_ascii=False, skipkeys=True) + def XPath(x): try: return _xpath_cache[x] @@ -43,6 +46,7 @@ def XPath(x): _xpath_cache[x] = ans = etree.XPath(x, namespaces=OPF2_NSMAP) return ans + def regex(r, flags=0): try: return _re_cache[(r, flags)] @@ -50,15 +54,18 @@ def regex(r, flags=0): _re_cache[(r, flags)] = ans = re.compile(r, flags) return ans + def remove_refines(e, refines): for x in refines[e.get('id')]: x.getparent().remove(x) refines.pop(e.get('id'), None) + def remove_element(e, refines): remove_refines(e, refines) e.getparent().remove(e) + def properties_for_id(item_id, refines): ans = {} if item_id: @@ -70,6 +77,7 @@ def properties_for_id(item_id, refines): ans[key] = val return ans + def properties_for_id_with_scheme(item_id, prefixes, refines): ans = {} if item_id: @@ -90,6 +98,7 @@ def properties_for_id_with_scheme(item_id, prefixes, refines): ans[key] = (scheme_ns, scheme, val) return ans + def getroot(elem): while True: q = elem.getparent() @@ -97,6 +106,7 @@ def getroot(elem): return elem elem = q + def ensure_id(elem): root = getroot(elem) eid = elem.get('id') @@ -105,17 +115,20 @@ def ensure_id(elem): elem.set('id', eid) return eid + def normalize_whitespace(text): if not text: return text return re.sub(r'\s+', ' ', text).strip() + def simple_text(f): @wraps(f) def wrapper(*args, **kw): return normalize_whitespace(f(*args, **kw)) return wrapper + def items_with_property(root, q, prefixes=None): if prefixes is None: prefixes = read_prefixes(root) @@ -147,17 +160,21 @@ CALIBRE_PREFIX = 'https://calibre-ebook.com' known_prefixes = reserved_prefixes.copy() known_prefixes['calibre'] = CALIBRE_PREFIX + def parse_prefixes(x): return {m.group(1):m.group(2) for m in re.finditer(r'(\S+): \s*(\S+)', x)} + def read_prefixes(root): ans = reserved_prefixes.copy() ans.update(parse_prefixes(root.get('prefix') or '')) return ans + def expand_prefix(raw, prefixes): return regex(r'(\S+)\s*:\s*(\S+)').sub(lambda m:(prefixes.get(m.group(1), m.group(1)) + ':' + m.group(2)), raw or '') + def ensure_prefix(root, prefixes, prefix, value=None): if prefixes is None: prefixes = read_prefixes(root) @@ -171,6 +188,8 @@ def ensure_prefix(root, prefixes, prefix, value=None): # }}} # Refines {{{ + + def read_refines(root): ans = defaultdict(list) for meta in XPath('./opf:metadata/opf:meta[@refines]')(root): @@ -179,9 +198,11 @@ def read_refines(root): ans[r[1:]].append(meta) return ans + def refdef(prop, val, scheme=None): return (prop, val, scheme) + def set_refines(elem, existing_refines, *new_refines): eid = ensure_id(elem) remove_refines(elem, existing_refines) @@ -197,6 +218,8 @@ def set_refines(elem, existing_refines, *new_refines): # }}} # Identifiers {{{ + + def parse_identifier(ident, val, refines): idid = ident.get('id') refines = refines[idid] @@ -237,6 +260,7 @@ def parse_identifier(ident, val, refines): prefix, rest = val.partition(':')[::2] return finalize(prefix, rest) + def read_identifiers(root, prefixes, refines): ans = defaultdict(list) for ident in XPath('./opf:metadata/dc:identifier')(root): @@ -247,6 +271,7 @@ def read_identifiers(root, prefixes, refines): ans[scheme].append(val) return ans + def set_identifiers(root, prefixes, refines, new_identifiers, force_identifiers=False): uid = root.get('unique-identifier') package_identifier = None @@ -272,6 +297,7 @@ def set_identifiers(root, prefixes, refines, new_identifiers, force_identifiers= p = package_identifier.getparent() p.insert(p.index(package_identifier), ident) + def identifier_writer(name): def writer(root, prefixes, refines, ival=None): uid = root.get('unique-identifier') @@ -300,6 +326,7 @@ set_uuid = identifier_writer('uuid') # Title {{{ + def find_main_title(root, refines, remove_blanks=False): first_title = main_title = None for title in XPath('./opf:metadata/dc:title')(root): @@ -317,11 +344,13 @@ def find_main_title(root, refines, remove_blanks=False): main_title = first_title return main_title + @simple_text def read_title(root, prefixes, refines): main_title = find_main_title(root, refines) return None if main_title is None else main_title.text.strip() + @simple_text def read_title_sort(root, prefixes, refines): main_title = find_main_title(root, refines) @@ -335,6 +364,7 @@ def read_title_sort(root, prefixes, refines): if ans: return ans + def set_title(root, prefixes, refines, title, title_sort=None): main_title = find_main_title(root, refines, remove_blanks=True) if main_title is None: @@ -350,6 +380,8 @@ def set_title(root, prefixes, refines, title, title_sort=None): # }}} # Languages {{{ + + def read_languages(root, prefixes, refines): ans = [] for lang in XPath('./opf:metadata/dc:language')(root): @@ -358,6 +390,7 @@ def read_languages(root, prefixes, refines): ans.append(val) return uniq(ans) + def set_languages(root, prefixes, refines, languages): opf_languages = [] for lang in XPath('./opf:metadata/dc:language')(root): @@ -380,6 +413,7 @@ def set_languages(root, prefixes, refines, languages): Author = namedtuple('Author', 'name sort') + def is_relators_role(props, q): role = props.get('role') if role: @@ -387,6 +421,7 @@ def is_relators_role(props, q): return role.lower() == q and (scheme_ns is None or (scheme_ns, scheme) == (reserved_prefixes['marc'], 'relators')) return False + def read_authors(root, prefixes, refines): roled_authors, unroled_authors = [], [] @@ -416,6 +451,7 @@ def read_authors(root, prefixes, refines): return uniq(roled_authors or unroled_authors) + def set_authors(root, prefixes, refines, authors): ensure_prefix(root, prefixes, 'marc') for item in XPath('./opf:metadata/dc:creator')(root): @@ -438,6 +474,7 @@ def set_authors(root, prefixes, refines, authors): m.text = author.sort metadata.append(m) + def read_book_producers(root, prefixes, refines): ans = [] for item in XPath('./opf:metadata/dc:contributor')(root): @@ -453,6 +490,7 @@ def read_book_producers(root, prefixes, refines): ans.append(normalize_whitespace(val)) return ans + def set_book_producers(root, prefixes, refines, producers): for item in XPath('./opf:metadata/dc:contributor')(root): props = properties_for_id_with_scheme(item.get('id'), prefixes, refines) @@ -473,6 +511,7 @@ def set_book_producers(root, prefixes, refines, producers): # Dates {{{ + def parse_date(raw, is_w3cdtf=False): raw = raw.strip() if is_w3cdtf: @@ -485,6 +524,7 @@ def parse_date(raw, is_w3cdtf=False): ans = fix_only_date(ans) return ans + def read_pubdate(root, prefixes, refines): for date in XPath('./opf:metadata/dc:date')(root): val = (date.text or '').strip() @@ -494,6 +534,7 @@ def read_pubdate(root, prefixes, refines): except Exception: continue + def set_pubdate(root, prefixes, refines, val): for date in XPath('./opf:metadata/dc:date')(root): remove_element(date, refines) @@ -504,6 +545,7 @@ def set_pubdate(root, prefixes, refines, val): d.text = val m.append(d) + def read_timestamp(root, prefixes, refines): pq = '%s:timestamp' % CALIBRE_PREFIX sq = '%s:w3cdtf' % reserved_prefixes['dcterms'] @@ -525,6 +567,7 @@ def read_timestamp(root, prefixes, refines): except Exception: continue + def set_timestamp(root, prefixes, refines, val): ensure_prefix(root, prefixes, 'calibre', CALIBRE_PREFIX) ensure_prefix(root, prefixes, 'dcterms') @@ -558,6 +601,7 @@ def read_last_modified(root, prefixes, refines): # Comments {{{ + def read_comments(root, prefixes, refines): ans = '' for dc in XPath('./opf:metadata/dc:description')(root): @@ -565,6 +609,7 @@ def read_comments(root, prefixes, refines): ans += '\n' + dc.text.strip() return ans.strip() + def set_comments(root, prefixes, refines, val): for dc in XPath('./opf:metadata/dc:description')(root): remove_element(dc, refines) @@ -579,12 +624,14 @@ def set_comments(root, prefixes, refines, val): # Publisher {{{ + @simple_text def read_publisher(root, prefixes, refines): for dc in XPath('./opf:metadata/dc:publisher')(root): if dc.text: return dc.text + def set_publisher(root, prefixes, refines, val): for dc in XPath('./opf:metadata/dc:publisher')(root): remove_element(dc, refines) @@ -599,6 +646,7 @@ def set_publisher(root, prefixes, refines, val): # Tags {{{ + def read_tags(root, prefixes, refines): ans = [] for dc in XPath('./opf:metadata/dc:subject')(root): @@ -606,6 +654,7 @@ def read_tags(root, prefixes, refines): ans.extend(map(normalize_whitespace, dc.text.split(','))) return uniq(filter(None, ans)) + def set_tags(root, prefixes, refines, val): for dc in XPath('./opf:metadata/dc:subject')(root): remove_element(dc, refines) @@ -621,6 +670,7 @@ def set_tags(root, prefixes, refines, val): # Rating {{{ + def read_rating(root, prefixes, refines): pq = '%s:rating' % CALIBRE_PREFIX for meta in XPath('./opf:metadata/opf:meta[@property]')(root): @@ -640,6 +690,7 @@ def read_rating(root, prefixes, refines): except Exception: continue + def set_rating(root, prefixes, refines, val): pq = '%s:rating' % CALIBRE_PREFIX for meta in XPath('./opf:metadata/opf:meta[@name="calibre:rating"]')(root): @@ -658,6 +709,7 @@ def set_rating(root, prefixes, refines, val): # Series {{{ + def read_series(root, prefixes, refines): series_index = 1.0 for meta in XPath('./opf:metadata/opf:meta[@property="belongs-to-collection" and @id]')(root): @@ -682,6 +734,7 @@ def read_series(root, prefixes, refines): return s, series_index return None, series_index + def set_series(root, prefixes, refines, series, series_index): for meta in XPath('./opf:metadata/opf:meta[@name="calibre:series" or @name="calibre:series_index"]')(root): remove_element(meta, refines) @@ -697,6 +750,7 @@ def set_series(root, prefixes, refines, series, series_index): # User metadata {{{ + def dict_reader(name, load=json.loads, try2=True): pq = '%s:%s' % (CALIBRE_PREFIX, name) @@ -727,6 +781,7 @@ def dict_reader(name, load=json.loads, try2=True): read_user_categories = dict_reader('user_categories') read_author_link_map = dict_reader('author_link_map') + def dict_writer(name, serialize=dump_dict, remove2=True): pq = '%s:%s' % (CALIBRE_PREFIX, name) @@ -749,6 +804,7 @@ def dict_writer(name, serialize=dump_dict, remove2=True): set_user_categories = dict_writer('user_categories') set_author_link_map = dict_writer('author_link_map') + def deserialize_user_metadata(val): val = json.loads(val, object_hook=from_json) ans = {} @@ -758,6 +814,7 @@ def deserialize_user_metadata(val): return ans read_user_metadata3 = dict_reader('user_metadata', load=deserialize_user_metadata, try2=False) + def read_user_metadata2(root): ans = {} for meta in XPath('./opf:metadata/opf:meta[starts-with(@name, "calibre:user_metadata:")]')(root): @@ -777,14 +834,17 @@ def read_user_metadata2(root): continue return ans + def read_user_metadata(root, prefixes, refines): return read_user_metadata3(root, prefixes, refines) or read_user_metadata2(root) + def serialize_user_metadata(val): return json.dumps(object_to_unicode(val), ensure_ascii=False, default=to_json, indent=2, sort_keys=True) set_user_metadata3 = dict_writer('user_metadata', serialize=serialize_user_metadata, remove2=False) + def set_user_metadata(root, prefixes, refines, val): for meta in XPath('./opf:metadata/opf:meta[starts-with(@name, "calibre:user_metadata:")]')(root): remove_element(meta, refines) @@ -800,6 +860,7 @@ def set_user_metadata(root, prefixes, refines, val): # Covers {{{ + def read_raster_cover(root, prefixes, refines): def get_href(item): @@ -821,6 +882,7 @@ def read_raster_cover(root, prefixes, refines): if href: return href + def ensure_is_only_raster_cover(root, prefixes, refines, raster_cover_item_href): for item in XPath('./opf:metadata/opf:meta[@name="cover"]')(root): remove_element(item, refines) @@ -838,12 +900,14 @@ def ensure_is_only_raster_cover(root, prefixes, refines, raster_cover_item_href) # Reading/setting Metadata objects {{{ + def first_spine_item(root, prefixes, refines): for i in XPath('./opf:spine/opf:itemref/@idref')(root): for item in XPath('./opf:manifest/opf:item')(root): if item.get('id') == i: return item.get('href') or None + def read_metadata(root, ver=None, return_extra_data=False): ans = Metadata(_('Unknown'), [_('Unknown')]) prefixes, refines = read_prefixes(root), read_refines(root) @@ -892,10 +956,12 @@ def read_metadata(root, ver=None, return_extra_data=False): ans = ans, ver, read_raster_cover(root, prefixes, refines), first_spine_item(root, prefixes, refines) return ans + def get_metadata(stream): root = parse_opf(stream) return read_metadata(root) + def apply_metadata(root, mi, cover_prefix='', cover_data=None, apply_null=False, update_timestamp=False, force_identifiers=False, add_missing_cover=True): prefixes, refines = read_prefixes(root), read_refines(root) current_mi = read_metadata(root) @@ -979,6 +1045,7 @@ def apply_metadata(root, mi, cover_prefix='', cover_data=None, apply_null=False, pretty_print_opf(root) return raster_cover + def set_metadata(stream, mi, cover_prefix='', cover_data=None, apply_null=False, update_timestamp=False, force_identifiers=False, add_missing_cover=True): root = parse_opf(stream) return apply_metadata( diff --git a/src/calibre/ebooks/metadata/opf3_test.py b/src/calibre/ebooks/metadata/opf3_test.py index 0acaf592d3..ed03e30270 100644 --- a/src/calibre/ebooks/metadata/opf3_test.py +++ b/src/calibre/ebooks/metadata/opf3_test.py @@ -32,6 +32,7 @@ read_author_link_map, read_user_categories, set_author_link_map, set_user_catego TEMPLATE = '''{metadata}{manifest}''' % CALIBRE_PREFIX # noqa default_refines = defaultdict(list) + class TestOPF3(unittest.TestCase): ae = unittest.TestCase.assertEqual @@ -57,6 +58,7 @@ class TestOPF3(unittest.TestCase): def test_identifiers(self): # {{{ def idt(val, scheme=None, iid=''): return '{val}'.format(scheme=('opf:scheme="%s"'%scheme if scheme else ''), val=val, id=iid) + def ri(root): return dict(read_identifiers(root, read_prefixes(root), default_refines)) @@ -94,6 +96,7 @@ class TestOPF3(unittest.TestCase): def test_title(self): # {{{ def rt(root): return read_title(root, read_prefixes(root), read_refines(root)) + def st(root, title, title_sort=None): set_title(root, read_prefixes(root), read_refines(root), title, title_sort) return rt(root) @@ -111,6 +114,7 @@ class TestOPF3(unittest.TestCase): def test_languages(self): # {{{ def rl(root): return read_languages(root, read_prefixes(root), read_refines(root)) + def st(root, languages): set_languages(root, read_prefixes(root), read_refines(root), languages) return rl(root) @@ -124,6 +128,7 @@ class TestOPF3(unittest.TestCase): def test_authors(self): # {{{ def rl(root): return read_authors(root, read_prefixes(root), read_refines(root)) + def st(root, authors): set_authors(root, read_prefixes(root), read_refines(root), authors) return rl(root) @@ -149,6 +154,7 @@ class TestOPF3(unittest.TestCase): def test_book_producer(self): # {{{ def rl(root): return read_book_producers(root, read_prefixes(root), read_refines(root)) + def st(root, producers): set_book_producers(root, read_prefixes(root), read_refines(root), producers) return rl(root) @@ -163,12 +169,15 @@ class TestOPF3(unittest.TestCase): def test_dates(self): # {{{ from calibre.utils.date import utcnow + def rl(root): return read_pubdate(root, read_prefixes(root), read_refines(root)), read_timestamp(root, read_prefixes(root), read_refines(root)) + def st(root, pd, ts): set_pubdate(root, read_prefixes(root), read_refines(root), pd) set_timestamp(root, read_prefixes(root), read_refines(root), ts) return rl(root) + def ae(root, y1=None, y2=None): x1, x2 = rl(root) for x, y in ((x1, y1), (x2, y2)): @@ -189,6 +198,7 @@ class TestOPF3(unittest.TestCase): def test_comments(self): # {{{ def rt(root): return read_comments(root, read_prefixes(root), read_refines(root)) + def st(root, val): set_comments(root, read_prefixes(root), read_refines(root), val) return rt(root) @@ -200,6 +210,7 @@ class TestOPF3(unittest.TestCase): def test_publisher(self): # {{{ def rt(root): return read_publisher(root, read_prefixes(root), read_refines(root)) + def st(root, val): set_publisher(root, read_prefixes(root), read_refines(root), val) return rt(root) @@ -225,6 +236,7 @@ class TestOPF3(unittest.TestCase): def test_tags(self): # {{{ def rt(root): return read_tags(root, read_prefixes(root), read_refines(root)) + def st(root, val): set_tags(root, read_prefixes(root), read_refines(root), val) return rt(root) @@ -236,6 +248,7 @@ class TestOPF3(unittest.TestCase): def test_rating(self): # {{{ def rt(root): return read_rating(root, read_prefixes(root), read_refines(root)) + def st(root, val): set_rating(root, read_prefixes(root), read_refines(root), val) return rt(root) @@ -249,6 +262,7 @@ class TestOPF3(unittest.TestCase): def test_series(self): # {{{ def rt(root): return read_series(root, read_prefixes(root), read_refines(root)) + def st(root, val, i): set_series(root, read_prefixes(root), read_refines(root), val, i) return rt(root) @@ -265,6 +279,7 @@ class TestOPF3(unittest.TestCase): def rt(root, name): f = globals()['read_' + name] return f(root, read_prefixes(root), read_refines(root)) + def st(root, name, val): f = globals()['set_' + name] f(root, read_prefixes(root), read_refines(root), val) @@ -275,8 +290,10 @@ class TestOPF3(unittest.TestCase): root = self.get_opf('''{"2":2}''' % (name, name)) self.ae({'2':2}, rt(root, name)) self.ae({'3':3}, st(root, name, {3:3})) + def ru(root): return read_user_metadata(root, read_prefixes(root), read_refines(root)) + def su(root, val): set_user_metadata(root, read_prefixes(root), read_refines(root), val) return ru(root) @@ -531,14 +548,17 @@ class TestOPF3(unittest.TestCase): # Run tests {{{ + def suite(): return unittest.TestLoader().loadTestsFromTestCase(TestOPF3) + class TestRunner(unittest.main): def createTests(self): self.test = suite() + def run(verbosity=4): TestRunner(verbosity=verbosity, exit=False) diff --git a/src/calibre/ebooks/metadata/pdb.py b/src/calibre/ebooks/metadata/pdb.py index 68614d45c7..48d8510312 100644 --- a/src/calibre/ebooks/metadata/pdb.py +++ b/src/calibre/ebooks/metadata/pdb.py @@ -31,6 +31,7 @@ MWRITER = { 'PNRdPPrs' : set_eReader, } + def get_metadata(stream, extract_cover=True): """ Return metadata as a L{MetaInfo} object @@ -45,6 +46,7 @@ def get_metadata(stream, extract_cover=True): return MetadataReader(stream, extract_cover) + def set_metadata(stream, mi): stream.seek(0) diff --git a/src/calibre/ebooks/metadata/pdf.py b/src/calibre/ebooks/metadata/pdf.py index 9e55696246..7c68e9ac7e 100644 --- a/src/calibre/ebooks/metadata/pdf.py +++ b/src/calibre/ebooks/metadata/pdf.py @@ -13,6 +13,7 @@ from calibre.ebooks.metadata import ( MetaInformation, string_to_authors, check_isbn, check_doi) from calibre.utils.ipc.simple_worker import fork_job, WorkerError + def get_tools(): from calibre.ebooks.pdf.pdftohtml import PDFTOHTML base = os.path.dirname(PDFTOHTML) @@ -21,6 +22,7 @@ def get_tools(): pdftoppm = os.path.join(base, 'pdftoppm') + suffix return pdfinfo, pdftoppm + def read_info(outputdir, get_cover): ''' Read info dict and cover from a pdf file named src.pdf in outputdir. Note that this function changes the cwd to outputdir and is therefore not @@ -77,6 +79,7 @@ def read_info(outputdir, get_cover): return ans + def page_images(pdfpath, outputdir, first=1, last=1): pdftoppm = get_tools()[1] outputdir = os.path.abspath(outputdir) @@ -91,6 +94,7 @@ def page_images(pdfpath, outputdir, first=1, last=1): except subprocess.CalledProcessError as e: raise ValueError('Failed to render PDF, pdftoppm errorcode: %s'%e.returncode) + def get_metadata(stream, cover=True): with TemporaryDirectory('_pdf_metadata_read') as pdfpath: stream.seek(0) @@ -165,6 +169,7 @@ get_quick_metadata = partial(get_metadata, cover=False) from calibre.utils.podofo import set_metadata as podofo_set_metadata + def set_metadata(stream, mi): stream.seek(0) return podofo_set_metadata(stream, mi) diff --git a/src/calibre/ebooks/metadata/plucker.py b/src/calibre/ebooks/metadata/plucker.py index 3f9513621d..ecf3b207af 100644 --- a/src/calibre/ebooks/metadata/plucker.py +++ b/src/calibre/ebooks/metadata/plucker.py @@ -18,6 +18,7 @@ from calibre.ebooks.pdb.header import PdbHeaderReader from calibre.ebooks.pdb.plucker.reader import SectionHeader, DATATYPE_METADATA, \ MIBNUM_TO_NAME + def get_metadata(stream, extract_cover=True): ''' Return metadata as a L{MetaInfo} object diff --git a/src/calibre/ebooks/metadata/pml.py b/src/calibre/ebooks/metadata/pml.py index bddb1a9695..fbacae4b17 100644 --- a/src/calibre/ebooks/metadata/pml.py +++ b/src/calibre/ebooks/metadata/pml.py @@ -18,6 +18,7 @@ from calibre.ptempfile import TemporaryDirectory from calibre.utils.zipfile import ZipFile from calibre import prepare_string_for_xml + def get_metadata(stream, extract_cover=True): """ Return metadata as a L{MetaInfo} object """ mi = MetaInformation(_('Unknown'), [_('Unknown')]) @@ -61,6 +62,7 @@ def get_metadata(stream, extract_cover=True): return mi + def get_cover(name, tdir, top_level=False): cover_path = '' cover_data = None diff --git a/src/calibre/ebooks/metadata/rar.py b/src/calibre/ebooks/metadata/rar.py index a6f968b2d9..1e7d0584c9 100644 --- a/src/calibre/ebooks/metadata/rar.py +++ b/src/calibre/ebooks/metadata/rar.py @@ -12,6 +12,7 @@ from io import BytesIO from calibre.utils.unrar import extract_member, names + def get_metadata(stream): from calibre.ebooks.metadata.archive import is_comic from calibre.ebooks.metadata.meta import get_metadata diff --git a/src/calibre/ebooks/metadata/rb.py b/src/calibre/ebooks/metadata/rb.py index c93840cc8d..65b92781e5 100644 --- a/src/calibre/ebooks/metadata/rb.py +++ b/src/calibre/ebooks/metadata/rb.py @@ -8,6 +8,7 @@ from calibre.ebooks.metadata import MetaInformation, string_to_authors MAGIC = '\xb0\x0c\xb0\x0c\x02\x00NUVO\x00\x00\x00\x00' + def get_metadata(stream): """ Return metadata as a L{MetaInfo} object """ title = 'Unknown' diff --git a/src/calibre/ebooks/metadata/rtf.py b/src/calibre/ebooks/metadata/rtf.py index 770d770909..ff89681782 100644 --- a/src/calibre/ebooks/metadata/rtf.py +++ b/src/calibre/ebooks/metadata/rtf.py @@ -15,6 +15,7 @@ comment_pat = re.compile(r'\{\\info.*?\{\\subject(.*?)(?\n%s\n' % ('\ufeff'.encode(encoding), raw_bytes) + def read_simple_property(elem): # A simple property if elem is not None: @@ -86,6 +91,7 @@ def read_simple_property(elem): return elem.text return elem.get(expand('rdf:resource'), '') + def read_lang_alt(parent): # A text value with possible alternate values in different languages items = XPath('descendant::rdf:li[@xml:lang="x-default"]')(parent) @@ -95,11 +101,13 @@ def read_lang_alt(parent): if items: return items[0] + def read_sequence(parent): # A sequence or set of values (assumes simple properties in the sequence) for item in XPath('descendant::rdf:li')(parent): yield read_simple_property(item) + def uniq(vals, kmap=lambda x:x): ''' Remove all duplicates from vals, while preserving order. kmap must be a callable that returns a hashable value for every item in vals ''' @@ -109,6 +117,7 @@ def uniq(vals, kmap=lambda x:x): seen_add = seen.add return tuple(x for x, k in zip(vals, lvals) if k not in seen and not seen_add(k)) + def multiple_sequences(expr, root): # Get all values for sequence elements matching expr, ensuring the returned # list contains distinct non-null elements preserving their order. @@ -117,6 +126,7 @@ def multiple_sequences(expr, root): ans += list(read_sequence(item)) return filter(None, uniq(ans)) + def first_alt(expr, root): # The first element matching expr, assumes that the element contains a # language alternate array @@ -125,6 +135,7 @@ def first_alt(expr, root): if q: return q + def first_simple(expr, root): # The value for the first occurrence of an element matching expr (assumes # simple property) @@ -133,12 +144,14 @@ def first_simple(expr, root): if q: return q + def first_sequence(expr, root): # The first item in a sequence for item in XPath(expr)(root): for ans in read_sequence(item): return ans + def read_series(root): for item in XPath('//calibre:series')(root): val = XPath('descendant::rdf:value')(item) @@ -156,6 +169,7 @@ def read_series(root): return series, series_index return None, None + def read_user_metadata(mi, root): from calibre.utils.config import from_json from calibre.ebooks.metadata.book.json_codec import decode_is_multiple @@ -179,6 +193,7 @@ def read_user_metadata(mi, root): import traceback traceback.print_exc() + def read_xmp_identifers(parent): ''' For example: URLhttp://foo.com @@ -200,6 +215,7 @@ def read_xmp_identifers(parent): else: yield scheme[0].text or '', value + def safe_parse_date(raw): if raw: try: @@ -207,6 +223,7 @@ def safe_parse_date(raw): except Exception: pass + def more_recent(one, two): if one is None: return two @@ -217,6 +234,7 @@ def more_recent(one, two): except Exception: return one + def metadata_from_xmp_packet(raw_bytes): root = parse_xmp_packet(raw_bytes) mi = Metadata(_('Unknown')) @@ -316,6 +334,7 @@ def metadata_from_xmp_packet(raw_bytes): return mi + def consolidate_metadata(info_mi, info): ''' When both the PDF Info dict and XMP metadata are present, prefer the xmp metadata unless the Info ModDate is never than the XMP MetadataDate. This @@ -348,14 +367,17 @@ def consolidate_metadata(info_mi, info): info_mi.authors, info_mi.tags = (info_authors if xmp_mi.is_null('authors') else xmp_mi.authors), xmp_mi.tags or info_tags return info_mi + def nsmap(*args): return {x:NS_MAP[x] for x in args} + def create_simple_property(parent, tag, value): e = parent.makeelement(expand(tag)) parent.append(e) e.text = value + def create_alt_property(parent, tag, value): e = parent.makeelement(expand(tag)) parent.append(e) @@ -366,6 +388,7 @@ def create_alt_property(parent, tag, value): li.set(expand('xml:lang'), 'x-default') li.text = value + def create_sequence_property(parent, tag, val, ordered=True): e = parent.makeelement(expand(tag)) parent.append(e) @@ -376,6 +399,7 @@ def create_sequence_property(parent, tag, val, ordered=True): li.text = x seq.append(li) + def create_identifiers(xmp, identifiers): xmpid = xmp.makeelement(expand('xmp:Identifier')) xmp.append(xmpid) @@ -392,6 +416,7 @@ def create_identifiers(xmp, identifiers): li.append(val) val.text = value + def create_series(calibre, series, series_index): s = calibre.makeelement(expand('calibre:series')) s.set(expand('rdf:parseType'), 'Resource') @@ -407,6 +432,7 @@ def create_series(calibre, series, series_index): si.text = '%.2f' % series_index s.append(si) + def create_user_metadata(calibre, all_user_metadata): from calibre.utils.config import to_json from calibre.ebooks.metadata.book.json_codec import object_to_unicode, encode_is_multiple @@ -436,6 +462,7 @@ def create_user_metadata(calibre, all_user_metadata): val.text = fm li.append(val) + def metadata_to_xmp_packet(mi): A = ElementMaker(namespace=NS_MAP['x'], nsmap=nsmap('x')) R = ElementMaker(namespace=NS_MAP['rdf'], nsmap=nsmap('rdf')) @@ -512,6 +539,7 @@ def metadata_to_xmp_packet(mi): create_user_metadata(calibre, all_user_metadata) return serialize_xmp_packet(root) + def find_used_namespaces(elem): getns = lambda x: (x.partition('}')[0][1:] if '}' in x else None) ans = {getns(x) for x in list(elem.attrib) + [elem.tag]} @@ -519,6 +547,7 @@ def find_used_namespaces(elem): ans |= find_used_namespaces(child) return ans + def find_preferred_prefix(namespace, elems): for elem in elems: ans = {v:k for k, v in elem.nsmap.iteritems()}.get(namespace, None) @@ -526,6 +555,7 @@ def find_preferred_prefix(namespace, elems): return ans return find_preferred_prefix(namespace, elem.iterchildren(etree.Element)) + def find_nsmap(elems): used_namespaces = set() for elem in elems: @@ -546,6 +576,7 @@ def find_nsmap(elems): ans['ns%d' % i] = ns return ans + def clone_into(parent, elem): ' Clone the element, assuming that all namespace declarations are present in parent ' clone = parent.makeelement(elem.tag) @@ -558,6 +589,7 @@ def clone_into(parent, elem): for child in elem.iterchildren(etree.Element): clone_into(clone, child) + def merge_xmp_packet(old, new): ''' Merge metadata present in the old packet that is not present in the new one into the new one. Assumes the new packet was generated by diff --git a/src/calibre/ebooks/metadata/zip.py b/src/calibre/ebooks/metadata/zip.py index ea7e107f4b..59725783bf 100644 --- a/src/calibre/ebooks/metadata/zip.py +++ b/src/calibre/ebooks/metadata/zip.py @@ -8,6 +8,7 @@ from calibre.utils.zipfile import ZipFile from calibre.ptempfile import TemporaryDirectory from calibre import CurrentDir + def get_metadata(stream): from calibre.ebooks.metadata.meta import get_metadata from calibre.ebooks.metadata.archive import is_comic diff --git a/src/calibre/ebooks/mobi/__init__.py b/src/calibre/ebooks/mobi/__init__.py index 2f213670d8..01bddd59f2 100644 --- a/src/calibre/ebooks/mobi/__init__.py +++ b/src/calibre/ebooks/mobi/__init__.py @@ -4,6 +4,7 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' + class MobiError(Exception): pass diff --git a/src/calibre/ebooks/mobi/debug/containers.py b/src/calibre/ebooks/mobi/debug/containers.py index 2d8f04d2fa..8712831e4a 100644 --- a/src/calibre/ebooks/mobi/debug/containers.py +++ b/src/calibre/ebooks/mobi/debug/containers.py @@ -10,6 +10,7 @@ from struct import unpack_from from calibre.ebooks.mobi.debug.headers import EXTHHeader + class ContainerHeader(object): def __init__(self, data): diff --git a/src/calibre/ebooks/mobi/debug/headers.py b/src/calibre/ebooks/mobi/debug/headers.py index f66ac2a432..5651f67966 100644 --- a/src/calibre/ebooks/mobi/debug/headers.py +++ b/src/calibre/ebooks/mobi/debug/headers.py @@ -16,6 +16,8 @@ from calibre.ebooks.mobi.debug import format_bytes from calibre.ebooks.mobi.utils import get_trailing_data # PalmDB {{{ + + class PalmDOCAttributes(object): class Attr(object): @@ -43,6 +45,7 @@ class PalmDOCAttributes(object): attrs = '\n\t'.join([str(x) for x in self.attributes]) return 'PalmDOC Attributes: %s\n\t%s'%(bin(self.val), attrs) + class PalmDB(object): def __init__(self, raw): @@ -101,6 +104,7 @@ class PalmDB(object): return '\n'.join(ans) # }}} + class Record(object): # {{{ def __init__(self, raw, header): @@ -114,6 +118,8 @@ class Record(object): # {{{ # }}} # EXTH {{{ + + class EXTHRecord(object): def __init__(self, type_, data, length): @@ -206,6 +212,7 @@ class EXTHRecord(object): def __str__(self): return '%s (%d): %r'%(self.name, self.type, self.data) + class EXTHHeader(object): def __init__(self, raw): @@ -252,6 +259,7 @@ class EXTHHeader(object): return '\n'.join(ans) # }}} + class MOBIHeader(object): # {{{ def __init__(self, record0, offset): @@ -500,6 +508,7 @@ class MOBIHeader(object): # {{{ return ans # }}} + class MOBIFile(object): def __init__(self, stream): @@ -571,6 +580,7 @@ class MOBIFile(object): self.decompress6, self.decompress8 = d6, d8 + class TextRecord(object): # {{{ def __init__(self, idx, record, extra_data_flags, decompress): diff --git a/src/calibre/ebooks/mobi/debug/index.py b/src/calibre/ebooks/mobi/debug/index.py index 11b265cdf6..42248cfe9a 100644 --- a/src/calibre/ebooks/mobi/debug/index.py +++ b/src/calibre/ebooks/mobi/debug/index.py @@ -31,6 +31,7 @@ FIELD_NAMES = {'len':'Header length', 'type':'Unknown', 'gen':'Index Type (0 - n 'total':'Total number of actual Index Entries in all records', 'ordt': 'ORDT Offset', 'ligt':'LIGT Offset', 'nligt':'Number of LIGT', 'ncncx':'Number of CNCX records', 'indices':'Geometry of index records'} + def read_variable_len_data(data, header): offset = header['tagx'] indices = [] @@ -56,6 +57,7 @@ def read_variable_len_data(data, header): raise ValueError('Traling bytes after last IDXT entry: %r' % trailing_bytes.rstrip(b'\0')) header['indices'] = indices + def read_index(sections, idx, codec): table, cncx = OrderedDict(), CNCX([], codec) @@ -83,6 +85,7 @@ def read_index(sections, idx, codec): read_variable_len_data(data, index_headers[-1]) return table, cncx, indx_header, index_headers + class Index(object): def __init__(self, idx, records, codec): @@ -128,6 +131,7 @@ class Index(object): def __iter__(self): return iter(self.records) + class SKELIndex(Index): def __init__(self, skelidx, records, codec): @@ -148,6 +152,7 @@ class SKELIndex(Index): tag_map[6][1]) # length ) + class SECTIndex(Index): def __init__(self, sectidx, records, codec): @@ -172,6 +177,7 @@ class SECTIndex(Index): ) ) + class GuideIndex(Index): def __init__(self, guideidx, records, codec): @@ -225,6 +231,7 @@ class NCXIndex(Index): if tag == which: entry[name] = self.cncx.get(fieldvalue, default_entry[name]) + def refindx(e, name): ans = e[name] if ans < 0: diff --git a/src/calibre/ebooks/mobi/debug/main.py b/src/calibre/ebooks/mobi/debug/main.py index 7f359d7bf8..d2c4c437af 100644 --- a/src/calibre/ebooks/mobi/debug/main.py +++ b/src/calibre/ebooks/mobi/debug/main.py @@ -13,6 +13,7 @@ from calibre.ebooks.mobi.debug.headers import MOBIFile from calibre.ebooks.mobi.debug.mobi6 import inspect_mobi as inspect_mobi6 from calibre.ebooks.mobi.debug.mobi8 import inspect_mobi as inspect_mobi8 + def inspect_mobi(path_or_stream, ddir=None): # {{{ stream = (path_or_stream if hasattr(path_or_stream, 'read') else open(path_or_stream, 'rb')) @@ -40,6 +41,7 @@ def inspect_mobi(path_or_stream, ddir=None): # {{{ # }}} + def main(): inspect_mobi(sys.argv[1]) diff --git a/src/calibre/ebooks/mobi/debug/mobi6.py b/src/calibre/ebooks/mobi/debug/mobi6.py index e903f3e505..8eb76f198d 100644 --- a/src/calibre/ebooks/mobi/debug/mobi6.py +++ b/src/calibre/ebooks/mobi/debug/mobi6.py @@ -35,6 +35,7 @@ class TagX(object): # {{{ self.num_values, bin(self.bitmask), self.eof) # }}} + class SecondaryIndexHeader(object): # {{{ def __init__(self, record): @@ -97,6 +98,7 @@ class SecondaryIndexHeader(object): # {{{ def __str__(self): ans = ['*'*20 + ' Secondary Index Header '+ '*'*20] a = ans.append + def u(w): a('Unknown: %r (%d bytes) (All zeros: %r)'%(w, len(w), not bool(w.replace(b'\0', b'')))) @@ -130,6 +132,7 @@ class SecondaryIndexHeader(object): # {{{ # }}} + class IndexHeader(object): # {{{ def __init__(self, record): @@ -197,6 +200,7 @@ class IndexHeader(object): # {{{ def __str__(self): ans = ['*'*20 + ' Index Header (%d bytes)'%len(self.record.raw)+ '*'*20] a = ans.append + def u(w): a('Unknown: %r (%d bytes) (All zeros: %r)'%(w, len(w), not bool(w.replace(b'\0', b'')))) @@ -230,6 +234,7 @@ class IndexHeader(object): # {{{ return '\n'.join(ans) # }}} + class Tag(object): # {{{ ''' @@ -284,6 +289,7 @@ class Tag(object): # {{{ # }}} + class IndexEntry(object): # {{{ ''' @@ -370,6 +376,7 @@ class IndexEntry(object): # {{{ # }}} + class IndexRecord(object): # {{{ ''' @@ -408,6 +415,7 @@ class IndexRecord(object): # {{{ def __str__(self): ans = ['*'*20 + ' Index Entries (%d entries) '%len(self.indices)+ '*'*20] a = ans.append + def u(w): a('Unknown: %r (%d bytes) (All zeros: %r)'%(w, len(w), not bool(w.replace(b'\0', b'')))) @@ -428,6 +436,7 @@ class IndexRecord(object): # {{{ # }}} + class CNCX(object): # {{{ ''' @@ -484,6 +493,7 @@ class ImageRecord(object): # {{{ # }}} + class BinaryRecord(object): # {{{ def __init__(self, idx, record): @@ -503,6 +513,7 @@ class BinaryRecord(object): # {{{ # }}} + class FontRecord(object): # {{{ def __init__(self, idx, record): @@ -522,6 +533,7 @@ class FontRecord(object): # {{{ # }}} + class TBSIndexing(object): # {{{ def __init__(self, text_records, indices, doc_type): @@ -590,6 +602,7 @@ class TBSIndexing(object): # {{{ ans.append(('\t\tIndex Entry: %s (Parent index: %s, ' 'Depth: %d, Offset: %d, Size: %d) [%s]')%( x.index, x.parent_index, x.depth, x.offset, x.size, x.label)) + def bin4(num): ans = bin(num)[2:] return bytes('0'*(4-len(ans)) + ans) @@ -711,6 +724,7 @@ class TBSIndexing(object): # {{{ # }}} + class MOBIFile(object): # {{{ def __init__(self, mf): @@ -784,6 +798,7 @@ class MOBIFile(object): # {{{ print (str(self.mobi_header).encode('utf-8'), file=f) # }}} + def inspect_mobi(mobi_file, ddir): f = MOBIFile(mobi_file) with open(os.path.join(ddir, 'header.txt'), 'wb') as out: diff --git a/src/calibre/ebooks/mobi/debug/mobi8.py b/src/calibre/ebooks/mobi/debug/mobi8.py index c8bbeb4f89..6d74f9d1d9 100644 --- a/src/calibre/ebooks/mobi/debug/mobi8.py +++ b/src/calibre/ebooks/mobi/debug/mobi8.py @@ -21,6 +21,7 @@ from calibre.ebooks.mobi.debug import format_bytes from calibre.ebooks.mobi.reader.headers import NULL_INDEX from calibre.utils.imghdr import what + class FDST(object): def __init__(self, raw): @@ -48,6 +49,7 @@ class FDST(object): return '\n'.join(ans) + class File(object): def __init__(self, skel, skeleton, text, first_aid, sections): @@ -67,6 +69,7 @@ class File(object): with open('sect-%04d.html'%i, 'wb') as f: f.write(text) + class MOBIFile(object): def __init__(self, mf): @@ -290,6 +293,7 @@ class MOBIFile(object): desc.append('') self.indexing_data.append('\n'.join(desc)) + def inspect_mobi(mobi_file, ddir): f = MOBIFile(mobi_file) with open(os.path.join(ddir, 'header.txt'), 'wb') as out: diff --git a/src/calibre/ebooks/mobi/huffcdic.py b/src/calibre/ebooks/mobi/huffcdic.py index 2a10f9d27b..f283d6d553 100644 --- a/src/calibre/ebooks/mobi/huffcdic.py +++ b/src/calibre/ebooks/mobi/huffcdic.py @@ -16,6 +16,7 @@ import struct from calibre.ebooks.mobi import MobiError + class Reader(object): def __init__(self): @@ -50,6 +51,7 @@ class Reader(object): phrases, bits = struct.unpack_from(b'>LL', cdic, 8) n = min(1<H').unpack_from + def getslice(off): blen, = h(cdic, 16+off) slice = cdic[18+off:18+off+(blen&0x7fff)] @@ -93,6 +95,7 @@ class Reader(object): s.append(slice_) return b''.join(s) + class HuffReader(object): def __init__(self, huffs): diff --git a/src/calibre/ebooks/mobi/langcodes.py b/src/calibre/ebooks/mobi/langcodes.py index dfff5559a4..7d5bef3ccb 100644 --- a/src/calibre/ebooks/mobi/langcodes.py +++ b/src/calibre/ebooks/mobi/langcodes.py @@ -309,6 +309,7 @@ IANA_MOBI = \ 'TW': (4, 4)}, 'zu': {None: (53, 0)}} + def iana2mobi(icode): langdict, subtags = IANA_MOBI[None], [] if icode: @@ -332,6 +333,7 @@ def iana2mobi(icode): break return pack('>HBB', 0, mcode[1], mcode[0]) + def mobi2iana(langcode, sublangcode): prefix = suffix = None for code, d in IANA_MOBI.items(): diff --git a/src/calibre/ebooks/mobi/mobiml.py b/src/calibre/ebooks/mobi/mobiml.py index 28ff90e30a..131a11418f 100644 --- a/src/calibre/ebooks/mobi/mobiml.py +++ b/src/calibre/ebooks/mobi/mobiml.py @@ -17,6 +17,8 @@ from calibre.ebooks.mobi.utils import convert_color_for_font_tag from calibre.utils.imghdr import identify MBP_NS = 'http://mobipocket.com/ns/mbp' + + def MBP(name): return '{%s}%s' % (MBP_NS, name) @@ -38,11 +40,13 @@ PAGE_BREAKS = set(['always', 'left', 'right']) COLLAPSE = re.compile(r'[ \t\r\n\v]+') + def asfloat(value): if not isinstance(value, (int, long, float)): return 0.0 return float(value) + def isspace(text): if not text: return True @@ -50,7 +54,9 @@ def isspace(text): return False return text.isspace() + class BlockState(object): + def __init__(self, body): self.body = body self.nested = [] @@ -63,7 +69,9 @@ class BlockState(object): self.istate = None self.content = False + class FormatState(object): + def __init__(self): self.rendered = False self.left = 0. @@ -102,6 +110,7 @@ class FormatState(object): class MobiMLizer(object): + def __init__(self, ignore_tables=False): self.ignore_tables = ignore_tables diff --git a/src/calibre/ebooks/mobi/reader/containers.py b/src/calibre/ebooks/mobi/reader/containers.py index 72ebaf29ee..43bbe6d195 100644 --- a/src/calibre/ebooks/mobi/reader/containers.py +++ b/src/calibre/ebooks/mobi/reader/containers.py @@ -10,9 +10,11 @@ from struct import unpack_from, error from calibre.utils.imghdr import what + def find_imgtype(data): return what(None, data) or 'unknown' + class Container(object): def __init__(self, data): diff --git a/src/calibre/ebooks/mobi/reader/headers.py b/src/calibre/ebooks/mobi/reader/headers.py index f9d0332424..cde6d2d4ed 100644 --- a/src/calibre/ebooks/mobi/reader/headers.py +++ b/src/calibre/ebooks/mobi/reader/headers.py @@ -19,6 +19,7 @@ from calibre.utils.config_base import tweaks NULL_INDEX = 0xffffffff + class EXTHHeader(object): # {{{ def __init__(self, raw, codec, title): @@ -167,6 +168,7 @@ class EXTHHeader(object): # {{{ # print 'unhandled metadata record', idx, repr(content) # }}} + class BookHeader(object): def __init__(self, raw, ident, user_encoding, log, try_extra_data_fix=False): @@ -266,6 +268,7 @@ class BookHeader(object): self.skelidx = self.dividx = self.othidx = self.fdstidx = \ NULL_INDEX + class MetadataHeader(BookHeader): def __init__(self, stream, log): diff --git a/src/calibre/ebooks/mobi/reader/index.py b/src/calibre/ebooks/mobi/reader/index.py index d0394c958e..1e9dcebc92 100644 --- a/src/calibre/ebooks/mobi/reader/index.py +++ b/src/calibre/ebooks/mobi/reader/index.py @@ -25,21 +25,26 @@ INDEX_HEADER_FIELDS = ( class InvalidFile(ValueError): pass + def check_signature(data, signature): if data[:len(signature)] != signature: raise InvalidFile('Not a valid %r section'%signature) + class NotAnINDXRecord(InvalidFile): pass + class NotATAGXSection(InvalidFile): pass + def format_bytes(byts): byts = bytearray(byts) byts = [hex(b)[2:] for b in byts] return ' '.join(byts) + def parse_indx_header(data): check_signature(data, b'INDX') words = INDEX_HEADER_FIELDS @@ -120,6 +125,7 @@ class CNCX(object): # {{{ return self.records.iteritems() # }}} + def parse_tagx_section(data): check_signature(data, b'TAGX') @@ -132,6 +138,7 @@ def parse_tagx_section(data): tags.append(TagX(*vals)) return control_byte_count, tags + def get_tag_map(control_byte_count, tagx, data, strict=False): ptags = [] ans = {} @@ -202,6 +209,7 @@ def get_tag_map(control_byte_count, tagx, data, strict=False): return ans + def parse_index_record(table, data, control_byte_count, tags, codec, ordt_map, strict=False): header = parse_indx_header(data) @@ -241,6 +249,7 @@ def parse_index_record(table, data, control_byte_count, tags, codec, table[ident] = tag_map return header + def read_index(sections, idx, codec): table, cncx = OrderedDict(), CNCX([], codec) diff --git a/src/calibre/ebooks/mobi/reader/markup.py b/src/calibre/ebooks/mobi/reader/markup.py index bf9c0e559f..8a35db6d0e 100644 --- a/src/calibre/ebooks/mobi/reader/markup.py +++ b/src/calibre/ebooks/mobi/reader/markup.py @@ -11,6 +11,7 @@ import re, os from calibre.ebooks.chardet import strip_encoding_declarations + def update_internal_links(mobi8_reader, log): # need to update all links that are internal which # are based on positions within the xhtml files **BEFORE** @@ -58,6 +59,7 @@ def update_internal_links(mobi8_reader, log): # All parts are now unicode and have no internal links return parts + def remove_kindlegen_markup(parts, aid_anchor_suffix, linked_aids): # we can safely remove all of the Kindlegen generated aid attributes and @@ -103,6 +105,7 @@ def remove_kindlegen_markup(parts, aid_anchor_suffix, linked_aids): part = "".join(srcpieces) parts[i] = part + def update_flow_links(mobi8_reader, resource_map, log): # kindle:embed:XXXX?mime=image/gif (png, jpeg, etc) (used for images) # kindle:flow:XXXX?mime=YYYY/ZZZ (used for style sheets, svg images, etc) @@ -213,6 +216,7 @@ def update_flow_links(mobi8_reader, resource_map, log): # All flows are now unicode and have links resolved return flows + def insert_flows_into_markup(parts, flows, mobi8_reader, log): mr = mobi8_reader @@ -245,6 +249,7 @@ def insert_flows_into_markup(parts, flows, mobi8_reader, log): # store away modified version parts[i] = part + def insert_images_into_markup(parts, resource_map, log): # Handle any embedded raster images links in the xhtml text # kindle:embed:XXXX?mime=image/gif (png, jpeg, etc) (used for images) @@ -316,12 +321,14 @@ def upshift_markup(parts): # store away modified version parts[i] = part + def handle_media_queries(raw): # cssutils cannot handle CSS 3 media queries. We look for media queries # that use amzn-mobi or amzn-kf8 and map them to a simple @media screen # rule. See https://bugs.launchpad.net/bugs/1406708 for an example import tinycss parser = tinycss.make_full_parser() + def replace(m): sheet = parser.parse_stylesheet(m.group() + '}') if len(sheet.rules) > 0: @@ -336,6 +343,7 @@ def handle_media_queries(raw): return re.sub(r'@media\s[^{;]*?[{;]', replace, raw) + def expand_mobi8_markup(mobi8_reader, resource_map, log): # First update all internal links that are based on offsets parts = update_internal_links(mobi8_reader, log) diff --git a/src/calibre/ebooks/mobi/reader/mobi6.py b/src/calibre/ebooks/mobi/reader/mobi6.py index f09f3bbd1c..bba572f8dd 100644 --- a/src/calibre/ebooks/mobi/reader/mobi6.py +++ b/src/calibre/ebooks/mobi/reader/mobi6.py @@ -24,9 +24,11 @@ from calibre.ebooks.mobi.reader.headers import BookHeader from calibre.utils.img import save_cover_data_to from calibre.utils.imghdr import what + class TopazError(ValueError): pass + class MobiReader(object): PAGE_BREAK_PAT = re.compile( r'<\s*/{0,1}\s*mbp:pagebreak((?:\s+[^/>]*){0,1})/{0,1}\s*>\s*(?:<\s*/{0,1}\s*mbp:pagebreak\s*/{0,1}\s*>)*', @@ -377,6 +379,7 @@ class MobiReader(object): 'x-large': '5', 'xx-large': '6', } + def barename(x): return x.rpartition(':')[-1] @@ -870,6 +873,7 @@ class MobiReader(object): continue self.image_names.append(os.path.basename(path)) + def test_mbp_regex(): for raw, m in { '':'', diff --git a/src/calibre/ebooks/mobi/reader/mobi8.py b/src/calibre/ebooks/mobi/reader/mobi8.py index 5199e92e23..edc2426355 100644 --- a/src/calibre/ebooks/mobi/reader/mobi8.py +++ b/src/calibre/ebooks/mobi/reader/mobi8.py @@ -37,6 +37,8 @@ FlowInfo = namedtuple('FlowInfo', 'type format dir fname') # locate beginning and ending positions of tag with specific aid attribute + + def locate_beg_end_of_tag(ml, aid): pattern = br'''<[^>]*\said\s*=\s*['"]%s['"][^>]*>''' % aid aid_pattern = re.compile(pattern, re.IGNORECASE) @@ -46,6 +48,7 @@ def locate_beg_end_of_tag(ml, aid): return plt, pgt return 0, 0 + def reverse_tag_iter(block): ''' Iterate over all tags in block in reverse order, i.e. last tag to first tag. ''' @@ -60,12 +63,14 @@ def reverse_tag_iter(block): yield block[plt:pgt+1] end = plt + def get_first_resource_index(first_image_index, num_of_text_records, first_text_record_number): first_resource_index = first_image_index if first_resource_index in {-1, NULL_INDEX}: first_resource_index = num_of_text_records + first_text_record_number return first_resource_index + class Mobi8Reader(object): def __init__(self, mobi6_reader, log, for_tweak=False): diff --git a/src/calibre/ebooks/mobi/reader/ncx.py b/src/calibre/ebooks/mobi/reader/ncx.py index 1381416350..c20d795c6c 100644 --- a/src/calibre/ebooks/mobi/reader/ncx.py +++ b/src/calibre/ebooks/mobi/reader/ncx.py @@ -49,6 +49,7 @@ default_entry = { 'image_attribution': None, } + def read_ncx(sections, index, codec): index_entries = [] @@ -80,6 +81,7 @@ def read_ncx(sections, index, codec): return index_entries + def build_toc(index_entries): ans = TOC(base_path=os.getcwdu()) levels = {x['hlvl'] for x in index_entries} diff --git a/src/calibre/ebooks/mobi/tweak.py b/src/calibre/ebooks/mobi/tweak.py index a85340ef2f..1f2bd070b8 100644 --- a/src/calibre/ebooks/mobi/tweak.py +++ b/src/calibre/ebooks/mobi/tweak.py @@ -21,9 +21,11 @@ from calibre.customize.ui import (plugin_for_input_format, plugin_for_output_format) from calibre.utils.ipc.simple_worker import fork_job + class BadFormat(ValueError): pass + def do_explode(path, dest): with open(path, 'rb') as stream: mr = MobiReader(stream, default_log, None, None) @@ -38,6 +40,7 @@ def do_explode(path, dest): return opf + def explode(path, dest, question=lambda x:True): with open(path, 'rb') as stream: raw = stream.read(3) @@ -71,6 +74,7 @@ def explode(path, dest, question=lambda x:True): return fork_job('calibre.ebooks.mobi.tweak', 'do_explode', args=(path, dest), no_output=True)['result'] + def set_cover(oeb): if 'cover' not in oeb.guide or oeb.metadata['cover']: return @@ -80,6 +84,7 @@ def set_cover(oeb): oeb.metadata.clear('cover') oeb.metadata.add('cover', item.id) + def do_rebuild(opf, dest_path): plumber = Plumber(opf, dest_path, default_log) plumber.setup_options() @@ -91,6 +96,7 @@ def do_rebuild(opf, dest_path): set_cover(oeb) outp.convert(oeb, dest_path, inp, plumber.opts, default_log) + def rebuild(src_dir, dest_path): opf = glob.glob(os.path.join(src_dir, '*.opf')) if not opf: diff --git a/src/calibre/ebooks/mobi/utils.py b/src/calibre/ebooks/mobi/utils.py index 2a5c1c0a96..899dffe942 100644 --- a/src/calibre/ebooks/mobi/utils.py +++ b/src/calibre/ebooks/mobi/utils.py @@ -19,6 +19,7 @@ from tinycss.color3 import parse_color_string IMAGE_MAX_SIZE = 10 * 1024 * 1024 RECORD_SIZE = 0x1000 # 4096 (Text record size (uncompressed)) + def decode_string(raw, codec='utf-8', ordt_map=''): length, = struct.unpack(b'>B', raw[0]) raw = raw[1:1+length] @@ -27,6 +28,7 @@ def decode_string(raw, codec='utf-8', ordt_map=''): return ''.join(ordt_map[ord(x)] for x in raw), consumed return raw.decode(codec), consumed + def decode_hex_number(raw, codec='utf-8'): ''' Return a variable length number encoded using hexadecimal encoding. These @@ -42,11 +44,13 @@ def decode_hex_number(raw, codec='utf-8'): raw, consumed = decode_string(raw, codec=codec) return int(raw, 16), consumed + def encode_string(raw): ans = bytearray(bytes(raw)) ans.insert(0, len(ans)) return bytes(ans) + def encode_number_as_hex(num): ''' Encode num as a variable length encoded hexadecimal number. Returns the @@ -61,6 +65,7 @@ def encode_number_as_hex(num): num = b'0'+num return encode_string(num) + def encint(value, forward=True): ''' Some parts of the Mobipocket format encode data as variable-width integers. @@ -97,6 +102,7 @@ def encint(value, forward=True): byts.reverse() return bytes(byts) + def decint(raw, forward=True): ''' Read a variable width integer from the bytestring or bytearray raw and return the @@ -123,6 +129,7 @@ def decint(raw, forward=True): return val, len(byts) + def test_decint(num): for d in (True, False): raw = encint(num, forward=d) @@ -131,6 +138,7 @@ def test_decint(num): raise ValueError('Failed for num %d, forward=%r: %r != %r' % ( num, d, (num, sz), decint(raw, forward=d))) + def rescale_image(data, maxsizeb=IMAGE_MAX_SIZE, dimen=None): ''' Convert image setting all transparent pixels to white and changing format @@ -172,6 +180,7 @@ def rescale_image(data, maxsizeb=IMAGE_MAX_SIZE, dimen=None): scale -= 0.05 return data + def get_trailing_data(record, extra_data_flags): ''' Given a text record as a bytestring and the extra data flags from the MOBI @@ -203,6 +212,7 @@ def get_trailing_data(record, extra_data_flags): record = record[:-sz] return data, record + def encode_trailing_data(raw): ''' Given some data in the bytestring raw, return a bytestring of the form @@ -223,6 +233,7 @@ def encode_trailing_data(raw): lsize += 1 return raw + encoded + def encode_fvwi(val, flags, flag_size=4): ''' Encode the value val and the flag_size bits from flags as a fvwi. This encoding is @@ -279,6 +290,7 @@ def decode_tbs(byts, flag_size=4): consumed += consumed2 return val, extra, consumed + def encode_tbs(val, extra, flag_size=4): ''' Encode the number val and the extra data in the extra dict as an fvwi. See @@ -297,6 +309,7 @@ def encode_tbs(val, extra, flag_size=4): ans += encint(extra[0b0001]) return ans + def utf8_text(text): ''' Convert a possibly null string to utf-8 bytes, guaranteeing to return a non @@ -311,6 +324,7 @@ def utf8_text(text): text = _('Unknown').encode('utf-8') return text + def align_block(raw, multiple=4, pad=b'\0'): ''' Return raw with enough pad bytes append to ensure its length is a multiple @@ -353,6 +367,7 @@ def detect_periodical(toc, log=None): return False return True + def count_set_bits(num): if num < 0: num = -num @@ -362,6 +377,7 @@ def count_set_bits(num): num >>= 1 return ans + def to_base(num, base=32, min_num_digits=None): digits = string.digits + string.ascii_uppercase sign = 1 if num >= 0 else -1 @@ -379,6 +395,7 @@ def to_base(num, base=32, min_num_digits=None): ans.reverse() return ''.join(ans) + def mobify_image(data): 'Convert PNG images to GIF as the idiotic Kindle cannot display some PNG' fmt = what(None, data) @@ -392,6 +409,8 @@ def mobify_image(data): return data # Font records {{{ + + def read_font_record(data, extent=1040): ''' Return the font encoded in the MOBI FONT record represented by data. @@ -467,6 +486,7 @@ def read_font_record(data, extent=1040): return ans + def write_font_record(data, obfuscate=True, compress=True): ''' Write the ttf/otf font represented by data into a font record. See @@ -499,6 +519,7 @@ def write_font_record(data, obfuscate=True, compress=True): # }}} + def create_text_record(text): ''' Return a Palmdoc record of size RECORD_SIZE from the text file object. @@ -549,6 +570,7 @@ def create_text_record(text): return data, overlap + class CNCX(object): # {{{ ''' @@ -595,6 +617,7 @@ class CNCX(object): # {{{ # }}} + def is_guide_ref_start(ref): return (ref.title.lower() == 'start' or (ref.type and ref.type.lower() in {'start', diff --git a/src/calibre/ebooks/mobi/writer2/indexer.py b/src/calibre/ebooks/mobi/writer2/indexer.py index 2e61fe15cc..e2c4433d2b 100644 --- a/src/calibre/ebooks/mobi/writer2/indexer.py +++ b/src/calibre/ebooks/mobi/writer2/indexer.py @@ -15,6 +15,7 @@ from collections import OrderedDict, defaultdict from calibre.ebooks.mobi.utils import (encint, encode_number_as_hex, encode_tbs, align_block, RECORD_SIZE, CNCX as CNCX_) + class CNCX(CNCX_): # {{{ def __init__(self, toc, is_periodical): @@ -30,6 +31,7 @@ class CNCX(CNCX_): # {{{ CNCX_.__init__(self, strings) # }}} + class TAGX(object): # {{{ BITMASKS = {11:0b1} @@ -132,6 +134,7 @@ class IndexEntry(object): def size(self): def fget(self): return self.length + def fset(self, val): self.length = val return property(fget=fget, fset=fset, doc='Alias for length') @@ -198,6 +201,7 @@ class IndexEntry(object): ans = buf.getvalue() return ans + class PeriodicalIndexEntry(IndexEntry): def __init__(self, offset, label_offset, class_offset, depth): @@ -206,6 +210,7 @@ class PeriodicalIndexEntry(IndexEntry): self.class_offset = class_offset self.control_byte_count = 2 + class SecondaryIndexEntry(IndexEntry): INDEX_MAP = {'author':73, 'caption':72, 'credit':71, 'description':70, @@ -238,6 +243,7 @@ class SecondaryIndexEntry(IndexEntry): # }}} + class TBS(object): # {{{ ''' @@ -422,6 +428,7 @@ class TBS(object): # {{{ # }}} + class Indexer(object): # {{{ def __init__(self, serializer, number_of_text_records, diff --git a/src/calibre/ebooks/mobi/writer2/main.py b/src/calibre/ebooks/mobi/writer2/main.py index 880db81563..7d9acebb82 100644 --- a/src/calibre/ebooks/mobi/writer2/main.py +++ b/src/calibre/ebooks/mobi/writer2/main.py @@ -28,12 +28,14 @@ NULL_INDEX = 0xffffffff FLIS = (b'FLIS\0\0\0\x08\0\x41\0\0\0\0\0\0\xff\xff\xff\xff\0\x01\0\x03\0\0\0\x03\0\0\0\x01'+ b'\xff'*4) + def fcis(text_length): fcis = b'FCIS\x00\x00\x00\x14\x00\x00\x00\x10\x00\x00\x00\x01\x00\x00\x00\x00' fcis += pack(b'>I', text_length) fcis += b'\x00\x00\x00\x00\x00\x00\x00\x20\x00\x00\x00\x08\x00\x01\x00\x01\x00\x00\x00\x00' return fcis + class MobiWriter(object): def __init__(self, opts, resources, kf8, write_page_breaks_after_item=True): diff --git a/src/calibre/ebooks/mobi/writer2/resources.py b/src/calibre/ebooks/mobi/writer2/resources.py index 2866ac41b5..661ffc1f48 100644 --- a/src/calibre/ebooks/mobi/writer2/resources.py +++ b/src/calibre/ebooks/mobi/writer2/resources.py @@ -19,6 +19,7 @@ from calibre.utils.imghdr import what PLACEHOLDER_GIF = b'GIF89a\x01\x00\x01\x00\xf0\x00\x00\x00\x00\x00\xff\xff\xff!\xf9\x04\x01\x00\x00\x00\x00!\xfe calibre-placeholder-gif-for-azw3\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02D\x01\x00;' # noqa + class Resources(object): def __init__(self, oeb, opts, is_periodical, add_fonts=False, diff --git a/src/calibre/ebooks/mobi/writer8/cleanup.py b/src/calibre/ebooks/mobi/writer8/cleanup.py index e40faa20ba..70bef1f651 100644 --- a/src/calibre/ebooks/mobi/writer8/cleanup.py +++ b/src/calibre/ebooks/mobi/writer8/cleanup.py @@ -9,6 +9,7 @@ __docformat__ = 'restructuredtext en' from calibre.ebooks.oeb.base import XPath + class CSSCleanup(object): def __init__(self, log, opts): @@ -24,6 +25,7 @@ class CSSCleanup(object): style = stylizer.style(body) style.drop('height') + def remove_duplicate_anchors(oeb): # The Kindle apparently has incorrect behavior for duplicate anchors, see # https://bugs.launchpad.net/calibre/+bug/1454199 diff --git a/src/calibre/ebooks/mobi/writer8/exth.py b/src/calibre/ebooks/mobi/writer8/exth.py index 93776682cd..23f54cb2a2 100644 --- a/src/calibre/ebooks/mobi/writer8/exth.py +++ b/src/calibre/ebooks/mobi/writer8/exth.py @@ -45,6 +45,7 @@ EXTH_CODES = { COLLAPSE_RE = re.compile(r'[ \t\r\n\v]+') + def build_exth(metadata, prefer_author_sort=False, is_periodical=False, share_not_sync=True, cover_offset=None, thumbnail_offset=None, start_offset=None, mobi_doctype=2, num_of_resources=None, diff --git a/src/calibre/ebooks/mobi/writer8/header.py b/src/calibre/ebooks/mobi/writer8/header.py index b8e7b6475e..069a36c11b 100644 --- a/src/calibre/ebooks/mobi/writer8/header.py +++ b/src/calibre/ebooks/mobi/writer8/header.py @@ -19,6 +19,7 @@ zeroes = lambda x: b'\0'*x nulls = lambda x: b'\xff'*x short = lambda x: pack(b'>H', x) + class Header(OrderedDict): HEADER_NAME = b'' diff --git a/src/calibre/ebooks/mobi/writer8/index.py b/src/calibre/ebooks/mobi/writer8/index.py index e554afed1f..01d72d1f8f 100644 --- a/src/calibre/ebooks/mobi/writer8/index.py +++ b/src/calibre/ebooks/mobi/writer8/index.py @@ -26,6 +26,7 @@ EndTagTable = TagMeta(('eof', 0, 0, 0, 1)) mask_to_bit_shifts = {1:0, 2:1, 3:0, 4:2, 8:3, 12:2, 16:4, 32:5, 48:4, 64:6, 128:7, 192: 6} + class IndexHeader(Header): # {{{ HEADER_NAME = b'INDX' @@ -91,6 +92,7 @@ class IndexHeader(Header): # {{{ POSITIONS = {'idxt_offset':'idxt'} # }}} + class Index(object): # {{{ control_byte_count = 1 @@ -234,6 +236,7 @@ class Index(object): # {{{ return self.records # }}} + class SkelIndex(Index): tag_types = tuple(map(TagMeta, ( @@ -275,6 +278,7 @@ class ChunkIndex(Index): }) for c in chunk_table ] + class GuideIndex(Index): tag_types = tuple(map(TagMeta, ( diff --git a/src/calibre/ebooks/mobi/writer8/main.py b/src/calibre/ebooks/mobi/writer8/main.py index 720d5b0d28..333a4314c0 100644 --- a/src/calibre/ebooks/mobi/writer8/main.py +++ b/src/calibre/ebooks/mobi/writer8/main.py @@ -38,6 +38,7 @@ XML_DOCS = OEB_DOCS | {SVG_MIME} # with 4 digits to_ref = partial(to_base, base=32, min_num_digits=4) + class KF8Writer(object): def __init__(self, oeb, opts, resources): @@ -279,6 +280,7 @@ class KF8Writer(object): root = self.data(item) aidbase = i * int(1e6) j = 0 + def in_table(elem): p = elem.getparent() if p is None: @@ -489,6 +491,7 @@ class KF8Writer(object): self.guide_table.sort(key=lambda x:x.type) # Needed by the Kindle self.guide_records = GuideIndex(self.guide_table)() + def create_kf8_book(oeb, opts, resources, for_joint=False): writer = KF8Writer(oeb, opts, resources) return KF8Book(writer, for_joint=for_joint) diff --git a/src/calibre/ebooks/mobi/writer8/mobi.py b/src/calibre/ebooks/mobi/writer8/mobi.py index 639376fee2..ffa75f750d 100644 --- a/src/calibre/ebooks/mobi/writer8/mobi.py +++ b/src/calibre/ebooks/mobi/writer8/mobi.py @@ -20,6 +20,7 @@ from calibre.utils.filenames import ascii_filename NULL_INDEX = 0xffffffff FLIS = b'FLIS\0\0\0\x08\0\x41\0\0\0\0\0\0\xff\xff\xff\xff\0\x01\0\x03\0\0\0\x03\0\0\0\x01'+ b'\xff'*4 + def fcis(text_length): fcis = b'FCIS\x00\x00\x00\x14\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\x00' fcis += pack(b'>L', text_length) @@ -27,6 +28,7 @@ def fcis(text_length): fcis += b'\x28\x00\x00\x00\x08\x00\x01\x00\x01\x00\x00\x00\x00' return fcis + class MOBIHeader(Header): # {{{ ''' @@ -214,6 +216,7 @@ HEADER_FIELDS = {'compression', 'text_length', 'last_text_record', 'book_type', 'guide_index', 'exth', 'full_title', 'extra_data_flags', 'flis_record', 'fcis_record', 'uid'} + class KF8Book(object): def __init__(self, writer, for_joint=False): diff --git a/src/calibre/ebooks/mobi/writer8/skeleton.py b/src/calibre/ebooks/mobi/writer8/skeleton.py index 6e0c7fa36a..d6e4246d69 100644 --- a/src/calibre/ebooks/mobi/writer8/skeleton.py +++ b/src/calibre/ebooks/mobi/writer8/skeleton.py @@ -38,9 +38,11 @@ _self_closing_pat = re.compile(bytes( 'style', 'title', 'head'}))), re.IGNORECASE) + def close_self_closing_tags(raw): return _self_closing_pat.sub(br'<\g\g>>', raw) + def path_to_node(node): ans = [] parent = node.getparent() @@ -50,6 +52,7 @@ def path_to_node(node): parent = parent.getparent() return tuple(reversed(ans)) + def node_from_path(root, path): parent = root for idx in path: @@ -58,6 +61,7 @@ def node_from_path(root, path): mychr = chr if ispy3 else unichr + def tostring(raw, **kwargs): ''' lxml *sometimes* represents non-ascii characters as hex entities in attribute values. I can't figure out exactly what circumstances cause it. @@ -75,6 +79,7 @@ def tostring(raw, **kwargs): return re.sub(r'&#x([0-9A-Fa-f]+);', lambda m:mychr(int(m.group(1), 16)), ans).encode(encoding) + class Chunk(object): def __init__(self, raw, selector): @@ -98,6 +103,7 @@ class Chunk(object): __str__ = __repr__ + class Skeleton(object): def __init__(self, file_number, item, root, chunks): @@ -150,6 +156,7 @@ class Skeleton(object): def raw_text(self): return b''.join([self.skeleton] + [x.raw for x in self.chunks]) + class Chunker(object): def __init__(self, oeb, data_func, placeholder_map): diff --git a/src/calibre/ebooks/mobi/writer8/tbs.py b/src/calibre/ebooks/mobi/writer8/tbs.py index 9ff3a20542..f8becc3f8e 100644 --- a/src/calibre/ebooks/mobi/writer8/tbs.py +++ b/src/calibre/ebooks/mobi/writer8/tbs.py @@ -28,6 +28,7 @@ Entry = namedtuple('IndexEntry', 'index start length depth parent ' 'first_child last_child title action start_offset length_offset ' 'text_record_length') + def fill_entry(entry, start_offset, text_record_length): length_offset = start_offset + entry.length if start_offset < 0: @@ -38,6 +39,7 @@ def fill_entry(entry, start_offset, text_record_length): return Entry(*(entry[:-4] + (action, start_offset, length_offset, text_record_length))) + def populate_strand(parent, entries): ans = [parent] children = [c for c in entries if c.parent == parent.index] @@ -65,6 +67,7 @@ def populate_strand(parent, entries): ans += siblings return ans + def separate_strands(entries): ans = [] while entries: @@ -78,6 +81,7 @@ def separate_strands(entries): ans.append(layers) return ans + def collect_indexing_data(entries, text_record_lengths): ''' For every text record calculate which index entries start, end, span or are contained within that record. Arrange these entries in 'strands'. ''' @@ -105,9 +109,11 @@ def collect_indexing_data(entries, text_record_lengths): return data + class NegativeStrandIndex(Exception): pass + def encode_strands_as_sequences(strands, tbs_type=8): ''' Encode the list of strands for a single text record into a list of sequences, ready to be converted into TBS bytes. ''' @@ -170,6 +176,7 @@ def encode_strands_as_sequences(strands, tbs_type=8): return ans + def sequences_to_bytes(sequences): ans = [] flag_size = 3 @@ -179,6 +186,7 @@ def sequences_to_bytes(sequences): # subsequent sequences could need the 0b1000 flag return b''.join(ans) + def calculate_all_tbs(indexing_data, tbs_type=8): rmap = {} for i, strands in enumerate(indexing_data): @@ -187,6 +195,7 @@ def calculate_all_tbs(indexing_data, tbs_type=8): rmap[i+1] = tbs_bytes return rmap + def apply_trailing_byte_sequences(index_table, records, text_record_lengths): entries = tuple(Entry(r['index'], r['offset'], r['length'], r['depth'], r.get('parent', None), r.get('first_child', None), r.get('last_child', diff --git a/src/calibre/ebooks/mobi/writer8/toc.py b/src/calibre/ebooks/mobi/writer8/toc.py index ab8ff87819..2fe55b1fae 100644 --- a/src/calibre/ebooks/mobi/writer8/toc.py +++ b/src/calibre/ebooks/mobi/writer8/toc.py @@ -34,6 +34,7 @@ TEMPLATE = ''' ''' + def find_previous_calibre_inline_toc(oeb): if 'toc' in oeb.guide: href = urlnormalize(oeb.guide['toc'].href.partition('#')[0]) @@ -42,6 +43,7 @@ def find_previous_calibre_inline_toc(oeb): if (hasattr(item.data, 'xpath') and XPath('//h:body[@id="calibre_generated_inline_toc"]')(item.data)): return item + class TOCAdder(object): def __init__(self, oeb, opts, replace_previous_inline_toc=True, ignore_existing_toc=False): diff --git a/src/calibre/ebooks/odt/input.py b/src/calibre/ebooks/odt/input.py index 0578518fa0..26ed6feaa9 100644 --- a/src/calibre/ebooks/odt/input.py +++ b/src/calibre/ebooks/odt/input.py @@ -20,6 +20,7 @@ from odf.namespaces import TEXTNS as odTEXTNS from calibre import CurrentDir, walk from calibre.ebooks.oeb.base import _css_logger + class Extract(ODF2XHTML): def extract_pictures(self, zf): diff --git a/src/calibre/ebooks/oeb/base.py b/src/calibre/ebooks/oeb/base.py index 582548c9ae..d6377dbf52 100644 --- a/src/calibre/ebooks/oeb/base.py +++ b/src/calibre/ebooks/oeb/base.py @@ -52,30 +52,39 @@ OPF1_NSMAP = {'dc': DC11_NS, 'oebpackage': OPF1_NS} OPF2_NSMAP = {'opf': OPF2_NS, 'dc': DC11_NS, 'dcterms': DCTERMS_NS, 'xsi': XSI_NS, 'calibre': CALIBRE_NS} + def XML(name): return '{%s}%s' % (XML_NS, name) + def OPF(name): return '{%s}%s' % (OPF2_NS, name) + def DC(name): return '{%s}%s' % (DC11_NS, name) + def XSI(name): return '{%s}%s' % (XSI_NS, name) + def DCTERMS(name): return '{%s}%s' % (DCTERMS_NS, name) + def NCX(name): return '{%s}%s' % (NCX_NS, name) + def SVG(name): return '{%s}%s' % (SVG_NS, name) + def XLINK(name): return '{%s}%s' % (XLINK_NS, name) + def CALIBRE(name): return '{%s}%s' % (CALIBRE_NS, name) @@ -97,12 +106,15 @@ _self_closing_pat = re.compile( r'<(?P%s)(?=[\s/])(?P[^>]*)/>'%('|'.join(self_closing_bad_tags)), re.IGNORECASE) + def close_self_closing_tags(raw): return _self_closing_pat.sub(r'<\g\g>>', raw) + def uuid_id(): return u'u'+uuid4() + def itercsslinks(raw): for match in _css_url_re.finditer(raw): yield match.group(1), match.start(1) @@ -111,6 +123,7 @@ def itercsslinks(raw): _link_attrs = set(html.defs.link_attrs) | {XLINK('href'), 'poster'} + def iterlinks(root, find_links_in_css=True): ''' Iterate over all links in a OEB Document. @@ -161,6 +174,7 @@ def iterlinks(root, find_links_in_css=True): for match in _css_url_re.finditer(attribs['style']): yield (el, 'style', match.group(1), match.start(1)) + def make_links_absolute(root, base_url): ''' Make all links in the document absolute, given the @@ -171,6 +185,7 @@ def make_links_absolute(root, base_url): return urljoin(base_url, href) rewrite_links(root, link_repl) + def resolve_base_href(root): base_href = None basetags = root.xpath('//base[@href]|//h:base[@href]', @@ -182,6 +197,7 @@ def resolve_base_href(root): return make_links_absolute(root, base_href, resolve_base_href=False) + def rewrite_links(root, link_repl_func, resolve_base_href=False): ''' Rewrite all the links in the document. For each link @@ -291,11 +307,13 @@ PREFIXNAME_RE = re.compile(r'^[^:]+[:][^:]+') XMLDECL_RE = re.compile(r'^\s*<[?]xml.*?[?]>') CSSURL_RE = re.compile(r'''url[(](?P["']?)(?P[^)]+)(?P=q)[)]''') + def element(parent, *args, **kwargs): if parent is not None: return etree.SubElement(parent, *args, **kwargs) return etree.Element(*args, **kwargs) + def prefixname(name, nsrmap): if not isqname(name): return name @@ -307,9 +325,11 @@ def prefixname(name, nsrmap): return barename(name) return ':'.join((prefix, barename(name))) + def isprefixname(name): return name and PREFIXNAME_RE.match(name) is not None + def qname(name, nsmap): if not isprefixname(name): return name @@ -318,15 +338,19 @@ def qname(name, nsmap): return name return '{%s}%s' % (nsmap[prefix], local) + def isqname(name): return name and QNAME_RE.match(name) is not None + def XPath(expr): return etree.XPath(expr, namespaces=XPNSMAP) + def xpath(elem, expr): return elem.xpath(expr, namespaces=XPNSMAP) + def xml2str(root, pretty_print=False, strip_comments=False, with_tail=True): if not strip_comments: # -- in comments trips up adobe digital editions @@ -345,15 +369,18 @@ def xml2str(root, pretty_print=False, strip_comments=False, with_tail=True): def xml2unicode(root, pretty_print=False): return etree.tostring(root, pretty_print=pretty_print) + def xml2text(elem): return etree.tostring(elem, method='text', encoding=unicode, with_tail=False) + def escape_cdata(root): pat = re.compile(r'[<>&]') for elem in root.iterdescendants('{%s}style' % XHTML_NS, '{%s}script' % XHTML_NS): if elem.text and pat.search(elem.text) is not None: elem.text = etree.CDATA(elem.text.replace(']]>', r'\]\]\>')) + def serialize(data, media_type, pretty_print=False): if isinstance(data, etree._Element): is_oeb_doc = media_type in OEB_DOCS @@ -382,6 +409,7 @@ URL_SAFE = set('ABCDEFGHIJKLMNOPQRSTUVWXYZ' '0123456789' '_.-/~') URL_UNSAFE = [ASCII_CHARS - URL_SAFE, UNIBYTE_CHARS - URL_SAFE] + def urlquote(href): """ Quote URL-unsafe characters, allowing IRI-safe characters. That is, this function returns valid IRIs not valid URIs. In particular, @@ -395,6 +423,7 @@ def urlquote(href): result.append(char) return ''.join(result) + def urlunquote(href, error_handling='strict'): # unquote must run on a bytestring and will return a bytestring # If it runs on a unicode object, it returns a double encoded unicode @@ -411,6 +440,7 @@ def urlunquote(href, error_handling='strict'): href = href.decode('utf-8', error_handling) return href + def urlnormalize(href): """Convert a URL into normalized form, with all and only URL-unsafe characters URL quoted. @@ -427,6 +457,7 @@ def urlnormalize(href): parts = (urlquote(part) for part in parts) return urlunparse(parts) + def extract(elem): """ Removes this element from the tree, including its children and @@ -443,6 +474,7 @@ def extract(elem): previous.tail = (previous.tail or '') + elem.tail parent.remove(elem) + class DummyHandler(logging.Handler): def __init__(self): @@ -463,10 +495,12 @@ _css_logger.setLevel(logging.WARNING) _css_log_handler = DummyHandler() _css_logger.addHandler(_css_log_handler) + class OEBError(Exception): """Generic OEB-processing error.""" pass + class NullContainer(object): """An empty container. @@ -488,6 +522,7 @@ class NullContainer(object): def namelist(self): return [] + class DirContainer(object): """Filesystem directory container.""" @@ -666,6 +701,7 @@ class Metadata(object): def content(self): def fget(self): return self.value + def fset(self, value): self.value = value return property(fget=fget, fset=fset) @@ -971,6 +1007,7 @@ class Manifest(object): - All other content is returned as a :class:`str` object with no special parsing. """ + def fget(self): data = self._data if data is None: @@ -997,8 +1034,10 @@ class Manifest(object): self.media_type = XHTML_MIME self._data = data return data + def fset(self, value): self._data = value + def fdel(self): self._data = None return property(fget, fset, fdel, doc=doc) @@ -1011,6 +1050,7 @@ class Manifest(object): with pt: pt.write(self._data) self.oeb._temp_files.append(pt.name) + def loader(*args): with open(pt.name, 'rb') as f: ans = f.read() @@ -1210,10 +1250,12 @@ class Manifest(object): ans = item break return ans + def fset(self, item): self._main_stylesheet = item return property(fget=fget, fset=fset) + class Spine(object): """Collection of manifest items composing an OEB data model book's main textual content. @@ -1222,6 +1264,7 @@ class Spine(object): content and the sequence in which they appear. Provides Python container access as a list-like object. """ + def __init__(self, oeb): self.oeb = oeb self.items = [] @@ -1367,6 +1410,7 @@ class Guide(object): @dynamic_property def item(self): doc = """The manifest item associated with this reference.""" + def fget(self): path = urldefrag(self.href)[0] hrefs = self.oeb.manifest.hrefs @@ -1446,6 +1490,7 @@ class TOC(object): :attr:`description`: Optional description attribute for periodicals :attr:`toc_thumbnail`: Optional toc thumbnail image """ + def __init__(self, title=None, href=None, klass=None, id=None, play_order=None, author=None, description=None, toc_thumbnail=None): self.title = title @@ -1615,6 +1660,7 @@ class TOC(object): if y is not None: x.play_order = y.play_order + class PageList(object): """Collection of named "pages" to mapped positions within an OEB data model book's textual content. diff --git a/src/calibre/ebooks/oeb/display/test-cfi/run.py b/src/calibre/ebooks/oeb/display/test-cfi/run.py index 775e6c3f5f..a2bf830112 100644 --- a/src/calibre/ebooks/oeb/display/test-cfi/run.py +++ b/src/calibre/ebooks/oeb/display/test-cfi/run.py @@ -17,6 +17,7 @@ except ImportError: init_calibre, serve from calibre.utils.serve_coffee import serve + def run_devel_server(): os.chdir(os.path.dirname(os.path.abspath(__file__))) serve(resources={'cfi.coffee':'../cfi.coffee', '/':'index.html'}) diff --git a/src/calibre/ebooks/oeb/display/test-cfi/run_rapydscript.py b/src/calibre/ebooks/oeb/display/test-cfi/run_rapydscript.py index 02e93db9c4..887708f454 100644 --- a/src/calibre/ebooks/oeb/display/test-cfi/run_rapydscript.py +++ b/src/calibre/ebooks/oeb/display/test-cfi/run_rapydscript.py @@ -11,6 +11,7 @@ import os, shutil, tempfile import SimpleHTTPServer import SocketServer + def run_devel_server(): base = os.path.dirname(os.path.abspath(__file__)) tdir = tempfile.gettempdir() diff --git a/src/calibre/ebooks/oeb/display/webview.py b/src/calibre/ebooks/oeb/display/webview.py index 1f0e8e8363..44dcc97d95 100644 --- a/src/calibre/ebooks/oeb/display/webview.py +++ b/src/calibre/ebooks/oeb/display/webview.py @@ -11,6 +11,7 @@ import re from calibre import guess_type + class EntityDeclarationProcessor(object): # {{{ def __init__(self, html): @@ -24,12 +25,14 @@ class EntityDeclarationProcessor(object): # {{{ self.processed_html = self.processed_html.replace('&%s;'%key, val) # }}} + def self_closing_sub(match): tag = match.group(1) if tag.lower().strip() == 'br': return match.group() return '<%s%s>'%(match.group(1), match.group(2), match.group(1)) + def load_html(path, view, codec='utf-8', mime_type=None, pre_load_callback=lambda x:None, path_is_html=False, force_as_html=False): diff --git a/src/calibre/ebooks/oeb/iterator/__init__.py b/src/calibre/ebooks/oeb/iterator/__init__.py index 7045364e82..34519ab7bb 100644 --- a/src/calibre/ebooks/oeb/iterator/__init__.py +++ b/src/calibre/ebooks/oeb/iterator/__init__.py @@ -11,21 +11,25 @@ import sys, os, re from calibre.customize.ui import available_input_formats + def is_supported(path): ext = os.path.splitext(path)[1].replace('.', '').lower() ext = re.sub(r'(x{0,1})htm(l{0,1})', 'html', ext) return ext in available_input_formats() or ext == 'kepub' + class UnsupportedFormatError(Exception): def __init__(self, fmt): Exception.__init__(self, _('%s format books are not supported')%fmt.upper()) + def EbookIterator(*args, **kwargs): 'For backwards compatibility' from calibre.ebooks.oeb.iterator.book import EbookIterator return EbookIterator(*args, **kwargs) + def get_preprocess_html(path_to_ebook, output=None): from calibre.ebooks.conversion.plumber import set_regex_wizard_callback, Plumber from calibre.utils.logging import DevNull diff --git a/src/calibre/ebooks/oeb/iterator/book.py b/src/calibre/ebooks/oeb/iterator/book.py index 47ac05536c..f419c000cb 100644 --- a/src/calibre/ebooks/oeb/iterator/book.py +++ b/src/calibre/ebooks/oeb/iterator/book.py @@ -29,6 +29,7 @@ TITLEPAGE = CoverManager.SVG_TEMPLATE.decode('utf-8').replace( '__ar__', 'none').replace('__viewbox__', '0 0 600 800' ).replace('__width__', '600').replace('__height__', '800') + class FakeOpts(object): verbose = 0 breadth_first = False @@ -45,6 +46,7 @@ def write_oebbook(oeb, path): if f.endswith('.opf'): return f + def extract_book(pathtoebook, tdir, log=None, view_kepub=False, processed=False, only_input_plugin=False): from calibre.ebooks.conversion.plumber import Plumber, create_oebbook from calibre.utils.logging import default_log @@ -79,6 +81,7 @@ def extract_book(pathtoebook, tdir, log=None, view_kepub=False, processed=False, book_format = 'KF8' + fs return book_format, pathtoopf, plumber.input_fmt + def run_extract_book(*args, **kwargs): from calibre.utils.ipc.simple_worker import fork_job ans = fork_job('calibre.ebooks.oeb.iterator.book', 'extract_book', args=args, kwargs=kwargs, timeout=3000, no_output=True) @@ -111,6 +114,7 @@ class EbookIterator(BookmarksMixin): raw = f.read().decode(path.encoding) root = parse(raw) fragments = [] + def serialize(elem): if elem.text: fragments.append(elem.text.lower()) diff --git a/src/calibre/ebooks/oeb/iterator/bookmarks.py b/src/calibre/ebooks/oeb/iterator/bookmarks.py index 68b653be2d..9141b7cb46 100644 --- a/src/calibre/ebooks/oeb/iterator/bookmarks.py +++ b/src/calibre/ebooks/oeb/iterator/bookmarks.py @@ -15,6 +15,7 @@ from calibre.utils.zipfile import safe_replace BM_FIELD_SEP = u'*|!|?|*' BM_LEGACY_ESC = u'esc-text-%&*#%(){}ads19-end-esc' + class BookmarksMixin(object): def __init__(self, copy_bookmarks_to_file=True): diff --git a/src/calibre/ebooks/oeb/iterator/spine.py b/src/calibre/ebooks/oeb/iterator/spine.py index 3d29ea5cd2..a7a6949d7a 100644 --- a/src/calibre/ebooks/oeb/iterator/spine.py +++ b/src/calibre/ebooks/oeb/iterator/spine.py @@ -16,6 +16,7 @@ from collections import namedtuple from calibre import guess_type, replace_entities from calibre.ebooks.chardet import xml_to_unicode + def character_count(html): ''' Return the number of "significant" text characters in a HTML string. ''' count = 0 @@ -24,6 +25,7 @@ def character_count(html): count += len(strip_space.sub(' ', match.group()))-2 return count + def anchor_map(html): ''' Return map of all anchor names to their offsets in the html ''' ans = {} @@ -33,6 +35,7 @@ def anchor_map(html): ans[anchor] = ans.get(anchor, match.start()) return ans + def all_links(html): ''' Return set of all links in the file ''' ans = set() @@ -41,6 +44,7 @@ def all_links(html): ans.add(replace_entities(match.group(2))) return ans + class SpineItem(unicode): def __new__(cls, path, mime_type=None, read_anchor_map=True, @@ -82,6 +86,7 @@ class SpineItem(unicode): obj.is_single_page = None return obj + class IndexEntry(object): def __init__(self, spine, toc_entry, num): @@ -123,6 +128,7 @@ class IndexEntry(object): self.end_spine_pos = self.spine_count - 1 self.end_anchor = None + def create_indexing_data(spine, toc): if not toc: return diff --git a/src/calibre/ebooks/oeb/normalize_css.py b/src/calibre/ebooks/oeb/normalize_css.py index 559324dd78..c7b6d71520 100644 --- a/src/calibre/ebooks/oeb/normalize_css.py +++ b/src/calibre/ebooks/oeb/normalize_css.py @@ -60,6 +60,7 @@ DEFAULTS = {'azimuth': 'center', 'background-attachment': 'scroll', # {{{ EDGES = ('top', 'right', 'bottom', 'left') BORDER_PROPS = ('color', 'style', 'width') + def normalize_edge(name, cssvalue): style = {} if isinstance(cssvalue, PropertyValue): @@ -89,6 +90,7 @@ def normalize_edge(name, cssvalue): def simple_normalizer(prefix, names, check_inherit=True): composition = tuple('%s-%s' %(prefix, n) for n in names) + @wraps(normalize_simple_composition) def wrapper(name, cssvalue): return normalize_simple_composition(name, cssvalue, composition, check_inherit=check_inherit) @@ -114,6 +116,7 @@ def normalize_simple_composition(name, cssvalue, composition, check_inherit=True font_composition = ('font-style', 'font-variant', 'font-weight', 'font-size', 'line-height', 'font-family') + def normalize_font(cssvalue, font_family_as_list=False): # See https://developer.mozilla.org/en-US/docs/Web/CSS/font composition = font_composition @@ -133,6 +136,7 @@ def normalize_font(cssvalue, font_family_as_list=False): ans['font-family'] = serialize_font_family(ans['font-family']) return ans + def normalize_border(name, cssvalue): style = normalizers['border-' + EDGES[0]]('border-' + EDGES[0], cssvalue) vals = style.copy() @@ -160,6 +164,8 @@ SHORTHAND_DEFAULTS = { } _safe_parser = None + + def safe_parser(): global _safe_parser if _safe_parser is None: @@ -167,6 +173,7 @@ def safe_parser(): _safe_parser = CSSParser(loglevel=logging.CRITICAL, validate=False) return _safe_parser + def normalize_filter_css(props): ans = set() p = safe_parser() @@ -179,6 +186,7 @@ def normalize_filter_css(props): ans |= set(n(prop, cssvalue)) return ans + def condense_edge(vals): edges = {x.name.rpartition('-')[-1]:x.value for x in vals} if len(edges) != 4 or set(edges) != {'left', 'top', 'right', 'bottom'}: @@ -200,6 +208,7 @@ def condense_edge(vals): return ce['top'] return ' '.join(ce[x] for x in ('top', 'left')) + def simple_condenser(prefix, func): @wraps(func) def condense_simple(style, props): @@ -210,6 +219,7 @@ def simple_condenser(prefix, func): style.setProperty(prefix, cp) return condense_simple + def condense_border(style, props): prop_map = {p.name:p for p in props} edge_vals = [] @@ -247,11 +257,13 @@ def condense_rule(style): if len(vals) > 1 and {x.priority for x in vals} == {''}: condensers[prefix[:-1]](style, vals) + def condense_sheet(sheet): for rule in sheet.cssRules: if rule.type == rule.STYLE_RULE: condense_rule(rule.style) + def test_normalization(return_tests=False): # {{{ import unittest from cssutils import parseStyle @@ -289,11 +301,13 @@ def test_normalization(return_tests=False): # {{{ for x, v in expected.iteritems(): ans['border-%s-%s' % (edge, x)] = v return ans + def border_dict(expected): ans = {} for edge in EDGES: ans.update(border_edge_dict(expected, edge)) return ans + def border_val_dict(expected, val='color'): ans = {'border-%s-%s' % (edge, val): DEFAULTS['border-%s-%s' % (edge, val)] for edge in EDGES} for edge in EDGES: diff --git a/src/calibre/ebooks/oeb/parse_utils.py b/src/calibre/ebooks/oeb/parse_utils.py index 4c3382e215..fcf6af2066 100644 --- a/src/calibre/ebooks/oeb/parse_utils.py +++ b/src/calibre/ebooks/oeb/parse_utils.py @@ -19,29 +19,36 @@ RECOVER_PARSER = etree.XMLParser(recover=True, no_network=True) XHTML_NS = 'http://www.w3.org/1999/xhtml' XMLNS_NS = 'http://www.w3.org/2000/xmlns/' + class NotHTML(Exception): def __init__(self, root_tag): Exception.__init__(self, 'Data is not HTML') self.root_tag = root_tag + def barename(name): return name.rpartition('}')[-1] + def namespace(name): return name.rpartition('}')[0][1:] + def XHTML(name): return '{%s}%s' % (XHTML_NS, name) + def xpath(elem, expr): return elem.xpath(expr, namespaces={'h':XHTML_NS}) + def XPath(expr): return etree.XPath(expr, namespaces={'h':XHTML_NS}) META_XP = XPath('/h:html/h:head/h:meta[@http-equiv="Content-Type"]') + def merge_multiple_html_heads_and_bodies(root, log=None): heads, bodies = xpath(root, '//h:head'), xpath(root, '//h:body') if not (len(heads) > 1 or len(bodies) > 1): @@ -61,6 +68,7 @@ def merge_multiple_html_heads_and_bodies(root, log=None): log.warn('Merging multiple and sections') return root + def clone_element(elem, nsmap={}, in_context=True): if in_context: maker = elem.getroottree().getroot().makeelement @@ -72,6 +80,7 @@ def clone_element(elem, nsmap={}, in_context=True): nelem.extend(elem) return nelem + def node_depth(node): ans = 0 p = node.getparent() @@ -80,10 +89,12 @@ def node_depth(node): p = p.getparent() return ans + def fix_self_closing_cdata_tags(data): from html5lib.constants import cdataElements, rcdataElements return re.sub(r'<\s*(%s)\s*[^>]*/\s*>' % ('|'.join(cdataElements|rcdataElements)), r'<\1>', data, flags=re.I) + def html5_parse(data, max_nesting_depth=100): import html5lib, warnings # HTML5 parsing algorithm idiocy: http://code.google.com/p/html5lib/issues/detail?id=195 @@ -178,6 +189,7 @@ def html5_parse(data, max_nesting_depth=100): XMLNS_NS} return clone_element(data, nsmap=fnsmap, in_context=False) + def _html4_parse(data, prefer_soup=False): if prefer_soup: from calibre.utils.soupparser import fromstring @@ -198,6 +210,7 @@ def _html4_parse(data, prefer_soup=False): data = etree.fromstring(data, parser=RECOVER_PARSER) return data + def clean_word_doc(data, log): prefixes = [] for match in re.finditer(r'xmlns:(\S+?)=".*?microsoft.*?"', data): @@ -216,14 +229,17 @@ def clean_word_doc(data, log): data = pat.sub('', data) return data + class HTML5Doc(ValueError): pass + def check_for_html5(prefix, root): if re.search(r'', prefix, re.IGNORECASE) is not None: if root.xpath('//svg'): raise HTML5Doc('This document appears to be un-namespaced HTML 5, should be parsed by the HTML 5 parser') + def parse_html(data, log=None, decoder=None, preprocessor=None, filename='', non_html_file_tags=frozenset()): if log is None: diff --git a/src/calibre/ebooks/oeb/polish/cascade.py b/src/calibre/ebooks/oeb/polish/cascade.py index 1e0fb45e79..8ee753c609 100644 --- a/src/calibre/ebooks/oeb/polish/cascade.py +++ b/src/calibre/ebooks/oeb/polish/cascade.py @@ -22,6 +22,7 @@ from tinycss.fonts3 import serialize_font_family, parse_font_family _html_css_stylesheet = None + def html_css_stylesheet(container): global _html_css_stylesheet if _html_css_stylesheet is None: @@ -29,11 +30,13 @@ def html_css_stylesheet(container): _html_css_stylesheet = container.parse_css(data, 'user-agent.css') return _html_css_stylesheet + def media_allowed(media): if not media or not media.mediaText: return True return media_ok(media.mediaText) + def iterrules(container, sheet_name, rules=None, media_rule_ok=media_allowed, rule_index_counter=None, rule_type=None, importing=None): ''' Iterate over all style rules in the specified sheet. Import and Media rules are automatically resolved. Yields (rule, sheet_name, rule_number). @@ -82,10 +85,12 @@ def iterrules(container, sheet_name, rules=None, media_rule_ok=media_allowed, ru StyleDeclaration = namedtuple('StyleDeclaration', 'index declaration pseudo_element') Specificity = namedtuple('Specificity', 'is_style num_id num_class num_elem rule_index') + def specificity(rule_index, selector, is_style=0): s = selector.specificity return Specificity(is_style, s[1], s[2], s[3], rule_index) + def iterdeclaration(decl): for p in all_properties(decl): n = normalizers.get(p.name) @@ -95,6 +100,7 @@ def iterdeclaration(decl): for k, v in n(p.name, p.propertyValue).iteritems(): yield Property(k, v, p.literalpriority) + class Values(tuple): ''' A tuple of `cssutils.css.Value ` (and its subclasses) objects. Also has a @@ -114,6 +120,7 @@ class Values(tuple): return self[0].cssText return tuple(x.cssText for x in self) + def normalize_style_declaration(decl, sheet_name): ans = {} for prop in iterdeclaration(decl): @@ -123,6 +130,7 @@ def normalize_style_declaration(decl, sheet_name): ans[prop.name] = Values(prop.propertyValue, sheet_name, prop.priority) return ans + def resolve_declarations(decls): property_names = set() for d in decls: @@ -141,12 +149,14 @@ def resolve_declarations(decls): ans[name] = first_val return ans + def resolve_pseudo_declarations(decls): groups = defaultdict(list) for d in decls: groups[d.pseudo_element].append(d) return {k:resolve_declarations(v) for k, v in groups.iteritems()} + def resolve_styles(container, name, select=None, sheet_callback=None): root = container.parsed(name) select = select or Select(root, ignore_inappropriate_pseudo_classes=True) @@ -216,6 +226,7 @@ def resolve_styles(container, name, select=None, sheet_callback=None): _defvals = None + def defvals(): global _defvals if _defvals is None: @@ -223,6 +234,7 @@ def defvals(): _defvals = {k:Values(Property(k, u(val)).propertyValue) for k, val in DEFAULTS.iteritems()} return _defvals + def resolve_property(style_map, elem, name): ''' Given a `style_map` previously generated by :func:`resolve_styles()` and a property `name`, returns the effective value of that property for the @@ -241,6 +253,7 @@ def resolve_property(style_map, elem, name): q = q.getparent() if inheritable else None return defvals().get(name) + def resolve_pseudo_property(style_map, pseudo_style_map, elem, prop, name, abort_on_missing=False): sub_map = pseudo_style_map.get(elem) if abort_on_missing and sub_map is None: diff --git a/src/calibre/ebooks/oeb/polish/check/base.py b/src/calibre/ebooks/oeb/polish/check/base.py index 3046a33c23..39c675f2c0 100644 --- a/src/calibre/ebooks/oeb/polish/check/base.py +++ b/src/calibre/ebooks/oeb/polish/check/base.py @@ -14,6 +14,7 @@ from calibre import detect_ncpus as cpu_count DEBUG, INFO, WARN, ERROR, CRITICAL = xrange(5) + class BaseError(object): HELP = '' @@ -32,6 +33,7 @@ class BaseError(object): __repr__ = __str__ + def worker(func, args): try: result = func(*args) @@ -42,6 +44,7 @@ def worker(func, args): tb = traceback.format_exc() return result, tb + def run_checkers(func, args_list): num = cpu_count() pool = ThreadPool(num) diff --git a/src/calibre/ebooks/oeb/polish/check/fonts.py b/src/calibre/ebooks/oeb/polish/check/fonts.py index 7d9fdd29ed..67bd23a43f 100644 --- a/src/calibre/ebooks/oeb/polish/check/fonts.py +++ b/src/calibre/ebooks/oeb/polish/check/fonts.py @@ -17,11 +17,13 @@ from calibre.ebooks.oeb.polish.fonts import change_font_in_declaration from calibre.utils.fonts.utils import get_all_font_names from tinycss.fonts3 import parse_font_family + class InvalidFont(BaseError): HELP = _('This font could not be processed. It most likely will' ' not work in an ebook reader, either') + def fix_sheet(sheet, css_name, font_name): changed = False for rule in sheet.cssRules: @@ -29,6 +31,7 @@ def fix_sheet(sheet, css_name, font_name): changed = change_font_in_declaration(rule.style, css_name, font_name) or changed return changed + class FontAliasing(BaseError): level = WARN @@ -67,6 +70,7 @@ class FontAliasing(BaseError): changed = True return changed + def check_fonts(container): font_map = {} errors = [] diff --git a/src/calibre/ebooks/oeb/polish/check/images.py b/src/calibre/ebooks/oeb/polish/check/images.py index 9cb2ac9ee2..2b7dac850a 100644 --- a/src/calibre/ebooks/oeb/polish/check/images.py +++ b/src/calibre/ebooks/oeb/polish/check/images.py @@ -13,6 +13,7 @@ from calibre import as_unicode from calibre.ebooks.oeb.polish.check.base import BaseError, WARN from calibre.ebooks.oeb.polish.check.parsing import EmptyFile + class InvalidImage(BaseError): HELP = _('An invalid image is an image that could not be loaded, typically because' @@ -21,6 +22,7 @@ class InvalidImage(BaseError): def __init__(self, msg, *args, **kwargs): BaseError.__init__(self, 'Invalid image: ' + msg, *args, **kwargs) + class CMYKImage(BaseError): HELP = _('Reader devices based on Adobe Digital Editions cannot display images whose' @@ -49,6 +51,7 @@ class CMYKImage(BaseError): f.write(raw) return True + def check_raster_images(name, mt, raw): if not raw: return [EmptyFile(name)] diff --git a/src/calibre/ebooks/oeb/polish/check/links.py b/src/calibre/ebooks/oeb/polish/check/links.py index 9fb01950ce..9646803f6c 100644 --- a/src/calibre/ebooks/oeb/polish/check/links.py +++ b/src/calibre/ebooks/oeb/polish/check/links.py @@ -21,12 +21,14 @@ from calibre.ebooks.oeb.polish.cover import get_raster_cover_name from calibre.ebooks.oeb.polish.utils import guess_type, actual_case_for_name, corrected_case_for_name from calibre.ebooks.oeb.polish.check.base import BaseError, WARN, INFO + class BadLink(BaseError): HELP = _('The resource pointed to by this link does not exist. You should' ' either fix, or remove the link.') level = WARN + class CaseMismatch(BadLink): def __init__(self, href, corrected_name, name, lnum, col): @@ -44,8 +46,10 @@ class CaseMismatch(BadLink): if frag: nhref += '#' + frag orig_href = self.href + class LinkReplacer(object): replaced = False + def __call__(self, url): if url != orig_href: return url @@ -55,6 +59,7 @@ class CaseMismatch(BadLink): container.replace_links(self.name, replacer) return replacer.replaced + class BadDestinationType(BaseError): level = WARN @@ -69,6 +74,7 @@ class BadDestinationType(BaseError): link_elem.get('href'), link_dest) self.bad_href = link_elem.get('href') + class BadDestinationFragment(BaseError): level = WARN @@ -81,21 +87,25 @@ class BadDestinationFragment(BaseError): ' or change the link to point to the correct location.').format( self.bad_href, fragment, link_dest) + class FileLink(BadLink): HELP = _('This link uses the file:// URL scheme. This does not work with many ebook readers.' ' Remove the file:// prefix and make sure the link points to a file inside the book.') + class LocalLink(BadLink): HELP = _('This link points to a file outside the book. It will not work if the' ' book is read on any computer other than the one it was created on.' ' Either fix or remove the link.') + class EmptyLink(BadLink): HELP = _('This link is empty. This is almost always a mistake. Either fill in the link destination or remove the link tag.') + class UnreferencedResource(BadLink): HELP = _('This file is included in the book but not referred to by any document in the spine.' @@ -106,6 +116,7 @@ class UnreferencedResource(BadLink): BadLink.__init__(self, _( 'The file %s is not referenced') % name, name) + class UnreferencedDoc(UnreferencedResource): HELP = _('This file is not in the book spine. All content documents must be in the spine.' @@ -125,6 +136,7 @@ class UnreferencedDoc(UnreferencedResource): container.dirty(container.opf_name) return True + class Unmanifested(BadLink): HELP = _('This file is not listed in the book manifest. While not strictly necessary' @@ -150,6 +162,7 @@ class Unmanifested(BadLink): container.add_name_to_manifest(self.name) return True + class DanglingLink(BadLink): def __init__(self, text, target_name, name, lnum, col): @@ -160,6 +173,7 @@ class DanglingLink(BadLink): def __call__(self, container): return bool(remove_links_to(container, lambda name, *a: name == self.target_name)) + class Bookmarks(BadLink): HELP = _( @@ -221,6 +235,7 @@ class MimetypeMismatch(BaseError): container.dirty(container.opf_name) return changed + def check_mimetypes(container): errors = [] a = errors.append @@ -232,6 +247,7 @@ def check_mimetypes(container): a(MimetypeMismatch(container, name, mt, gt)) return errors + def check_link_destination(container, dest_map, name, href, a, errors): if href.startswith('#'): tname = name @@ -279,6 +295,7 @@ def check_link_destinations(container): return errors + def check_links(container): links_map = defaultdict(set) xml_types = {guess_type('a.opf'), guess_type('a.ncx')} @@ -361,6 +378,7 @@ def check_links(container): return errors + def check_external_links(container, progress_callback=lambda num, total:None): progress_callback(0, 0) external_links = defaultdict(list) diff --git a/src/calibre/ebooks/oeb/polish/check/main.py b/src/calibre/ebooks/oeb/polish/check/main.py index 0a7feb6ff3..cfc59ed657 100644 --- a/src/calibre/ebooks/oeb/polish/check/main.py +++ b/src/calibre/ebooks/oeb/polish/check/main.py @@ -22,6 +22,7 @@ from calibre.ebooks.oeb.polish.check.opf import check_opf XML_TYPES = frozenset(map(guess_type, ('a.xml', 'a.svg', 'a.opf', 'a.ncx'))) | {'application/oebps-page-map+xml'} + def run_checks(container): errors = [] @@ -81,6 +82,7 @@ def run_checks(container): return errors + def fix_errors(container, errors): # Fix parsing changed = False diff --git a/src/calibre/ebooks/oeb/polish/check/opf.py b/src/calibre/ebooks/oeb/polish/check/opf.py index 4a099f7936..d3c83bda5e 100644 --- a/src/calibre/ebooks/oeb/polish/check/opf.py +++ b/src/calibre/ebooks/oeb/polish/check/opf.py @@ -14,6 +14,7 @@ from calibre.ebooks.oeb.polish.toc import find_existing_nav_toc from calibre.ebooks.oeb.polish.utils import guess_type from calibre.ebooks.oeb.base import OPF, OPF2_NS, DC, DC11_NS, XHTML_MIME + class MissingSection(BaseError): def __init__(self, name, section_name): @@ -21,6 +22,7 @@ class MissingSection(BaseError): self.HELP = xml(_( 'The <%s> section is required in the OPF file. You have to create one.') % section_name) + class IncorrectIdref(BaseError): def __init__(self, name, idref, lnum): @@ -28,6 +30,7 @@ class IncorrectIdref(BaseError): self.HELP = xml(_( 'The idref="%s" points to an id that does not exist in the OPF') % idref) + class IncorrectCover(BaseError): def __init__(self, name, lnum, cover): @@ -35,6 +38,7 @@ class IncorrectCover(BaseError): self.HELP = xml(_( 'The meta cover tag points to an item with id="%s" which does not exist in the manifest') % cover) + class NookCover(BaseError): HELP = _( @@ -52,6 +56,7 @@ class NookCover(BaseError): container.dirty(container.opf_name) return True + class IncorrectToc(BaseError): def __init__(self, name, lnum, bad_idref=None, bad_mimetype=None): @@ -63,6 +68,7 @@ class IncorrectToc(BaseError): self.HELP = _('The media type for the table of contents must be %s') % guess_type('a.ncx') BaseError.__init__(self, msg, name, lnum) + class NoHref(BaseError): HELP = _('This manifest entry has no href attribute. Either add the href attribute or remove the entry.') @@ -81,6 +87,7 @@ class NoHref(BaseError): container.dirty(container.opf_name) return changed + class MissingNCXRef(BaseError): HELP = _('The tag has no reference to the NCX table of contents file.' @@ -101,6 +108,7 @@ class MissingNCXRef(BaseError): container.dirty(container.opf_name) return changed + class MissingNav(BaseError): HELP = _('This book has no Navigation document. According to the EPUB 3 specification, a navigation document' @@ -110,6 +118,7 @@ class MissingNav(BaseError): def __init__(self, name, lnum): BaseError.__init__(self, _('Missing navigation document'), name, lnum) + class MissingHref(BaseError): HELP = _('A file listed in the manifest is missing, you should either remove' @@ -126,6 +135,7 @@ class MissingHref(BaseError): container.dirty(container.opf_name) return True + class NonLinearItems(BaseError): level = WARN @@ -149,6 +159,7 @@ class NonLinearItems(BaseError): container.dirty(container.opf_name) return True + class DuplicateHref(BaseError): has_multiple_locations = True @@ -173,6 +184,7 @@ class DuplicateHref(BaseError): container.dirty(self.name) return True + class MultipleCovers(BaseError): has_multiple_locations = True @@ -190,6 +202,7 @@ class MultipleCovers(BaseError): container.dirty(self.name) return True + class NoUID(BaseError): HELP = xml(_( @@ -217,6 +230,7 @@ class NoUID(BaseError): container.dirty(container.opf_name) return True + class BadSpineMime(BaseError): def __init__(self, name, iid, mt, lnum, opf_name): @@ -238,6 +252,7 @@ class BadSpineMime(BaseError): container.refresh_mime_map() return True + def check_opf(container): errors = [] opf_version = container.opf_version_parsed diff --git a/src/calibre/ebooks/oeb/polish/check/parsing.py b/src/calibre/ebooks/oeb/polish/check/parsing.py index 507c45878d..c756eb794c 100644 --- a/src/calibre/ebooks/oeb/polish/check/parsing.py +++ b/src/calibre/ebooks/oeb/polish/check/parsing.py @@ -26,6 +26,7 @@ ALL_ENTITIES = HTML_ENTITTIES | XML_ENTITIES replace_pat = re.compile('&(%s);' % '|'.join(re.escape(x) for x in sorted((HTML_ENTITTIES - XML_ENTITIES)))) mismatch_pat = re.compile('tag mismatch:.+?line (\d+).+?line \d+') + class EmptyFile(BaseError): HELP = _('This file is empty, it contains nothing, you should probably remove it.') @@ -38,6 +39,7 @@ class EmptyFile(BaseError): container.remove_item(self.name) return True + class DecodeError(BaseError): is_parsing_error = True @@ -51,6 +53,7 @@ class DecodeError(BaseError): def __init__(self, name): BaseError.__init__(self, _('Parsing of %s failed, could not decode') % name, name) + class XMLParseError(BaseError): is_parsing_error = True @@ -68,6 +71,7 @@ class XMLParseError(BaseError): self.has_multiple_locations = True self.all_locations = [(self.name, int(m.group(1)), None), (self.name, self.line, self.col)] + class HTMLParseError(XMLParseError): HELP = _('A parsing error in an HTML file means that the HTML syntax is incorrect.' @@ -75,6 +79,7 @@ class HTMLParseError(XMLParseError): ' incorrect display of content. These errors can usually be fixed automatically,' ' however, automatic fixing can sometimes "do the wrong thing".') + class NamedEntities(BaseError): level = WARN @@ -100,6 +105,7 @@ class NamedEntities(BaseError): f.write(nraw.encode('utf-8')) return changed + class EscapedName(BaseError): level = WARN @@ -108,6 +114,7 @@ class EscapedName(BaseError): from calibre.utils.filenames import ascii_filename BaseError.__init__(self, _('Filename contains unsafe characters'), name) qname = urlquote(name) + def esc(n): return ''.join(x if x in URL_SAFE else '_' for x in n) self.sname = '/'.join(esc(ascii_filename(x)) for x in name.split('/')) @@ -131,6 +138,7 @@ class EscapedName(BaseError): rename_files(container, {self.name:self.sname}) return True + class TooLarge(BaseError): level = INFO @@ -141,6 +149,7 @@ class TooLarge(BaseError): def __init__(self, name): BaseError.__init__(self, _('File too large'), name) + class BadEntity(BaseError): HELP = _('This is an invalid (unrecognized) entity. Replace it with whatever' @@ -149,6 +158,7 @@ class BadEntity(BaseError): def __init__(self, ent, name, lnum, col): BaseError.__init__(self, _('Invalid entity: %s') % ent, name, lnum, col) + class BadNamespace(BaseError): INDIVIDUAL_FIX = _( @@ -167,6 +177,7 @@ class BadNamespace(BaseError): container.dirty(self.name) return True + class NonUTF8(BaseError): level = WARN @@ -186,6 +197,7 @@ class NonUTF8(BaseError): container.open(self.name, 'wb').write(raw.encode('utf-8')) return True + class EntitityProcessor(object): def __init__(self, mt): @@ -219,6 +231,7 @@ class EntitityProcessor(object): self.bad_entities.append((m.start(), m.group())) return b' ' * len(m.group()) + def check_html_size(name, mt, raw): errors = [] if len(raw) > TooLarge.MAX_SIZE: @@ -227,6 +240,7 @@ def check_html_size(name, mt, raw): entity_pat = re.compile(br'&(#{0,1}[a-zA-Z0-9]{1,8});') + def check_encoding_declarations(name, container): errors = [] enc = find_declared_encoding(container.raw_data(name)) @@ -234,6 +248,7 @@ def check_encoding_declarations(name, container): errors.append(NonUTF8(name, enc)) return errors + def check_xml_parsing(name, mt, raw): if not raw: return [EmptyFile(name)] @@ -271,6 +286,7 @@ def check_xml_parsing(name, mt, raw): return errors + class CSSError(BaseError): is_parsing_error = True @@ -306,6 +322,7 @@ class CSSError(BaseError): pos_pats = (re.compile(r'\[(\d+):(\d+)'), re.compile(r'(\d+), (\d+)\)')) + class DuplicateId(BaseError): has_multiple_locations = True @@ -328,6 +345,7 @@ class DuplicateId(BaseError): container.dirty(self.name) return True + class InvalidId(BaseError): level = WARN @@ -357,6 +375,7 @@ class InvalidId(BaseError): replace_ids(container, {self.name:{self.invalid_id:newid}}) return changed + class BareTextInBody(BaseError): INDIVIDUAL_FIX = _('Wrap the bare text in a p tag') @@ -389,6 +408,7 @@ class BareTextInBody(BaseError): container.dirty(self.name) return True + class ErrorHandler(object): ' Replacement logger to get useful error/warning info out of cssutils during parsing ' @@ -423,6 +443,7 @@ class ErrorHandler(object): self.__handle(WARN, *args) warning = warn + def check_css_parsing(name, raw, line_offset=0, is_declaration=False): log = ErrorHandler(name) parser = cssutils.CSSParser(fetcher=lambda x: (None, None), log=log) @@ -437,6 +458,7 @@ def check_css_parsing(name, raw, line_offset=0, is_declaration=False): err.line += line_offset return log.errors + def check_filenames(container): errors = [] all_names = set(container.name_path_map) - container.names_that_must_not_be_changed @@ -447,6 +469,7 @@ def check_filenames(container): valid_id = re.compile(r'^[a-zA-Z][a-zA-Z0-9_:.-]*$') + def check_ids(container): errors = [] mts = set(OEB_DOCS) | {guess_type('a.opf'), guess_type('a.ncx')} @@ -468,6 +491,7 @@ def check_ids(container): errors.extend(DuplicateId(name, eid, locs) for eid, locs in dups.iteritems()) return errors + def check_markup(container): errors = [] for name, mt in container.mime_map.iteritems(): diff --git a/src/calibre/ebooks/oeb/polish/container.py b/src/calibre/ebooks/oeb/polish/container.py index 9ea7c0a408..b38a0babe2 100644 --- a/src/calibre/ebooks/oeb/polish/container.py +++ b/src/calibre/ebooks/oeb/polish/container.py @@ -48,11 +48,13 @@ exists, join, relpath = os.path.exists, os.path.join, os.path.relpath OEB_FONTS = {guess_type('a.ttf'), guess_type('b.otf'), guess_type('a.woff'), 'application/x-font-ttf', 'application/x-font-otf', 'application/font-sfnt'} OPF_NAMESPACES = {'opf':OPF2_NS, 'dc':DC11_NS} + class CSSPreProcessor(cssp): def __call__(self, data): return self.MS_PAT.sub(self.ms_sub, data) + def clone_dir(src, dest): ' Clone a directory using hard links for the files, dest must already exist ' for x in os.listdir(src): @@ -67,6 +69,7 @@ def clone_dir(src, dest): except: shutil.copy2(spath, dpath) + def clone_container(container, dest_dir): ' Efficiently clone a container using hard links ' dest_dir = os.path.abspath(os.path.realpath(dest_dir)) @@ -76,18 +79,22 @@ def clone_container(container, dest_dir): return cls(None, None, container.log, clone_data=clone_data) return cls(None, container.log, clone_data=clone_data) + def name_to_abspath(name, root): return os.path.abspath(join(root, *name.split('/'))) + def abspath_to_name(path, root): return relpath(os.path.abspath(path), root).replace(os.sep, '/') + def name_to_href(name, root, base=None, quote=urlquote): fullpath = name_to_abspath(name, root) basepath = root if base is None else os.path.dirname(name_to_abspath(base, root)) path = relpath(fullpath, basepath).replace(os.sep, '/') return quote(path) + def href_to_name(href, root, base=None): base = root if base is None else os.path.dirname(name_to_abspath(base, root)) purl = urlparse(href) @@ -102,6 +109,7 @@ def href_to_name(href, root, base=None): fullpath = os.path.join(base, *href.split('/')) return abspath_to_name(fullpath, root) + class ContainerBase(object): # {{{ ''' A base class that implements just the parsing methods. Useful to create @@ -190,6 +198,7 @@ class ContainerBase(object): # {{{ css_preprocessor=(None if self.tweak_mode else self.css_preprocessor)) # }}} + class Container(ContainerBase): # {{{ ''' @@ -1007,9 +1016,12 @@ class Container(ContainerBase): # {{{ # }}} # EPUB {{{ + + class InvalidEpub(InvalidBook): pass + class ObfuscationKeyMissing(InvalidEpub): pass @@ -1017,6 +1029,7 @@ OCF_NS = 'urn:oasis:names:tc:opendocument:xmlns:container' VCS_IGNORE_FILES = frozenset('.gitignore .hgignore .agignore .bzrignore'.split()) VCS_DIRS = frozenset(('.git', '.hg', '.svn', '.bzr')) + def walk_dir(basedir): for dirpath, dirnames, filenames in os.walk(basedir): for vcsdir in VCS_DIRS: @@ -1030,6 +1043,7 @@ def walk_dir(basedir): if fname not in VCS_IGNORE_FILES: yield is_root, dirpath, fname + class EpubContainer(Container): book_type = 'epub' @@ -1280,6 +1294,7 @@ class EpubContainer(Container): def path_to_ebook(self): def fget(self): return self.pathtoepub + def fset(self, val): self.pathtoepub = val return property(fget=fget, fset=fset) @@ -1287,9 +1302,12 @@ class EpubContainer(Container): # }}} # AZW3 {{{ + + class InvalidMobi(InvalidBook): pass + def do_explode(path, dest): from calibre.ebooks.mobi.reader.mobi6 import MobiReader from calibre.ebooks.mobi.reader.mobi8 import Mobi8Reader @@ -1326,11 +1344,13 @@ def opf_to_azw3(opf, outpath, container): set_cover(oeb) outp.convert(oeb, outpath, inp, plumber.opts, container.log) + def epub_to_azw3(epub, outpath=None): container = get_container(epub, tweak_mode=True) outpath = outpath or (epub.rpartition('.')[0] + '.azw3') opf_to_azw3(container.name_to_abspath(container.opf_name), outpath, container) + class AZW3Container(Container): book_type = 'azw3' @@ -1401,6 +1421,7 @@ class AZW3Container(Container): def path_to_ebook(self): def fget(self): return self.pathtoazw3 + def fset(self, val): self.pathtoazw3 = val return property(fget=fget, fset=fset) @@ -1410,6 +1431,7 @@ class AZW3Container(Container): return set(self.name_path_map) # }}} + def get_container(path, log=None, tdir=None, tweak_mode=False): if log is None: log = default_log @@ -1422,6 +1444,7 @@ def get_container(path, log=None, tdir=None, tweak_mode=False): ebook.tweak_mode = tweak_mode return ebook + def test_roundtrip(): ebook = get_container(sys.argv[-1]) p = PersistentTemporaryFile(suffix='.'+sys.argv[-1].rpartition('.')[-1]) diff --git a/src/calibre/ebooks/oeb/polish/cover.py b/src/calibre/ebooks/oeb/polish/cover.py index ea2e5d6a52..c0d2d7f2bf 100644 --- a/src/calibre/ebooks/oeb/polish/cover.py +++ b/src/calibre/ebooks/oeb/polish/cover.py @@ -13,6 +13,7 @@ from calibre.ebooks.oeb.base import OPF, OEB_DOCS, XPath, XLINK, xml2text from calibre.ebooks.oeb.polish.replace import replace_links, get_recommended_folders from calibre.utils.imghdr import identify + def set_azw3_cover(container, cover_path, report, options=None): existing_image = options is not None and options.get('existing_image', False) name = None @@ -39,11 +40,13 @@ def set_azw3_cover(container, cover_path, report, options=None): container.dirty(container.opf_name) report(_('Cover updated') if found else _('Cover inserted')) + def get_azw3_raster_cover_name(container): items = container.opf_xpath('//opf:guide/opf:reference[@href and contains(@type, "cover")]') if items: return container.href_to_name(items[0].get('href')) + def mark_as_cover_azw3(container, name): href = container.name_to_href(name, container.opf_name) found = False @@ -56,16 +59,19 @@ def mark_as_cover_azw3(container, name): OPF('reference'), href=href, type='cover')) container.dirty(container.opf_name) + def get_raster_cover_name(container): if container.book_type == 'azw3': return get_azw3_raster_cover_name(container) return find_cover_image(container, strict=True) + def get_cover_page_name(container): if container.book_type == 'azw3': return return find_cover_page(container) + def set_cover(container, cover_path, report=None, options=None): ''' Set the cover of the book to the image pointed to by cover_path. @@ -86,6 +92,7 @@ def set_cover(container, cover_path, report=None, options=None): else: set_epub_cover(container, cover_path, report, options=options) + def mark_as_cover(container, name): ''' Mark the specified image as the cover image. @@ -103,6 +110,7 @@ def mark_as_cover(container, name): ############################################################################### # The delightful EPUB cover processing + def is_raster_image(media_type): return media_type and media_type.lower() in { 'image/png', 'image/jpeg', 'image/jpg', 'image/gif'} @@ -113,6 +121,7 @@ COVER_TYPES = { 'other.ms-coverimage', 'other.ms-thumbimage-standard', 'other.ms-thumbimage', 'thumbimagestandard', 'cover'} + def find_cover_image2(container, strict=False): manifest_id_map = container.manifest_id_map mm = container.mime_map @@ -145,6 +154,7 @@ def find_cover_image2(container, strict=False): if largest_cover[0]: return largest_cover[0] + def find_cover_image3(container): for name in container.manifest_items_with_property('cover-image'): return name @@ -157,6 +167,7 @@ def find_cover_image3(container): if is_raster_image(media_type): return name + def find_cover_image(container, strict=False): 'Find a raster image marked as a cover in the OPF' ver = container.opf_version_parsed @@ -165,6 +176,7 @@ def find_cover_image(container, strict=False): else: return find_cover_image3(container) + def get_guides(container): guides = container.opf_xpath('//opf:guide') if not guides: @@ -209,6 +221,7 @@ def mark_as_cover_epub(container, name): container.dirty(container.opf_name) + def mark_as_titlepage(container, name, move_to_start=True): ''' Mark the specified HTML file as the titlepage of the EPUB. @@ -236,6 +249,7 @@ def mark_as_titlepage(container, name, move_to_start=True): container.dirty(container.opf_name) + def find_cover_page(container): 'Find a document marked as a cover in the OPF' ver = container.opf_version_parsed @@ -249,6 +263,7 @@ def find_cover_page(container): for name in container.manifest_items_with_property('calibre:title-page'): return name + def find_cover_image_in_page(container, cover_page): root = container.parsed(cover_page) body = XPath('//h:body')(root) @@ -268,6 +283,7 @@ def find_cover_image_in_page(container, cover_page): if images: return images[0] + def clean_opf(container): 'Remove all references to covers from the OPF' manifest_id_map = container.manifest_id_map @@ -290,6 +306,7 @@ def clean_opf(container): yield name container.dirty(container.opf_name) + def create_epub_cover(container, cover_path, existing_image, options=None): from calibre.ebooks.conversion.config import load_defaults from calibre.ebooks.oeb.transforms.cover import CoverManager @@ -386,6 +403,7 @@ def create_epub_cover(container, cover_path, existing_image, options=None): return raster_cover, titlepage + def remove_cover_image_in_page(container, page, cover_images): for img in container.parsed(page).xpath('//*[local-name()="img" and @src]'): href = img.get('src') @@ -394,6 +412,7 @@ def remove_cover_image_in_page(container, page, cover_images): img.getparent().remove(img) break + def set_epub_cover(container, cover_path, report, options=None): existing_image = options is not None and options.get('existing_image', False) if existing_image: diff --git a/src/calibre/ebooks/oeb/polish/create.py b/src/calibre/ebooks/oeb/polish/create.py index 792372b5bf..e91ab88652 100644 --- a/src/calibre/ebooks/oeb/polish/create.py +++ b/src/calibre/ebooks/oeb/polish/create.py @@ -26,6 +26,7 @@ from calibre.utils.zipfile import ZipFile, ZIP_STORED valid_empty_formats = {'epub', 'txt', 'docx', 'azw3'} + def create_toc(mi, opf, html_name, lang): uuid = '' for u in opf.xpath('//*[@id="uuid_id"]'): @@ -34,6 +35,7 @@ def create_toc(mi, opf, html_name, lang): toc.add(_('Start'), html_name) return create_ncx(toc, lambda x:x, mi.title, lang, uuid) + def create_book(mi, path, fmt='epub', opf_name='metadata.opf', html_name='start.xhtml', toc_name='toc.ncx'): ''' Create an empty book in the specified format at the specified location. ''' if fmt not in valid_empty_formats: diff --git a/src/calibre/ebooks/oeb/polish/css.py b/src/calibre/ebooks/oeb/polish/css.py index 77c1735119..215010ea33 100644 --- a/src/calibre/ebooks/oeb/polish/css.py +++ b/src/calibre/ebooks/oeb/polish/css.py @@ -36,6 +36,7 @@ def filter_used_rules(rules, log, select): if not used: yield rule + def get_imported_sheets(name, container, sheets, recursion_level=10, sheet=None): ans = set() sheet = sheet or sheets[name] @@ -186,6 +187,7 @@ def remove_unused_css(container, report=None, remove_unused_classes=False, merge report(_('No style rules that could be merged found')) return num_changes > 0 + def filter_declaration(style, properties=()): changed = False for prop in properties: @@ -204,6 +206,7 @@ def filter_declaration(style, properties=()): style.setProperty(prop, normalized[prop]) return changed + def filter_sheet(sheet, properties=()): from cssutils.css import CSSRule changed = False @@ -262,6 +265,7 @@ def transform_css(container, transform_sheet=None, transform_style=None, names=( return doc_changed + def filter_css(container, properties, names=()): ''' Remove the specified CSS properties from all CSS rules in the book. @@ -273,6 +277,7 @@ def filter_css(container, properties, names=()): return transform_css(container, transform_sheet=partial(filter_sheet, properties=properties), transform_style=partial(filter_declaration, properties=properties), names=names) + def _classes_in_selector(selector, classes): for attr in ('selector', 'subselector', 'parsed_tree'): s = getattr(selector, attr, None) @@ -282,6 +287,7 @@ def _classes_in_selector(selector, classes): if cn is not None: classes.add(cn) + def classes_in_selector(text): classes = set() try: @@ -291,6 +297,7 @@ def classes_in_selector(text): pass return classes + def classes_in_rule_list(css_rules): classes = set() for rule in css_rules: @@ -300,6 +307,7 @@ def classes_in_rule_list(css_rules): classes |= classes_in_rule_list(rule.cssRules) return classes + def iter_declarations(sheet_or_rule): if hasattr(sheet_or_rule, 'cssRules'): for rule in sheet_or_rule.cssRules: @@ -310,6 +318,7 @@ def iter_declarations(sheet_or_rule): elif isinstance(sheet_or_rule, CSSStyleDeclaration): yield sheet_or_rule + def remove_property_value(prop, predicate): ''' Remove the Values that match the predicate from this property. If all values of the property would be removed, the property is removed from its @@ -329,6 +338,7 @@ def remove_property_value(prop, predicate): RULE_PRIORITIES = {t:i for i, t in enumerate((CSSRule.COMMENT, CSSRule.CHARSET_RULE, CSSRule.IMPORT_RULE, CSSRule.NAMESPACE_RULE))} + def sort_sheet(container, sheet_or_text): ''' Sort the rules in a stylesheet. Note that in the general case this can change the effective styles, but for most common sheets, it should be safe. diff --git a/src/calibre/ebooks/oeb/polish/download.py b/src/calibre/ebooks/oeb/polish/download.py index a4010e6bff..c3871cab0d 100644 --- a/src/calibre/ebooks/oeb/polish/download.py +++ b/src/calibre/ebooks/oeb/polish/download.py @@ -47,6 +47,7 @@ def get_external_resources(container): ans[link].append(name) return ans + def get_filename(original_url_parsed, response): ans = get_download_filename_from_response(response) or posixpath.basename(original_url_parsed.path) or 'unknown' ct = response.info().get('Content-Type', '') @@ -60,6 +61,7 @@ def get_filename(original_url_parsed, response): ans += exts[0] return ans + def get_content_length(response): cl = response.info().get('Content-Length') try: @@ -67,6 +69,7 @@ def get_content_length(response): except Exception: return -1 + class ProgressTracker(object): def __init__(self, fobj, url, sz, progress_report): @@ -83,6 +86,7 @@ class ProgressTracker(object): pass return ret + def download_one(tdir, timeout, progress_report, url): try: purl = urlparse(url) @@ -108,6 +112,7 @@ def download_one(tdir, timeout, progress_report, url): except Exception as err: return False, url, as_unicode(err) + def download_external_resources(container, urls, timeout=60, progress_report=lambda url, done, total: None): failures = {} replacements = {} @@ -125,14 +130,16 @@ def download_external_resources(container, urls, timeout=60, progress_report=lam failures[url] = err return replacements, failures + def replacer(url_map): def replace(url): r = url_map.get(url) - replace.replaced |= r is not None + replace.replaced |= r != url return url if r is None else r replace.replaced = False return replace + def replace_resources(container, urls, replacements): url_maps = defaultdict(dict) changed = False diff --git a/src/calibre/ebooks/oeb/polish/embed.py b/src/calibre/ebooks/oeb/polish/embed.py index ccdc434983..be88ce39c3 100644 --- a/src/calibre/ebooks/oeb/polish/embed.py +++ b/src/calibre/ebooks/oeb/polish/embed.py @@ -18,6 +18,7 @@ from calibre.utils.filenames import ascii_filename props = {'font-family':None, 'font-weight':'normal', 'font-style':'normal', 'font-stretch':'normal'} + def matching_rule(font, rules): ff = font['font-family'] if not isinstance(ff, basestring): @@ -35,6 +36,7 @@ def matching_rule(font, rules): if icu_lower(ff) == family: return rule + def embed_font(container, font, all_font_rules, report, warned): rule = matching_rule(font, all_font_rules) ff = font['font-family'] @@ -80,6 +82,7 @@ def embed_font(container, font, all_font_rules, report, warned): rule['name'] = name return rule + def embed_all_fonts(container, stats, report): all_font_rules = tuple(stats.all_font_rules.itervalues()) warned = set() diff --git a/src/calibre/ebooks/oeb/polish/errors.py b/src/calibre/ebooks/oeb/polish/errors.py index e59bd28ba3..fdc9992e9e 100644 --- a/src/calibre/ebooks/oeb/polish/errors.py +++ b/src/calibre/ebooks/oeb/polish/errors.py @@ -9,12 +9,16 @@ __docformat__ = 'restructuredtext en' from calibre.ebooks import DRMError as _DRMError + class InvalidBook(ValueError): pass + class DRMError(_DRMError): + def __init__(self): super(DRMError, self).__init__(_('This file is locked with DRM. It cannot be edited.')) + class MalformedMarkup(ValueError): pass diff --git a/src/calibre/ebooks/oeb/polish/fonts.py b/src/calibre/ebooks/oeb/polish/fonts.py index 248d0ca541..774cbaf00b 100644 --- a/src/calibre/ebooks/oeb/polish/fonts.py +++ b/src/calibre/ebooks/oeb/polish/fonts.py @@ -10,11 +10,13 @@ from calibre.ebooks.oeb.polish.container import OEB_STYLES, OEB_DOCS from calibre.ebooks.oeb.normalize_css import normalize_font from tinycss.fonts3 import parse_font_family, parse_font, serialize_font_family, serialize_font + def unquote(x): if x and len(x) > 1 and x[0] == x[-1] and x[0] in ('"', "'"): x = x[1:-1] return x + def font_family_data_from_declaration(style, families): font_families = [] f = style.getProperty('font') @@ -29,6 +31,7 @@ def font_family_data_from_declaration(style, families): for f in font_families: families[f] = families.get(f, False) + def font_family_data_from_sheet(sheet, families): for rule in sheet.cssRules: if rule.type == rule.STYLE_RULE: @@ -39,6 +42,7 @@ def font_family_data_from_sheet(sheet, families): for f in parse_font_family(ff.propertyValue.cssText): families[f] = True + def font_family_data(container): families = {} for name, mt in container.mime_map.iteritems(): @@ -57,6 +61,7 @@ def font_family_data(container): font_family_data_from_declaration(style, families) return families + def change_font_in_declaration(style, old_name, new_name=None): changed = False ff = style.getProperty('font-family') @@ -83,6 +88,7 @@ def change_font_in_declaration(style, old_name, new_name=None): changed = True return changed + def remove_embedded_font(container, sheet, rule, sheet_name): src = getattr(rule.style.getProperty('src'), 'value') if src is not None: @@ -95,6 +101,7 @@ def remove_embedded_font(container, sheet, rule, sheet_name): if container.has_name(name): container.remove_item(name) + def change_font_in_sheet(container, sheet, old_name, new_name, sheet_name): changed = False removals = [] @@ -112,6 +119,7 @@ def change_font_in_sheet(container, sheet, old_name, new_name, sheet_name): remove_embedded_font(container, sheet, rule, sheet_name) return changed + def change_font(container, old_name, new_name=None): ''' Change a font family from old_name to new_name. Changes all occurrences of diff --git a/src/calibre/ebooks/oeb/polish/images.py b/src/calibre/ebooks/oeb/polish/images.py index 23d06d979f..f20e49d1c6 100644 --- a/src/calibre/ebooks/oeb/polish/images.py +++ b/src/calibre/ebooks/oeb/polish/images.py @@ -11,6 +11,7 @@ from Queue import Queue, Empty from calibre import detect_ncpus, human_readable, force_unicode, filesystem_encoding + class Worker(Thread): daemon = True @@ -57,6 +58,7 @@ class Worker(Thread): after = os.path.getsize(path) self.results[name] = (True, (before, after)) + def get_compressible_images(container): mt_map = container.manifest_type_map images = set() @@ -64,6 +66,7 @@ def get_compressible_images(container): images |= set(mt_map.get('image/' + mt, ())) return images + def compress_images(container, report=None, names=None, jpeg_quality=None, progress_callback=lambda n, t, name:True): images = get_compressible_images(container) if names is not None: @@ -73,6 +76,7 @@ def compress_images(container, report=None, names=None, jpeg_quality=None, progr abort = Event() for name in images: queue.put(name) + def pc(name): keep_going = progress_callback(len(results), len(images), name) if not keep_going: diff --git a/src/calibre/ebooks/oeb/polish/import_book.py b/src/calibre/ebooks/oeb/polish/import_book.py index 9ea49a6f23..9f9cd3152d 100644 --- a/src/calibre/ebooks/oeb/polish/import_book.py +++ b/src/calibre/ebooks/oeb/polish/import_book.py @@ -17,6 +17,7 @@ from calibre.utils.logging import default_log IMPORTABLE = {'htm', 'xhtml', 'html', 'xhtm', 'docx'} + def auto_fill_manifest(container): manifest_id_map = container.manifest_id_map manifest_name_map = {v:k for k, v in manifest_id_map.iteritems()} @@ -30,6 +31,7 @@ def auto_fill_manifest(container): manifest_name_map[name] = mitem.get('id') manifest_id_map[mitem.get('id')] = name + def import_book_as_epub(srcpath, destpath, log=default_log): if not destpath.lower().endswith('.epub'): raise ValueError('Can only import books into the EPUB format, not %s' % (os.path.basename(destpath))) diff --git a/src/calibre/ebooks/oeb/polish/jacket.py b/src/calibre/ebooks/oeb/polish/jacket.py index 6eeee295c1..ac9cd14271 100644 --- a/src/calibre/ebooks/oeb/polish/jacket.py +++ b/src/calibre/ebooks/oeb/polish/jacket.py @@ -13,6 +13,7 @@ from calibre.ebooks.oeb.base import XPath, OPF from calibre.ebooks.oeb.polish.cover import find_cover_page from calibre.ebooks.oeb.transforms.jacket import render_jacket as render, referenced_images + def render_jacket(container, jacket): mi = container.mi ps = load_defaults('page_setup') @@ -32,14 +33,17 @@ def render_jacket(container, jacket): img.set('src', href) return root + def is_legacy_jacket(root): return len(root.xpath( '//*[starts-with(@class,"calibrerescale") and (local-name()="h1" or local-name()="h2")]')) > 0 + def is_current_jacket(root): return len(XPath( '//h:meta[@name="calibre-content" and @content="jacket"]')(root)) > 0 + def find_existing_jacket(container): for item in container.spine_items: name = container.abspath_to_name(item) @@ -53,11 +57,13 @@ def find_existing_jacket(container): if is_current_jacket(root) or is_legacy_jacket(root): return name + def replace_jacket(container, name): root = render_jacket(container, name) container.parsed_cache[name] = root container.dirty(name) + def remove_jacket(container): ' Remove an existing jacket, if any. Returns False if no existing jacket was found. ' name = find_existing_jacket(container) @@ -67,6 +73,7 @@ def remove_jacket(container): return True return False + def remove_jacket_images(container, name): root = container.parsed_cache[name] for img in root.xpath('//*[local-name() = "img" and @src]'): @@ -74,6 +81,7 @@ def remove_jacket_images(container, name): if container.has_name(iname): container.remove_item(iname) + def add_or_replace_jacket(container): ''' Either create a new jacket from the book's metadata or replace an existing jacket. Returns True if an existing jacket was replaced. ''' diff --git a/src/calibre/ebooks/oeb/polish/main.py b/src/calibre/ebooks/oeb/polish/main.py index 95a0f1df0f..78cd6d6353 100644 --- a/src/calibre/ebooks/oeb/polish/main.py +++ b/src/calibre/ebooks/oeb/polish/main.py @@ -114,6 +114,7 @@ affecting image quality.

} + def hfix(name, raw): if name == 'about': return raw.format('') @@ -126,6 +127,7 @@ def hfix(name, raw): CLI_HELP = {x:hfix(x, re.sub('<.*?>', '', y)) for x, y in HELP.iteritems()} # }}} + def update_metadata(ebook, new_opf): from calibre.ebooks.metadata.opf import get_metadata, set_metadata with ebook.open(ebook.opf_name, 'r+b') as stream, open(new_opf, 'rb') as ns: @@ -136,6 +138,7 @@ def update_metadata(ebook, new_opf): stream.truncate() stream.write(opfbytes) + def polish_one(ebook, opts, report, customization=None): rt = lambda x: report('\n### ' + x) jacket = None @@ -228,6 +231,7 @@ def polish(file_map, opts, log, report): REPORT = '{0} REPORT {0}'.format('-'*30) + def gui_polish(data): files = data.pop('files') if not data.pop('metadata'): @@ -248,6 +252,7 @@ def gui_polish(data): log(msg) return '\n\n'.join(report) + def tweak_polish(container, actions, customization=None): opts = ALL_OPTS.copy() opts.update(actions) @@ -257,6 +262,7 @@ def tweak_polish(container, actions, customization=None): changed = polish_one(container, opts, report.append, customization=customization) return report, changed + def option_parser(): from calibre.utils.config import OptionParser USAGE = _('%prog [options] input_file [output_file]\n\n') + re.sub( @@ -281,6 +287,7 @@ def option_parser(): return parser + def main(args=None): parser = option_parser() opts, args = parser.parse_args(args or sys.argv[1:]) diff --git a/src/calibre/ebooks/oeb/polish/opf.py b/src/calibre/ebooks/oeb/polish/opf.py index cb796f675a..fe047aad92 100644 --- a/src/calibre/ebooks/oeb/polish/opf.py +++ b/src/calibre/ebooks/oeb/polish/opf.py @@ -11,6 +11,7 @@ from lxml import etree from calibre.ebooks.oeb.polish.container import OPF_NAMESPACES from calibre.utils.localization import canonicalize_lang + def get_book_language(container): for lang in container.opf_xpath('//dc:language'): raw = lang.text @@ -19,6 +20,7 @@ def get_book_language(container): if code: return code + def set_guide_item(container, item_type, title, name, frag=None): ref_tag = '{%s}reference' % OPF_NAMESPACES['opf'] href = None diff --git a/src/calibre/ebooks/oeb/polish/parsing.py b/src/calibre/ebooks/oeb/polish/parsing.py index eb54259a6b..5e3b0e2f93 100644 --- a/src/calibre/ebooks/oeb/polish/parsing.py +++ b/src/calibre/ebooks/oeb/polish/parsing.py @@ -29,6 +29,7 @@ html_ns = namespaces['html'] xlink_ns = namespaces['xlink'] xml_ns = namespaces['xmlns'] + class NamespacedHTMLPresent(ValueError): def __init__(self, prefix): @@ -36,6 +37,8 @@ class NamespacedHTMLPresent(ValueError): self.prefix = prefix # Nodes {{{ + + def ElementFactory(name, namespace=None, context=None): context = context or create_lxml_context() ns = namespace or namespaces['html'] @@ -44,6 +47,7 @@ def ElementFactory(name, namespace=None, context=None): except ValueError: return context.makeelement('{%s}%s' % (ns, to_xml_name(name)), nsmap={None:ns}) + class Element(ElementBase): ''' Implements the interface required by the html5lib tree builders (see @@ -68,6 +72,7 @@ class Element(ElementBase): def childNodes(self): def fget(self): return self + def fset(self, val): self[:] = list(val) return property(fget=fget, fset=fset) @@ -129,12 +134,14 @@ class Element(ElementBase): for child in self: new_parent.append(child) + class Comment(CommentBase): @dynamic_property def data(self): def fget(self): return self.text + def fset(self, val): self.text = val.replace('--', '- -') return property(fget=fget, fset=fset) @@ -180,6 +187,7 @@ class Comment(CommentBase): def cloneNode(self): return copy.copy(self) + class Document(object): def __init__(self): @@ -192,12 +200,14 @@ class Document(object): elif isinstance(child, DocType): self.doctype = child + class DocType(object): def __init__(self, name, public_id, system_id): self.text = self.name = name self.public_id, self.system_id = public_id, system_id + def create_lxml_context(): parser = XMLParser(no_network=True) parser.set_element_class_lookup(ElementDefaultClassLookup(element=Element, comment=Comment)) @@ -205,6 +215,7 @@ def create_lxml_context(): # }}} + def clean_attrib(name, val, nsmap, attrib, namespaced_attribs): if isinstance(name, tuple): @@ -247,6 +258,7 @@ def clean_attrib(name, val, nsmap, attrib, namespaced_attribs): return name, False + def makeelement_ns(ctx, namespace, prefix, name, attrib, nsmap): nns = attrib.pop('xmlns', None) if nns is not None: @@ -307,6 +319,7 @@ def makeelement_ns(ctx, namespace, prefix, name, attrib, nsmap): return elem + class TreeBuilder(BaseTreeBuilder): elementClass = ElementFactory @@ -454,6 +467,7 @@ class TreeBuilder(BaseTreeBuilder): parent = self.openElements[-1] parent.appendChild(Comment(token["data"].replace('--', '- -'))) + def makeelement(ctx, name, attrib): attrib.pop('xmlns', None) try: @@ -471,6 +485,7 @@ def makeelement(ctx, name, attrib): elem.set(to_xml_name(k), v) return elem + class NoNamespaceTreeBuilder(TreeBuilder): def __init__(self, namespaceHTMLElements=False, linenumber_attribute=None): @@ -528,6 +543,7 @@ class NoNamespaceTreeBuilder(TreeBuilder): # Input Stream {{{ _regex_cache = {} + class FastStream(object): __slots__ = ('raw', 'pos', 'errors', 'new_lines', 'track_position', 'charEncoding') @@ -593,6 +609,7 @@ if len("\U0010FFFF") == 1: # UCS4 build else: replace_chars = re.compile("([\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?
%s
' % text, decoder=lambda x:x.decode('utf-8')) @@ -117,6 +126,7 @@ opf_spell_tags = {'title', 'creator', 'subject', 'description', 'publisher'} # We can only use barename() for tag names and simple attribute checks so that # this code matches up with the syntax highlighter base spell checking + def read_words_from_opf(root, words, file_name, book_locale): for tag in root.iterdescendants('*'): if tag.text is not None and barename(tag.tag) in opf_spell_tags: @@ -129,6 +139,7 @@ def read_words_from_opf(root, words, file_name, book_locale): ncx_spell_tags = {'text'} xml_spell_tags = opf_spell_tags | ncx_spell_tags + def read_words_from_ncx(root, words, file_name, book_locale): for tag in root.xpath('//*[local-name()="text"]'): if tag.text is not None: @@ -136,6 +147,7 @@ def read_words_from_ncx(root, words, file_name, book_locale): html_spell_tags = {'script', 'style', 'link'} + def read_words_from_html_tag(tag, words, file_name, parent_locale, locale): if tag.text is not None and barename(tag.tag) not in html_spell_tags: add_words_from_text(tag, 'text', words, file_name, locale) @@ -144,6 +156,7 @@ def read_words_from_html_tag(tag, words, file_name, parent_locale, locale): if tag.tail is not None and tag.getparent() is not None and barename(tag.getparent().tag) not in html_spell_tags: add_words_from_text(tag, 'tail', words, file_name, parent_locale) + def locale_from_tag(tag): if 'lang' in tag.attrib: try: @@ -160,6 +173,7 @@ def locale_from_tag(tag): if loc is not None: return loc + def read_words_from_html(root, words, file_name, book_locale): stack = [(root, book_locale)] while stack: @@ -168,6 +182,7 @@ def read_words_from_html(root, words, file_name, book_locale): read_words_from_html_tag(parent, words, file_name, parent_locale, locale) stack.extend((tag, locale) for tag in parent.iterchildren('*')) + def group_sort(locations): order = {} for loc in locations: @@ -175,6 +190,7 @@ def group_sort(locations): order[loc.file_name] = len(order) return sorted(locations, key=lambda l:(order[l.file_name], l.sourceline)) + def get_checkable_file_names(container): file_names = [name for name, linear in container.spine_names] + [container.opf_name] for f in (find_existing_ncx_toc, find_existing_nav_toc): @@ -183,6 +199,7 @@ def get_checkable_file_names(container): file_names.append(toc) return file_names, toc + def get_all_words(container, book_locale, get_word_count=False): words = defaultdict(list) words[None] = 0 @@ -203,9 +220,11 @@ def get_all_words(container, book_locale, get_word_count=False): return count, ans return ans + def merge_locations(locs1, locs2): return group_sort(locs1 + locs2) + def replace(text, original_word, new_word, lang): indices = [] original_word, new_word, text = unicode(original_word), unicode(new_word), unicode(text) @@ -222,6 +241,7 @@ def replace(text, original_word, new_word, lang): text = text[:idx] + new_word + text[idx+len(original_word):] return text, bool(indices) + def replace_word(container, new_word, locations, locale, undo_cache=None): changed = set() for loc in locations: @@ -244,6 +264,7 @@ def replace_word(container, new_word, locations, locale, undo_cache=None): changed.add(loc.file_name) return changed + def undo_replace_word(container, undo_cache): changed = set() for (file_name, node, is_attr, attr), text in undo_cache.iteritems(): diff --git a/src/calibre/ebooks/oeb/polish/split.py b/src/calibre/ebooks/oeb/polish/split.py index 929abaa821..1930a34cf9 100644 --- a/src/calibre/ebooks/oeb/polish/split.py +++ b/src/calibre/ebooks/oeb/polish/split.py @@ -15,9 +15,11 @@ from calibre.ebooks.oeb.polish.errors import MalformedMarkup from calibre.ebooks.oeb.polish.toc import node_from_loc from calibre.ebooks.oeb.polish.replace import LinkRebaser + class AbortError(ValueError): pass + def in_table(node): while node is not None: if node.tag.endswith('}table'): @@ -25,6 +27,7 @@ def in_table(node): node = node.getparent() return False + def adjust_split_point(split_point, log): ''' Move the split point up its ancestor chain if it has no content @@ -49,9 +52,11 @@ def adjust_split_point(split_point, log): return sp + def get_body(root): return root.find('h:body', namespaces=XPNSMAP) + def do_split(split_point, log, before=True): ''' Split tree into a *before* and an *after* tree at ``split_point``. @@ -143,6 +148,7 @@ def do_split(split_point, log, before=True): return tree, tree2 + class SplitLinkReplacer(object): def __init__(self, base, bottom_anchors, top_name, bottom_name, container): @@ -163,6 +169,7 @@ class SplitLinkReplacer(object): self.replaced = True return url + def split(container, name, loc_or_xpath, before=True, totals=None): ''' Split the file specified by name at the position specified by loc_or_xpath. @@ -247,6 +254,7 @@ def split(container, name, loc_or_xpath, before=True, totals=None): container.dirty(container.opf_name) return bottom_name + def multisplit(container, name, xpath, before=True): ''' Split the specified file at multiple locations (all tags that match the specified XPath expression. See also: :func:`split`. @@ -311,9 +319,11 @@ def add_text(body, text): else: body.text = (body.text or '') + text + def all_anchors(root): return set(root.xpath('//*/@id')) | set(root.xpath('//*/@name')) + def all_stylesheets(container, name): for link in XPath('//h:head/h:link[@href]')(container.parsed(name)): name = container.href_to_name(link.get('href'), name) @@ -321,6 +331,7 @@ def all_stylesheets(container, name): if typ == 'text/css': yield name + def unique_anchor(seen_anchors, current): c = 0 ans = current @@ -329,6 +340,7 @@ def unique_anchor(seen_anchors, current): ans = '%s_%d' % (current, c) return ans + def remove_name_attributes(root): # Remove all name attributes, replacing them with id attributes for elem in root.xpath('//*[@id and @name]'): @@ -336,6 +348,7 @@ def remove_name_attributes(root): for elem in root.xpath('//*[@name]'): elem.set('id', elem.attrib.pop('name')) + def merge_html(container, names, master): p = container.parsed root = p(master) @@ -420,6 +433,7 @@ def merge_html(container, names, master): repl = MergeLinkReplacer(fname, anchor_map, master, container) container.replace_links(fname, repl) + def merge_css(container, names, master): p = container.parsed msheet = p(master) diff --git a/src/calibre/ebooks/oeb/polish/stats.py b/src/calibre/ebooks/oeb/polish/stats.py index bfb44eb828..15211ac361 100644 --- a/src/calibre/ebooks/oeb/polish/stats.py +++ b/src/calibre/ebooks/oeb/polish/stats.py @@ -18,6 +18,7 @@ from calibre.ebooks.oeb.polish.cascade import iterrules, resolve_styles, iterdec from calibre.utils.icu import ord_string, safe_chr from tinycss.fonts3 import parse_font_family + def normalize_font_properties(font): w = font.get('font-weight', None) if not w and w != 0: @@ -47,6 +48,7 @@ widths = {x:i for i, x in enumerate(('ultra-condensed', 'semi-expanded', 'expanded', 'extra-expanded', 'ultra-expanded' ))} + def get_matching_rules(rules, font): matches = [] @@ -99,6 +101,7 @@ def get_matching_rules(rules, font): return m return [] + def get_css_text(elem, resolve_pseudo_property, which='before'): text = resolve_pseudo_property(elem, which, 'content')[0].value if text and len(text) > 2 and text[0] == '"' and text[-1] == '"': @@ -107,6 +110,7 @@ def get_css_text(elem, resolve_pseudo_property, which='before'): caps_variants = {'smallcaps', 'small-caps', 'all-small-caps', 'petite-caps', 'all-petite-caps', 'unicase'} + def get_element_text(elem, resolve_property, resolve_pseudo_property, capitalize_pat, for_pseudo=None): ans = [] before = get_css_text(elem, resolve_pseudo_property) @@ -144,6 +148,7 @@ def get_element_text(elem, resolve_property, resolve_pseudo_property, capitalize ans += icu_upper(m.group()) return ans + def get_font_dict(elem, resolve_property, pseudo=None): ans = {} if pseudo is None: @@ -163,11 +168,13 @@ exclude_chars = frozenset(ord_string('\n\r\t')) skip_tags = {XHTML(x) for x in 'script style title meta link'.split()} font_keys = {'font-weight', 'font-style', 'font-stretch', 'font-family'} + def prepare_font_rule(cssdict): cssdict['font-family'] = frozenset(cssdict['font-family'][:1]) cssdict['width'] = widths[cssdict['font-stretch']] cssdict['weight'] = int(cssdict['font-weight']) + class StatsCollector(object): first_letter_pat = capitalize_pat = None diff --git a/src/calibre/ebooks/oeb/polish/subset.py b/src/calibre/ebooks/oeb/polish/subset.py index 2dd32c86ba..97355fb526 100644 --- a/src/calibre/ebooks/oeb/polish/subset.py +++ b/src/calibre/ebooks/oeb/polish/subset.py @@ -17,6 +17,7 @@ from calibre.utils.fonts.sfnt.subset import subset from calibre.utils.fonts.sfnt.errors import UnsupportedFont from calibre.utils.fonts.utils import get_font_names + def remove_font_face_rules(container, sheet, remove_names, base): changed = False for rule in tuple(sheet.cssRules): @@ -32,6 +33,7 @@ def remove_font_face_rules(container, sheet, remove_names, base): changed = True return changed + def subset_all_fonts(container, font_stats, report): remove = set() total_old = total_new = 0 diff --git a/src/calibre/ebooks/oeb/polish/tests/base.py b/src/calibre/ebooks/oeb/polish/tests/base.py index c8ef3ddf32..41c0f013f6 100644 --- a/src/calibre/ebooks/oeb/polish/tests/base.py +++ b/src/calibre/ebooks/oeb/polish/tests/base.py @@ -14,6 +14,7 @@ from calibre.ptempfile import PersistentTemporaryDirectory from calibre.utils.logging import DevNull import calibre.ebooks.oeb.polish.container as pc + def get_cache(): from calibre.constants import cache_dir cache = os.path.join(cache_dir(), 'polish-test') @@ -21,6 +22,7 @@ def get_cache(): os.mkdir(cache) return cache + def needs_recompile(obj, srcs): if isinstance(srcs, type('')): srcs = [srcs] @@ -33,10 +35,12 @@ def needs_recompile(obj, srcs): return True return False + def build_book(src, dest, args=()): from calibre.ebooks.conversion.cli import main main(['ebook-convert', src, dest] + list(args)) + def add_resources(raw, rmap): for placeholder, path in rmap.iteritems(): fname = os.path.basename(path) @@ -44,6 +48,7 @@ def add_resources(raw, rmap): raw = raw.replace(placeholder, fname) return raw + def get_simple_book(fmt='epub'): cache = get_cache() ans = os.path.join(cache, 'simple.'+fmt) @@ -66,6 +71,7 @@ def get_simple_book(fmt='epub'): '--level1-toc=//h:h2', '--language=en', '--authors=Kovid Goyal', '--cover=lt.png']) return ans + def get_split_book(fmt='epub'): cache = get_cache() ans = os.path.join(cache, 'split.'+fmt) @@ -84,6 +90,7 @@ def get_split_book(fmt='epub'): devnull = DevNull() + class BaseTest(unittest.TestCase): longMessage = True diff --git a/src/calibre/ebooks/oeb/polish/tests/cascade.py b/src/calibre/ebooks/oeb/polish/tests/cascade.py index 56da16e5fd..f54b7731d2 100644 --- a/src/calibre/ebooks/oeb/polish/tests/cascade.py +++ b/src/calibre/ebooks/oeb/polish/tests/cascade.py @@ -19,6 +19,7 @@ from calibre.ebooks.oeb.polish.stats import StatsCollector, font_keys, normalize from calibre.ebooks.oeb.polish.tests.base import BaseTest from calibre.utils.logging import Log, Stream + class VirtualContainer(ContainerBase): tweak_mode = True @@ -56,6 +57,7 @@ class VirtualContainer(ContainerBase): if self.mime_map[name] in OEB_DOCS: yield name, True + class CascadeTest(BaseTest): def test_iterrules(self): @@ -145,6 +147,7 @@ class CascadeTest(BaseTest): def test_font_stats(self): embeds = '@font-face { font-family: X; src: url(X.otf) }\n@font-face { font-family: X; src: url(XB.otf); font-weight: bold }' + def get_stats(html, *fonts): styles = [] html = '{}'.format(html) diff --git a/src/calibre/ebooks/oeb/polish/tests/container.py b/src/calibre/ebooks/oeb/polish/tests/container.py index 69c8ad80e8..3a076c7b43 100644 --- a/src/calibre/ebooks/oeb/polish/tests/container.py +++ b/src/calibre/ebooks/oeb/polish/tests/container.py @@ -17,10 +17,12 @@ from calibre.ebooks.oeb.polish.split import split, merge from calibre.utils.filenames import nlinks_file from calibre.ptempfile import TemporaryFile, TemporaryDirectory + def get_container(*args, **kwargs): kwargs['tweak_mode'] = True return _gc(*args, **kwargs) + class ContainerTests(BaseTest): def test_clone(self): @@ -93,6 +95,7 @@ class ContainerTests(BaseTest): ' Test renaming of files ' book = get_simple_book() count = [0] + def new_container(): count[0] += 1 tdir = os.mkdir(os.path.join(self.tdir, str(count[0]))) diff --git a/src/calibre/ebooks/oeb/polish/tests/main.py b/src/calibre/ebooks/oeb/polish/tests/main.py index 62d841a9e0..f0948c1a05 100644 --- a/src/calibre/ebooks/oeb/polish/tests/main.py +++ b/src/calibre/ebooks/oeb/polish/tests/main.py @@ -9,6 +9,7 @@ __copyright__ = '2013, Kovid Goyal ' import os from calibre.utils.run_tests import find_tests_in_dir, run_tests + def find_tests(): base = os.path.dirname(os.path.abspath(__file__)) return find_tests_in_dir(base) diff --git a/src/calibre/ebooks/oeb/polish/tests/parsing.py b/src/calibre/ebooks/oeb/polish/tests/parsing.py index 630a279bfb..64d70348ed 100644 --- a/src/calibre/ebooks/oeb/polish/tests/parsing.py +++ b/src/calibre/ebooks/oeb/polish/tests/parsing.py @@ -16,6 +16,7 @@ from calibre.ebooks.oeb.polish.parsing import parse_html5 as parse from calibre.ebooks.oeb.base import XPath, XHTML_NS, SVG_NS, XLINK_NS from calibre.ebooks.oeb.parse_utils import html5_parse + def nonvoid_cdata_elements(test, parse_function): ''' If self closed version of non-void cdata elements like are present, the HTML5 parsing algorithm treats all following data as CDATA ''' @@ -29,8 +30,10 @@ def nonvoid_cdata_elements(test, parse_function): len(XPath('//h:body[@id="test"]')(root)), 1, 'Incorrect parsing for <%s/>, parsed markup:\n' % x + etree.tostring(root)) + def namespaces(test, parse_function): ae = test.assertEqual + def match_and_prefix(root, xpath, prefix, err=''): matches = XPath(xpath)(root) ae(len(matches), 1, err) @@ -87,6 +90,7 @@ def namespaces(test, parse_function): markup = '<html><body><ns1:tag1 xmlns:ns1="NS"><ns2:tag2 xmlns:ns2="NS" ns1:id="test"/><ns1:tag3 xmlns:ns1="NS2" ns1:id="test"/></ns1:tag1>' root = parse_function(markup) err = 'Arbitrary namespaces not preserved, parsed markup:\n' + etree.tostring(root) + def xpath(expr): return etree.XPath(expr, namespaces={'ns1':'NS', 'ns2':'NS2'})(root) ae(len(xpath('//ns1:tag1')), 1, err) @@ -106,6 +110,7 @@ def namespaces(test, parse_function): ae(len(root.xpath('//*[@lang="es"]')), 1, err) ae(len(XPath('//*[@xml:lang]')(root)), 0, err) + def space_characters(test, parse_function): markup = '<html><p>\u000c</p>' root = parse_function(markup) @@ -116,18 +121,21 @@ def space_characters(test, parse_function): test.assertNotIn('\u000b', root.xpath('//*[local-name()="p"]')[0].text, err) test.assertNotIn('\u000c', root.xpath('//*[local-name()="p"]')[0].text, err) + def case_insensitive_element_names(test, parse_function): markup = '<HTML><P> </p>' root = parse_function(markup) err = 'case sensitive parsing, parsed markup:\n' + etree.tostring(root) test.assertEqual(len(XPath('//h:p')(root)), 1, err) + def entities(test, parse_function): markup = '<html><p> '</p>' root = parse_function(markup) err = 'Entities not handled, parsed markup:\n' + etree.tostring(root) test.assertEqual('\xa0\'', root.xpath('//*[local-name()="p"]')[0].text, err) + def multiple_html_and_body(test, parse_function): markup = '<html id="1"><body id="2"><p><html lang="en"><body lang="de"></p>' root = parse_function(markup) @@ -137,12 +145,14 @@ def multiple_html_and_body(test, parse_function): test.assertEqual(len(XPath('//h:html[@id and @lang]')(root)), 1, err) test.assertEqual(len(XPath('//h:body[@id and @lang]')(root)), 1, err) + def attribute_replacement(test, parse_function): markup = '<html><body><svg viewbox="0"></svg><svg xmlns="%s" viewbox="1">' % SVG_NS root = parse_function(markup) err = 'SVG attributes not normalized, parsed markup:\n' + etree.tostring(root) test.assertEqual(len(XPath('//svg:svg[@viewBox]')(root)), 2, err) + def comments(test, parse_function): markup = '<html><!-- -- ---><body/></html>' root = parse_function(markup) @@ -153,6 +163,7 @@ basic_checks = (nonvoid_cdata_elements, namespaces, space_characters, case_insensitive_element_names, entities, comments, multiple_html_and_body, attribute_replacement) + class ParsingTests(BaseTest): def test_conversion_parser(self): @@ -189,6 +200,7 @@ class ParsingTests(BaseTest): root = parse('<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" xmlns:extra="extra"><body/></html>') self.assertIn('extra', root.nsmap, 'Extra namespace declaration on <html> tag not preserved') + def timing(): import sys from calibre.ebooks.chardet import xml_to_unicode diff --git a/src/calibre/ebooks/oeb/polish/tests/structure.py b/src/calibre/ebooks/oeb/polish/tests/structure.py index 59ddf97ab6..703d937984 100644 --- a/src/calibre/ebooks/oeb/polish/tests/structure.py +++ b/src/calibre/ebooks/oeb/polish/tests/structure.py @@ -32,10 +32,12 @@ OPF_TEMPLATE = ''' <guide>{guide}</guide> </package>''' % CALIBRE_PREFIX # noqa + def create_manifest_item(name, data=b'', properties=None): return (name, data, properties) cmi = create_manifest_item + def create_epub(manifest, spine=(), guide=(), meta_cover=None, ver=3): mo = [] for name, data, properties in manifest: diff --git a/src/calibre/ebooks/oeb/polish/toc.py b/src/calibre/ebooks/oeb/polish/toc.py index 6abdab6669..4a9ddd504a 100644 --- a/src/calibre/ebooks/oeb/polish/toc.py +++ b/src/calibre/ebooks/oeb/polish/toc.py @@ -111,6 +111,7 @@ class TOC(object): ans['dest_error'] = self.dest_error return ans + def child_xpath(tag, name): return tag.xpath('./*[calibre:lower-case(local-name()) = "%s"]'%name) @@ -158,6 +159,7 @@ def parse_ncx(container, ncx_name): break return toc_root + def add_from_li(container, li, parent, nav_name): dest = frag = text = None for x in li.iterchildren(XHTML('a'), XHTML('span')): @@ -169,12 +171,14 @@ def add_from_li(container, li, parent, nav_name): break return parent.add(text or None, dest or None, frag or None) + def first_child(parent, tagname): try: return next(parent.iterchildren(tagname)) except StopIteration: return None + def process_nav_node(container, node, toc_parent, nav_name): for li in node.iterchildren(XHTML('li')): child = add_from_li(container, li, toc_parent, nav_name) @@ -182,6 +186,7 @@ def process_nav_node(container, node, toc_parent, nav_name): if child is not None and ol is not None: process_nav_node(container, ol, child, nav_name) + def parse_nav(container, nav_name): root = container.parsed(nav_name) toc_root = TOC() @@ -200,6 +205,7 @@ def parse_nav(container, nav_name): break return toc_root + def verify_toc_destinations(container, toc): anchor_map = {} anchor_xpath = XPath('//*/@id|//h:a/@name') @@ -230,6 +236,7 @@ def verify_toc_destinations(container, toc): 'The anchor %(a)s does not exist in file %(f)s')%dict( a=item.frag, f=name) + def find_existing_ncx_toc(container): toc = container.opf_xpath('//opf:spine/@toc') if toc: @@ -239,10 +246,12 @@ def find_existing_ncx_toc(container): toc = container.manifest_type_map.get(ncx, [None])[0] return toc or None + def find_existing_nav_toc(container): for name in container.manifest_items_with_property('nav'): return name + def get_x_toc(container, find_toc, parse_toc, verify_destinations=True): def empty_toc(): ans = TOC() @@ -255,6 +264,7 @@ def get_x_toc(container, find_toc, parse_toc, verify_destinations=True): verify_toc_destinations(container, ans) return ans + def get_toc(container, verify_destinations=True): ver = container.opf_version_parsed if ver.major < 3: @@ -265,6 +275,7 @@ def get_toc(container, verify_destinations=True): ans = get_x_toc(container, find_existing_ncx_toc, parse_ncx, verify_destinations=verify_destinations) return ans + def ensure_id(elem): if elem.tag == XHTML('a'): anchor = elem.get('name', None) @@ -276,6 +287,7 @@ def ensure_id(elem): elem.set('id', uuid_id()) return True, elem.get('id') + def elem_to_toc_text(elem): text = xml2text(elem).strip() if not text: @@ -288,6 +300,7 @@ def elem_to_toc_text(elem): text = _('(Untitled)') return text + def item_at_top(elem): try: body = XPath('//h:body')(elem.getroottree().getroot())[0] @@ -310,6 +323,7 @@ def item_at_top(elem): return False return True + def from_xpaths(container, xpaths): ''' Generate a Table of Contents from a list of XPath expressions. Each @@ -377,6 +391,7 @@ def from_xpaths(container, xpaths): return tocroot + def from_links(container): ''' Generate a Table of Contents from links in the book. @@ -407,6 +422,7 @@ def from_links(container): toc.remove(child) return toc + def find_text(node): LIMIT = 200 pat = re.compile(r'\s+') @@ -423,6 +439,7 @@ def find_text(node): else: return text + def from_files(container): ''' Generate a Table of Contents from files in the book. @@ -442,6 +459,7 @@ def from_files(container): toc.add(text, name) return toc + def node_from_loc(root, locs, totals=None): node = root.xpath('//*[local-name()="body"]')[0] for i, loc in enumerate(locs): @@ -451,6 +469,7 @@ def node_from_loc(root, locs, totals=None): node = children[loc] return node + def add_id(container, name, loc, totals=None): root = container.parsed(name) try: @@ -473,6 +492,7 @@ def add_id(container, name, loc, totals=None): container.commit_item(name, keep_parsed=True) return node.get('id') + def create_ncx(toc, to_href, btitle, lang, uid): lang = lang.replace('_', '-') ncx = etree.Element(NCX('ncx'), @@ -552,6 +572,7 @@ def commit_ncx_toc(container, toc, lang=None, uid=None): container.replace(tocname, root) container.pretty_print.add(tocname) + def commit_nav_toc(container, toc, lang=None): from calibre.ebooks.oeb.polish.pretty import pretty_xml_tree tocname = find_existing_nav_toc(container) @@ -613,11 +634,13 @@ def commit_nav_toc(container, toc, lang=None): li[0].tail = None container.replace(tocname, root) + def commit_toc(container, toc, lang=None, uid=None): commit_ncx_toc(container, toc, lang=lang, uid=uid) if container.opf_version_parsed.major > 2: commit_nav_toc(container, toc, lang=lang) + def remove_names_from_toc(container, names): changed = [] names = frozenset(names) @@ -638,11 +661,13 @@ def remove_names_from_toc(container, names): changed.append(find_toc(container)) return changed + def find_inline_toc(container): for name, linear in container.spine_names: if container.parsed(name).xpath('//*[local-name()="body" and @id="calibre_generated_inline_toc"]'): return name + def toc_to_html(toc, container, toc_name, title, lang=None): def process_node(html_parent, toc, level=1, indent=' ', style_level=2): @@ -691,6 +716,7 @@ def toc_to_html(toc, container, toc_name, title, lang=None): pretty_html_tree(container, html) return html + def create_inline_toc(container, title=None): ''' Create an inline (HTML) Table of Contents from an existing NCX table of contents. diff --git a/src/calibre/ebooks/oeb/polish/utils.py b/src/calibre/ebooks/oeb/polish/utils.py index 23b573857f..b1dd850925 100644 --- a/src/calibre/ebooks/oeb/polish/utils.py +++ b/src/calibre/ebooks/oeb/polish/utils.py @@ -11,9 +11,11 @@ from bisect import bisect from calibre import guess_type as _guess_type, replace_entities + def guess_type(x): return _guess_type(x)[0] or 'application/octet-stream' + def setup_cssutils_serialization(tab_width=2): import cssutils prefs = cssutils.ser.prefs @@ -21,6 +23,7 @@ def setup_cssutils_serialization(tab_width=2): prefs.indentClosingBrace = False prefs.omitLastSemicolon = False + def actual_case_for_name(container, name): from calibre.utils.filenames import samefile if not container.exists(name): @@ -45,6 +48,7 @@ def actual_case_for_name(container, name): ans.append(correctx) return '/'.join(ans) + def corrected_case_for_name(container, name): parts = name.split('/') ans = [] @@ -67,6 +71,7 @@ def corrected_case_for_name(container, name): ans.append(correctx) return '/'.join(ans) + class PositionFinder(object): def __init__(self, raw): @@ -81,6 +86,7 @@ class PositionFinder(object): offset = pos return (lnum + 1, offset) + class CommentFinder(object): def __init__(self, raw, pat=r'(?s)/\*.*?\*/'): @@ -95,6 +101,7 @@ class CommentFinder(object): q = bisect(self.starts, offset) - 1 return q >= 0 and self.starts[q] <= offset <= self.ends[q] + def link_stylesheets(container, names, sheets, remove=False, mtype='text/css'): from calibre.ebooks.oeb.base import XPath, XHTML changed_names = set() @@ -127,6 +134,7 @@ def link_stylesheets(container, names, sheets, remove=False, mtype='text/css'): return changed_names + def lead_text(top_elem, num_words=10): ''' Return the leading text contained in top_elem (including descendants) upto a maximum of num_words words. More efficient than using @@ -150,6 +158,7 @@ def lead_text(top_elem, num_words=10): stack.extend(reversed(list((c, 'text') for c in elem.iterchildren('*')))) return ' '.join(words[:num_words]) + def parse_css(data, fname='<string>', is_declaration=False, decode=None, log_level=None, css_preprocessor=None): if log_level is None: import logging @@ -172,9 +181,11 @@ def parse_css(data, fname='<string>', is_declaration=False, decode=None, log_lev data = parser.parseString(data, href=fname, validate=False) return data + def handle_entities(text, func): return func(replace_entities(text)) + def apply_func_to_match_groups(match, func=icu_upper, handle_entities=handle_entities): '''Apply the specified function to individual groups in the match object (the result of re.search() or the whole match if no groups were defined. Returns the replaced string.''' @@ -198,6 +209,7 @@ def apply_func_to_match_groups(match, func=icu_upper, handle_entities=handle_ent parts.append(match.string[pos:match.end()]) return ''.join(parts) + def apply_func_to_html_text(match, func=icu_upper, handle_entities=handle_entities): ''' Apply the specified function only to text between HTML tag definitions. ''' f = lambda text:handle_entities(text, func) @@ -205,6 +217,7 @@ def apply_func_to_html_text(match, func=icu_upper, handle_entities=handle_entiti parts = (x if x.startswith('<') else f(x) for x in parts) return ''.join(parts) + def extract(elem): ''' Remove an element from the tree, keeping elem.tail ''' p = elem.getparent() diff --git a/src/calibre/ebooks/oeb/reader.py b/src/calibre/ebooks/oeb/reader.py index b432db8203..58bb6b4156 100644 --- a/src/calibre/ebooks/oeb/reader.py +++ b/src/calibre/ebooks/oeb/reader.py @@ -32,6 +32,7 @@ from calibre import guess_type, xml_replace_entities __all__ = ['OEBReader'] + class OEBReader(object): """Read an OEBPS 1.x or OPF/OPS 2.0 file collection.""" diff --git a/src/calibre/ebooks/oeb/stylizer.py b/src/calibre/ebooks/oeb/stylizer.py index b97bd9ec60..bfe6e50ca4 100644 --- a/src/calibre/ebooks/oeb/stylizer.py +++ b/src/calibre/ebooks/oeb/stylizer.py @@ -26,6 +26,7 @@ cssutils_log.setLevel(logging.WARN) _html_css_stylesheet = None + def html_css_stylesheet(): global _html_css_stylesheet if _html_css_stylesheet is None: @@ -53,6 +54,7 @@ FONT_SIZE_NAMES = { ALLOWED_MEDIA_TYPES = frozenset({'screen', 'all', 'aural', 'amzn-kf8'}) IGNORED_MEDIA_FEATURES = frozenset('width min-width max-width height min-height max-height device-width min-device-width max-device-width device-height min-device-height max-device-height aspect-ratio min-aspect-ratio max-aspect-ratio device-aspect-ratio min-device-aspect-ratio max-device-aspect-ratio color min-color max-color color-index min-color-index max-color-index monochrome min-monochrome max-monochrome -webkit-min-device-pixel-ratio resolution min-resolution max-resolution scan grid'.split()) # noqa + def media_ok(raw): if not raw: return True @@ -78,6 +80,7 @@ def media_ok(raw): pass return True + def test_media_ok(): assert media_ok(None) assert media_ok('') @@ -90,6 +93,7 @@ def test_media_ok(): assert media_ok('screen, (device-width:10px)') assert not media_ok('screen and (device-width:10px)') + class Stylizer(object): STYLESHEETS = WeakKeyDictionary() diff --git a/src/calibre/ebooks/oeb/transforms/cover.py b/src/calibre/ebooks/oeb/transforms/cover.py index 03663a51aa..25e1d97c92 100644 --- a/src/calibre/ebooks/oeb/transforms/cover.py +++ b/src/calibre/ebooks/oeb/transforms/cover.py @@ -12,6 +12,7 @@ from lxml import etree from calibre import guess_type from calibre.utils.imghdr import identify + class CoverManager(object): SVG_TEMPLATE = textwrap.dedent('''\ diff --git a/src/calibre/ebooks/oeb/transforms/data_url.py b/src/calibre/ebooks/oeb/transforms/data_url.py index c560d5adc5..fc62a8ceda 100644 --- a/src/calibre/ebooks/oeb/transforms/data_url.py +++ b/src/calibre/ebooks/oeb/transforms/data_url.py @@ -9,6 +9,7 @@ __copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>' import re from calibre.ebooks.oeb.base import XPath, urlunquote + class DataURL(object): def __call__(self, oeb, opts): diff --git a/src/calibre/ebooks/oeb/transforms/embed_fonts.py b/src/calibre/ebooks/oeb/transforms/embed_fonts.py index 66a8e39d01..1d2b36851a 100644 --- a/src/calibre/ebooks/oeb/transforms/embed_fonts.py +++ b/src/calibre/ebooks/oeb/transforms/embed_fonts.py @@ -18,6 +18,7 @@ from calibre.ebooks.oeb.transforms.subset import get_font_properties, find_font_ from calibre.utils.filenames import ascii_filename from calibre.utils.fonts.scanner import font_scanner, NoFonts + def used_font(style, embedded_fonts): ff = [unicode(f) for f in style.get('font-family', []) if unicode(f).lower() not in { 'serif', 'sansserif', 'sans-serif', 'fantasy', 'cursive', 'monospace'}] diff --git a/src/calibre/ebooks/oeb/transforms/filenames.py b/src/calibre/ebooks/oeb/transforms/filenames.py index 11102dc617..5b76cd8678 100644 --- a/src/calibre/ebooks/oeb/transforms/filenames.py +++ b/src/calibre/ebooks/oeb/transforms/filenames.py @@ -12,6 +12,7 @@ from lxml import etree from calibre.ebooks.oeb.base import rewrite_links, urlnormalize + class RenameFiles(object): # {{{ ''' @@ -85,6 +86,7 @@ class RenameFiles(object): # {{{ # }}} + class UniqueFilenames(object): # {{{ 'Ensure that every item in the manifest has a unique filename' @@ -136,6 +138,7 @@ class UniqueFilenames(object): # {{{ return suffix # }}} + class FlatFilenames(object): # {{{ 'Ensure that every item in the manifest has a unique filename without subdirectories.' diff --git a/src/calibre/ebooks/oeb/transforms/flatcss.py b/src/calibre/ebooks/oeb/transforms/flatcss.py index cf13540990..4b51154414 100644 --- a/src/calibre/ebooks/oeb/transforms/flatcss.py +++ b/src/calibre/ebooks/oeb/transforms/flatcss.py @@ -24,11 +24,13 @@ from calibre.utils.icu import numeric_sort_key COLLAPSE = re.compile(r'[ \t\r\n\v]+') STRIPNUM = re.compile(r'[-0-9]+$') + def asfloat(value, default): if not isinstance(value, (int, long, float)): value = default return float(value) + def dynamic_rescale_factor(node): classes = node.get('class', '').split(' ') classes = [x.replace('calibre_rescale_', '') for x in classes if @@ -45,6 +47,7 @@ def dynamic_rescale_factor(node): class KeyMapper(object): + def __init__(self, sbase, dbase, dkey): self.sbase = float(sbase) self.dprop = [(self.relate(x, dbase), float(x)) for x in dkey] @@ -92,7 +95,9 @@ class KeyMapper(object): dsize = min(diff)[1] return dsize + class ScaleMapper(object): + def __init__(self, sbase, dbase): self.dscale = float(dbase) / float(sbase) @@ -101,13 +106,16 @@ class ScaleMapper(object): dsize = ssize * self.dscale return dsize + class NullMapper(object): + def __init__(self): pass def __getitem__(self, ssize): return ssize + def FontMapper(sbase=None, dbase=None, dkey=None): if sbase and dbase and dkey: return KeyMapper(sbase, dbase, dkey) @@ -116,6 +124,7 @@ def FontMapper(sbase=None, dbase=None, dkey=None): else: return NullMapper() + class EmbedFontsCSSRules(object): def __init__(self, body_font_family, rules): @@ -134,7 +143,9 @@ class EmbedFontsCSSRules(object): data=sheet).href return self.href + class CSSFlattener(object): + def __init__(self, fbase=None, fkey=None, lineh=None, unfloat=False, untable=False, page_break_on_body=False, specializer=None, transform_css_rules=()): @@ -542,6 +553,7 @@ class CSSFlattener(object): def flatten_head(self, item, href, global_href): html = item.data head = html.find(XHTML('head')) + def safe_lower(x): try: x = x.lower() diff --git a/src/calibre/ebooks/oeb/transforms/htmltoc.py b/src/calibre/ebooks/oeb/transforms/htmltoc.py index c7a0cfce94..a7a84dd8b3 100644 --- a/src/calibre/ebooks/oeb/transforms/htmltoc.py +++ b/src/calibre/ebooks/oeb/transforms/htmltoc.py @@ -44,7 +44,9 @@ body > .calibre_toc_block { """ } + class HTMLTOCAdder(object): + def __init__(self, title=None, style='nested', position='end'): self.title = title self.style = style diff --git a/src/calibre/ebooks/oeb/transforms/jacket.py b/src/calibre/ebooks/oeb/transforms/jacket.py index e273580875..defd29ae67 100644 --- a/src/calibre/ebooks/oeb/transforms/jacket.py +++ b/src/calibre/ebooks/oeb/transforms/jacket.py @@ -24,6 +24,7 @@ from calibre.ebooks.metadata import fmt_sidx JACKET_XPATH = '//h:meta[@name="calibre-content" and @content="jacket"]' + class SafeFormatter(Formatter): def get_value(self, *args, **kwargs): @@ -32,6 +33,7 @@ class SafeFormatter(Formatter): except KeyError: return '' + class Jacket(object): ''' Book jacket manipulation. Remove first image and insert comments at start of @@ -130,6 +132,7 @@ class Jacket(object): # Render Jacket {{{ + def get_rating(rating, rchar, e_rchar): ans = '' try: @@ -144,6 +147,7 @@ def get_rating(rating, rchar, e_rchar): ans = ("%s%s") % (rchar * int(num), e_rchar * (5 - int(num))) return ans + class Series(unicode): def __new__(self, series, series_index): @@ -157,6 +161,7 @@ class Series(unicode): s.roman = roman return s + class Tags(unicode): def __new__(self, tags, output_profile): @@ -166,6 +171,7 @@ class Tags(unicode): t.tags_list = tags return t + def render_jacket(mi, output_profile, alt_title=_('Unknown'), alt_tags=[], alt_comments='', alt_publisher=(''), rescale_fonts=False): @@ -312,6 +318,7 @@ def render_jacket(mi, output_profile, # }}} + def linearize_jacket(oeb): for x in oeb.spine[:4]: if XPath(JACKET_XPATH)(x.data): @@ -321,6 +328,7 @@ def linearize_jacket(oeb): e.tag = XHTML('span') break + def referenced_images(root): for img in XPath('//h:img[@src]')(root): src = img.get('src') diff --git a/src/calibre/ebooks/oeb/transforms/linearize_tables.py b/src/calibre/ebooks/oeb/transforms/linearize_tables.py index e724d0ec14..99d1cf8417 100644 --- a/src/calibre/ebooks/oeb/transforms/linearize_tables.py +++ b/src/calibre/ebooks/oeb/transforms/linearize_tables.py @@ -8,6 +8,7 @@ __docformat__ = 'restructuredtext en' from calibre.ebooks.oeb.base import OEB_DOCS, XPath, XHTML + class LinearizeTables(object): def linearize(self, root): diff --git a/src/calibre/ebooks/oeb/transforms/manglecase.py b/src/calibre/ebooks/oeb/transforms/manglecase.py index 240f7e7726..8ab6e14a98 100644 --- a/src/calibre/ebooks/oeb/transforms/manglecase.py +++ b/src/calibre/ebooks/oeb/transforms/manglecase.py @@ -21,7 +21,9 @@ CASE_MANGLER_CSS = """ TEXT_TRANSFORMS = set(['capitalize', 'uppercase', 'lowercase']) + class CaseMangler(object): + @classmethod def config(cls, cfg): return cfg diff --git a/src/calibre/ebooks/oeb/transforms/metadata.py b/src/calibre/ebooks/oeb/transforms/metadata.py index a9e416707d..b1141f72b4 100644 --- a/src/calibre/ebooks/oeb/transforms/metadata.py +++ b/src/calibre/ebooks/oeb/transforms/metadata.py @@ -10,6 +10,7 @@ import os, re from calibre.utils.date import isoformat, now from calibre import guess_type + def meta_info_to_oeb_metadata(mi, m, log, override_input_metadata=False): from calibre.ebooks.oeb.base import OPF if not mi.is_null('title'): diff --git a/src/calibre/ebooks/oeb/transforms/page_margin.py b/src/calibre/ebooks/oeb/transforms/page_margin.py index 2ebff552c7..b4cea39543 100644 --- a/src/calibre/ebooks/oeb/transforms/page_margin.py +++ b/src/calibre/ebooks/oeb/transforms/page_margin.py @@ -11,6 +11,7 @@ from collections import Counter from calibre.ebooks.oeb.base import barename, XPath + class RemoveAdobeMargins(object): ''' Remove margins specified in Adobe's page templates. @@ -33,9 +34,11 @@ class RemoveAdobeMargins(object): attr = 'margin-'+margin elem.attrib.pop(attr, None) + class NegativeTextIndent(Exception): pass + class RemoveFakeMargins(object): ''' diff --git a/src/calibre/ebooks/oeb/transforms/rasterize.py b/src/calibre/ebooks/oeb/transforms/rasterize.py index 6803f2e247..2dee80c102 100644 --- a/src/calibre/ebooks/oeb/transforms/rasterize.py +++ b/src/calibre/ebooks/oeb/transforms/rasterize.py @@ -23,10 +23,13 @@ from calibre.utils.imghdr import what IMAGE_TAGS = set([XHTML('img'), XHTML('object')]) KEEP_ATTRS = set(['class', 'style', 'width', 'height', 'align']) + class Unavailable(Exception): pass + class SVGRasterizer(object): + def __init__(self, base_css=''): self.base_css = base_css from calibre.gui2 import must_use_qt diff --git a/src/calibre/ebooks/oeb/transforms/rescale.py b/src/calibre/ebooks/oeb/transforms/rescale.py index 28e888ba3d..f3c1c95b37 100644 --- a/src/calibre/ebooks/oeb/transforms/rescale.py +++ b/src/calibre/ebooks/oeb/transforms/rescale.py @@ -8,6 +8,7 @@ __docformat__ = 'restructuredtext en' from calibre import fit_image + class RescaleImages(object): 'Rescale all images to fit inside given screen size' diff --git a/src/calibre/ebooks/oeb/transforms/split.py b/src/calibre/ebooks/oeb/transforms/split.py index 8576d187ba..1a707484b3 100644 --- a/src/calibre/ebooks/oeb/transforms/split.py +++ b/src/calibre/ebooks/oeb/transforms/split.py @@ -26,9 +26,11 @@ XPath = functools.partial(_XPath, namespaces=NAMESPACES) SPLIT_POINT_ATTR = 'csp' + def tostring(root): return etree.tostring(root, encoding='utf-8') + class SplitError(ValueError): def __init__(self, path, root): @@ -38,6 +40,7 @@ class SplitError(ValueError): '%(path)s Sub-tree size: %(size)d KB')%dict( path=path, size=size)) + class Split(object): def __init__(self, split_on_page_breaks=True, page_breaks_xpath=None, diff --git a/src/calibre/ebooks/oeb/transforms/structure.py b/src/calibre/ebooks/oeb/transforms/structure.py index 83711eff04..1665b8e61b 100644 --- a/src/calibre/ebooks/oeb/transforms/structure.py +++ b/src/calibre/ebooks/oeb/transforms/structure.py @@ -15,6 +15,7 @@ from collections import OrderedDict, Counter from calibre.ebooks.oeb.base import XPNSMAP, TOC, XHTML, xml2text, barename from calibre.ebooks import ConversionError + def XPath(x): try: return etree.XPath(x, namespaces=XPNSMAP) @@ -22,9 +23,11 @@ def XPath(x): raise ConversionError( 'The syntax of the XPath expression %s is invalid.' % repr(x)) + def isspace(x): return not x or x.replace(u'\xa0', u'').isspace() + def at_start(elem): ' Return True if there is no content before elem ' body = XPath('ancestor-or-self::h:body')(elem) @@ -42,6 +45,7 @@ def at_start(elem): return False return False + class DetectStructure(object): def __call__(self, oeb, opts): diff --git a/src/calibre/ebooks/oeb/transforms/subset.py b/src/calibre/ebooks/oeb/transforms/subset.py index ac24939312..030fe4aba0 100644 --- a/src/calibre/ebooks/oeb/transforms/subset.py +++ b/src/calibre/ebooks/oeb/transforms/subset.py @@ -13,6 +13,7 @@ from calibre.ebooks.oeb.base import urlnormalize from calibre.utils.fonts.sfnt.subset import subset, NoGlyphs, UnsupportedFont from tinycss.fonts3 import parse_font_family + def get_font_properties(rule, default=None): ''' Given a CSS rule, extract normalized font properties from diff --git a/src/calibre/ebooks/oeb/transforms/trimmanifest.py b/src/calibre/ebooks/oeb/transforms/trimmanifest.py index 67d55a581e..9f638e97ec 100644 --- a/src/calibre/ebooks/oeb/transforms/trimmanifest.py +++ b/src/calibre/ebooks/oeb/transforms/trimmanifest.py @@ -11,7 +11,9 @@ from urlparse import urldefrag from calibre.ebooks.oeb.base import CSS_MIME, OEB_DOCS from calibre.ebooks.oeb.base import urlnormalize, iterlinks + class ManifestTrimmer(object): + @classmethod def config(cls, cfg): return cfg diff --git a/src/calibre/ebooks/oeb/transforms/unsmarten.py b/src/calibre/ebooks/oeb/transforms/unsmarten.py index 05f0fd59ca..2b82f092d4 100644 --- a/src/calibre/ebooks/oeb/transforms/unsmarten.py +++ b/src/calibre/ebooks/oeb/transforms/unsmarten.py @@ -9,6 +9,7 @@ __docformat__ = 'restructuredtext en' from calibre.ebooks.oeb.base import OEB_DOCS, XPath, barename from calibre.utils.unsmarten import unsmarten_text + class UnsmartenPunctuation(object): def __init__(self): diff --git a/src/calibre/ebooks/oeb/writer.py b/src/calibre/ebooks/oeb/writer.py index 25365991d0..450a8f141d 100644 --- a/src/calibre/ebooks/oeb/writer.py +++ b/src/calibre/ebooks/oeb/writer.py @@ -12,6 +12,7 @@ from calibre.ebooks.oeb.base import DirContainer, OEBError __all__ = ['OEBWriter'] + class OEBWriter(object): DEFAULT_PROFILE = 'PRS505' """Default renderer profile for content written with this Writer.""" diff --git a/src/calibre/ebooks/pdb/__init__.py b/src/calibre/ebooks/pdb/__init__.py index 020f64f613..378e4998c2 100644 --- a/src/calibre/ebooks/pdb/__init__.py +++ b/src/calibre/ebooks/pdb/__init__.py @@ -4,11 +4,13 @@ __license__ = 'GPL v3' __copyright__ = '2009, John Schember <john@nachtimwald.com>' __docformat__ = 'restructuredtext en' + class PDBError(Exception): pass FORMAT_READERS = None + def _import_readers(): global FORMAT_READERS from calibre.ebooks.pdb.ereader.reader import Reader as ereader_reader @@ -31,6 +33,8 @@ def _import_readers(): ALL_FORMAT_WRITERS = {'doc', 'ztxt', 'ereader'} FORMAT_WRITERS = None + + def _import_writers(): global FORMAT_WRITERS from calibre.ebooks.pdb.palmdoc.writer import Writer as palmdoc_writer @@ -77,6 +81,7 @@ IDENTITY_TO_NAME = { 'BDOCWrdS': 'WordSmith', } + def get_reader(identity): ''' Returns None if no reader is found for the identity. @@ -86,6 +91,7 @@ def get_reader(identity): _import_readers() return FORMAT_READERS.get(identity, None) + def get_writer(extension): ''' Returns None if no writer is found for extension. diff --git a/src/calibre/ebooks/pdb/ereader/__init__.py b/src/calibre/ebooks/pdb/ereader/__init__.py index 9157d016bd..f8c4f7b04b 100644 --- a/src/calibre/ebooks/pdb/ereader/__init__.py +++ b/src/calibre/ebooks/pdb/ereader/__init__.py @@ -6,9 +6,11 @@ __docformat__ = 'restructuredtext en' import os + class EreaderError(Exception): pass + def image_name(name, taken_names=[]): name = os.path.basename(name) diff --git a/src/calibre/ebooks/pdb/ereader/inspector.py b/src/calibre/ebooks/pdb/ereader/inspector.py index 34daa81528..4f0c957d3e 100644 --- a/src/calibre/ebooks/pdb/ereader/inspector.py +++ b/src/calibre/ebooks/pdb/ereader/inspector.py @@ -13,6 +13,7 @@ import sys from calibre.ebooks.pdb.ereader import EreaderError from calibre.ebooks.pdb.header import PdbHeaderReader + def ereader_header_info(header): h0 = header.section_data(0) @@ -29,6 +30,7 @@ def ereader_header_info(header): else: raise EreaderError('Size mismatch. eReader header record size %i KB is not supported.' % len(h0)) + def pdb_header_info(header): print 'PDB Header Info:' print '' @@ -37,6 +39,7 @@ def pdb_header_info(header): print 'Title: %s' % header.title print '' + def ereader_header_info132(h0): print 'Ereader Record 0 (Header) Info:' print '' @@ -73,6 +76,7 @@ def ereader_header_info132(h0): print '' + def ereader_header_info202(h0): print 'Ereader Record 0 (Header) Info:' print '' @@ -119,6 +123,7 @@ def section_lengths(header): print 'Section %i: %i %s' % (i, size, message) + def main(args=sys.argv): if len(args) < 2: print 'Error: requires input file.' diff --git a/src/calibre/ebooks/pdb/ereader/reader.py b/src/calibre/ebooks/pdb/ereader/reader.py index 71ba3efdc6..65af647a13 100644 --- a/src/calibre/ebooks/pdb/ereader/reader.py +++ b/src/calibre/ebooks/pdb/ereader/reader.py @@ -13,6 +13,7 @@ from calibre.ebooks.pdb.formatreader import FormatReader from calibre.ebooks.pdb.ereader.reader132 import Reader132 from calibre.ebooks.pdb.ereader.reader202 import Reader202 + class Reader(FormatReader): def __init__(self, header, stream, log, options): diff --git a/src/calibre/ebooks/pdb/ereader/reader132.py b/src/calibre/ebooks/pdb/ereader/reader132.py index 7eecd95abf..29a2414360 100644 --- a/src/calibre/ebooks/pdb/ereader/reader132.py +++ b/src/calibre/ebooks/pdb/ereader/reader132.py @@ -19,6 +19,7 @@ from calibre.ebooks.metadata.opf2 import OPFCreator from calibre.ebooks.pdb.ereader import EreaderError from calibre.ebooks.pdb.formatreader import FormatReader + class HeaderRecord(object): ''' The first record in the file is always the header record. It holds diff --git a/src/calibre/ebooks/pdb/ereader/reader202.py b/src/calibre/ebooks/pdb/ereader/reader202.py index 746ea37686..5d4aa91e1f 100644 --- a/src/calibre/ebooks/pdb/ereader/reader202.py +++ b/src/calibre/ebooks/pdb/ereader/reader202.py @@ -15,6 +15,7 @@ from calibre.ebooks.metadata.opf2 import OPFCreator from calibre.ebooks.pdb.formatreader import FormatReader from calibre.ebooks.pdb.ereader import EreaderError + class HeaderRecord(object): ''' The first record in the file is always the header record. It holds diff --git a/src/calibre/ebooks/pdb/ereader/writer.py b/src/calibre/ebooks/pdb/ereader/writer.py index e73c652a16..296fef69e7 100644 --- a/src/calibre/ebooks/pdb/ereader/writer.py +++ b/src/calibre/ebooks/pdb/ereader/writer.py @@ -30,6 +30,7 @@ IDENTITY = 'PNRdPPrs' # record size is unknown. MAX_RECORD_SIZE = 8192 + class Writer(FormatWriter): def __init__(self, opts, log): diff --git a/src/calibre/ebooks/pdb/haodoo/reader.py b/src/calibre/ebooks/pdb/haodoo/reader.py index f97ee0d90f..cdacf7881e 100644 --- a/src/calibre/ebooks/pdb/haodoo/reader.py +++ b/src/calibre/ebooks/pdb/haodoo/reader.py @@ -49,11 +49,13 @@ punct_table = { u" ": u" ", } + def fix_punct(line): for (key, value) in punct_table.items(): line = line.replace(key, value) return line + class LegacyHeaderRecord(object): def __init__(self, raw): @@ -64,6 +66,7 @@ class LegacyHeaderRecord(object): lambda x: fix_punct(x.decode('cp950', 'replace').rstrip(b'\x00')), fields[2:]) + class UnicodeHeaderRecord(object): def __init__(self, raw): @@ -75,6 +78,7 @@ class UnicodeHeaderRecord(object): lambda x: fix_punct(x.decode('utf_16_le', 'replace').rstrip(b'\x00')), fields[2].split(b'\r\x00\n\x00')) + class Reader(FormatReader): def __init__(self, header, stream, log, options): diff --git a/src/calibre/ebooks/pdb/header.py b/src/calibre/ebooks/pdb/header.py index 753c5e29b9..fe9c6fc130 100644 --- a/src/calibre/ebooks/pdb/header.py +++ b/src/calibre/ebooks/pdb/header.py @@ -11,6 +11,7 @@ import re import struct import time + class PdbHeaderReader(object): def __init__(self, stream): diff --git a/src/calibre/ebooks/pdb/palmdoc/reader.py b/src/calibre/ebooks/pdb/palmdoc/reader.py index 439492ba0c..67c4b2518e 100644 --- a/src/calibre/ebooks/pdb/palmdoc/reader.py +++ b/src/calibre/ebooks/pdb/palmdoc/reader.py @@ -14,6 +14,7 @@ from cStringIO import StringIO from calibre.ebooks.pdb.formatreader import FormatReader + class HeaderRecord(object): ''' The first record in the file is always the header record. It holds diff --git a/src/calibre/ebooks/pdb/palmdoc/writer.py b/src/calibre/ebooks/pdb/palmdoc/writer.py index 5e9b77d75c..390329b124 100644 --- a/src/calibre/ebooks/pdb/palmdoc/writer.py +++ b/src/calibre/ebooks/pdb/palmdoc/writer.py @@ -17,6 +17,7 @@ from calibre.ebooks.txt.newlines import TxtNewlines, specified_newlines MAX_RECORD_SIZE = 4096 + class Writer(FormatWriter): def __init__(self, opts, log): diff --git a/src/calibre/ebooks/pdb/pdf/reader.py b/src/calibre/ebooks/pdb/pdf/reader.py index 0b3c2e09c5..71153add6f 100644 --- a/src/calibre/ebooks/pdb/pdf/reader.py +++ b/src/calibre/ebooks/pdb/pdf/reader.py @@ -12,6 +12,7 @@ __docformat__ = 'restructuredtext en' from calibre.ebooks.pdb.formatreader import FormatReader from calibre.ptempfile import PersistentTemporaryFile + class Reader(FormatReader): def __init__(self, header, stream, log, options): diff --git a/src/calibre/ebooks/pdb/plucker/reader.py b/src/calibre/ebooks/pdb/plucker/reader.py index c69e3289d0..4dc74a6141 100644 --- a/src/calibre/ebooks/pdb/plucker/reader.py +++ b/src/calibre/ebooks/pdb/plucker/reader.py @@ -110,6 +110,7 @@ MIBNUM_TO_NAME = { 2258: 'cp1258', } + class HeaderRecord(object): ''' Plucker header. PDB record 0. diff --git a/src/calibre/ebooks/pdb/ztxt/reader.py b/src/calibre/ebooks/pdb/ztxt/reader.py index d0dfcb4b2f..ebdd7ead94 100644 --- a/src/calibre/ebooks/pdb/ztxt/reader.py +++ b/src/calibre/ebooks/pdb/ztxt/reader.py @@ -18,6 +18,7 @@ from calibre.ebooks.pdb.ztxt import zTXTError SUPPORTED_VERSION = (1, 40) + class HeaderRecord(object): ''' The first record in the file is always the header record. It holds diff --git a/src/calibre/ebooks/pdb/ztxt/writer.py b/src/calibre/ebooks/pdb/ztxt/writer.py index 7c9056fe69..5545349545 100644 --- a/src/calibre/ebooks/pdb/ztxt/writer.py +++ b/src/calibre/ebooks/pdb/ztxt/writer.py @@ -17,6 +17,7 @@ from calibre.ebooks.txt.newlines import TxtNewlines, specified_newlines MAX_RECORD_SIZE = 8192 + class Writer(FormatWriter): def __init__(self, opts, log): diff --git a/src/calibre/ebooks/pdf/outline_writer.py b/src/calibre/ebooks/pdf/outline_writer.py index a206a24bb2..a752cb7dac 100644 --- a/src/calibre/ebooks/pdf/outline_writer.py +++ b/src/calibre/ebooks/pdf/outline_writer.py @@ -10,6 +10,7 @@ __docformat__ = 'restructuredtext en' import os from collections import defaultdict + class Outline(object): def __init__(self, toc, items): diff --git a/src/calibre/ebooks/pdf/pageoptions.py b/src/calibre/ebooks/pdf/pageoptions.py index ae8fbce0f0..d50378ccfe 100644 --- a/src/calibre/ebooks/pdf/pageoptions.py +++ b/src/calibre/ebooks/pdf/pageoptions.py @@ -15,6 +15,7 @@ UNITS = { 'devicepixel' : QPrinter.DevicePixel, } + def unit(unit): return UNITS.get(unit, QPrinter.Inch) @@ -52,6 +53,7 @@ PAPER_SIZES = { # 'custom' : QPrinter.Custom, # Unknown, or a user defined size. } + def paper_size(size): return PAPER_SIZES.get(size, QPrinter.Letter) @@ -60,9 +62,11 @@ ORIENTATIONS = { 'landscape' : QPrinter.Landscape, } + def orientation(orientation): return ORIENTATIONS.get(orientation, QPrinter.Portrait) + def size(size): try: return int(size) diff --git a/src/calibre/ebooks/pdf/pdftohtml.py b/src/calibre/ebooks/pdf/pdftohtml.py index f80346d99c..ff2112e620 100644 --- a/src/calibre/ebooks/pdf/pdftohtml.py +++ b/src/calibre/ebooks/pdf/pdftohtml.py @@ -27,6 +27,7 @@ if iswindows and hasattr(sys, 'frozen'): if (islinux or isbsd) and getattr(sys, 'frozen', False): PDFTOHTML = os.path.join(sys.executables_location, 'bin', 'pdftohtml') + def pdftohtml(output_dir, pdf_path, no_images, as_xml=False): ''' Convert the pdf into html using the pdftohtml app. @@ -123,6 +124,7 @@ def pdftohtml(output_dir, pdf_path, no_images, as_xml=False): except: pass + def parse_outline(raw, output_dir): from lxml import etree from calibre.ebooks.oeb.parse_utils import RECOVER_PARSER @@ -158,6 +160,7 @@ def flip_image(img, flip): f.seek(0), f.truncate() f.write(image_to_data(img, fmt=fmt)) + def flip_images(raw): for match in re.finditer(b'<IMG[^>]+/?>', raw, flags=re.I): img = match.group() diff --git a/src/calibre/ebooks/pdf/reflow.py b/src/calibre/ebooks/pdf/reflow.py index 92c33769cb..e2f4541220 100644 --- a/src/calibre/ebooks/pdf/reflow.py +++ b/src/calibre/ebooks/pdf/reflow.py @@ -10,6 +10,7 @@ import sys, os from lxml import etree + class Font(object): def __init__(self, spec): @@ -18,6 +19,7 @@ class Font(object): self.color = spec.get('color') self.family = spec.get('family') + class Element(object): def __init__(self): @@ -30,6 +32,7 @@ class Element(object): def __hash__(self): return hash(self.id) + class Image(Element): def __init__(self, img, opts, log, idc): @@ -99,6 +102,7 @@ class Text(Element): f.write(self.to_html().encode('utf-8')) f.write('\n') + class FontSizeStats(dict): def __init__(self, stats): @@ -110,6 +114,7 @@ class FontSizeStats(dict): self.most_common_size, self.chars_at_most_common_size = sz, chars self[sz] = chars/total + class Interval(object): def __init__(self, left, right): @@ -135,6 +140,7 @@ class Interval(object): def __hash__(self): return hash('(%f,%f)'%self.left, self.right) + class Column(object): # A column contains an element is the element bulges out to @@ -221,6 +227,7 @@ class Box(list): ans.append('</%s>'%self.tag) return ans + class ImageBox(Box): def __init__(self, img): diff --git a/src/calibre/ebooks/pdf/render/common.py b/src/calibre/ebooks/pdf/render/common.py index bd5e8e2fe1..d4e71608fa 100644 --- a/src/calibre/ebooks/pdf/render/common.py +++ b/src/calibre/ebooks/pdf/render/common.py @@ -59,11 +59,13 @@ PAPER_SIZES = {k:globals()[k.upper()] for k in ('a0 a1 a2 a3 a4 a5 a6 b0 b1 b2' ic = str if ispy3 else unicode icb = (lambda x: str(x).encode('ascii')) if ispy3 else bytes + def fmtnum(o): if isinstance(o, float): return pdf_float(o) return ic(o) + def serialize(o, stream): if isinstance(o, float): stream.write_raw(pdf_float(o).encode('ascii')) @@ -84,6 +86,7 @@ def serialize(o, stream): else: raise ValueError('Unknown object: %r'%o) + class Name(unicode): def pdf_serialize(self, stream): @@ -94,6 +97,7 @@ class Name(unicode): in raw] stream.write(b'/'+b''.join(buf)) + def escape_pdf_string(bytestring): indices = [] bad = [] @@ -128,6 +132,7 @@ class String(unicode): raw = codecs.BOM_UTF16_BE + self.encode('utf-16-be') stream.write(b'('+escape_pdf_string(raw)+b')') + class UTF16String(unicode): def pdf_serialize(self, stream): @@ -139,6 +144,7 @@ class UTF16String(unicode): else: stream.write(b'('+escape_pdf_string(raw)+b')') + class Dictionary(dict): def pdf_serialize(self, stream): @@ -153,6 +159,7 @@ class Dictionary(dict): stream.write(EOL) stream.write(b'>>' + EOL) + class InlineDictionary(Dictionary): def pdf_serialize(self, stream): @@ -164,6 +171,7 @@ class InlineDictionary(Dictionary): stream.write(b' ') stream.write(b'>>') + class Array(list): def pdf_serialize(self, stream): @@ -174,6 +182,7 @@ class Array(list): serialize(o, stream) stream.write(b']') + class Stream(BytesIO): def __init__(self, compress=False): @@ -212,6 +221,7 @@ class Stream(BytesIO): def write_raw(self, raw): BytesIO.write(self, raw) + class Reference(object): def __init__(self, num, obj): diff --git a/src/calibre/ebooks/pdf/render/engine.py b/src/calibre/ebooks/pdf/render/engine.py index f410960a3a..be5fa469b5 100644 --- a/src/calibre/ebooks/pdf/render/engine.py +++ b/src/calibre/ebooks/pdf/render/engine.py @@ -25,10 +25,12 @@ Point = namedtuple('Point', 'x y') ColorState = namedtuple('ColorState', 'color opacity do') GlyphInfo = namedtuple('GlyphInfo', 'name size stretch positions indices') + def repr_transform(t): vals = map(fmtnum, (t.m11(), t.m12(), t.m21(), t.m22(), t.dx(), t.dy())) return '[%s]'%' '.join(vals) + def store_error(func): @wraps(func) @@ -41,12 +43,14 @@ def store_error(func): return errh + class Font(FontMetrics): def __init__(self, sfnt): FontMetrics.__init__(self, sfnt) self.glyph_map = {} + class PdfEngine(QPaintEngine): FEATURES = QPaintEngine.AllFeatures & ~( @@ -336,6 +340,7 @@ class PdfEngine(QPaintEngine): link.append((llx, lly, urx, ury)) self.pdf.links.add(current_item, start_page, links, anchors) + class PdfDevice(QPaintDevice): # {{{ def __init__(self, file_object, page_size=A4, left_margin=inch, diff --git a/src/calibre/ebooks/pdf/render/fonts.py b/src/calibre/ebooks/pdf/render/fonts.py index 750b3c3851..c7c5945cee 100644 --- a/src/calibre/ebooks/pdf/render/fonts.py +++ b/src/calibre/ebooks/pdf/render/fonts.py @@ -47,6 +47,7 @@ first. Each number gets mapped to a glyph id equal to itself by the import textwrap + class FontStream(Stream): def __init__(self, is_otf, compress=False): @@ -58,9 +59,11 @@ class FontStream(Stream): if self.is_otf: d['Subtype'] = Name('CIDFontType0C') + def to_hex_string(c): return bytes(hex(int(c))[2:]).rjust(4, b'0').decode('ascii') + class CMap(Stream): skeleton = textwrap.dedent('''\ @@ -106,6 +109,7 @@ class CMap(Stream): mapping.append('%d beginbfchar\n%s\nendbfchar'%(len(m), meat)) self.write(self.skeleton.format(name=name, mapping='\n'.join(mapping))) + class Font(object): def __init__(self, metrics, num, objects, compress): diff --git a/src/calibre/ebooks/pdf/render/from_html.py b/src/calibre/ebooks/pdf/render/from_html.py index 2fc45f37bb..c51f424483 100644 --- a/src/calibre/ebooks/pdf/render/from_html.py +++ b/src/calibre/ebooks/pdf/render/from_html.py @@ -25,6 +25,7 @@ from calibre.ebooks.pdf.render.common import (inch, cm, mm, pica, cicero, from calibre.ebooks.pdf.render.engine import PdfDevice from calibre.ptempfile import PersistentTemporaryFile + def get_page_size(opts, for_comic=False): # {{{ use_profile = not (opts.override_profile_size or opts.output_profile.short_name == 'default' or @@ -60,6 +61,7 @@ def get_page_size(opts, for_comic=False): # {{{ return page_size # }}} + class Page(QWebPage): # {{{ def __init__(self, opts, log): @@ -104,6 +106,7 @@ class Page(QWebPage): # {{{ # }}} + def draw_image_page(page_rect, painter, p, preserve_aspect_ratio=True): if preserve_aspect_ratio: aspect_ratio = float(p.width())/p.height() @@ -121,6 +124,7 @@ def draw_image_page(page_rect, painter, p, preserve_aspect_ratio=True): page_rect.setWidth(nnw) painter.drawPixmap(page_rect, p, p.rect()) + class PDFWriter(QObject): def _pass_json_value_getter(self): diff --git a/src/calibre/ebooks/pdf/render/gradients.py b/src/calibre/ebooks/pdf/render/gradients.py index 10c2817d99..a9532d49ef 100644 --- a/src/calibre/ebooks/pdf/render/gradients.py +++ b/src/calibre/ebooks/pdf/render/gradients.py @@ -18,6 +18,7 @@ from calibre.ebooks.pdf.render.common import Name, Array, Dictionary Stop = namedtuple('Stop', 't color') + class LinearGradientPattern(Dictionary): def __init__(self, brush, matrix, pdf, pixel_page_width, pixel_page_height): diff --git a/src/calibre/ebooks/pdf/render/graphics.py b/src/calibre/ebooks/pdf/render/graphics.py index 30cac0cb9f..53739e9cfc 100644 --- a/src/calibre/ebooks/pdf/render/graphics.py +++ b/src/calibre/ebooks/pdf/render/graphics.py @@ -18,6 +18,7 @@ from calibre.ebooks.pdf.render.common import ( from calibre.ebooks.pdf.render.serialize import Path from calibre.ebooks.pdf.render.gradients import LinearGradientPattern + def convert_path(path): # {{{ p = Path() i = 0 @@ -45,6 +46,7 @@ def convert_path(path): # {{{ Brush = namedtuple('Brush', 'origin brush color') + class TilingPattern(Stream): def __init__(self, cache_key, matrix, w=8, h=8, paint_type=2, compress=False): @@ -67,6 +69,7 @@ class TilingPattern(Stream): d['Matrix'] = Array(self.matrix) d['Resources'] = self.resources + class QtPattern(TilingPattern): qt_patterns = ( # {{{ @@ -224,6 +227,7 @@ class QtPattern(TilingPattern): super(QtPattern, self).__init__(pattern_num, matrix) self.write(self.qt_patterns[pattern_num-2]) + class TexturePattern(TilingPattern): def __init__(self, pixmap, matrix, pdf, clone=None): @@ -246,6 +250,7 @@ class TexturePattern(TilingPattern): self.resources['XObject'] = Dictionary(clone.resources['XObject']) self.write(clone.getvalue()) + class GraphicsState(object): FIELDS = ('fill', 'stroke', 'opacity', 'transform', 'brush_origin', @@ -279,6 +284,7 @@ class GraphicsState(object): ans.do_fill, ans.do_stroke = self.do_fill, self.do_stroke return ans + class Graphics(object): def __init__(self, page_width_px, page_height_px): diff --git a/src/calibre/ebooks/pdf/render/links.py b/src/calibre/ebooks/pdf/render/links.py index e360f2387b..1ab2beb22d 100644 --- a/src/calibre/ebooks/pdf/render/links.py +++ b/src/calibre/ebooks/pdf/render/links.py @@ -13,6 +13,7 @@ from urllib2 import unquote from calibre.ebooks.pdf.render.common import Array, Name, Dictionary, String, UTF16String + class Destination(Array): def __init__(self, start_page, pos, get_pageref): @@ -25,6 +26,7 @@ class Destination(Array): pref, Name('XYZ'), pos['left'], pos['top'], None ]) + class Links(object): def __init__(self, pdf, mark_links, page_size): diff --git a/src/calibre/ebooks/pdf/render/serialize.py b/src/calibre/ebooks/pdf/render/serialize.py index f6467ea2f4..a20daa8149 100644 --- a/src/calibre/ebooks/pdf/render/serialize.py +++ b/src/calibre/ebooks/pdf/render/serialize.py @@ -22,6 +22,7 @@ from calibre.utils.date import utcnow PDFVER = b'%PDF-1.4' # 1.4 is needed for XMP metadata + class IndirectObjects(object): def __init__(self): @@ -78,6 +79,7 @@ class IndirectObjects(object): stream.write(line.encode('ascii') + EOL) return self.xref_offset + class Page(Stream): def __init__(self, parentref, *args, **kwargs): @@ -149,6 +151,7 @@ class Page(Stream): # objects.commit(ret, stream) return ret + class Path(object): def __init__(self): @@ -166,12 +169,14 @@ class Path(object): def close(self): self.ops.append(('h',)) + class Catalog(Dictionary): def __init__(self, pagetree): super(Catalog, self).__init__({'Type':Name('Catalog'), 'Pages': pagetree}) + class PageTree(Dictionary): def __init__(self, page_size): @@ -193,6 +198,7 @@ class PageTree(Dictionary): except ValueError: return -1 + class HashingStream(object): def __init__(self, f): @@ -210,6 +216,7 @@ class HashingStream(object): if raw: self.last_char = raw[-1] + class Image(Stream): def __init__(self, data, w, h, depth, mask, soft_mask, dct): @@ -239,6 +246,7 @@ class Image(Stream): if self.soft_mask is not None: d['SMask'] = self.soft_mask + class Metadata(Stream): def __init__(self, mi): @@ -250,6 +258,7 @@ class Metadata(Stream): d['Type'] = Name('Metadata') d['Subtype'] = Name('XML') + class PDFStream(object): PATH_OPS = { diff --git a/src/calibre/ebooks/pdf/render/test.py b/src/calibre/ebooks/pdf/render/test.py index ac417d9e66..809a782160 100644 --- a/src/calibre/ebooks/pdf/render/test.py +++ b/src/calibre/ebooks/pdf/render/test.py @@ -16,6 +16,7 @@ QBrush, QColor, QPoint, QPixmap, QPainterPath, QRectF, Qt, QPointF from calibre.ebooks.pdf.render.engine import PdfDevice + def full(p, xmax, ymax): p.drawRect(0, 0, xmax, ymax) p.drawPolyline(QPoint(0, 0), QPoint(xmax, 0), QPoint(xmax, ymax), @@ -82,6 +83,7 @@ def run(dev, func): if dev.engine.errors_occurred: raise SystemExit(1) + def brush(p, xmax, ymax): x = 0 y = 0 @@ -94,12 +96,14 @@ def brush(p, xmax, ymax): p.fillRect(x, y, w, w, QBrush(g)) p.drawRect(x, y, w, w) + def pen(p, xmax, ymax): pix = QPixmap(I('lt.png')) pen = QPen(QBrush(pix), 60) p.setPen(pen) p.drawRect(0, xmax/3, xmax/3, xmax/2) + def text(p, xmax, ymax): f = p.font() f.setPixelSize(24) @@ -108,6 +112,7 @@ def text(p, xmax, ymax): p.drawText(QPoint(0, 100), 'Test intra glyph spacing ffagain imceo') + def main(): app = QApplication([]) app diff --git a/src/calibre/ebooks/pdf/render/toc.py b/src/calibre/ebooks/pdf/render/toc.py index 8da02edf33..ea84f2b0a3 100644 --- a/src/calibre/ebooks/pdf/render/toc.py +++ b/src/calibre/ebooks/pdf/render/toc.py @@ -11,6 +11,7 @@ import os from lxml.html import tostring from lxml.html.builder import (HTML, HEAD, BODY, TABLE, TR, TD, H2, STYLE) + def convert_node(toc, table, level, pdf): tr = TR( TD(toc.text or _('Unknown')), TD(), @@ -37,6 +38,7 @@ def process_children(toc, table, level, pdf): convert_node(child, table, level, pdf) process_children(child, table, level+1, pdf) + def toc_as_html(toc, pdf, opts): pdf = pdf.engine.pdf indents = [] diff --git a/src/calibre/ebooks/pdf/writer.py b/src/calibre/ebooks/pdf/writer.py index ff9d2a2c57..07d158989b 100644 --- a/src/calibre/ebooks/pdf/writer.py +++ b/src/calibre/ebooks/pdf/writer.py @@ -22,6 +22,7 @@ from calibre.ptempfile import PersistentTemporaryFile from calibre import (__appname__, __version__, fit_image, isosx) from calibre.ebooks.oeb.display.webview import load_html + def get_custom_size(opts): custom_size = None if opts.custom_size is not None: @@ -35,6 +36,7 @@ def get_custom_size(opts): custom_size = None return custom_size + def get_pdf_printer(opts, for_comic=False, output_file_name=None): # {{{ from calibre.gui2 import must_use_qt must_use_qt() @@ -81,6 +83,7 @@ def get_pdf_printer(opts, for_comic=False, output_file_name=None): # {{{ return printer # }}} + def draw_image_page(printer, painter, p, preserve_aspect_ratio=True): page_rect = printer.pageRect() if preserve_aspect_ratio: @@ -133,6 +136,7 @@ class Page(QWebPage): # {{{ self.log(unicode(msg)) # }}} + class PDFWriter(QObject): # {{{ def __init__(self, opts, log, cover_data=None, toc=None): @@ -339,6 +343,7 @@ class PDFWriter(QObject): # {{{ # }}} + class ImagePDFWriter(object): # {{{ def __init__(self, opts, log, cover_data=None, toc=None): diff --git a/src/calibre/ebooks/pml/__init__.py b/src/calibre/ebooks/pml/__init__.py index 9bda82bafb..9435504b5e 100644 --- a/src/calibre/ebooks/pml/__init__.py +++ b/src/calibre/ebooks/pml/__init__.py @@ -46,6 +46,7 @@ U_CHARS = Latin_ExtendedA + Latin_ExtendedB + IPA_Extensions + \ Mathematical_Operators + Enclosed_Alphanumerics + Miscellaneous_Symbols + \ Dingbats + Private_Use_Area + Alphabetic_Presentation_Forms + def unipmlcode(char): try: val = ord(char.encode('cp1252')) diff --git a/src/calibre/ebooks/pml/pmlconverter.py b/src/calibre/ebooks/pml/pmlconverter.py index f7612b3a6c..6259fdec7b 100644 --- a/src/calibre/ebooks/pml/pmlconverter.py +++ b/src/calibre/ebooks/pml/pmlconverter.py @@ -16,6 +16,7 @@ from copy import deepcopy from calibre import my_unichr, prepare_string_for_xml from calibre.ebooks.metadata.toc import TOC + class PML_HTMLizer(object): STATES = [ @@ -751,6 +752,7 @@ def pml_to_html(pml): hizer = PML_HTMLizer() return hizer.parse_pml(pml) + def footnote_sidebar_to_html(pre_id, id, pml): id = id.strip('\x01') if id.strip(): @@ -760,8 +762,10 @@ def footnote_sidebar_to_html(pre_id, id, pml): html = '<br /><br style="page-break-after: always;" /><div>%s</div>' % pml_to_html(pml) return html + def footnote_to_html(id, pml): return footnote_sidebar_to_html('fn', id, pml) + def sidebar_to_html(id, pml): return footnote_sidebar_to_html('sb', id, pml) diff --git a/src/calibre/ebooks/pml/pmlml.py b/src/calibre/ebooks/pml/pmlml.py index e44d62611e..c54dca0f5b 100644 --- a/src/calibre/ebooks/pml/pmlml.py +++ b/src/calibre/ebooks/pml/pmlml.py @@ -70,6 +70,7 @@ SEPARATE_TAGS = [ 'tr', ] + class PMLMLizer(object): def __init__(self, log): diff --git a/src/calibre/ebooks/rb/__init__.py b/src/calibre/ebooks/rb/__init__.py index 33e9882d9a..acf9c04995 100644 --- a/src/calibre/ebooks/rb/__init__.py +++ b/src/calibre/ebooks/rb/__init__.py @@ -8,6 +8,7 @@ import os HEADER = '\xb0\x0c\xb0\x0c\x02\x00NUVO\x00\x00\x00\x00' + class RocketBookError(Exception): pass diff --git a/src/calibre/ebooks/rb/rbml.py b/src/calibre/ebooks/rb/rbml.py index d6dd36ca41..fd181bb17a 100644 --- a/src/calibre/ebooks/rb/rbml.py +++ b/src/calibre/ebooks/rb/rbml.py @@ -53,6 +53,7 @@ STYLES = [ ('text-align', {'center' : 'center'}), ] + class RBMLizer(object): def __init__(self, log, name_map={}): diff --git a/src/calibre/ebooks/rb/reader.py b/src/calibre/ebooks/rb/reader.py index 5ff4ddb505..7463b88b00 100644 --- a/src/calibre/ebooks/rb/reader.py +++ b/src/calibre/ebooks/rb/reader.py @@ -15,6 +15,7 @@ from calibre.ebooks.rb import RocketBookError from calibre.ebooks.metadata.rb import get_metadata from calibre.ebooks.metadata.opf2 import OPFCreator + class RBToc(list): class Item(object): diff --git a/src/calibre/ebooks/rb/writer.py b/src/calibre/ebooks/rb/writer.py index 952ded84ed..ab46ca45a0 100644 --- a/src/calibre/ebooks/rb/writer.py +++ b/src/calibre/ebooks/rb/writer.py @@ -22,6 +22,7 @@ from calibre.constants import __appname__, __version__ TEXT_RECORD_SIZE = 4096 + class TocItem(object): def __init__(self, name, size, flags): diff --git a/src/calibre/ebooks/readability/cleaners.py b/src/calibre/ebooks/readability/cleaners.py index 4d4705487e..4c98971346 100644 --- a/src/calibre/ebooks/readability/cleaners.py +++ b/src/calibre/ebooks/readability/cleaners.py @@ -14,11 +14,13 @@ htmlstrip = re.compile("<" # open ">" # end , re.I) + def clean_attributes(html): while htmlstrip.search(html): html = htmlstrip.sub('<\\1\\2>', html) return html + def normalize_spaces(s): if not s: return '' diff --git a/src/calibre/ebooks/readability/debug.py b/src/calibre/ebooks/readability/debug.py index 8c2f42a83e..77711270ae 100644 --- a/src/calibre/ebooks/readability/debug.py +++ b/src/calibre/ebooks/readability/debug.py @@ -5,6 +5,8 @@ def save_to_file(text, filename): f.close() uids = {} + + def describe(node, depth=2): if not hasattr(node, 'tag'): return "[%s]" % type(node) diff --git a/src/calibre/ebooks/readability/htmls.py b/src/calibre/ebooks/readability/htmls.py index d528c86a0e..6815df06aa 100644 --- a/src/calibre/ebooks/readability/htmls.py +++ b/src/calibre/ebooks/readability/htmls.py @@ -6,11 +6,13 @@ import lxml.html from calibre.ebooks.readability.cleaners import normalize_spaces, clean_attributes from calibre.ebooks.chardet import xml_to_unicode + def build_doc(page): page_unicode = xml_to_unicode(page, strip_encoding_pats=True)[0] doc = lxml.html.document_fromstring(page_unicode) return doc + def js_re(src, pattern, flags, repl): return re.compile(pattern, flags).sub(src, repl.replace('$', '\\')) @@ -32,9 +34,11 @@ def normalize_entities(cur_title): return cur_title + def norm_title(title): return normalize_entities(normalize_spaces(title)) + def get_title(doc): try: title = doc.find('.//title').text @@ -45,12 +49,14 @@ def get_title(doc): return norm_title(title) + def add_match(collection, text, orig): text = norm_title(text) if len(text.split()) >= 2 and len(text) >= 15: if text.replace('"', '') in orig.replace('"', ''): collection.add(text) + def shorten_title(doc): title = doc.find('.//title').text if not title: @@ -110,6 +116,7 @@ def shorten_title(doc): return title + def get_body(doc): [elem.drop_tree() for elem in doc.xpath('.//script | .//link | .//style')] raw_html = unicode(tostring(doc.body or doc)) diff --git a/src/calibre/ebooks/readability/readability.py b/src/calibre/ebooks/readability/readability.py index 6dc4864c5c..d4c130f531 100644 --- a/src/calibre/ebooks/readability/readability.py +++ b/src/calibre/ebooks/readability/readability.py @@ -13,6 +13,7 @@ from lxml.html import (fragment_fromstring, document_fromstring, from calibre.ebooks.readability.htmls import build_doc, get_body, get_title, shorten_title from calibre.ebooks.readability.cleaners import html_cleaner, clean_attributes + def tounicode(tree_or_node, **kwargs): kwargs['encoding'] = unicode return htostring(tree_or_node, **kwargs) @@ -33,6 +34,7 @@ REGEXES = { # skipFootnoteLink: /^\s*(\[?[a-z0-9]{1,2}\]?|^|edit|citation needed)\s*$/i, } + def describe(node, depth=1): if not hasattr(node, 'tag'): return "[%s]" % type(node) @@ -47,6 +49,7 @@ def describe(node, depth=1): return name+' - '+describe(node.getparent(), depth-1) return name + def to_int(x): if not x: return None @@ -57,17 +60,21 @@ def to_int(x): return int(x[:-2]) * 12 return int(x) + def clean(text): text = re.sub('\s*\n\s*', '\n', text) text = re.sub('[ \t]{2,}', ' ', text) return text.strip() + def text_length(i): return len(clean(i.text_content() or "")) + class Unparseable(ValueError): pass + class Document: TEXT_LENGTH_THRESHOLD = 25 RETRY_LENGTH = 250 @@ -466,6 +473,7 @@ class Document: return clean_attributes(tounicode(node)) + def option_parser(): from calibre.utils.config import OptionParser parser = OptionParser(usage='%prog: [options] file') @@ -478,6 +486,7 @@ def option_parser(): return parser + def main(): from calibre.utils.logging import default_log parser = option_parser() diff --git a/src/calibre/ebooks/rtf/preprocess.py b/src/calibre/ebooks/rtf/preprocess.py index e550eb7eb9..42ffa3b57e 100644 --- a/src/calibre/ebooks/rtf/preprocess.py +++ b/src/calibre/ebooks/rtf/preprocess.py @@ -14,82 +14,107 @@ At this point this will tokenize a RTF file then rebuild it from the tokens. In the process the UTF8 tokens are altered to be supported by the RTF2XML and also remain RTF specification compilant. """ + class tokenDelimitatorStart(): def __init__(self): pass + def toRTF(self): return b'{' + def __repr__(self): return '{' + class tokenDelimitatorEnd(): def __init__(self): pass + def toRTF(self): return b'}' + def __repr__(self): return '}' + class tokenControlWord(): def __init__(self, name, separator=''): self.name = name self.separator = separator + def toRTF(self): return self.name + self.separator + def __repr__(self): return self.name + self.separator + class tokenControlWordWithNumericArgument(): def __init__(self, name, argument, separator=''): self.name = name self.argument = argument self.separator = separator + def toRTF(self): return self.name + repr(self.argument) + self.separator + def __repr__(self): return self.name + repr(self.argument) + self.separator + class tokenControlSymbol(): def __init__(self, name): self.name = name + def toRTF(self): return self.name + def __repr__(self): return self.name + class tokenData(): def __init__(self, data): self.data = data + def toRTF(self): return self.data + def __repr__(self): return self.data + class tokenBinN(): def __init__(self, data, separator=''): self.data = data self.separator = separator + def toRTF(self): return "\\bin" + repr(len(self.data)) + self.separator + self.data + def __repr__(self): return "\\bin" + repr(len(self.data)) + self.separator + self.data + class token8bitChar(): def __init__(self, data): self.data = data + def toRTF(self): return "\\'" + self.data + def __repr__(self): return "\\'" + self.data + class tokenUnicode(): def __init__(self, data, separator='', current_ucn=1, eqList=[]): @@ -97,6 +122,7 @@ class tokenUnicode(): self.separator = separator self.current_ucn = current_ucn self.eqList = eqList + def toRTF(self): result = '\\u' + repr(self.data) + ' ' ucn = self.current_ucn @@ -109,6 +135,7 @@ class tokenUnicode(): break result = result + eq.toRTF() return result + def __repr__(self): return '\\u' + repr(self.data) @@ -116,12 +143,15 @@ class tokenUnicode(): def isAsciiLetter(value): return ((value >= 'a') and (value <= 'z')) or ((value >= 'A') and (value <= 'Z')) + def isDigit(value): return (value >= '0') and (value <= '9') + def isChar(value, char): return value == char + def isString(buffer, string): return buffer == string diff --git a/src/calibre/ebooks/rtf/rtfml.py b/src/calibre/ebooks/rtf/rtfml.py index 65d7592eb6..af252ffbf8 100644 --- a/src/calibre/ebooks/rtf/rtfml.py +++ b/src/calibre/ebooks/rtf/rtfml.py @@ -68,6 +68,7 @@ TODO: * Fonts ''' + def txt2rtf(text): # Escape { and } in the text. text = text.replace('{', r'\'7b') diff --git a/src/calibre/ebooks/rtf2xml/ParseRtf.py b/src/calibre/ebooks/rtf2xml/ParseRtf.py index c30edc17ce..dfb385f91f 100755 --- a/src/calibre/ebooks/rtf2xml/ParseRtf.py +++ b/src/calibre/ebooks/rtf2xml/ParseRtf.py @@ -68,21 +68,27 @@ def Handle_Main(): except ParseRtf.RtfInvalidCodeException, msg: sys.stderr.write(msg) """ + + class InvalidRtfException(Exception): """ handle invalid RTF """ pass + + class RtfInvalidCodeException(Exception): """ handle bugs in program """ pass + class ParseRtf: """ Main class for controlling the rest of the parsing. """ + def __init__(self, in_file, out_file='', diff --git a/src/calibre/ebooks/rtf2xml/add_brackets.py b/src/calibre/ebooks/rtf2xml/add_brackets.py index 0d38a1004b..c3f42d30d2 100755 --- a/src/calibre/ebooks/rtf2xml/add_brackets.py +++ b/src/calibre/ebooks/rtf2xml/add_brackets.py @@ -16,6 +16,7 @@ import sys, os from calibre.ebooks.rtf2xml import copy, check_brackets from calibre.ptempfile import better_mktemp + class AddBrackets: """ Add brackets for old RTF. @@ -24,6 +25,7 @@ class AddBrackets: and in the list of allowed words, this will add brackets to facilitate the treatment of the file """ + def __init__(self, in_file, bug_handler, copy=None, diff --git a/src/calibre/ebooks/rtf2xml/body_styles.py b/src/calibre/ebooks/rtf2xml/body_styles.py index 975266da9c..7e439517d5 100755 --- a/src/calibre/ebooks/rtf2xml/body_styles.py +++ b/src/calibre/ebooks/rtf2xml/body_styles.py @@ -17,11 +17,14 @@ from calibre.ptempfile import better_mktemp """ Simply write the list of strings after style table """ + + class BodyStyles: """ Insert table data for tables. Logic: """ + def __init__(self, in_file, list_of_styles, @@ -46,6 +49,7 @@ class BodyStyles: self.__run_level = run_level self.__write_to = better_mktemp() # self.__write_to = 'table_info.data' + def insert_info(self): """ """ diff --git a/src/calibre/ebooks/rtf2xml/border_parse.py b/src/calibre/ebooks/rtf2xml/border_parse.py index 04c77aa0ff..29852eeba1 100755 --- a/src/calibre/ebooks/rtf2xml/border_parse.py +++ b/src/calibre/ebooks/rtf2xml/border_parse.py @@ -11,10 +11,13 @@ # # ######################################################################### import sys + + class BorderParse: """ Parse a border line and return a dictionary of attributes and values """ + def __init__(self): # cw<bd<bor-t-r-hi<nu<true self.__border_dict = { @@ -70,6 +73,7 @@ class BorderParse: 'bdr-engra_' : 'engrave', 'bdr-frame_' : 'frame', } + def parse_border(self, line): """ Requires: @@ -124,6 +128,7 @@ class BorderParse: new_border_dict = self.__determine_styles(border_type, border_style_list) border_dict.update(new_border_dict) return border_dict + def __determine_styles(self, border_type, border_style_list): new_border_dict = {} att = '%s-style' % border_type diff --git a/src/calibre/ebooks/rtf2xml/check_brackets.py b/src/calibre/ebooks/rtf2xml/check_brackets.py index 89f9838b6e..cb1709e9d2 100755 --- a/src/calibre/ebooks/rtf2xml/check_brackets.py +++ b/src/calibre/ebooks/rtf2xml/check_brackets.py @@ -10,8 +10,11 @@ # # # # ######################################################################### + + class CheckBrackets: """Check that brackets match up""" + def __init__(self, bug_handler=None, file=None): self.__file=file self.__bug_handler = bug_handler diff --git a/src/calibre/ebooks/rtf2xml/check_encoding.py b/src/calibre/ebooks/rtf2xml/check_encoding.py index 19ee724106..4b074fba0c 100755 --- a/src/calibre/ebooks/rtf2xml/check_encoding.py +++ b/src/calibre/ebooks/rtf2xml/check_encoding.py @@ -1,6 +1,7 @@ #!/usr/bin/env python2 import sys + class CheckEncoding: def __init__(self, bug_handler): diff --git a/src/calibre/ebooks/rtf2xml/colors.py b/src/calibre/ebooks/rtf2xml/colors.py index f9615a276b..254d538ac9 100755 --- a/src/calibre/ebooks/rtf2xml/colors.py +++ b/src/calibre/ebooks/rtf2xml/colors.py @@ -15,10 +15,12 @@ import sys, os, re from calibre.ebooks.rtf2xml import copy from calibre.ptempfile import better_mktemp + class Colors: """ Change lines with color info from color numbers to the actual color names. """ + def __init__(self, in_file, bug_handler, diff --git a/src/calibre/ebooks/rtf2xml/combine_borders.py b/src/calibre/ebooks/rtf2xml/combine_borders.py index 047b9a2dde..90b301db00 100755 --- a/src/calibre/ebooks/rtf2xml/combine_borders.py +++ b/src/calibre/ebooks/rtf2xml/combine_borders.py @@ -15,8 +15,10 @@ import os from calibre.ebooks.rtf2xml import copy from calibre.ptempfile import better_mktemp + class CombineBorders: """Combine borders in RTF tokens to make later processing easier""" + def __init__(self, in_file , bug_handler, diff --git a/src/calibre/ebooks/rtf2xml/configure_txt.py b/src/calibre/ebooks/rtf2xml/configure_txt.py index 05262a222c..b2100400e0 100755 --- a/src/calibre/ebooks/rtf2xml/configure_txt.py +++ b/src/calibre/ebooks/rtf2xml/configure_txt.py @@ -1,4 +1,6 @@ import os, sys + + class Configure: def __init__(self, @@ -19,6 +21,7 @@ class Configure: self.__debug_dir = debug_dir self.__bug_handler = bug_handler self.__show_config_file = show_config_file + def get_configuration(self, type): self.__configuration_file = self.__get_file_name() return_dict = {} @@ -59,6 +62,7 @@ class Configure: % self.__configuration_file) raise self.__bug_handler, msg return return_dict + def __get_file_name(self): home_var = os.environ.get('HOME') if home_var: @@ -74,6 +78,7 @@ class Configure: if os.path.isfile(script_file): return script_file return self.__configuration_file + def __parse_dict(self, return_dict): allowable = [ 'configuration-directory', diff --git a/src/calibre/ebooks/rtf2xml/convert_to_tags.py b/src/calibre/ebooks/rtf2xml/convert_to_tags.py index a21dc982f1..3fa48888a8 100755 --- a/src/calibre/ebooks/rtf2xml/convert_to_tags.py +++ b/src/calibre/ebooks/rtf2xml/convert_to_tags.py @@ -6,10 +6,12 @@ from calibre.ptempfile import better_mktemp public_dtd = 'rtf2xml1.0.dtd' + class ConvertToTags: """ Convert file to XML """ + def __init__(self, in_file, bug_handler, diff --git a/src/calibre/ebooks/rtf2xml/copy.py b/src/calibre/ebooks/rtf2xml/copy.py index 1135205113..edfd2957b2 100755 --- a/src/calibre/ebooks/rtf2xml/copy.py +++ b/src/calibre/ebooks/rtf2xml/copy.py @@ -12,9 +12,11 @@ ######################################################################### import os, shutil + class Copy: """Copy each changed file to a directory for debugging purposes""" __dir = "" + def __init__(self, bug_handler, file=None, deb_dir=None, ): self.__file = file self.__bug_handler = bug_handler diff --git a/src/calibre/ebooks/rtf2xml/default_encoding.py b/src/calibre/ebooks/rtf2xml/default_encoding.py index c6c1c7a01a..32e2e3b26f 100755 --- a/src/calibre/ebooks/rtf2xml/default_encoding.py +++ b/src/calibre/ebooks/rtf2xml/default_encoding.py @@ -57,6 +57,7 @@ Codepages as to RTF 1.9.1: ''' import re + class DefaultEncoding: """ Find the default encoding for the doc diff --git a/src/calibre/ebooks/rtf2xml/delete_info.py b/src/calibre/ebooks/rtf2xml/delete_info.py index 16532ec4b8..fdfce2be6c 100755 --- a/src/calibre/ebooks/rtf2xml/delete_info.py +++ b/src/calibre/ebooks/rtf2xml/delete_info.py @@ -15,8 +15,10 @@ import sys, os from calibre.ebooks.rtf2xml import copy from calibre.ptempfile import better_mktemp + class DeleteInfo: """Delete unecessary destination groups""" + def __init__(self, in_file , bug_handler, diff --git a/src/calibre/ebooks/rtf2xml/field_strings.py b/src/calibre/ebooks/rtf2xml/field_strings.py index 53b5c55dd0..72a87a2114 100755 --- a/src/calibre/ebooks/rtf2xml/field_strings.py +++ b/src/calibre/ebooks/rtf2xml/field_strings.py @@ -11,11 +11,14 @@ # # ######################################################################### import sys, re + + class FieldStrings: """ This module is given a string. It processes the field instruction string and returns a list of three values. """ + def __init__(self, bug_handler, run_level=1): """ Requires: @@ -26,6 +29,7 @@ class FieldStrings: self.__run_level = run_level self.__bug_handler = bug_handler self.__initiate_values() + def __initiate_values(self): """ Requires: @@ -175,6 +179,7 @@ class FieldStrings: self.__quote_exp = re.compile(r'"(.*?)"') self.__filter_switch = re.compile(r'\\c\s{1,}(.*?)\s') self.__link_switch = re.compile(r'\\l\s{1,}(.*?)\s') + def process_string(self, my_string, type): """ Requires: @@ -218,6 +223,7 @@ class FieldStrings: the_list = self.__fall_back_func(field_name, line) return the_list return the_list + def __default_inst_func(self, field_name, name, line): """ Requires: @@ -230,6 +236,7 @@ class FieldStrings: I only need the changed name for the field. """ return [None, None, name] + def __fall_back_func(self, field_name, line): """ Requires: @@ -244,6 +251,7 @@ class FieldStrings: the_string = field_name the_string += '<update>none' return [None, None, the_string] + def __equation_func(self, field_name, name, line): """ Requried: @@ -255,6 +263,7 @@ class FieldStrings: Logic: """ return [None, None, name] + def __no_switch_func(self, field_name, name, line): """ Required: @@ -267,6 +276,7 @@ class FieldStrings: Logic: """ return [None, None, name] + def __num_type_and_format_func(self, field_name, name, line): """ Required: @@ -293,6 +303,7 @@ class FieldStrings: arg = match_group.group(1) the_string += '<argument>%s' % arg return [None, None, the_string] + def __num_format_func(self, field_name, name, line): """ Required: @@ -308,6 +319,7 @@ class FieldStrings: if num_format: the_string += '<number-format>%s' % num_format return [None, None, the_string] + def __parse_num_format(self, the_string): """ Required: @@ -320,6 +332,7 @@ class FieldStrings: match_group = re.search(self.__date_exp, the_string) if match_group: return match_group(1) + def __parse_num_type(self, the_string): """ Required: @@ -344,6 +357,7 @@ class FieldStrings: sys.stderr.write('module is fields_string\n') sys.stderr.write('method is __parse_num_type\n') sys.stderr.write('no dictionary entry for %s\n' % name) + def __date_func(self, field_name, name, line): """ Required: @@ -360,6 +374,7 @@ class FieldStrings: if match_group: the_string += '<date-format>%s' % match_group.group(1) return [None, None, the_string] + def __simple_info_func(self, field_name, name, line): """ Requried: @@ -387,6 +402,7 @@ class FieldStrings: sys.stderr.write('method is __parse_num_type\n') sys.stderr.write('no dictionary entry for %s\n' % name) return [None, None, the_string] + def __hyperlink_func(self, field_name, name, line): """ Requried: @@ -424,6 +440,7 @@ class FieldStrings: if index > -1: the_string += '<no-history>true' return [None, None, the_string] + def __include_text_func(self, field_name, name, line): """ Requried: @@ -465,6 +482,7 @@ class FieldStrings: if index > -1: the_string += '<no-field-update>true' return [None, None, the_string] + def __include_pict_func(self, field_name, name, line): """ Requried: @@ -496,6 +514,7 @@ class FieldStrings: if index > -1: the_string += '<external>true' return [None, None, the_string] + def __ref_func(self, field_name, name, line): """ Requires: @@ -549,6 +568,7 @@ class FieldStrings: if index > -1: the_string += '<insert-number-full>true' return [None, None, the_string] + def __toc_table_func(self, field_name, name, line): """ Requires: @@ -567,6 +587,7 @@ class FieldStrings: the_string = the_string.replace('table-of-contents', 'table-of-figures') # don't really need the first value in this list, I don't believe return [name, None, the_string] + def __sequence_func(self, field_name, name, line): """ Requires: @@ -585,6 +606,7 @@ class FieldStrings: label = fields[1] my_string = '%s<label>%s' % (name, label) return [None, None, my_string] + def __ta_func(self, field_name, name, line): """ Requires: @@ -615,6 +637,7 @@ class FieldStrings: if index > -1: the_string += '<italics>true' return [None, None, the_string] + def __index_func(self, field_name, name, line): """ Requires: @@ -687,6 +710,7 @@ class FieldStrings: if index > -1: the_string += '<enable-yomi-text>true' return [None, None, the_string] + def __page_ref_func(self, field_name, name, line): """ Requires: @@ -717,6 +741,7 @@ class FieldStrings: if index > -1: the_string += '<paragraph-relative-position>true' return [None, None, the_string] + def __note_ref_func(self, field_name, name, line): """ Requires: @@ -744,6 +769,7 @@ class FieldStrings: if index > -1: the_string += '<include-note-number>true' return [None, None, the_string] + def __symbol_func(self, field_name, name, line): """ Requires: diff --git a/src/calibre/ebooks/rtf2xml/fields_large.py b/src/calibre/ebooks/rtf2xml/fields_large.py index 1c9ab49430..70bd452e59 100755 --- a/src/calibre/ebooks/rtf2xml/fields_large.py +++ b/src/calibre/ebooks/rtf2xml/fields_large.py @@ -14,6 +14,7 @@ import sys, os from calibre.ebooks.rtf2xml import field_strings, copy from calibre.ptempfile import better_mktemp + class FieldsLarge: """ ========================= @@ -88,6 +89,7 @@ Examples </paragraph-definition> </field-block> """ + def __init__(self, in_file, bug_handler, @@ -109,6 +111,7 @@ Examples self.__copy = copy self.__run_level = run_level self.__write_to = better_mktemp() + def __initiate_values(self): """ Initiate all values. @@ -142,6 +145,7 @@ Examples self.__par_in_field = [] # paragraphs in field? self.__sec_in_field = [] # sections in field? self.__field_string = [] # list of field strings + def __before_body_func(self, line): """ Requried: @@ -155,6 +159,7 @@ Examples if self.__token_info == 'mi<mk<body-open_': self.__state = 'in_body' self.__write_obj.write(line) + def __in_body_func(self, line): """ Required: @@ -168,6 +173,7 @@ Examples if action: action(line) self.__write_obj.write(line) + def __found_field_func(self, line): """ Requires: @@ -185,6 +191,7 @@ Examples self.__field_count.append(ob_count) self.__sec_in_field.append(0) self.__par_in_field.append(0) + def __in_field_func(self, line): """ Requires: @@ -205,6 +212,7 @@ Examples action(line) else: self.__field_string[-1] += line + def __par_in_field_func(self, line): """ Requires: @@ -217,6 +225,7 @@ Examples """ self.__field_string[-1] += line self.__par_in_field[-1] = 1 + def __sec_in_field_func(self, line): """ Requires: @@ -229,6 +238,7 @@ Examples """ self.__field_string[-1] += line self.__sec_in_field[-1] = 1 + def __found_field_instruction_func(self, line): """ Requires: @@ -242,6 +252,7 @@ Examples self.__state = 'field_instruction' self.__field_instruction_count = self.__ob_count self.__cb_count = 0 + def __field_instruction_func(self, line): """ Requires: @@ -267,6 +278,7 @@ Examples self.__field_instruction_string = '' else: self.__field_instruction_string += line + def __end_field_func(self): """ Requires: @@ -321,9 +333,11 @@ Examples else: self.__field_string[-1] += inner_field_string self.__symbol = 0 + def __write_field_string(self, the_string): self.__state = 'in_body' self.__write_obj.write(the_string) + def fix_fields(self): """ Requires: diff --git a/src/calibre/ebooks/rtf2xml/fields_small.py b/src/calibre/ebooks/rtf2xml/fields_small.py index 4de67f5e02..edbcb1f222 100755 --- a/src/calibre/ebooks/rtf2xml/fields_small.py +++ b/src/calibre/ebooks/rtf2xml/fields_small.py @@ -15,6 +15,7 @@ import sys, os, re from calibre.ebooks.rtf2xml import field_strings, copy from calibre.ptempfile import better_mktemp + class FieldsSmall: """ ================= @@ -32,6 +33,7 @@ until the closing bracket entry is found. Send the string to the module field_strings to process it. Write the processed string to the output file. """ + def __init__(self, in_file, bug_handler, diff --git a/src/calibre/ebooks/rtf2xml/fonts.py b/src/calibre/ebooks/rtf2xml/fonts.py index 226e85f6f7..148e58cb51 100755 --- a/src/calibre/ebooks/rtf2xml/fonts.py +++ b/src/calibre/ebooks/rtf2xml/fonts.py @@ -15,10 +15,12 @@ import sys, os from calibre.ebooks.rtf2xml import copy from calibre.ptempfile import better_mktemp + class Fonts: """ Change lines with font info from font numbers to the actual font names. """ + def __init__(self, in_file, bug_handler, diff --git a/src/calibre/ebooks/rtf2xml/footnote.py b/src/calibre/ebooks/rtf2xml/footnote.py index cccc023002..bb2014a01a 100755 --- a/src/calibre/ebooks/rtf2xml/footnote.py +++ b/src/calibre/ebooks/rtf2xml/footnote.py @@ -15,6 +15,7 @@ import os from calibre.ebooks.rtf2xml import copy from calibre.ptempfile import better_mktemp + class Footnote: """ Two public methods are available. The first separates all of the @@ -22,6 +23,7 @@ class Footnote: they are easier to process. The second joins those footnotes to the proper places in the body. """ + def __init__(self, in_file , bug_handler, diff --git a/src/calibre/ebooks/rtf2xml/get_char_map.py b/src/calibre/ebooks/rtf2xml/get_char_map.py index 88ac7d8291..55a7d41647 100755 --- a/src/calibre/ebooks/rtf2xml/get_char_map.py +++ b/src/calibre/ebooks/rtf2xml/get_char_map.py @@ -11,6 +11,7 @@ # # ######################################################################### + class GetCharMap: """ diff --git a/src/calibre/ebooks/rtf2xml/get_options.py b/src/calibre/ebooks/rtf2xml/get_options.py index 22cf70c6fa..fcffff7fcd 100755 --- a/src/calibre/ebooks/rtf2xml/get_options.py +++ b/src/calibre/ebooks/rtf2xml/get_options.py @@ -15,6 +15,8 @@ Gets options for main part of script """ import sys, os from calibre.ebooks.rtf2xml import options_trem, configure_txt + + class GetOptions: def __init__(self, @@ -27,6 +29,7 @@ class GetOptions: self.__rtf_dir = rtf_dir self.__configuration_file = configuration_file self.__bug_handler = bug_handler + def get_options(self): """ return valid, output, help, show_warnings, debug, file @@ -260,6 +263,7 @@ class GetOptions: return_options['valid'] = 0 """ return return_options + def __get_config_options(self): configure_obj = configure_txt.Configure( bug_handler=self.__bug_handler, diff --git a/src/calibre/ebooks/rtf2xml/group_borders.py b/src/calibre/ebooks/rtf2xml/group_borders.py index 239ecb7454..ce290f4ae6 100755 --- a/src/calibre/ebooks/rtf2xml/group_borders.py +++ b/src/calibre/ebooks/rtf2xml/group_borders.py @@ -14,6 +14,7 @@ import sys, os, re from calibre.ebooks.rtf2xml import copy from calibre.ptempfile import better_mktemp + class GroupBorders: """ Form lists. @@ -21,6 +22,7 @@ class GroupBorders: list. Use indents to determine items and how lists are nested. """ + def __init__(self, in_file, bug_handler, @@ -44,6 +46,7 @@ class GroupBorders: self.__run_level = run_level self.__write_to = better_mktemp() self.__wrap = wrap + def __initiate_values(self): """ Required: @@ -113,6 +116,7 @@ class GroupBorders: self.__line_num = 0 self.__border_regex = re.compile(r'(<border-paragraph[^<]+|<border-for-every-paragraph[^<]+)') self.__last_border_string = '' + def __in_pard_func(self, line): """ Required: @@ -128,6 +132,7 @@ class GroupBorders: self.__state = 'after_pard' else: self.__write_obj.write(line) + def __after_pard_func(self, line): """ Required: @@ -155,12 +160,14 @@ class GroupBorders: self.__write_obj.write(line) else: self.__list_chunk += line + def __close_pard_(self, line): self.__write_obj.write(self.__list_chunk) self.__write_obj.write('mi<tg<close_____<paragraph-definition\n') self.__write_end_wrap() self.__list_chunk = '' self.__state = 'default' + def __pard_after_par_def_func(self, line): """ Required: @@ -197,6 +204,7 @@ class GroupBorders: self.__state = 'in_pard' self.__last_border_string = border_string self.__list_chunk = '' + def __default_func(self, line): """ Required: @@ -221,6 +229,7 @@ class GroupBorders: self.__write_obj.write(line) else: self.__write_obj.write(line) + def __write_start_border_tag(self, the_string): self.__write_obj.write('mi<mk<start-brdg\n') self.__border_num += 1 @@ -228,15 +237,18 @@ class GroupBorders: num_string = 's%s' % num the_string += '<num>%s' % num_string self.__write_obj.write('mi<tg<open-att__<border-group%s\n' % the_string) + def __write_end_border_tag(self): self.__write_obj.write('mi<mk<end-brdg__\n') self.__write_obj.write('mi<tg<close_____<border-group\n') + def __is_border_func(self, line): line = re.sub(self.__name_regex, '', line) index = line.find('border-paragraph') if index > -1: return 1 return 0 + def __parse_pard_with_border(self, line): border_string = '' pard_string = '' @@ -247,6 +259,7 @@ class GroupBorders: else: pard_string += token return border_string, pard_string + def __write_pard_with_border(self, line): border_string = '' pard_string = '' @@ -258,9 +271,11 @@ class GroupBorders: pard_string += token self.__write_start_border_tag(border_string) self.__write_obj.write(pard_string) + def __get_style_name(self, line): if self.__token_info == 'mi<mk<style-name': self.__style_name = line[17:-1] + def group_borders(self): """ Required: diff --git a/src/calibre/ebooks/rtf2xml/group_styles.py b/src/calibre/ebooks/rtf2xml/group_styles.py index e1aab26a39..dbebf436b6 100755 --- a/src/calibre/ebooks/rtf2xml/group_styles.py +++ b/src/calibre/ebooks/rtf2xml/group_styles.py @@ -14,6 +14,7 @@ import sys, os, re from calibre.ebooks.rtf2xml import copy from calibre.ptempfile import better_mktemp + class GroupStyles: """ Form lists. @@ -21,6 +22,7 @@ class GroupStyles: list. Use indents to determine items and how lists are nested. """ + def __init__(self, in_file, bug_handler, @@ -44,6 +46,7 @@ class GroupStyles: self.__run_level = run_level self.__write_to = better_mktemp() self.__wrap = wrap + def __initiate_values(self): """ Required: @@ -102,6 +105,7 @@ class GroupStyles: self.__name_regex = re.compile(r'<name>') self.__found_appt = 0 self.__line_num = 0 + def __in_pard_func(self, line): """ Required: @@ -117,6 +121,7 @@ class GroupStyles: self.__state = 'after_pard' else: self.__write_obj.write(line) + def __after_pard_func(self, line): """ Required: @@ -144,22 +149,26 @@ class GroupStyles: self.__write_obj.write(line) else: self.__list_chunk += line + def __close_pard_(self, line): self.__write_obj.write(self.__list_chunk) self.__write_obj.write('mi<tg<close_____<paragraph-definition\n') self.__write_end_wrap() self.__list_chunk = '' self.__state = 'default' + def __write_start_wrap(self, name): if self.__wrap: self.__write_obj.write('mi<mk<style-grp_<%s\n' % name) self.__write_obj.write('mi<tg<open-att__<style-group<name>%s\n' % name) self.__write_obj.write('mi<mk<style_grp_<%s\n' % name) + def __write_end_wrap(self): if self.__wrap: self.__write_obj.write('mi<mk<style_gend\n') self.__write_obj.write('mi<tg<close_____<style-group\n') self.__write_obj.write('mi<mk<stylegend_\n') + def __pard_after_par_def_func(self, line): """ Required: @@ -188,6 +197,7 @@ class GroupStyles: self.__state = 'in_pard' self.__last_style_name = self.__style_name self.__list_chunk = '' + def __default_func(self, line): """ Required: @@ -207,9 +217,11 @@ class GroupStyles: self.__write_obj.write(line) else: self.__write_obj.write(line) + def __get_style_name(self, line): if self.__token_info == 'mi<mk<style-name': self.__style_name = line[17:-1] + def group_styles(self): """ Required: diff --git a/src/calibre/ebooks/rtf2xml/header.py b/src/calibre/ebooks/rtf2xml/header.py index d214610eae..9b51f5975e 100755 --- a/src/calibre/ebooks/rtf2xml/header.py +++ b/src/calibre/ebooks/rtf2xml/header.py @@ -15,6 +15,7 @@ import sys, os from calibre.ebooks.rtf2xml import copy from calibre.ptempfile import better_mktemp + class Header: """ Two public methods are available. The first separates all of the headers @@ -22,6 +23,7 @@ class Header: they are easier to process. The second joins those headers and footers to the proper places in the body. """ + def __init__(self, in_file , bug_handler, diff --git a/src/calibre/ebooks/rtf2xml/headings_to_sections.py b/src/calibre/ebooks/rtf2xml/headings_to_sections.py index 37c66af729..aea20c8098 100755 --- a/src/calibre/ebooks/rtf2xml/headings_to_sections.py +++ b/src/calibre/ebooks/rtf2xml/headings_to_sections.py @@ -14,9 +14,11 @@ import os, re from calibre.ebooks.rtf2xml import copy from calibre.ptempfile import better_mktemp + class HeadingsToSections: """ """ + def __init__(self, in_file, bug_handler, @@ -37,6 +39,7 @@ class HeadingsToSections: self.__bug_handler = bug_handler self.__copy = copy self.__write_to = better_mktemp() + def __initiate_values(self): """ Required: @@ -79,6 +82,7 @@ class HeadingsToSections: ] self.__section_num = [0] self.__id_regex = re.compile(r'\<list-id\>(\d+)') + def __close_lists(self): """ Required: @@ -104,6 +108,7 @@ class HeadingsToSections: num_levels_closed += 1 self.__all_lists = self.__all_lists[num_levels_closed:] self.__all_lists.reverse() + def __close_sections(self, current_level): self.__all_sections.reverse() num_levels_closed = 0 @@ -113,6 +118,7 @@ class HeadingsToSections: num_levels_closed += 1 self.__all_sections = self.__all_sections[num_levels_closed:] self.__all_sections.reverse() + def __write_start_section(self, current_level, name): section_num = '' for the_num in self.__section_num: @@ -129,9 +135,11 @@ class HeadingsToSections: '<type>%s\n' % (section_num, num_in_level, level, name) ) + def __write_end_section(self): self.__write_obj.write('mi<mk<sect-close\n') self.__write_obj.write('mi<tg<close_____<section\n') + def __default_func(self, line): """ Required: @@ -160,6 +168,7 @@ class HeadingsToSections: if self.__token_info == 'mi<mk<body-close': self.__state = 'after_body' self.__write_obj.write(line) + def __handle_heading(self, name): num = self.__headings.index(name) + 1 self.__close_sections(num) @@ -171,10 +180,12 @@ class HeadingsToSections: else: self.__section_num[-1] += 1 self.__write_start_section(num, name) + def __in_table_func(self, line): if self.__token_info == 'mi<mk<table-end_': self.__state = 'default' self.__write_obj.write(line) + def __in_list_func(self, line): if self.__token_info == 'mi<mk<list_close': self.__list_depth -= 1 @@ -183,8 +194,10 @@ class HeadingsToSections: if self.__list_depth == 0: self.__state = 'default' self.__write_obj.write(line) + def __after_body_func(self, line): self.__write_obj.write(line) + def make_sections(self): """ Required: diff --git a/src/calibre/ebooks/rtf2xml/hex_2_utf8.py b/src/calibre/ebooks/rtf2xml/hex_2_utf8.py index 39b40fe962..a597732d40 100755 --- a/src/calibre/ebooks/rtf2xml/hex_2_utf8.py +++ b/src/calibre/ebooks/rtf2xml/hex_2_utf8.py @@ -16,10 +16,12 @@ from calibre.ebooks.rtf2xml import get_char_map, copy from calibre.ebooks.rtf2xml.char_set import char_set from calibre.ptempfile import better_mktemp + class Hex2Utf8: """ Convert Microsoft hexidecimal numbers to utf-8 """ + def __init__(self, in_file, area_to_convert, diff --git a/src/calibre/ebooks/rtf2xml/info.py b/src/calibre/ebooks/rtf2xml/info.py index f9c0ee132d..b0ef8c70a9 100755 --- a/src/calibre/ebooks/rtf2xml/info.py +++ b/src/calibre/ebooks/rtf2xml/info.py @@ -15,10 +15,12 @@ import sys, os, re from calibre.ebooks.rtf2xml import copy from calibre.ptempfile import better_mktemp + class Info: """ Make tags for document-information """ + def __init__(self, in_file, bug_handler, diff --git a/src/calibre/ebooks/rtf2xml/inline.py b/src/calibre/ebooks/rtf2xml/inline.py index 606554d590..59d1efe8af 100755 --- a/src/calibre/ebooks/rtf2xml/inline.py +++ b/src/calibre/ebooks/rtf2xml/inline.py @@ -14,11 +14,14 @@ States. 2. paragraph end -- close out all tags 3. footnote beg -- close out all tags """ + + class Inline: """ Make inline tags within lists. Logic: """ + def __init__(self, in_file, bug_handler, diff --git a/src/calibre/ebooks/rtf2xml/line_endings.py b/src/calibre/ebooks/rtf2xml/line_endings.py index 199fbb30b4..3e2b8156e8 100755 --- a/src/calibre/ebooks/rtf2xml/line_endings.py +++ b/src/calibre/ebooks/rtf2xml/line_endings.py @@ -16,8 +16,10 @@ from calibre.ebooks.rtf2xml import copy from calibre.utils.cleantext import clean_ascii_chars from calibre.ptempfile import better_mktemp + class FixLineEndings: """Fix line endings""" + def __init__(self, bug_handler, in_file=None, diff --git a/src/calibre/ebooks/rtf2xml/list_numbers.py b/src/calibre/ebooks/rtf2xml/list_numbers.py index 5cf9ff1380..f0f74d8b4f 100755 --- a/src/calibre/ebooks/rtf2xml/list_numbers.py +++ b/src/calibre/ebooks/rtf2xml/list_numbers.py @@ -14,11 +14,13 @@ import os from calibre.ebooks.rtf2xml import copy from calibre.ptempfile import better_mktemp + class ListNumbers: """ RTF puts list numbers outside of the paragraph. The public method in this class put the list numbers inside the paragraphs. """ + def __init__(self, in_file, bug_handler, @@ -39,6 +41,7 @@ class ListNumbers: self.__bug_handler = bug_handler self.__copy = copy self.__write_to = better_mktemp() + def __initiate_values(self): """ initiate values for fix_list_numbers. @@ -57,6 +60,7 @@ class ListNumbers: 'list_text' : self.__list_text_func, 'after_list_text' : self.__after_list_text_func } + def __after_ob_func(self, line): """ Handle the line immediately after an open bracket. @@ -75,6 +79,7 @@ class ListNumbers: self.__write_obj.write(self.__previous_line) self.__write_obj.write(line) self.__state = 'default' + def __after_list_text_func(self, line): """ Look for an open bracket or a line of text, and then print out the @@ -93,6 +98,7 @@ class ListNumbers: self.__write_obj.write('mi<mk<lst-tx-end\n') self.__list_chunk = '' self.__write_obj.write(line) + def __determine_list_type(self, chunk): """ Determine if the list is ordered or itemized @@ -117,6 +123,7 @@ class ListNumbers: """ # must be some type of ordered list -- just a guess! return 'unordered' + def __list_text_func(self, line): """ Handle lines that are part of the list text. If the end of the list @@ -134,6 +141,7 @@ class ListNumbers: self.__write_obj.write('mi<mk<list-type_<%s\n' % self.__list_type) if self.__token_info != 'cw<pf<par-def___': self.__list_chunk = self.__list_chunk + line + def __default_func(self, line): """ Handle the lines that are not part of any special state. Look for an @@ -150,6 +158,7 @@ class ListNumbers: self.__previous_line = line else: self.__write_obj.write(line) + def fix_list_numbers(self): """ Required: diff --git a/src/calibre/ebooks/rtf2xml/list_table.py b/src/calibre/ebooks/rtf2xml/list_table.py index 18f58c8abc..41605ce403 100755 --- a/src/calibre/ebooks/rtf2xml/list_table.py +++ b/src/calibre/ebooks/rtf2xml/list_table.py @@ -10,11 +10,14 @@ # # # # ######################################################################### + + class ListTable: """ Parse the list table line. Make a string. Form a dictionary. Return the string and the dictionary. """ + def __init__( self, bug_handler, @@ -23,6 +26,7 @@ class ListTable: self.__bug_handler = bug_handler self.__initiate_values() self.__run_level = run_level + def __initiate_values(self): self.__list_table_final = '' self.__state = 'default' @@ -72,6 +76,7 @@ class ListTable: ] ], """ + def __parse_lines(self, line): """ Required : line --line to parse @@ -97,6 +102,7 @@ class ListTable: action(line) self.__write_final_string() # self.__add_to_final_line() + def __default_func(self, line): """ Requires: line --line to process @@ -107,6 +113,7 @@ class ListTable: """ if self.__token_info == 'ob<nu<open-brack': self.__state = 'unsure_ob' + def __found_list_func(self, line): """ Requires: line -- line to process @@ -126,6 +133,7 @@ class ListTable: self.__all_lists.append([]) the_dict = {'list-id': []} self.__all_lists[-1].append(the_dict) + def __list_func(self, line): """ Requires: line --line to process @@ -147,6 +155,7 @@ class ListTable: # dictionary is always the first item in the last list # [{att:value}, [], [att:value, []] self.__all_lists[-1][0][att] = value + def __found_level_func(self, line): """ Requires: line -- line to process @@ -174,6 +183,7 @@ class ListTable: the_dict = {} self.__all_lists[-1][-1].append(the_dict) self.__level_dict + def __level_func(self, line): """ Requires: @@ -195,6 +205,7 @@ class ListTable: if att: value = line[20:] self.__all_lists[-1][-1][0][att] = value + def __level_number_func(self, line): """ Requires: @@ -226,6 +237,7 @@ class ListTable: level = 'level%s-show-level' % level self.__all_lists[-1][-1][0][level] = 'true' """ + def __level_text_func(self, line): """ Requires: @@ -264,6 +276,7 @@ class ListTable: elif self.__token_info == 'cw<ls<lv-tem-id_': value = line[20:] self.__all_lists[-1][-1][0]['level-template-id'] = value + def __parse_level_text_length(self, line): """ Requires: @@ -289,6 +302,7 @@ class ListTable: prefix_marker = 'level%s-prefix' % the_string self.__all_lists[-1][-1][0][prefix_marker] = self.__prefix_string self.__prefix_string = None + def __list_name_func(self, line): """ Requires: @@ -301,6 +315,7 @@ class ListTable: if self.__token_info == 'cb<nu<clos-brack' and\ self.__cb_count == self.__list_name_ob_count: self.__state = 'list' + def __after_bracket_func(self, line): """ Requires: @@ -331,6 +346,7 @@ class ListTable: msg = 'No matching token after open bracket\n' msg += 'token is "%s\n"' % (line) raise self.__bug_handler + def __add_to_final_line(self): """ Method no longer used. @@ -341,6 +357,7 @@ class ListTable: self.__list_table_final += \ 'mi<mk<listab-end\n' + 'mi<tg<close_____<list-table\n' self.__list_table_final += 'mi<mk<listabend_\n' + def __write_final_string(self): """ Requires: @@ -412,6 +429,7 @@ class ListTable: self.__list_table_final += \ 'mi<mk<listab-end\n' + 'mi<tg<close_____<list-table\n' self.__list_table_final += 'mi<mk<listabend_\n' + def parse_list_table(self, line): """ Requires: diff --git a/src/calibre/ebooks/rtf2xml/make_lists.py b/src/calibre/ebooks/rtf2xml/make_lists.py index 724b84bfb1..954e573f11 100755 --- a/src/calibre/ebooks/rtf2xml/make_lists.py +++ b/src/calibre/ebooks/rtf2xml/make_lists.py @@ -14,6 +14,7 @@ import sys, os, re from calibre.ebooks.rtf2xml import copy from calibre.ptempfile import better_mktemp + class MakeLists: """ Form lists. @@ -21,6 +22,7 @@ class MakeLists: list. Use indents to determine items and how lists are nested. """ + def __init__(self, in_file, bug_handler, @@ -50,6 +52,7 @@ class MakeLists: self.__write_to = better_mktemp() self.__list_of_lists = list_of_lists self.__write_list_info = write_list_info + def __initiate_values(self): """ Required: @@ -104,6 +107,7 @@ class MakeLists: self.__lv_regex = re.compile(r'\<list-level\>(\d+)') self.__found_appt = 0 self.__line_num = 0 + def __in_pard_func(self, line): """ Required: @@ -117,6 +121,7 @@ class MakeLists: if self.__token_info == 'mi<mk<pard-end__': self.__state = 'after_pard' self.__write_obj.write(line) + def __after_pard_func(self, line): """ Required: @@ -176,6 +181,7 @@ class MakeLists: self.__write_obj.write(line) else: self.__list_chunk += line + def __list_after_par_def_func(self, line, id): """ Required: @@ -209,6 +215,7 @@ class MakeLists: self.__write_obj.write(self.__list_chunk) self.__write_start_item() self.__list_chunk = '' + def __close_lists(self): """ Required: @@ -239,6 +246,7 @@ class MakeLists: num_levels_closed += 1 self.__all_lists = self.__all_lists[num_levels_closed:] self.__all_lists.reverse() + def __write_end_list(self): """ Required: @@ -250,6 +258,7 @@ class MakeLists: """ self.__write_obj.write('mi<tg<close_____<list\n') self.__write_obj.write('mi<mk<list_close\n') + def __write_start_list(self, id): """ Required: @@ -325,6 +334,7 @@ class MakeLists: 'mi<mk<liststart_\n' ) self.__write_start_item() + def __get_index_of_list(self, id): """ Requires: @@ -359,14 +369,17 @@ class MakeLists: # if self.__run_level > 3: # msg = 'level is "%s"\n' % self.__run_level # self.__bug_handler + def __write_start_item(self): self.__write_obj.write('mi<mk<item_start\n') self.__write_obj.write('mi<tg<open______<item\n') self.__write_obj.write('mi<mk<itemstart_\n') + def __write_end_item(self): self.__write_obj.write('mi<tg<item_end__\n') self.__write_obj.write('mi<tg<close_____<item\n') self.__write_obj.write('mi<tg<item__end_\n') + def __default_func(self, line): """ Required: @@ -390,6 +403,7 @@ class MakeLists: self.__level = search_obj_lv.group(1) self.__write_start_list(num) self.__write_obj.write(line) + def __is_a_heading(self): if self.__style_name in self.__headings: if self.__headings_to_sections: @@ -401,17 +415,21 @@ class MakeLists: return 0 else: return 0 + def __get_indent(self, line): if self.__token_info == 'mi<mk<left_inden': self.__left_indent = float(line[17:-1]) + def __get_list_type(self, line): if self.__token_info == 'mi<mk<list-type_': # <ordered self.__list_type = line[17:-1] if self.__list_type == 'item': self.__list_type = "unordered" + def __get_style_name(self, line): if self.__token_info == 'mi<mk<style-name': self.__style_name = line[17:-1] + def make_lists(self): """ Required: diff --git a/src/calibre/ebooks/rtf2xml/old_rtf.py b/src/calibre/ebooks/rtf2xml/old_rtf.py index b42c3004af..8c34ef9226 100755 --- a/src/calibre/ebooks/rtf2xml/old_rtf.py +++ b/src/calibre/ebooks/rtf2xml/old_rtf.py @@ -12,6 +12,7 @@ ######################################################################### import sys + class OldRtf: """ Check to see if the RTF is an older version @@ -19,6 +20,7 @@ class OldRtf: If allowable control word/properties happen in text without being enclosed in brackets the file will be considered old rtf """ + def __init__(self, in_file, bug_handler, run_level, diff --git a/src/calibre/ebooks/rtf2xml/options_trem.py b/src/calibre/ebooks/rtf2xml/options_trem.py index fea4754d0f..2aacbc26a9 100755 --- a/src/calibre/ebooks/rtf2xml/options_trem.py +++ b/src/calibre/ebooks/rtf2xml/options_trem.py @@ -1,4 +1,6 @@ import sys + + class ParseOptions: """ Requires: @@ -28,6 +30,7 @@ class ParseOptions: The result will be: {indents:None, output:'/home/paul/file'}, ['/home/paul/input'] """ + def __init__(self, system_string, options_dict): self.__system_string = system_string[1:] long_list = self.__make_long_list_func(options_dict) @@ -41,6 +44,7 @@ class ParseOptions: self.__opt_with_args = self.__make_options_with_arg_list(options_dict) # # print self.__opt_with_args self.__options_okay = 1 + def __make_long_list_func(self, options_dict): """ Required: @@ -54,6 +58,7 @@ class ParseOptions: key = '--' + key legal_list.append(key) return legal_list + def __make_short_list_func(self, options_dict): """ Required: @@ -70,6 +75,7 @@ class ParseOptions: except IndexError: pass return legal_list + def __make_short_long_dict_func(self, options_dict): """ Required: @@ -90,6 +96,7 @@ class ParseOptions: except IndexError: pass return short_long_dict + def __make_options_with_arg_list(self, options_dict): """ Required: @@ -107,6 +114,7 @@ class ParseOptions: except IndexError: pass return opt_with_arg + def __sub_short_with_long(self): """ Required: @@ -123,6 +131,7 @@ class ParseOptions: item = self.__short_long_dict[item] new_string.append(item) return new_string + def __pair_arg_with_option(self): """ Required: @@ -173,6 +182,7 @@ class ParseOptions: else: new_system_string.append(arg) return new_system_string + def __get_just_options(self): """ Requires: @@ -205,6 +215,7 @@ class ParseOptions: sys.stderr.write('%s is an argument in an option list\n' % item) self.__options_okay = 0 return just_options, arguments + def __is_legal_option_func(self): """ Requires: @@ -227,6 +238,7 @@ class ParseOptions: sys.stderr.write('The following options are not permitted:\n') for not_legal in illegal_options: sys.stderr.write('%s\n' % not_legal) + def __make_options_dict(self, options): options_dict = {} for item in options: @@ -241,6 +253,7 @@ class ParseOptions: option = option[1:] options_dict[option] = arg return options_dict + def parse_options(self): self.__system_string = self.__sub_short_with_long() # # print 'subbed list is %s' % self.__system_string diff --git a/src/calibre/ebooks/rtf2xml/output.py b/src/calibre/ebooks/rtf2xml/output.py index 4bd2167902..8b1e475012 100755 --- a/src/calibre/ebooks/rtf2xml/output.py +++ b/src/calibre/ebooks/rtf2xml/output.py @@ -13,10 +13,12 @@ import sys, os # , codecs + class Output: """ Output file """ + def __init__(self, file, orig_file, diff --git a/src/calibre/ebooks/rtf2xml/override_table.py b/src/calibre/ebooks/rtf2xml/override_table.py index 48a146729a..5296f6e8cc 100755 --- a/src/calibre/ebooks/rtf2xml/override_table.py +++ b/src/calibre/ebooks/rtf2xml/override_table.py @@ -10,6 +10,8 @@ # # # # ######################################################################### + + class OverrideTable: """ Parse a line of text to make the override table. Return a string @@ -18,6 +20,7 @@ class OverrideTable: dictionary that is first passed to this module. This module modifies the dictionary, assigning lists numbers to each list. """ + def __init__( self, list_of_lists, @@ -26,6 +29,7 @@ class OverrideTable: self.__list_of_lists = list_of_lists self.__initiate_values() self.__run_level = run_level + def __initiate_values(self): self.__override_table_final = '' self.__state = 'default' @@ -39,6 +43,7 @@ class OverrideTable: 'cw<ls<lis-tbl-id' : 'list-table-id', 'cw<ls<list-id___' : 'list-id', } + def __override_func(self, line): """ Requires: @@ -59,6 +64,7 @@ class OverrideTable: if att: value = line[20:] self.__override_list[-1][att] = value + def __parse_override_dict(self): """ Requires: @@ -96,6 +102,7 @@ class OverrideTable: self.__list_of_lists[counter][0]['list-id'].append(list_id) break counter += 1 + def __parse_lines(self, line): """ Requires: @@ -123,6 +130,7 @@ class OverrideTable: action(line) self.__write_final_string() # self.__add_to_final_line() + def __default_func(self, line): """ Requires: @@ -134,6 +142,7 @@ class OverrideTable: """ if self.__token_info == 'ob<nu<open-brack': self.__state = 'unsure_ob' + def __after_bracket_func(self, line): """ Requires: @@ -157,6 +166,7 @@ class OverrideTable: msg = 'No matching token after open bracket\n' msg += 'token is "%s\n"' % (line) raise self.__bug_handler, msg + def __write_final_string(self): """ Requires: @@ -183,6 +193,7 @@ class OverrideTable: self.__override_table_final += \ 'mi<mk<overri-end\n' + 'mi<tg<close_____<override-table\n' self.__override_table_final += 'mi<mk<overribend_\n' + def parse_override_table(self, line): """ Requires: diff --git a/src/calibre/ebooks/rtf2xml/paragraph_def.py b/src/calibre/ebooks/rtf2xml/paragraph_def.py index 34dbd9302a..683be7d18c 100755 --- a/src/calibre/ebooks/rtf2xml/paragraph_def.py +++ b/src/calibre/ebooks/rtf2xml/paragraph_def.py @@ -14,6 +14,7 @@ import sys, os from calibre.ebooks.rtf2xml import copy, border_parse from calibre.ptempfile import better_mktemp + class ParagraphDef: """ ================= @@ -48,6 +49,7 @@ be closed: 'mi<mk<para-start' changes state to in_paragraphs if another paragraph_def is found, the state changes to collect_tokens. """ + def __init__(self, in_file, bug_handler, @@ -71,6 +73,7 @@ if another paragraph_def is found, the state changes to collect_tokens. self.__copy = copy self.__run_level = run_level self.__write_to = better_mktemp() + def __initiate_values(self): """ Initiate all values. @@ -300,6 +303,7 @@ if another paragraph_def is found, the state changes to collect_tokens. 'mi<mk<fldbk-end_' : self.__stop_block_func, 'mi<mk<lst-txbeg_' : self.__stop_block_func, } + def __before_1st_para_def_func(self, line): """ Required: @@ -314,11 +318,13 @@ if another paragraph_def is found, the state changes to collect_tokens. self.__found_para_def_func() else: self.__write_obj.write(line) + def __found_para_def_func(self): self.__state = 'collect_tokens' # not exactly right--have to reset the dictionary--give it default # values self.__reset_dict() + def __collect_tokens_func(self, line): """ Required: @@ -350,12 +356,14 @@ if another paragraph_def is found, the state changes to collect_tokens. token = self.__token_dict.get(line[6:16]) if token: self.__att_val_dict[token] = line[20:-1] + def __tab_stop_func(self, line): """ """ self.__att_val_dict['tabs'] += '%s:' % self.__tab_type self.__att_val_dict['tabs'] += '%s;' % line[20:-1] self.__tab_type = 'left' + def __tab_type_func(self, line): """ """ @@ -366,6 +374,7 @@ if another paragraph_def is found, the state changes to collect_tokens. if self.__run_level > 3: msg = 'no entry for %s\n' % self.__token_info raise self.__bug_handler, msg + def __tab_leader_func(self, line): """ """ @@ -376,12 +385,14 @@ if another paragraph_def is found, the state changes to collect_tokens. if self.__run_level > 3: msg = 'no entry for %s\n' % self.__token_info raise self.__bug_handler, msg + def __tab_bar_func(self, line): """ """ # self.__att_val_dict['tabs-bar'] += '%s:' % line[20:-1] self.__att_val_dict['tabs'] += 'bar:%s;' % (line[20:-1]) self.__tab_type = 'left' + def __parse_border(self, line): """ Requires: @@ -394,6 +405,7 @@ if another paragraph_def is found, the state changes to collect_tokens. """ border_dict = self.__border_obj.parse_border(line) self.__att_val_dict.update(border_dict) + def __para_def_in_para_def_func(self, line): """ Requires: @@ -407,6 +419,7 @@ if another paragraph_def is found, the state changes to collect_tokens. # Change this self.__state = 'collect_tokens' self.__reset_dict() + def __end_para_def_func(self, line): """ Requires: @@ -422,6 +435,7 @@ if another paragraph_def is found, the state changes to collect_tokens. self.__write_para_def_beg() self.__write_obj.write(line) self.__state = 'in_paragraphs' + def __start_para_after_def_func(self, line): """ Requires: @@ -438,6 +452,7 @@ if another paragraph_def is found, the state changes to collect_tokens. self.__write_para_def_beg() self.__write_obj.write(line) self.__state = 'in_paragraphs' + def __after_para_def_func(self, line): """ Requires: @@ -455,6 +470,7 @@ if another paragraph_def is found, the state changes to collect_tokens. action(line) else: self.__write_obj.write(line) + def __in_paragraphs_func(self, line): """ Requires: @@ -469,6 +485,7 @@ if another paragraph_def is found, the state changes to collect_tokens. action(line) else: self.__write_obj.write(line) + def __found_para_end_func(self,line): """ Requires: @@ -482,6 +499,7 @@ if another paragraph_def is found, the state changes to collect_tokens. """ self.__state = 'after_para_end' self.__write_obj.write(line) + def __after_para_end_func(self, line): """ Requires: @@ -505,6 +523,7 @@ if another paragraph_def is found, the state changes to collect_tokens. action = self.__after_para_end_dict.get(self.__token_info) if action: action(line) + def __continue_block_func(self, line): """ Requires: @@ -521,6 +540,7 @@ if another paragraph_def is found, the state changes to collect_tokens. self.__write_obj.write(self.__text_string) self.__text_string = '' # found a new paragraph definition after an end of a paragraph + def __new_para_def_func(self, line): """ Requires: @@ -536,6 +556,7 @@ if another paragraph_def is found, the state changes to collect_tokens. self.__write_para_def_end_func() self.__found_para_def_func() # after a paragraph and found reason to stop this block + def __stop_block_func(self, line): """ Requires: @@ -550,6 +571,7 @@ if another paragraph_def is found, the state changes to collect_tokens. """ self.__write_para_def_end_func() self.__state = 'after_para_def' + def __write_para_def_end_func(self): """ Requires: @@ -571,6 +593,7 @@ if another paragraph_def is found, the state changes to collect_tokens. self.__write_obj.write('mi<mk<font-end__\n') if 'caps' in keys: self.__write_obj.write('mi<mk<caps-end__\n') + def __get_num_of_style(self): """ Requires: @@ -602,6 +625,7 @@ if another paragraph_def is found, the state changes to collect_tokens. self.__att_val_dict['style-num'] = 's' + str(num) if new_style: self.__write_body_styles() + def __write_body_styles(self): style_string = '' style_string += 'mi<tg<empty-att_<paragraph-style-in-body' @@ -621,6 +645,7 @@ if another paragraph_def is found, the state changes to collect_tokens. style_string += ('<%s>%s' % (key, self.__att_val_dict[key])) style_string += '\n' self.__body_style_strings.append(style_string) + def __write_para_def_beg(self): """ Requires: @@ -679,10 +704,12 @@ if another paragraph_def is found, the state changes to collect_tokens. if 'caps' in keys: value = self.__att_val_dict['caps'] self.__write_obj.write('mi<mk<caps______<%s\n' % value) + def __empty_table_element_func(self, line): self.__write_obj.write('mi<mk<in-table__\n') self.__write_obj.write(line) self.__state = 'after_para_def' + def __reset_dict(self): """ Requires: @@ -703,6 +730,7 @@ if another paragraph_def is found, the state changes to collect_tokens. self.__att_val_dict['tabs-decimal'] = '' self.__att_val_dict['tabs-bar'] = '' self.__att_val_dict['tabs'] = '' + def make_paragraph_def(self): """ Requires: diff --git a/src/calibre/ebooks/rtf2xml/paragraphs.py b/src/calibre/ebooks/rtf2xml/paragraphs.py index 1ec11f1f16..be4e2d8669 100755 --- a/src/calibre/ebooks/rtf2xml/paragraphs.py +++ b/src/calibre/ebooks/rtf2xml/paragraphs.py @@ -15,6 +15,7 @@ import sys, os from calibre.ebooks.rtf2xml import copy from calibre.ptempfile import better_mktemp + class Paragraphs: """ ================= @@ -38,6 +39,7 @@ class Paragraphs: a paragraph definition; the end of a field-block; and the beginning of a section. (How about the end of a section or the end of a field-block?) """ + def __init__(self, in_file, bug_handler, diff --git a/src/calibre/ebooks/rtf2xml/preamble_div.py b/src/calibre/ebooks/rtf2xml/preamble_div.py index a4aa08efca..94d17e14a6 100755 --- a/src/calibre/ebooks/rtf2xml/preamble_div.py +++ b/src/calibre/ebooks/rtf2xml/preamble_div.py @@ -14,10 +14,12 @@ import sys, os from calibre.ebooks.rtf2xml import copy, override_table, list_table from calibre.ptempfile import better_mktemp + class PreambleDiv: """ Break the preamble into divisions. """ + def __init__(self, in_file, bug_handler, copy=None, @@ -40,6 +42,7 @@ class PreambleDiv: self.__no_namespace = no_namespace self.__write_to = better_mktemp() self.__run_level = run_level + def __initiate_values(self): """ Set values, including those for the dictionary. @@ -121,6 +124,7 @@ class PreambleDiv: run_level=self.__run_level, bug_handler=self.__bug_handler, ) + def __ignore_func(self, line): """ Ignore all lines, until the bracket is found that marks the end of @@ -128,8 +132,10 @@ class PreambleDiv: """ if self.__ignore_num == self.__cb_count: self.__state = self.__previous_state + def __found_rtf_head_func(self, line): self.__state = 'rtf_header' + def __rtf_head_func(self, line): if self.__ob_count == '0002': self.__rtf_final = ( @@ -151,6 +157,7 @@ class PreambleDiv: self.__write_obj.write(line) else: self.__rtf_final = self.__rtf_final + line + def __make_default_font_table(self): """ If not font table is fount, need to write one out. @@ -163,6 +170,7 @@ class PreambleDiv: self.__font_table_final += 'mi<mk<fontit-end\n' self.__font_table_final += 'mi<mk<fonttb-end\n' self.__font_table_final += 'mi<tg<close_____<font-table\n' + def __make_default_color_table(self): """ If no color table is found, write a string for a default one @@ -174,6 +182,7 @@ class PreambleDiv: self.__color_table_final += 'cw<ci<blue______<en<00\n' self.__color_table_final += 'mi<mk<clrtbl-end\n' self.__color_table_final += 'mi<tg<close_____<color-table\n' + def __make_default_style_table(self): """ If not font table is found, make a string for a default one @@ -200,6 +209,7 @@ mi<mk<stylei-end mi<mk<styles-end mi<tg<close_____<style-table """ + def __found_font_table_func(self, line): if self.__found_font_table: self.__state = 'ignore' @@ -209,6 +219,7 @@ mi<tg<close_____<style-table self.__close_group_count = self.__ob_count self.__cb_count = 0 self.__found_font_table = 1 + def __font_table_func(self, line): """ Keep adding to the self.__individual_font string until end of group @@ -251,6 +262,7 @@ cw<ci<font-style<nu<0 self.__individual_font = 1 self.__font_table_final += 'mi<mk<fontit-beg\n' self.__font_table_final += line + def __old_font_func(self, line): """ Required: @@ -262,6 +274,7 @@ cw<ci<font-style<nu<0 \f3\fswiss\fcharset77 Helvetica-Oblique;\f4\fnil\fcharset77 Geneva;} Note how each font is not divided by a bracket """ + def __found_color_table_func(self, line): """ all functions that start with __found operate the same. They set the @@ -272,6 +285,7 @@ cw<ci<font-style<nu<0 self.__color_table_final = '' self.__close_group_count = self.__ob_count self.__cb_count = 0 + def __color_table_func(self, line): if int(self.__cb_count) == int(self.__close_group_count): self.__state = 'preamble' @@ -281,11 +295,13 @@ cw<ci<font-style<nu<0 'mi<mk<clrtbl-end\n' + 'mi<tg<close_____<color-table\n' else: self.__color_table_final += line + def __found_style_sheet_func(self, line): self.__state = 'style_sheet' self.__style_sheet_final = '' self.__close_group_count = self.__ob_count self.__cb_count = 0 + def __style_sheet_func(self, line): """ Same logic as the font_table_func. @@ -306,11 +322,13 @@ cw<ci<font-style<nu<0 'mi<mk<stylei-end\n' else: self.__style_sheet_final += line + def __found_list_table_func(self, line): self.__state = 'list_table' self.__list_table_final = '' self.__close_group_count = self.__ob_count self.__cb_count = 0 + def __list_table_func(self, line): if self.__cb_count == self.__close_group_count: self.__state = 'preamble' @@ -323,6 +341,7 @@ cw<ci<font-style<nu<0 else: self.__list_table_final += line pass + def __found_override_table_func(self, line): self.__override_table_obj = override_table.OverrideTable( run_level=self.__run_level, @@ -333,6 +352,7 @@ cw<ci<font-style<nu<0 self.__close_group_count = self.__ob_count self.__cb_count = 0 # cw<it<lovr-table + def __override_table_func(self, line): if self.__cb_count == self.__close_group_count: self.__state = 'preamble' @@ -342,11 +362,13 @@ cw<ci<font-style<nu<0 pass else: self.__override_table_final += line + def __found_revision_table_func(self, line): self.__state = 'revision_table' self.__revision_table_final = '' self.__close_group_count = self.__ob_count self.__cb_count = 0 + def __revision_table_func(self, line): if int(self.__cb_count) == int(self.__close_group_count): self.__state = 'preamble' @@ -356,11 +378,13 @@ cw<ci<font-style<nu<0 'mi<mk<revtbl-end\n' + 'mi<tg<close_____<revision-table\n' else: self.__revision_table_final += line + def __found_doc_info_func(self, line): self.__state = 'doc_info' self.__doc_info_table_final = '' self.__close_group_count = self.__ob_count self.__cb_count = 0 + def __doc_info_func(self, line): if self.__cb_count == self.__close_group_count: self.__state = 'preamble' @@ -378,6 +402,7 @@ cw<ci<font-style<nu<0 'mi<mk<docinf-end\n' else: self.__doc_info_table_final += line + def __margin_func(self, line): """ Handles lines that describe page info. Add the apporpriate info in the @@ -390,6 +415,7 @@ cw<ci<font-style<nu<0 else: self.__page[changed] = line[20:-1] # cw<pa<margin-lef<nu<1728 + def __print_page_info(self): self.__write_obj.write('mi<tg<empty-att_<page-definition') for key in self.__page.keys(): @@ -398,6 +424,7 @@ cw<ci<font-style<nu<0 ) self.__write_obj.write('\n') # mi<tg<open-att__<footn + def __print_sec_info(self): """ Check if there is any section info. If so, print it out. @@ -416,6 +443,7 @@ cw<ci<font-style<nu<0 '<%s>%s' % (key, self.__section[key]) ) self.__write_obj.write('\n') + def __section_func(self, line): """ Add info pertaining to section to the self.__section dictionary, to be @@ -426,11 +454,14 @@ cw<ci<font-style<nu<0 sys.stderr.write('woops!\n') else: self.__section[info] = 'true' + def __body_func(self, line): self.__write_obj.write(line) + def __default_func(self, line): # either in preamble or in body pass + def __para_def_func(self, line): # if self.__ob_group == 1 # this tells dept of group @@ -438,6 +469,7 @@ cw<ci<font-style<nu<0 self.__state = 'body' self.__write_preamble() self.__write_obj.write(line) + def __text_func(self, line): """ If the cb_count is less than 1, you have hit the body @@ -456,6 +488,7 @@ cw<ci<font-style<nu<0 self.__state = 'body' self.__write_preamble() self.__write_obj.write(line) + def __row_def_func(self, line): # if self.__ob_group == 1 # this tells dept of group @@ -463,6 +496,7 @@ cw<ci<font-style<nu<0 self.__state = 'body' self.__write_preamble() self.__write_obj.write(line) + def __new_section_func(self, line): """ This is new. The start of a section marks the end of the preamble @@ -475,6 +509,7 @@ cw<ci<font-style<nu<0 sys.stderr.write('method is __new_section_func\n') sys.stderr.write('bracket count should be 2?\n') self.__write_obj.write(line) + def __write_preamble(self): """ Write all the strings, which represent all the data in the preamble. @@ -514,6 +549,7 @@ cw<ci<font-style<nu<0 # self.__write_obj.write('mi<mk<head_foot_<\n') # self.__write_obj.write('mi<tg<close_____<headers-and-footers\n') self.__write_obj.write('mi<mk<body-open_\n') + def __preamble_func(self, line): """ Check if the token info belongs to the dictionary. If so, take the @@ -522,6 +558,7 @@ cw<ci<font-style<nu<0 action = self.__state_dict.get(self.__token_info) if action: action(line) + def make_preamble_divisions(self): self.__initiate_values() read_obj = open(self.__file, 'r') diff --git a/src/calibre/ebooks/rtf2xml/preamble_rest.py b/src/calibre/ebooks/rtf2xml/preamble_rest.py index d3b02bd7ac..8ea2d0bbd6 100755 --- a/src/calibre/ebooks/rtf2xml/preamble_rest.py +++ b/src/calibre/ebooks/rtf2xml/preamble_rest.py @@ -14,6 +14,7 @@ import sys,os from calibre.ebooks.rtf2xml import copy + class Preamble: """ Fix the reamaing parts of the preamble. This module does very little. It @@ -21,6 +22,7 @@ class Preamble: future, when I understand how to interpret the revision table and list table, I will make these methods more functional. """ + def __init__(self, file, bug_handler, platform, diff --git a/src/calibre/ebooks/rtf2xml/process_tokens.py b/src/calibre/ebooks/rtf2xml/process_tokens.py index 36d624eb6d..fc78304097 100755 --- a/src/calibre/ebooks/rtf2xml/process_tokens.py +++ b/src/calibre/ebooks/rtf2xml/process_tokens.py @@ -15,12 +15,14 @@ import os, re from calibre.ebooks.rtf2xml import copy, check_brackets from calibre.ptempfile import better_mktemp + class ProcessTokens: """ Process each token on a line and add information that will be useful for later processing. Information will be put on one line, delimited by "<" for main fields, and ">" for sub fields """ + def __init__(self, in_file, exception_handler, @@ -605,6 +607,7 @@ class ProcessTokens: 'picprop' : ('un', 'unknown___', self.default_func), 'blipuid' : ('un', 'unknown___', self.default_func), """ + def __ms_hex_func(self, pre, token, num): num = num[1:] # chop off leading 0, which I added num = num.upper() # the mappings store hex in caps diff --git a/src/calibre/ebooks/rtf2xml/replace_illegals.py b/src/calibre/ebooks/rtf2xml/replace_illegals.py index 3b008af8a4..fb424d25fe 100755 --- a/src/calibre/ebooks/rtf2xml/replace_illegals.py +++ b/src/calibre/ebooks/rtf2xml/replace_illegals.py @@ -16,10 +16,12 @@ from calibre.ebooks.rtf2xml import copy from calibre.utils.cleantext import clean_ascii_chars from calibre.ptempfile import better_mktemp + class ReplaceIllegals: """ reaplace illegal lower ascii characters """ + def __init__(self, in_file, copy=None, diff --git a/src/calibre/ebooks/rtf2xml/sections.py b/src/calibre/ebooks/rtf2xml/sections.py index 685eb3305d..be794ceed7 100755 --- a/src/calibre/ebooks/rtf2xml/sections.py +++ b/src/calibre/ebooks/rtf2xml/sections.py @@ -15,6 +15,7 @@ import sys, os from calibre.ebooks.rtf2xml import copy from calibre.ptempfile import better_mktemp + class Sections: """ ================= @@ -50,6 +51,7 @@ class Sections: CHANGE (2004-04-26) No longer write sections that occurr in field-blocks. Instead, ingore all section information in a field-block. """ + def __init__(self, in_file, bug_handler, @@ -70,6 +72,7 @@ class Sections: self.__copy = copy self.__run_level = run_level self.__write_to = better_mktemp() + def __initiate_values(self): """ Initiate all values. @@ -119,6 +122,7 @@ class Sections: # 'cw<sc<section___' : self.__found_section_in_field_func, # 'cw<sc<sect-defin' : self.__found_section_def_in_field_func, } + def __found_section_def_func(self, line): """ Required: @@ -132,6 +136,7 @@ class Sections: """ self.__state = 'section_def' self.__section_values.clear() + def __attribute_func(self, line, name): """ Required: @@ -149,6 +154,7 @@ class Sections: attribute = name value = line[20:-1] self.__section_values[attribute] = value + def __found_section_func(self, line): """ Requires: @@ -162,6 +168,7 @@ class Sections: self.__state = 'section' self.__write_obj.write(line) self.__section_num += 1 + def __found_section_def_bef_sec_func(self, line): """ Requires: @@ -175,6 +182,7 @@ class Sections: self.__section_num += 1 self.__found_section_def_func(line) self.__write_obj.write(line) + def __section_func(self, line): """ Requires: @@ -186,6 +194,7 @@ class Sections: if self.__token_info == 'cw<sc<sect-defin': self.__found_section_def_func(line) self.__write_obj.write(line) + def __section_def_func(self, line): """ Required: @@ -207,6 +216,7 @@ class Sections: self.__write_obj.write(line) else: self.__write_obj.write(line) + def __end_sec_def_func(self, line, name): """ Requires: @@ -223,6 +233,7 @@ class Sections: else: self.__state = 'sec_in_field' self.__write_section(line) + def __end_sec_premature_func(self, line, name): """ Requires: @@ -244,6 +255,7 @@ class Sections: self.__write_obj.write('cw<pf<par-def___<nu<true\n') self.__write_obj.write('ob<nu<open-brack<0000\n') self.__write_obj.write('cb<nu<clos-brack<0000\n') + def __write_section(self, line): """ Requires: @@ -278,6 +290,7 @@ class Sections: elif self.__run_level > 3: msg = 'missed a flag\n' raise self.__bug_handler, msg + def __handle_sec_def(self, my_string): """ Requires: @@ -290,6 +303,7 @@ class Sections: """ values_dict = self.__section_values self.__list_of_sec_values.append(values_dict) + def __body_func(self, line): """ Requires: @@ -305,6 +319,7 @@ class Sections: action(line) else: self.__write_obj.write(line) + def __before_body_func(self, line): """ Requires: @@ -317,6 +332,7 @@ class Sections: if self.__token_info == 'mi<mk<body-open_': self.__state = 'before_first_sec' self.__write_obj.write(line) + def __before_first_sec_func(self, line): """ Requires: @@ -357,6 +373,7 @@ class Sections: ) self.__found_first_sec = 1 self.__write_obj.write(line) + def __found_sec_in_field_func(self, line): """ Requires: @@ -371,6 +388,7 @@ class Sections: self.__state = 'sec_in_field' self.__sec_in_field_string = line self.__in_field = 1 + def __sec_in_field_func(self, line): """ Requires: @@ -390,6 +408,7 @@ class Sections: # change this 2004-04-26 # self.__sec_in_field_string += line self.__write_obj.write(line) + def __end_sec_in_field_func(self, line): """ Requires: @@ -415,6 +434,7 @@ class Sections: self.__in_field = 0 # this is changed too self.__write_obj.write(line) + def __print_field_sec_attributes(self): """ Requires: @@ -452,6 +472,7 @@ class Sections: self.__write_obj.write('<num-in-level>%s' % str(self.__section_num)) self.__write_obj.write('\n') # Look here + def __found_section_in_field_func(self, line): """ Requires: @@ -465,6 +486,7 @@ class Sections: self.__section_num += 1 self.__field_num.append(self.__section_num) self.__sec_in_field_string += line + def __found_section_def_in_field_func(self, line): """ Requires: @@ -477,6 +499,7 @@ class Sections: """ self.__state = 'section_def' self.__section_values.clear() + def make_sections(self): """ Requires: diff --git a/src/calibre/ebooks/rtf2xml/styles.py b/src/calibre/ebooks/rtf2xml/styles.py index f5ace5df5f..1b72a7feb5 100755 --- a/src/calibre/ebooks/rtf2xml/styles.py +++ b/src/calibre/ebooks/rtf2xml/styles.py @@ -14,10 +14,12 @@ import sys, os from calibre.ebooks.rtf2xml import copy, border_parse from calibre.ptempfile import better_mktemp + class Styles: """ Change lines with style numbers to actual style names. """ + def __init__(self, in_file, bug_handler, @@ -39,6 +41,7 @@ class Styles: self.__copy = copy self.__write_to = better_mktemp() self.__run_level = run_level + def __initiate_values(self): """ Initiate all values. @@ -258,6 +261,7 @@ class Styles: self.__tabs_list = self.__tabs_dict.keys() self.__tab_type = 'left' self.__leader_found = 0 + def __in_individual_style_func(self, line): """ Required: @@ -303,6 +307,7 @@ class Styles: self.__enter_dict_entry(att, value) elif line[0:2] == 'tx': self.__text_string += line[17:-1] + def __tab_stop_func(self, line): """ Requires: @@ -331,6 +336,7 @@ class Styles: self.__styles_dict['par'][self.__styles_num]['tabs'] += '%s;' % line[20:-1] self.__tab_type = 'left' self.__leader_found = 0 + def __tab_type_func(self, line): """ """ @@ -341,6 +347,7 @@ class Styles: if self.__run_level > 3: msg = 'no entry for %s\n' % self.__token_info raise self.__bug_handler, msg + def __tab_leader_func(self, line): """ Requires: @@ -365,6 +372,7 @@ class Styles: if self.__run_level > 3: msg = 'no entry for %s\n' % self.__token_info raise self.__bug_handler, msg + def __tab_bar_func(self, line): """ Requires: @@ -388,6 +396,7 @@ class Styles: self.__styles_dict['par'][self.__styles_num]['tabs']\ += '%s;' % line[20:-1] self.__tab_type = 'left' + def __enter_dict_entry(self, att, value): """ Required: @@ -404,6 +413,7 @@ class Styles: self.__styles_dict[self.__type_of_style][self.__styles_num][att] = value except KeyError: self.__add_dict_entry(att, value) + def __add_dict_entry(self, att, value): """ Required: @@ -433,6 +443,7 @@ class Styles: smallest_dict[att] = value type_dict[self.__styles_num] = smallest_dict self.__styles_dict[self.__type_of_style] = type_dict + def __para_style_func(self, line): """ Required: @@ -452,6 +463,7 @@ class Styles: self.__enter_dict_entry('tabs-decimal', '') self.__enter_dict_entry('tabs-bar', '') """ + def __char_style_func(self, line): """ Required: @@ -464,6 +476,7 @@ class Styles: """ self.__type_of_style = 'char' self.__styles_num = line[20:-1] + def __found_beg_ind_style_func(self, line): """ Required: @@ -476,6 +489,7 @@ class Styles: dictionary. """ self.__state = 'in_individual_style' + def __found_end_ind_style_func(self, line): name = self.__text_string[:-1] # get rid of semicolon # add 2005-04-29 @@ -483,6 +497,7 @@ class Styles: name = name.strip() self.__enter_dict_entry('name', name) self.__text_string = '' + def __found_end_styles_table_func(self, line): """ Required: @@ -497,6 +512,7 @@ class Styles: self.__state = 'after_styles_table' self.__fix_based_on() self.__print_style_table() + def __fix_based_on(self): """ Requires: @@ -536,6 +552,7 @@ class Styles: msg = 'There is no style with %s\n' % value raise self.__bug_handler, msg del self.__styles_dict[type][key][style] + def __print_style_table(self): """ Required: @@ -574,6 +591,7 @@ class Styles: self.__write_obj.write( 'mi<tg<close_____<%s-styles\n' % prefix ) + def __found_styles_table_func(self, line): """ Required: @@ -584,6 +602,7 @@ class Styles: Change the state to in the style table when the marker has been found. """ self.__state = 'in_styles_table' + def __before_styles_func(self, line): """ Required: @@ -600,6 +619,7 @@ class Styles: self.__write_obj.write(line) else: action(line) + def __in_styles_func(self, line): """ Required: @@ -615,6 +635,7 @@ class Styles: self.__write_obj.write(line) else: action(line) + def __para_style_in_body_func(self, line, type): """ Required: @@ -645,6 +666,7 @@ class Styles: self.__write_obj.write( 'cw<ss<%s_style<nu<not-defined\n' % prefix ) + def __after_styles_func(self, line): """ Required: @@ -661,6 +683,7 @@ class Styles: action(line, type) else: self.__write_obj.write(line) + def convert_styles(self): """ Requires: diff --git a/src/calibre/ebooks/rtf2xml/table.py b/src/calibre/ebooks/rtf2xml/table.py index ca73fd38f0..c440162bd6 100755 --- a/src/calibre/ebooks/rtf2xml/table.py +++ b/src/calibre/ebooks/rtf2xml/table.py @@ -40,6 +40,8 @@ States. 1. 'mi<mk<not-in-tbl', end table 2. 'cw<tb<cell______', end cell """ + + class Table: """ Make tables. @@ -47,6 +49,7 @@ class Table: Read one line at a time. The default state (self.__state) is 'not_in_table'. Look for either a 'cw<tb<in-table__', or a row definition. """ + def __init__(self, in_file, bug_handler, @@ -67,6 +70,7 @@ class Table: self.__copy = copy self.__run_level = run_level self.__write_to = better_mktemp() + def __initiate_values(self): """ Initiate all values. @@ -103,6 +107,7 @@ class Table: self.__row_dict = {} self.__cell_list = [] self.__cell_widths = [] + def __in_table_func(self, line): """ Requires: @@ -127,6 +132,7 @@ class Table: self.__start_row_func(line) self.__empty_cell(line) self.__write_obj.write(line) + def __not_in_table_func(self, line): """ Requires: @@ -143,6 +149,7 @@ class Table: if action: action(line) self.__write_obj.write(line) + def __close_table(self, line): """ Requires: @@ -162,6 +169,7 @@ class Table: self.__table_data[-1]['average-cells-per-row'] = average_cells_in_row average_cell_width = self.__mode(self.__cell_widths) self.__table_data[-1]['average-cell-width'] = average_cell_width + def __found_row_def_func(self, line): """ Requires: @@ -178,6 +186,7 @@ class Table: self.__cell_list = [] self.__cell_list.append({}) self.__cell_widths = [] + def __start_table_func(self, line): """ Requires: @@ -197,6 +206,7 @@ class Table: self.__list_of_cells_in_row = [] self.__write_obj.write('mi<mk<tabl-start\n') self.__state.append('in_table') + def __end_row_table_func(self, line): """ Requires: @@ -207,6 +217,7 @@ class Table: ? """ self.__close_table(self, line) + def __end_row_def_func(self, line): """ Requires: @@ -229,6 +240,7 @@ class Table: width_list = widths.split(',') num_cells = len(width_list) self.__row_dict['number-of-cells'] = num_cells + def __in_row_def_func(self, line): """ Requires: @@ -271,6 +283,7 @@ class Table: self.__write_obj.write(line) else: self.__write_obj.write(line) + def __handle_row_token(self, line): """ Requires: @@ -311,6 +324,7 @@ class Table: self.__row_dict['left-row-position'] = position elif self.__token_info == 'cw<tb<row-header': self.__row_dict['header'] = 'true' + def __start_cell_func(self, line): """ Required: @@ -341,6 +355,7 @@ class Table: self.__write_obj.write('mi<tg<open______<cell\n') self.__cells_in_table += 1 self.__cells_in_row += 1 + def __start_row_func(self, line): """ Required: @@ -359,6 +374,7 @@ class Table: self.__write_obj.write('\n') self.__cells_in_row = 0 self.__rows_in_table += 1 + def __found_cell_position(self, line): """ needs: @@ -389,6 +405,7 @@ class Table: self.__cell_list[-1]['width'] = width self.__cell_list.append({}) self.__cell_widths.append(width) + def __in_cell_func(self, line): """ Required: @@ -417,6 +434,7 @@ class Table: self.__end_cell_func(line) else: self.__write_obj.write(line) + def __end_cell_func(self, line): """ Requires: @@ -432,6 +450,7 @@ class Table: self.__write_obj.write('mi<mk<close_cell\n') self.__write_obj.write('mi<tg<close_____<cell\n') self.__write_obj.write('mi<mk<closecell_\n') + def __in_row_func(self, line): if self.__token_info == 'mi<mk<not-in-tbl' or\ self.__token_info == 'mi<mk<sect-start' or\ @@ -455,6 +474,7 @@ class Table: else: self.__write_obj.write(line) """ + def __end_row_func(self, line): """ """ @@ -467,6 +487,7 @@ class Table: if self.__cells_in_row > self.__max_number_cells_in_row: self.__max_number_cells_in_row = self.__cells_in_row self.__list_of_cells_in_row.append(self.__cells_in_row) + def __empty_cell(self, line): """ Required: @@ -488,6 +509,7 @@ class Table: self.__write_obj.write('mi<tg<empty_____<cell\n') self.__cells_in_table += 1 self.__cells_in_row += 1 + def __mode(self, the_list): """ Required: @@ -506,6 +528,7 @@ class Table: mode = item max = num_of_values return mode + def make_table(self): """ Requires: diff --git a/src/calibre/ebooks/rtf2xml/table_info.py b/src/calibre/ebooks/rtf2xml/table_info.py index cf2dfbab46..a7490a8d9e 100755 --- a/src/calibre/ebooks/rtf2xml/table_info.py +++ b/src/calibre/ebooks/rtf2xml/table_info.py @@ -17,11 +17,14 @@ from calibre.ptempfile import better_mktemp # note to self. This is the first module in which I use tempfile. A good idea? """ """ + + class TableInfo: """ Insert table data for tables. Logic: """ + def __init__(self, in_file, bug_handler, @@ -46,6 +49,7 @@ class TableInfo: self.__run_level = run_level self.__write_to = better_mktemp() # self.__write_to = 'table_info.data' + def insert_info(self): """ """ diff --git a/src/calibre/ebooks/rtf2xml/tokenize.py b/src/calibre/ebooks/rtf2xml/tokenize.py index 105297d79a..5771a5b480 100755 --- a/src/calibre/ebooks/rtf2xml/tokenize.py +++ b/src/calibre/ebooks/rtf2xml/tokenize.py @@ -16,8 +16,10 @@ from calibre.ebooks.rtf2xml import copy from calibre.utils.mreplace import MReplace from calibre.ptempfile import better_mktemp + class Tokenize: """Tokenize RTF into one line per field. Each line will contain information useful for the rest of the script""" + def __init__(self, in_file, bug_handler, diff --git a/src/calibre/ebooks/sgmllib.py b/src/calibre/ebooks/sgmllib.py index 676dab9e50..73338d023a 100644 --- a/src/calibre/ebooks/sgmllib.py +++ b/src/calibre/ebooks/sgmllib.py @@ -461,10 +461,13 @@ class SGMLParser(markupbase.ParserBase): # To be overridden -- handlers for unknown objects def unknown_starttag(self, tag, attrs): pass + def unknown_endtag(self, tag): pass + def unknown_charref(self, ref): pass + def unknown_entityref(self, ref): pass diff --git a/src/calibre/ebooks/snb/snbfile.py b/src/calibre/ebooks/snb/snbfile.py index cc30476d17..6189314350 100644 --- a/src/calibre/ebooks/snb/snbfile.py +++ b/src/calibre/ebooks/snb/snbfile.py @@ -8,17 +8,21 @@ import sys, struct, zlib, bz2, os from calibre import guess_type + class FileStream: def IsBinary(self): return self.attr & 0x41000000 != 0x41000000 + def compareFileStream(file1, file2): return cmp(file1.fileName, file2.fileName) + class BlockData: pass + class SNBFile: MAGIC = 'SNBP000B' @@ -300,6 +304,7 @@ class SNBFile: tempFile.write(f.fileBody) tempFile.close() + def usage(): print "This unit test is for INTERNAL usage only!" print "This unit test accept two parameters." @@ -307,6 +312,7 @@ def usage(): print "The input file will be extracted and write to dest file. " print "Meta data of the file will be shown during this process." + def main(): if len(sys.argv) != 3: usage() diff --git a/src/calibre/ebooks/snb/snbml.py b/src/calibre/ebooks/snb/snbml.py index a21940e7fe..89e112ac70 100644 --- a/src/calibre/ebooks/snb/snbml.py +++ b/src/calibre/ebooks/snb/snbml.py @@ -53,6 +53,7 @@ CALIBRE_SNB_IMG_TAG = "<$$calibre_snb_temp_img$$>" CALIBRE_SNB_BM_TAG = "<$$calibre_snb_bm_tag$$>" CALIBRE_SNB_PRE_TAG = "<$$calibre_snb_pre_tag$$>" + class SNBMLizer(object): curSubItem = "" diff --git a/src/calibre/ebooks/textile/functions.py b/src/calibre/ebooks/textile/functions.py index 92569dd266..d7c559f952 100755 --- a/src/calibre/ebooks/textile/functions.py +++ b/src/calibre/ebooks/textile/functions.py @@ -66,6 +66,7 @@ from urlparse import urlparse from calibre.utils.smartypants import smartyPants + def _normalize_newlines(string): out = re.sub(r'\r\n', '\n', string) out = re.sub(r'\n{3,}', '\n\n', out) @@ -73,6 +74,7 @@ def _normalize_newlines(string): out = re.sub(r'"$', '" ', out) return out + def getimagesize(url): """ Attempts to determine an image's width and height, and returns a string @@ -111,6 +113,7 @@ def getimagesize(url): except (IOError, ValueError): return None + class Textile(object): hlgn = r'(?:\<(?!>)|(?<!<)\>|\<\>|\=|[()]+(?! ))' vlgn = r'[\-^~]' @@ -1074,6 +1077,7 @@ def textile(text, head_offset=0, html_type='xhtml', encoding=None, output=None): return Textile().textile(text, head_offset=head_offset, html_type=html_type) + def textile_restricted(text, lite=True, noimage=True, html_type='xhtml'): """ Restricted version of Textile designed for weblog comments and other diff --git a/src/calibre/ebooks/textile/unsmarten.py b/src/calibre/ebooks/textile/unsmarten.py index fc7b3787a7..1762e91764 100644 --- a/src/calibre/ebooks/textile/unsmarten.py +++ b/src/calibre/ebooks/textile/unsmarten.py @@ -6,6 +6,7 @@ __docformat__ = 'restructuredtext en' import re + def unsmarten(txt): txt = re.sub(u'¢|¢|¢', r'{c\}', txt) # cent txt = re.sub(u'£|£|£', r'{L-}', txt) # pound diff --git a/src/calibre/ebooks/tweak.py b/src/calibre/ebooks/tweak.py index 27f1475da6..3a566ad18b 100644 --- a/src/calibre/ebooks/tweak.py +++ b/src/calibre/ebooks/tweak.py @@ -16,9 +16,11 @@ from calibre.libunzip import extract as zipextract from calibre.utils.zipfile import ZipFile, ZIP_DEFLATED, ZIP_STORED from calibre.utils.ipc.simple_worker import WorkerError + class Error(ValueError): pass + def ask_cli_question(msg): prints(msg, end=' [y/N]: ') sys.stdout.flush() @@ -40,6 +42,7 @@ def ask_cli_question(msg): print() return ans == b'y' + def mobi_exploder(path, tdir, question=lambda x:True): from calibre.ebooks.mobi.tweak import explode, BadFormat try: @@ -47,6 +50,7 @@ def mobi_exploder(path, tdir, question=lambda x:True): except BadFormat as e: raise Error(as_unicode(e)) + def zip_exploder(path, tdir, question=lambda x:True): zipextract(path, tdir) for f in walk(tdir): @@ -54,6 +58,7 @@ def zip_exploder(path, tdir, question=lambda x:True): return f raise Error('Invalid book: Could not find .opf') + def zip_rebuilder(tdir, path): with ZipFile(path, 'w', compression=ZIP_DEFLATED) as zf: # Write mimetype @@ -70,6 +75,7 @@ def zip_rebuilder(tdir, path): zfn = unicodedata.normalize('NFC', os.path.relpath(absfn, tdir).replace(os.sep, '/')) zf.write(absfn, zfn) + def docx_exploder(path, tdir, question=lambda x:True): zipextract(path, tdir) from calibre.ebooks.docx.dump import pretty_all_xml_in_dir @@ -79,6 +85,7 @@ def docx_exploder(path, tdir, question=lambda x:True): return f raise Error('Invalid book: Could not find document.xml') + def get_tools(fmt): fmt = fmt.lower() @@ -94,6 +101,7 @@ def get_tools(fmt): return ans + def tweak(ebook_file): ''' Command line interface to the Tweak Book tool ''' fmt = ebook_file.rpartition('.')[-1].lower() diff --git a/src/calibre/ebooks/txt/markdownml.py b/src/calibre/ebooks/txt/markdownml.py index 7e46e29276..3c35564e8c 100644 --- a/src/calibre/ebooks/txt/markdownml.py +++ b/src/calibre/ebooks/txt/markdownml.py @@ -16,6 +16,7 @@ from calibre.ebooks.htmlz.oeb2html import OEB2HTML from calibre.ebooks.oeb.base import XHTML, XHTML_NS, barename, namespace, rewrite_links from calibre.ebooks.oeb.stylizer import Stylizer + class MarkdownMLizer(OEB2HTML): def extract_content(self, oeb_book, opts): diff --git a/src/calibre/ebooks/txt/newlines.py b/src/calibre/ebooks/txt/newlines.py index d7e97654b4..0b34116c64 100644 --- a/src/calibre/ebooks/txt/newlines.py +++ b/src/calibre/ebooks/txt/newlines.py @@ -6,6 +6,7 @@ __docformat__ = 'restructuredtext en' import os + class TxtNewlines(object): NEWLINE_TYPES = { @@ -18,6 +19,7 @@ class TxtNewlines(object): def __init__(self, newline_type): self.newline = self.NEWLINE_TYPES.get(newline_type.lower(), os.linesep) + def specified_newlines(newline, text): # Convert all newlines to \n text = text.replace('\r\n', '\n') diff --git a/src/calibre/ebooks/txt/processor.py b/src/calibre/ebooks/txt/processor.py index a397dfaa03..3f328ee787 100644 --- a/src/calibre/ebooks/txt/processor.py +++ b/src/calibre/ebooks/txt/processor.py @@ -18,6 +18,7 @@ from calibre.utils.cleantext import clean_ascii_chars HTML_TEMPLATE = u'<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"/><title>%s \n%s\n' + def clean_txt(txt): ''' Run transformations on the text to put it into @@ -45,6 +46,7 @@ def clean_txt(txt): return txt + def split_txt(txt, epub_split_size_kb=0): ''' Ensure there are split points for converting @@ -71,6 +73,7 @@ def split_txt(txt, epub_split_size_kb=0): return txt + def convert_basic(txt, title='', epub_split_size_kb=0): ''' Converts plain text to html by putting all paragraphs in @@ -95,6 +98,7 @@ def convert_basic(txt, title='', epub_split_size_kb=0): return HTML_TEMPLATE % (title, u'\n'.join(lines)) + def convert_markdown(txt, title='', extensions=('footnotes', 'tables', 'toc')): from calibre.ebooks.conversion.plugins.txt_input import MD_EXTENSIONS from calibre.ebooks.markdown import Markdown @@ -102,24 +106,29 @@ def convert_markdown(txt, title='', extensions=('footnotes', 'tables', 'toc')): md = Markdown(extensions=extensions) return HTML_TEMPLATE % (title, md.convert(txt)) + def convert_textile(txt, title=''): from calibre.ebooks.textile import textile html = textile(txt, encoding='utf-8') return HTML_TEMPLATE % (title, html) + def normalize_line_endings(txt): txt = txt.replace('\r\n', '\n') txt = txt.replace('\r', '\n') return txt + def separate_paragraphs_single_line(txt): txt = txt.replace('\n', '\n\n') return txt + def separate_paragraphs_print_formatted(txt): txt = re.sub(u'(?miu)^(?P\t+|[ ]{2,})(?=.)', lambda mo: '\n%s' % mo.group('indent'), txt) return txt + def separate_hard_scene_breaks(txt): def sep_break(line): if len(line.strip()) > 0: @@ -129,10 +138,12 @@ def separate_hard_scene_breaks(txt): txt = re.sub(u'(?miu)^[ \t-=~\/_]+$', lambda mo: sep_break(mo.group()), txt) return txt + def block_to_single_line(txt): txt = re.sub(r'(?<=.)\n(?=.)', ' ', txt) return txt + def preserve_spaces(txt): ''' Replaces spaces multiple spaces with   entities. @@ -141,6 +152,7 @@ def preserve_spaces(txt): txt = txt.replace('\t', '    ') return txt + def remove_indents(txt): ''' Remove whitespace at the beginning of each line. @@ -148,6 +160,7 @@ def remove_indents(txt): txt = re.sub('(?miu)^\s+', '', txt) return txt + def opf_writer(path, opf_name, manifest, spine, mi): opf = OPFCreator(path, mi) opf.create_manifest(manifest) @@ -155,6 +168,7 @@ def opf_writer(path, opf_name, manifest, spine, mi): with open(os.path.join(path, opf_name), 'wb') as opffile: opf.render(opffile) + def split_string_separator(txt, size): ''' Splits the text by putting \n\n at the point size. @@ -165,6 +179,7 @@ def split_string_separator(txt, size): xrange(0, len(txt), size)]) return txt + def detect_paragraph_type(txt): ''' Tries to determine the paragraph type of the document. diff --git a/src/calibre/ebooks/txt/textileml.py b/src/calibre/ebooks/txt/textileml.py index 5efaa99e24..1786c0a4ff 100644 --- a/src/calibre/ebooks/txt/textileml.py +++ b/src/calibre/ebooks/txt/textileml.py @@ -17,6 +17,7 @@ from calibre.ebooks.oeb.stylizer import Stylizer from calibre.ebooks import unit_convert from calibre.ebooks.textile.unsmarten import unsmarten + class TextileMLizer(OEB2HTML): MAX_EM = 10 diff --git a/src/calibre/ebooks/txt/txtml.py b/src/calibre/ebooks/txt/txtml.py index 9d19679962..26da604093 100644 --- a/src/calibre/ebooks/txt/txtml.py +++ b/src/calibre/ebooks/txt/txtml.py @@ -44,6 +44,7 @@ SPACE_TAGS = [ 'br', ] + class TXTMLizer(object): def __init__(self, log): diff --git a/src/calibre/ebooks/unihandecode/__init__.py b/src/calibre/ebooks/unihandecode/__init__.py index 2ce9dab886..dc7b0dad63 100644 --- a/src/calibre/ebooks/unihandecode/__init__.py +++ b/src/calibre/ebooks/unihandecode/__init__.py @@ -19,6 +19,7 @@ Tranliterate the string from unicode characters to ASCII in Chinese and others. ''' import unicodedata + class Unihandecoder(object): preferred_encoding = None decoder = None diff --git a/src/calibre/ebooks/unihandecode/jadecoder.py b/src/calibre/ebooks/unihandecode/jadecoder.py index 93b28da550..b49c1144c0 100644 --- a/src/calibre/ebooks/unihandecode/jadecoder.py +++ b/src/calibre/ebooks/unihandecode/jadecoder.py @@ -23,6 +23,7 @@ from calibre.ebooks.unihandecode.unicodepoints import CODEPOINTS from calibre.ebooks.unihandecode.jacodepoints import CODEPOINTS as JACODES from calibre.ebooks.unihandecode.pykakasi.kakasi import kakasi + class Jadecoder(Unidecoder): kakasi = None codepoints = {} diff --git a/src/calibre/ebooks/unihandecode/krdecoder.py b/src/calibre/ebooks/unihandecode/krdecoder.py index 5c1befae7e..af5b3b39e8 100644 --- a/src/calibre/ebooks/unihandecode/krdecoder.py +++ b/src/calibre/ebooks/unihandecode/krdecoder.py @@ -14,6 +14,7 @@ from calibre.ebooks.unihandecode.unidecoder import Unidecoder from calibre.ebooks.unihandecode.krcodepoints import CODEPOINTS as HANCODES from calibre.ebooks.unihandecode.unicodepoints import CODEPOINTS + class Krdecoder(Unidecoder): codepoints = {} diff --git a/src/calibre/ebooks/unihandecode/pykakasi/h2a.py b/src/calibre/ebooks/unihandecode/pykakasi/h2a.py index 9c2d37b2f9..ad42edba39 100644 --- a/src/calibre/ebooks/unihandecode/pykakasi/h2a.py +++ b/src/calibre/ebooks/unihandecode/pykakasi/h2a.py @@ -21,6 +21,7 @@ # * # */ + class H2a (object): H2a_table = { diff --git a/src/calibre/ebooks/unihandecode/pykakasi/j2h.py b/src/calibre/ebooks/unihandecode/pykakasi/j2h.py index 78eadef779..d3f23f31a0 100644 --- a/src/calibre/ebooks/unihandecode/pykakasi/j2h.py +++ b/src/calibre/ebooks/unihandecode/pykakasi/j2h.py @@ -24,6 +24,7 @@ from calibre.ebooks.unihandecode.pykakasi.jisyo import jisyo import re + class J2H (object): kanwa = None diff --git a/src/calibre/ebooks/unihandecode/pykakasi/jisyo.py b/src/calibre/ebooks/unihandecode/pykakasi/jisyo.py index 5623980d1f..1f7cea332b 100644 --- a/src/calibre/ebooks/unihandecode/pykakasi/jisyo.py +++ b/src/calibre/ebooks/unihandecode/pykakasi/jisyo.py @@ -5,6 +5,7 @@ import cPickle, marshal from zlib import decompress + class jisyo (object): kanwadict = None itaijidict = None diff --git a/src/calibre/ebooks/unihandecode/pykakasi/k2a.py b/src/calibre/ebooks/unihandecode/pykakasi/k2a.py index dade813fbe..63a564b9c8 100644 --- a/src/calibre/ebooks/unihandecode/pykakasi/k2a.py +++ b/src/calibre/ebooks/unihandecode/pykakasi/k2a.py @@ -23,6 +23,7 @@ from calibre.ebooks.unihandecode.pykakasi.jisyo import jisyo + class K2a (object): kanwa = None diff --git a/src/calibre/ebooks/unihandecode/pykakasi/kakasi.py b/src/calibre/ebooks/unihandecode/pykakasi/kakasi.py index ad8c99580c..39b84b545a 100644 --- a/src/calibre/ebooks/unihandecode/pykakasi/kakasi.py +++ b/src/calibre/ebooks/unihandecode/pykakasi/kakasi.py @@ -25,6 +25,7 @@ from calibre.ebooks.unihandecode.pykakasi.j2h import J2H from calibre.ebooks.unihandecode.pykakasi.h2a import H2a from calibre.ebooks.unihandecode.pykakasi.k2a import K2a + class kakasi(object): j2h = None diff --git a/src/calibre/ebooks/unihandecode/unidecoder.py b/src/calibre/ebooks/unihandecode/unidecoder.py index 3ef6fa15c1..e8a4bab22a 100644 --- a/src/calibre/ebooks/unihandecode/unidecoder.py +++ b/src/calibre/ebooks/unihandecode/unidecoder.py @@ -63,6 +63,7 @@ import re from calibre.ebooks.unihandecode.unicodepoints import CODEPOINTS from calibre.ebooks.unihandecode.zhcodepoints import CODEPOINTS as HANCODES + class Unidecoder(object): codepoints = {} diff --git a/src/calibre/ebooks/unihandecode/vndecoder.py b/src/calibre/ebooks/unihandecode/vndecoder.py index 7ad6439961..76d926d7b7 100644 --- a/src/calibre/ebooks/unihandecode/vndecoder.py +++ b/src/calibre/ebooks/unihandecode/vndecoder.py @@ -13,6 +13,7 @@ from calibre.ebooks.unihandecode.unidecoder import Unidecoder from calibre.ebooks.unihandecode.vncodepoints import CODEPOINTS as HANCODES from calibre.ebooks.unihandecode.unicodepoints import CODEPOINTS + class Vndecoder(Unidecoder): codepoints = {} diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 7842ad6d58..0e0e67c795 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -153,6 +153,7 @@ UNDEFINED_QDATETIME = QDateTime(UNDEFINED_DATE) ALL_COLUMNS = ['title', 'ondevice', 'authors', 'size', 'timestamp', 'rating', 'publisher', 'tags', 'series', 'pubdate'] + def _config(): # {{{ c = Config('gui', 'preferences for the calibre GUI') c.add_opt('send_to_storage_card_by_default', default=False, @@ -262,24 +263,30 @@ QSettings.setPath(QSettings.IniFormat, QSettings.UserScope, config_dir) QSettings.setPath(QSettings.IniFormat, QSettings.SystemScope, config_dir) QSettings.setDefaultFormat(QSettings.IniFormat) + def available_heights(): desktop = QCoreApplication.instance().desktop() return map(lambda x: x.height(), map(desktop.availableGeometry, range(desktop.screenCount()))) + def available_height(): desktop = QCoreApplication.instance().desktop() return desktop.availableGeometry().height() + def max_available_height(): return max(available_heights()) + def min_available_height(): return min(available_heights()) + def available_width(): desktop = QCoreApplication.instance().desktop() return desktop.availableGeometry().width() + def get_windows_color_depth(): import win32gui, win32con, win32print hwin = win32gui.GetDesktopWindow() @@ -288,12 +295,14 @@ def get_windows_color_depth(): win32gui.ReleaseDC(hwin, hwindc) return ans + def get_screen_dpi(): d = QApplication.desktop() return (d.logicalDpiX(), d.logicalDpiY()) _is_widescreen = None + def is_widescreen(): global _is_widescreen if _is_widescreen is None: @@ -303,6 +312,7 @@ def is_widescreen(): _is_widescreen = False return _is_widescreen + def extension(path): return os.path.splitext(path)[1][1:].lower() @@ -317,6 +327,7 @@ def warning_dialog(parent, title, msg, det_msg='', show=False, return d.exec_() return d + def error_dialog(parent, title, msg, det_msg='', show=False, show_copy_button=True): from calibre.gui2.dialogs.message_box import MessageBox @@ -327,6 +338,7 @@ def error_dialog(parent, title, msg, det_msg='', show=False, return d.exec_() return d + def question_dialog(parent, title, msg, det_msg='', show_copy_button=False, default_yes=True, # Skippable dialogs @@ -366,6 +378,7 @@ def question_dialog(parent, title, msg, det_msg='', show_copy_button=False, return ret + def info_dialog(parent, title, msg, det_msg='', show=False, show_copy_button=True): from calibre.gui2.dialogs.message_box import MessageBox @@ -376,12 +389,14 @@ def info_dialog(parent, title, msg, det_msg='', show=False, return d.exec_() return d + def show_restart_warning(msg, parent=None): d = warning_dialog(parent, _('Restart needed'), msg, show_copy_button=False) b = d.bb.addButton(_('Restart calibre now'), d.bb.AcceptRole) b.setIcon(QIcon(I('lt.png'))) d.do_restart = False + def rf(): d.do_restart = True b.clicked.connect(rf) @@ -416,6 +431,7 @@ class Dispatcher(QObject): def dispatch(self, args, kwargs): self.func(*args, **kwargs) + class FunctionDispatcher(QObject): ''' Convenience class to use Qt signals with arbitrary python functions. @@ -458,6 +474,7 @@ class FunctionDispatcher(QObject): res = None q.put(res) + class GetMetadata(QObject): ''' Convenience class to ensure that metadata readers are used only in the @@ -496,6 +513,7 @@ class GetMetadata(QObject): mi = MetaInformation('', [_('Unknown')]) self.metadata.emit(id, mi) + class FileIconProvider(QFileIconProvider): ICONS = EXT_MAP @@ -563,16 +581,20 @@ class FileIconProvider(QFileIconProvider): return QFileIconProvider.icon(self, arg) _file_icon_provider = None + + def initialize_file_icon_provider(): global _file_icon_provider if _file_icon_provider is None: _file_icon_provider = FileIconProvider() + def file_icon_provider(): global _file_icon_provider initialize_file_icon_provider() return _file_icon_provider + def select_initial_dir(q): while q: c = os.path.dirname(q) @@ -583,7 +605,9 @@ def select_initial_dir(q): q = c return expanduser(u'~') + class FileDialog(QObject): + def __init__(self, title=_('Choose Files'), filters=[], add_all_files_filter=True, @@ -763,6 +787,7 @@ else: return fd.get_files() return None + def choose_osx_app(window, name, title, default_dir='/Applications'): fd = FileDialog(title=title, parent=window, name=name, mode=QFileDialog.ExistingFile, default_dir=default_dir) @@ -771,6 +796,7 @@ def choose_osx_app(window, name, title, default_dir='/Applications'): if app: return app + def pixmap_to_data(pixmap, format='JPEG', quality=90): ''' Return the QPixmap pixmap as a string saved in the specified format. @@ -781,6 +807,7 @@ def pixmap_to_data(pixmap, format='JPEG', quality=90): pixmap.save(buf, format, quality=quality) return bytes(ba.data()) + def decouple(prefix): ' Ensure that config files used by utility code are not the same as those used by the main calibre GUI ' dynamic.decouple(prefix) @@ -789,13 +816,16 @@ def decouple(prefix): _gui_prefs = gprefs + def gui_prefs(): return _gui_prefs + def set_gui_prefs(prefs): global _gui_prefs _gui_prefs = prefs + class ResizableDialog(QDialog): # This class is present only for backwards compat with third party plugins @@ -811,6 +841,7 @@ class ResizableDialog(QDialog): nw = min(self.width(), nw) self.resize(nw, nh) + class Translator(QTranslator): ''' Translator to load translations for strings in Qt from the calibre @@ -833,6 +864,7 @@ qt_app = None builtin_fonts_loaded = False + def load_builtin_fonts(): global _rating_font, builtin_fonts_loaded # Load the builtin fonts and any fonts added to calibre by the user to @@ -854,11 +886,13 @@ def load_builtin_fonts(): if u'calibre Symbols' in fam: _rating_font = u'calibre Symbols' + def setup_gui_option_parser(parser): if islinux: parser.add_option('--detach', default=False, action='store_true', help=_('Detach from the controlling terminal, if any (linux only)')) + def show_temp_dir_error(err): import traceback extra = _('Click "Show details" for more information.') @@ -1065,9 +1099,11 @@ class Application(QApplication): @dynamic_property def current_custom_colors(self): from PyQt5.Qt import QColorDialog, QColor + def fget(self): return [col.getRgb() for col in (QColorDialog.customColor(i) for i in xrange(QColorDialog.customCount()))] + def fset(self, colors): num = min(len(colors), QColorDialog.customCount()) for i in xrange(num): @@ -1115,6 +1151,7 @@ class Application(QApplication): _store_app = None + @contextmanager def sanitize_env_vars(): '''Unset various environment variables that calibre uses. This @@ -1153,6 +1190,7 @@ def sanitize_env_vars(): del os.environ[var] SanitizeLibraryPath = sanitize_env_vars # For old plugins + def open_url(qurl): # Qt 5 requires QApplication to be constructed before trying to use # QDesktopServices::openUrl() @@ -1162,6 +1200,7 @@ def open_url(qurl): with sanitize_env_vars(): QDesktopServices.openUrl(qurl) + def get_current_db(): ''' This method will try to return the current database in use by the user as @@ -1175,6 +1214,7 @@ def get_current_db(): from calibre.library import db return db() + def open_local_file(path): if iswindows: with sanitize_env_vars(): @@ -1185,6 +1225,7 @@ def open_local_file(path): _ea_lock = Lock() + def ensure_app(headless=True): global _store_app with _ea_lock: @@ -1204,6 +1245,7 @@ def ensure_app(headless=True): # dont feel like going through all the code and making sure no # unhandled exceptions ever occur. All the actual GUI apps already # override sys.except_hook with a proper error handler. + def eh(t, v, tb): try: traceback.print_exception(t, v, tb, file=sys.stderr) @@ -1211,9 +1253,11 @@ def ensure_app(headless=True): pass sys.excepthook = eh + def app_is_headless(): return getattr(_store_app, 'headless', False) + def must_use_qt(headless=True): ''' This function should be called if you want to use Qt for some non-GUI task like rendering HTML/SVG or using a headless browser. It will raise a @@ -1227,6 +1271,7 @@ def must_use_qt(headless=True): if gui_thread is not QThread.currentThread(): raise RuntimeError('Cannot use Qt in non GUI thread') + def is_ok_to_use_qt(): try: must_use_qt() @@ -1234,15 +1279,19 @@ def is_ok_to_use_qt(): return False return True + def is_gui_thread(): global gui_thread return gui_thread is QThread.currentThread() _rating_font = 'Arial Unicode MS' if iswindows else 'sans-serif' + + def rating_font(): global _rating_font return _rating_font + def elided_text(text, font=None, width=300, pos='middle'): ''' Return a version of text that is no wider than width pixels when rendered, replacing characters from the left, middle or right (as per pos) @@ -1262,6 +1311,7 @@ def elided_text(text, font=None, width=300, pos='middle'): text = chomp(text) return unicode(text) + def find_forms(srcdir): base = os.path.join(srcdir, 'calibre', 'gui2') forms = [] @@ -1272,9 +1322,11 @@ def find_forms(srcdir): return forms + def form_to_compiled_form(form): return form.rpartition('.')[0]+'_ui.py' + def build_forms(srcdir, info=None, summary=False, check_for_migration=False): import re, cStringIO from PyQt5.uic import compileUi @@ -1283,6 +1335,7 @@ def build_forms(srcdir, info=None, summary=False, check_for_migration=False): from calibre import prints info = prints pat = re.compile(r'''(['"]):/images/([^'"]+)\1''') + def sub(match): ans = 'I(%s%s%s)'%(match.group(1), match.group(2), match.group(1)) return ans @@ -1321,6 +1374,7 @@ _df = os.environ.get('CALIBRE_DEVELOP_FROM', None) if _df and os.path.exists(_df): build_forms(_df, check_for_migration=True) + def event_type_name(ev_or_etype): from PyQt5.QtCore import QEvent etype = ev_or_etype.type() if isinstance(ev_or_etype, QEvent) else ev_or_etype diff --git a/src/calibre/gui2/actions/__init__.py b/src/calibre/gui2/actions/__init__.py index f472ebebf2..66d5607137 100644 --- a/src/calibre/gui2/actions/__init__.py +++ b/src/calibre/gui2/actions/__init__.py @@ -16,9 +16,11 @@ from calibre.constants import isosx from calibre.gui2 import Dispatcher from calibre.gui2.keyboard import NameConflict + def menu_action_unique_name(plugin, unique_name): return u'%s : menu action : %s'%(plugin.unique_name, unique_name) + class InterfaceAction(QObject): ''' diff --git a/src/calibre/gui2/actions/add.py b/src/calibre/gui2/actions/add.py index b800ab5b41..c0980b5d3a 100644 --- a/src/calibre/gui2/actions/add.py +++ b/src/calibre/gui2/actions/add.py @@ -26,6 +26,7 @@ from calibre.gui2 import question_dialog from calibre.ebooks.metadata import MetaInformation from calibre.ptempfile import PersistentTemporaryFile + def get_filters(): return [ (_('Books'), BOOK_EXTENSIONS), @@ -502,6 +503,7 @@ class AddAction(InterfaceAction): return paths = [p for p in view.model().paths(rows) if p is not None] ve = self.gui.device_manager.device.VIRTUAL_BOOK_EXTENSIONS + def ext(x): ans = os.path.splitext(x)[1] ans = ans[1:] if len(ans) > 1 else ans diff --git a/src/calibre/gui2/actions/add_to_library.py b/src/calibre/gui2/actions/add_to_library.py index e1cdf8fba5..e91d1e131f 100644 --- a/src/calibre/gui2/actions/add_to_library.py +++ b/src/calibre/gui2/actions/add_to_library.py @@ -7,6 +7,7 @@ __docformat__ = 'restructuredtext en' from calibre.gui2.actions import InterfaceAction + class AddToLibraryAction(InterfaceAction): name = 'Add To Library' diff --git a/src/calibre/gui2/actions/annotate.py b/src/calibre/gui2/actions/annotate.py index fed1719b75..6f1163b4e6 100644 --- a/src/calibre/gui2/actions/annotate.py +++ b/src/calibre/gui2/actions/annotate.py @@ -13,6 +13,7 @@ from calibre.gui2.actions import InterfaceAction from calibre.devices.usbms.device import Device from calibre.gui2.dialogs.progress import ProgressDialog + class Updater(QThread): # {{{ update_progress = pyqtSignal(int) @@ -57,6 +58,7 @@ class Updater(QThread): # {{{ # }}} + class FetchAnnotationsAction(InterfaceAction): name = 'Fetch Annotations' diff --git a/src/calibre/gui2/actions/catalog.py b/src/calibre/gui2/actions/catalog.py index f7ad8f7af0..5dd7ae47a0 100644 --- a/src/calibre/gui2/actions/catalog.py +++ b/src/calibre/gui2/actions/catalog.py @@ -15,6 +15,7 @@ from calibre.utils.config import dynamic from calibre.gui2.actions import InterfaceAction from calibre import sanitize_file_name_unicode + class GenerateCatalogAction(InterfaceAction): name = 'Generate Catalog' diff --git a/src/calibre/gui2/actions/choose_library.py b/src/calibre/gui2/actions/choose_library.py index 4d141f130f..3937975e76 100644 --- a/src/calibre/gui2/actions/choose_library.py +++ b/src/calibre/gui2/actions/choose_library.py @@ -20,10 +20,12 @@ from calibre.gui2 import (gprefs, warning_dialog, Dispatcher, error_dialog, question_dialog, info_dialog, open_local_file, choose_dir) from calibre.gui2.actions import InterfaceAction + def db_class(): from calibre.db.legacy import LibraryDatabase return LibraryDatabase + class LibraryUsageStats(object): # {{{ def __init__(self): @@ -95,6 +97,7 @@ class LibraryUsageStats(object): # {{{ self.write_stats() # }}} + class MovedDialog(QDialog): # {{{ def __init__(self, stats, location, parent=None): @@ -151,6 +154,7 @@ class MovedDialog(QDialog): # {{{ QDialog.accept(self) # }}} + class BackupStatus(QDialog): # {{{ def __init__(self, gui): diff --git a/src/calibre/gui2/actions/convert.py b/src/calibre/gui2/actions/convert.py index f831be5185..fede66a70a 100644 --- a/src/calibre/gui2/actions/convert.py +++ b/src/calibre/gui2/actions/convert.py @@ -16,6 +16,7 @@ from calibre.utils.config import prefs, tweaks from calibre.gui2.actions import InterfaceAction from calibre.customize.ui import plugin_for_input_format + class ConvertAction(InterfaceAction): name = 'Convert Books' diff --git a/src/calibre/gui2/actions/copy_to_library.py b/src/calibre/gui2/actions/copy_to_library.py index a6abf908bf..8a97104397 100644 --- a/src/calibre/gui2/actions/copy_to_library.py +++ b/src/calibre/gui2/actions/copy_to_library.py @@ -27,6 +27,7 @@ from calibre.utils.config import prefs, tweaks from calibre.utils.date import now from calibre.utils.icu import sort_key + def ask_about_cc_mismatch(gui, db, newdb, missing_cols, incompatible_cols): # {{{ source_metadata = db.field_metadata.custom_field_metadata(include_composites=True) ndbname = os.path.basename(newdb.library_path) @@ -92,6 +93,7 @@ def ask_about_cc_mismatch(gui, db, newdb, missing_cols, incompatible_cols): # { return False # }}} + class Worker(Thread): # {{{ def __init__(self, ids, db, loc, progress, done, delete_after, add_duplicates): @@ -292,6 +294,7 @@ class ChooseLibrary(QDialog): # {{{ return (unicode(self.le.text()), self.delete_after_copy) # }}} + class DuplicatesQuestion(QDialog): # {{{ def __init__(self, parent, duplicates, loc): @@ -348,6 +351,7 @@ class DuplicatesQuestion(QDialog): # {{{ # checked for compatibility libraries_with_checked_columns = defaultdict(set) + class CopyToLibraryAction(InterfaceAction): name = 'Copy To Library' diff --git a/src/calibre/gui2/actions/delete.py b/src/calibre/gui2/actions/delete.py index 3510e98077..eb2965e4f1 100644 --- a/src/calibre/gui2/actions/delete.py +++ b/src/calibre/gui2/actions/delete.py @@ -21,6 +21,7 @@ from calibre.utils.recycle_bin import can_recycle single_shot = partial(QTimer.singleShot, 10) + class MultiDeleter(QObject): # {{{ def __init__(self, gui, ids, callback): @@ -82,6 +83,7 @@ class MultiDeleter(QObject): # {{{ ' for details.'), det_msg='\n\n'.join(msg), show=True) # }}} + class DeleteAction(InterfaceAction): name = 'Remove Books' diff --git a/src/calibre/gui2/actions/device.py b/src/calibre/gui2/actions/device.py index f22d491928..1eb13c2c21 100644 --- a/src/calibre/gui2/actions/device.py +++ b/src/calibre/gui2/actions/device.py @@ -17,6 +17,7 @@ from calibre.gui2.dialogs.smartdevice import SmartdeviceDialog from calibre.gui2 import info_dialog, question_dialog from calibre.library.server import server_config as content_server_config + class ShareConnMenu(QMenu): # {{{ connect_to_folder = pyqtSignal() diff --git a/src/calibre/gui2/actions/edit_collections.py b/src/calibre/gui2/actions/edit_collections.py index ee53cf318b..fbeac1878b 100644 --- a/src/calibre/gui2/actions/edit_collections.py +++ b/src/calibre/gui2/actions/edit_collections.py @@ -8,6 +8,7 @@ __docformat__ = 'restructuredtext en' from calibre.gui2 import error_dialog from calibre.gui2.actions import InterfaceAction + class EditCollectionsAction(InterfaceAction): name = 'Edit Collections' diff --git a/src/calibre/gui2/actions/embed.py b/src/calibre/gui2/actions/embed.py index e3faa9733c..f65043c991 100644 --- a/src/calibre/gui2/actions/embed.py +++ b/src/calibre/gui2/actions/embed.py @@ -14,6 +14,7 @@ from calibre import force_unicode from calibre.gui2 import gprefs from calibre.gui2.actions import InterfaceAction + class EmbedAction(InterfaceAction): name = 'Embed Metadata' @@ -120,6 +121,7 @@ class EmbedAction(InterfaceAction): pd.setValue(i) db = self.gui.current_db.new_api book_id = book_ids[i] + def report_error(mi, fmt, tb): mi.book_id = book_id errors.append((mi, fmt, tb)) diff --git a/src/calibre/gui2/actions/fetch_news.py b/src/calibre/gui2/actions/fetch_news.py index 889cb24676..eef41d33d5 100644 --- a/src/calibre/gui2/actions/fetch_news.py +++ b/src/calibre/gui2/actions/fetch_news.py @@ -14,6 +14,7 @@ from calibre.gui2 import Dispatcher from calibre.gui2.tools import fetch_scheduled_recipe from calibre.gui2.actions import InterfaceAction + class FetchNewsAction(InterfaceAction): name = 'Fetch News' diff --git a/src/calibre/gui2/actions/help.py b/src/calibre/gui2/actions/help.py index 0b6f2a66e9..b51d1ee606 100644 --- a/src/calibre/gui2/actions/help.py +++ b/src/calibre/gui2/actions/help.py @@ -11,6 +11,7 @@ from calibre.gui2 import open_url from calibre.gui2.actions import InterfaceAction from calibre.utils.localization import localize_user_manual_link + class HelpAction(InterfaceAction): name = 'Help' diff --git a/src/calibre/gui2/actions/mark_books.py b/src/calibre/gui2/actions/mark_books.py index 1fcd7e4bf6..90946d6203 100644 --- a/src/calibre/gui2/actions/mark_books.py +++ b/src/calibre/gui2/actions/mark_books.py @@ -13,6 +13,7 @@ from PyQt5.Qt import QTimer, QApplication, Qt from calibre.gui2 import error_dialog from calibre.gui2.actions import InterfaceAction + class MarkBooksAction(InterfaceAction): name = 'Mark Books' diff --git a/src/calibre/gui2/actions/match_books.py b/src/calibre/gui2/actions/match_books.py index d7e5bfc88f..7b9d62b15d 100644 --- a/src/calibre/gui2/actions/match_books.py +++ b/src/calibre/gui2/actions/match_books.py @@ -11,6 +11,7 @@ from calibre.gui2 import error_dialog from calibre.gui2.actions import InterfaceAction from calibre.gui2.dialogs.match_books import MatchBooks + class MatchBookAction(InterfaceAction): name = 'Match Books' diff --git a/src/calibre/gui2/actions/next_match.py b/src/calibre/gui2/actions/next_match.py index 4581093707..8aa904cbbf 100644 --- a/src/calibre/gui2/actions/next_match.py +++ b/src/calibre/gui2/actions/next_match.py @@ -7,6 +7,7 @@ __docformat__ = 'restructuredtext en' from calibre.gui2.actions import InterfaceAction + class NextMatchAction(InterfaceAction): name = 'Move to next highlighted book' action_spec = (_('Move to next match'), 'arrow-down.png', diff --git a/src/calibre/gui2/actions/open.py b/src/calibre/gui2/actions/open.py index 2f21dd3236..1aa274b9cb 100644 --- a/src/calibre/gui2/actions/open.py +++ b/src/calibre/gui2/actions/open.py @@ -8,6 +8,7 @@ __docformat__ = 'restructuredtext en' from calibre.gui2.actions import InterfaceAction + class OpenFolderAction(InterfaceAction): name = 'Open Folder' diff --git a/src/calibre/gui2/actions/plugin_updates.py b/src/calibre/gui2/actions/plugin_updates.py index 83ab5a3e15..37a04724a3 100644 --- a/src/calibre/gui2/actions/plugin_updates.py +++ b/src/calibre/gui2/actions/plugin_updates.py @@ -12,6 +12,7 @@ from calibre.gui2.actions import InterfaceAction from calibre.gui2.dialogs.plugin_updater import (PluginUpdaterDialog, FILTER_ALL, FILTER_UPDATE_AVAILABLE) + class PluginUpdaterAction(InterfaceAction): name = 'Plugin Updater' diff --git a/src/calibre/gui2/actions/polish.py b/src/calibre/gui2/actions/polish.py index f7245819fc..0b92edd184 100644 --- a/src/calibre/gui2/actions/polish.py +++ b/src/calibre/gui2/actions/polish.py @@ -24,6 +24,7 @@ from calibre.gui2.dialogs.progress import ProgressDialog from calibre.ptempfile import PersistentTemporaryDirectory from calibre.utils.config_base import tweaks + class Polish(QDialog): # {{{ def __init__(self, db, book_id_map, parent=None): @@ -296,6 +297,7 @@ class Polish(QDialog): # {{{ self.jobs.append((desc, data, book_id, base, is_orig)) # }}} + class Report(QDialog): # {{{ def __init__(self, parent): @@ -389,6 +391,7 @@ class Report(QDialog): # {{{ super(Report, self).reject() # }}} + class PolishAction(InterfaceAction): name = 'Polish Books' diff --git a/src/calibre/gui2/actions/preferences.py b/src/calibre/gui2/actions/preferences.py index 65edb4dbc1..15c3280e44 100644 --- a/src/calibre/gui2/actions/preferences.py +++ b/src/calibre/gui2/actions/preferences.py @@ -14,6 +14,7 @@ from calibre.gui2.preferences.main import Preferences from calibre.gui2 import error_dialog, show_restart_warning from calibre.constants import DEBUG, isosx + class PreferencesAction(InterfaceAction): name = 'Preferences' diff --git a/src/calibre/gui2/actions/random.py b/src/calibre/gui2/actions/random.py index 87c029fd2a..3e49a0a7a5 100644 --- a/src/calibre/gui2/actions/random.py +++ b/src/calibre/gui2/actions/random.py @@ -11,6 +11,7 @@ import random from calibre.gui2.actions import InterfaceAction + class PickRandomAction(InterfaceAction): name = 'Pick Random Book' diff --git a/src/calibre/gui2/actions/restart.py b/src/calibre/gui2/actions/restart.py index 635a7af8a1..904cad8e4c 100644 --- a/src/calibre/gui2/actions/restart.py +++ b/src/calibre/gui2/actions/restart.py @@ -8,6 +8,7 @@ __docformat__ = 'restructuredtext en' from calibre.gui2.actions import InterfaceAction + class RestartAction(InterfaceAction): name = 'Restart' diff --git a/src/calibre/gui2/actions/save_to_disk.py b/src/calibre/gui2/actions/save_to_disk.py index 45c93de635..bdca138d66 100644 --- a/src/calibre/gui2/actions/save_to_disk.py +++ b/src/calibre/gui2/actions/save_to_disk.py @@ -14,6 +14,7 @@ from calibre.utils.config import prefs from calibre.gui2 import error_dialog, Dispatcher, choose_dir from calibre.gui2.actions import InterfaceAction + class SaveToDiskAction(InterfaceAction): name = "Save To Disk" diff --git a/src/calibre/gui2/actions/show_book_details.py b/src/calibre/gui2/actions/show_book_details.py index 457ff54fd2..1dbac2f315 100644 --- a/src/calibre/gui2/actions/show_book_details.py +++ b/src/calibre/gui2/actions/show_book_details.py @@ -11,6 +11,7 @@ from calibre.gui2.actions import InterfaceAction from calibre.gui2.dialogs.book_info import BookInfo from calibre.gui2 import error_dialog + class ShowBookDetailsAction(InterfaceAction): name = 'Show Book Details' diff --git a/src/calibre/gui2/actions/show_quickview.py b/src/calibre/gui2/actions/show_quickview.py index c09eb7a92d..1f5c725bcc 100644 --- a/src/calibre/gui2/actions/show_quickview.py +++ b/src/calibre/gui2/actions/show_quickview.py @@ -12,6 +12,7 @@ from calibre.gui2.actions import InterfaceAction from calibre.gui2.dialogs.quickview import Quickview from calibre.gui2 import error_dialog + class ShowQuickviewAction(InterfaceAction): name = 'Show Quickview' diff --git a/src/calibre/gui2/actions/show_template_tester.py b/src/calibre/gui2/actions/show_template_tester.py index 2989bc5552..7f62c291fe 100644 --- a/src/calibre/gui2/actions/show_template_tester.py +++ b/src/calibre/gui2/actions/show_template_tester.py @@ -10,6 +10,7 @@ from calibre.gui2.actions import InterfaceAction from calibre.gui2.dialogs.template_dialog import TemplateDialog from calibre.gui2 import error_dialog + class ShowTemplateTesterAction(InterfaceAction): name = 'Template tester' diff --git a/src/calibre/gui2/actions/similar_books.py b/src/calibre/gui2/actions/similar_books.py index 3edb37fd29..acfae1c31e 100644 --- a/src/calibre/gui2/actions/similar_books.py +++ b/src/calibre/gui2/actions/similar_books.py @@ -11,6 +11,7 @@ from PyQt5.Qt import QToolButton from calibre.gui2.actions import InterfaceAction + class SimilarBooksAction(InterfaceAction): name = 'Similar Books' diff --git a/src/calibre/gui2/actions/sort.py b/src/calibre/gui2/actions/sort.py index d147429e0d..fc2467cd13 100644 --- a/src/calibre/gui2/actions/sort.py +++ b/src/calibre/gui2/actions/sort.py @@ -11,6 +11,7 @@ from PyQt5.Qt import QToolButton, QAction, pyqtSignal, QIcon from calibre.gui2.actions import InterfaceAction from calibre.utils.icu import sort_key + class SortAction(QAction): sort_requested = pyqtSignal(object, object) @@ -23,6 +24,7 @@ class SortAction(QAction): def __call__(self): self.sort_requested.emit(self.key, self.ascending) + class SortByAction(InterfaceAction): name = 'Sort By' diff --git a/src/calibre/gui2/actions/store.py b/src/calibre/gui2/actions/store.py index 2184754b3b..2a55ca65ff 100644 --- a/src/calibre/gui2/actions/store.py +++ b/src/calibre/gui2/actions/store.py @@ -14,6 +14,7 @@ from calibre.gui2 import error_dialog from calibre.gui2.actions import InterfaceAction from calibre.gui2.dialogs.confirm_delete import confirm + class StoreAction(InterfaceAction): name = 'Store' diff --git a/src/calibre/gui2/actions/tag_mapper.py b/src/calibre/gui2/actions/tag_mapper.py index a0e5e5891a..af0f3f310e 100644 --- a/src/calibre/gui2/actions/tag_mapper.py +++ b/src/calibre/gui2/actions/tag_mapper.py @@ -9,6 +9,7 @@ from future_builtins import map from calibre.gui2 import gprefs from calibre.gui2.actions import InterfaceAction + class TagMapAction(InterfaceAction): name = 'Tag Mapper' diff --git a/src/calibre/gui2/actions/toc_edit.py b/src/calibre/gui2/actions/toc_edit.py index 831902d6ce..d9870c1724 100644 --- a/src/calibre/gui2/actions/toc_edit.py +++ b/src/calibre/gui2/actions/toc_edit.py @@ -17,6 +17,7 @@ from calibre.gui2.actions import InterfaceAction SUPPORTED = {'EPUB', 'AZW3'} + class ChooseFormat(QDialog): # {{{ def __init__(self, formats, parent=None): @@ -55,6 +56,7 @@ class ChooseFormat(QDialog): # {{{ for b in self.buttons: if b.isChecked(): yield unicode(b.text())[1:] + def fset(self, formats): formats = {x.upper() for x in formats} for b in self.buttons: @@ -63,6 +65,7 @@ class ChooseFormat(QDialog): # {{{ # }}} + class ToCEditAction(InterfaceAction): name = 'Edit ToC' diff --git a/src/calibre/gui2/actions/tweak_epub.py b/src/calibre/gui2/actions/tweak_epub.py index f991710e5c..99c5cc316a 100755 --- a/src/calibre/gui2/actions/tweak_epub.py +++ b/src/calibre/gui2/actions/tweak_epub.py @@ -13,6 +13,7 @@ from PyQt5.Qt import QTimer, QDialog, QDialogButtonBox, QCheckBox, QVBoxLayout, from calibre.gui2 import error_dialog from calibre.gui2.actions import InterfaceAction + class Choose(QDialog): def __init__(self, fmts, parent=None): diff --git a/src/calibre/gui2/actions/unpack_book.py b/src/calibre/gui2/actions/unpack_book.py index c72e93cfa6..3251980f71 100644 --- a/src/calibre/gui2/actions/unpack_book.py +++ b/src/calibre/gui2/actions/unpack_book.py @@ -18,6 +18,7 @@ from calibre.ptempfile import (PersistentTemporaryDirectory, PersistentTemporaryFile) from calibre.utils.config import prefs, tweaks + class UnpackBook(QDialog): def __init__(self, parent, book_id, fmts, db): @@ -282,6 +283,7 @@ class UnpackBook(QDialog): if b.isChecked(): return unicode(b.text()) + class UnpackBookAction(InterfaceAction): name = 'Unpack Book' diff --git a/src/calibre/gui2/actions/view.py b/src/calibre/gui2/actions/view.py index 8da26ab055..07c095bc8b 100644 --- a/src/calibre/gui2/actions/view.py +++ b/src/calibre/gui2/actions/view.py @@ -19,6 +19,7 @@ from calibre.utils.config import prefs, tweaks from calibre.ptempfile import PersistentTemporaryFile from calibre.gui2.actions import InterfaceAction + class HistoryAction(QAction): view_historical = pyqtSignal(object) diff --git a/src/calibre/gui2/add.py b/src/calibre/gui2/add.py index 0b6f02c4d0..5adb286046 100644 --- a/src/calibre/gui2/add.py +++ b/src/calibre/gui2/add.py @@ -30,6 +30,7 @@ from calibre.utils import join_with_timeout from calibre.utils.config import prefs from calibre.utils.ipc.pool import Pool, Failure + def validate_source(source, parent=None): # {{{ if isinstance(source, basestring): if not os.path.exists(source): @@ -54,6 +55,7 @@ def validate_source(source, parent=None): # {{{ return True # }}} + class Adder(QObject): do_one_signal = pyqtSignal() diff --git a/src/calibre/gui2/add_filters.py b/src/calibre/gui2/add_filters.py index a14c8c46d7..c16e2973d0 100644 --- a/src/calibre/gui2/add_filters.py +++ b/src/calibre/gui2/add_filters.py @@ -19,6 +19,7 @@ from calibre.utils.config import JSONConfig add_filters = JSONConfig('add-filter-rules') + class RuleEdit(RuleEditBase): ACTION_MAP = OrderedDict(( @@ -85,6 +86,7 @@ class RuleEdit(RuleEditBase): return False return ans + class RuleEditDialog(RuleEditDialogBase): PREFS_NAME = 'edit-add-filter-rule' @@ -101,6 +103,7 @@ class RuleItem(RuleItemBase): action=RuleEdit.ACTION_MAP[rule['action']], match_type=RuleEdit.MATCH_TYPE_MAP[rule['match_type']], query=query) return text + class Rules(RulesBase): RuleItemClass = RuleItem @@ -112,6 +115,7 @@ class Rules(RulesBase): ' "add" or an "ignore" rule matches. If no rules match, the file will be added only' ' if its file extension is of a known ebook type.') + class Tester(TesterBase): DIALOG_TITLE = _('Test filename filter rules') diff --git a/src/calibre/gui2/auto_add.py b/src/calibre/gui2/auto_add.py index ec5f95c7e4..eb23796214 100644 --- a/src/calibre/gui2/auto_add.py +++ b/src/calibre/gui2/auto_add.py @@ -22,6 +22,7 @@ from calibre.gui2.dialogs.duplicates import DuplicatesQuestion AUTO_ADDED = frozenset(BOOK_EXTENSIONS) - {'pdr', 'mbp', 'tan'} + class AllAllowed(object): def __init__(self): @@ -39,6 +40,7 @@ def allowed_formats(): allowed = AUTO_ADDED - frozenset(gprefs['blocked_auto_formats']) return allowed + class Worker(Thread): def __init__(self, path, callback): diff --git a/src/calibre/gui2/bars.py b/src/calibre/gui2/bars.py index 5a00ab79d6..6713c1ba41 100644 --- a/src/calibre/gui2/bars.py +++ b/src/calibre/gui2/bars.py @@ -15,6 +15,7 @@ from PyQt5.Qt import ( from calibre.constants import isosx from calibre.gui2 import gprefs, native_menubar_defaults, config + class RevealBar(QWidget): # {{{ def __init__(self, parent): @@ -58,6 +59,7 @@ class RevealBar(QWidget): # {{{ painter.fillRect(self.rect(), col) # }}} + class ToolBar(QToolBar): # {{{ def __init__(self, donate, location_manager, parent): @@ -237,6 +239,7 @@ class ToolBar(QToolBar): # {{{ # }}} + class MenuAction(QAction): # {{{ def __init__(self, clone, parent): @@ -465,6 +468,7 @@ else: # }}} + class BarsManager(QObject): def __init__(self, donate_button, location_manager, parent): diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py index 397dd8217b..ac7267525c 100644 --- a/src/calibre/gui2/book_details.py +++ b/src/calibre/gui2/book_details.py @@ -27,6 +27,8 @@ from calibre.utils.img import image_from_x, blend_image from calibre.utils.localization import is_rtl _css = None + + def css(): global _css if _css is None: @@ -89,6 +91,7 @@ def render_html(mi, css, vertical, widget, all_fields=False, render_data_func=No % (table, right_pane)) return ans + def get_field_list(fm, use_defaults=False): from calibre.gui2.ui import get_gui db = get_gui().current_db @@ -109,6 +112,7 @@ def get_field_list(fm, use_defaults=False): available = frozenset(fm.displayable_field_keys()) return [(f, d) for f, d in fieldlist if f in available] + def render_data(mi, use_roman_numbers=True, all_fields=False): field_list = get_field_list(getattr(mi, 'field_metadata', field_metadata)) field_list = [(x, all_fields or display) for x, display in field_list] @@ -117,6 +121,7 @@ def render_data(mi, use_roman_numbers=True, all_fields=False): # }}} + def details_context_menu_event(view, ev, book_info): # {{{ p = view.page() mf = p.mainFrame() @@ -210,6 +215,7 @@ def details_context_menu_event(view, ev, book_info): # {{{ menu.exec_(ev.globalPos()) # }}} + class CoverView(QWidget): # {{{ cover_changed = pyqtSignal(object, object) @@ -414,6 +420,7 @@ class CoverView(QWidget): # {{{ # Book Info {{{ + class BookInfo(QWebView): link_clicked = pyqtSignal(object) @@ -619,6 +626,7 @@ class DetailsLayout(QLayout): # {{{ # }}} + class BookDetails(QWidget): # {{{ show_book_info = pyqtSignal() diff --git a/src/calibre/gui2/catalog/catalog_bibtex.py b/src/calibre/gui2/catalog/catalog_bibtex.py index 578749d449..ff22a65e76 100644 --- a/src/calibre/gui2/catalog/catalog_bibtex.py +++ b/src/calibre/gui2/catalog/catalog_bibtex.py @@ -11,6 +11,7 @@ from calibre.gui2 import gprefs from calibre.gui2.catalog.catalog_bibtex_ui import Ui_Form from PyQt5.Qt import QWidget, QListWidgetItem + class PluginWidget(QWidget, Ui_Form): TITLE = _('BibTeX Options') diff --git a/src/calibre/gui2/catalog/catalog_csv_xml.py b/src/calibre/gui2/catalog/catalog_csv_xml.py index 3f0f85647e..42b15bba7e 100644 --- a/src/calibre/gui2/catalog/catalog_csv_xml.py +++ b/src/calibre/gui2/catalog/catalog_csv_xml.py @@ -10,6 +10,7 @@ from calibre.gui2 import gprefs from calibre.gui2.ui import get_gui from PyQt5.Qt import QWidget, QListWidgetItem, Qt, QVBoxLayout, QLabel, QListWidget + class PluginWidget(QWidget): TITLE = _('CSV/XML Options') diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.py b/src/calibre/gui2/catalog/catalog_epub_mobi.py index a046284e04..d89909275b 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.py +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.py @@ -22,6 +22,7 @@ from PyQt5.Qt import (Qt, QAbstractItemView, QCheckBox, QComboBox, QSize, QSizePolicy, QTableWidget, QTableWidgetItem, QTextEdit, QToolButton, QUrl, QVBoxLayout, QWidget) + class PluginWidget(QWidget,Ui_Form): TITLE = _('E-book options') @@ -817,6 +818,7 @@ class PluginWidget(QWidget,Ui_Form): ''' open_url(QUrl(localize_user_manual_link('https://manual.calibre-ebook.com/catalogs.html'))) + class CheckableTableWidgetItem(QTableWidgetItem): ''' @@ -846,12 +848,14 @@ class CheckableTableWidgetItem(QTableWidgetItem): else: return self.checkState() == Qt.Checked + class NoWheelComboBox(QComboBox): def wheelEvent(self, event): # Disable the mouse wheel on top of the combo box changing selection as plays havoc in a grid event.ignore() + class ComboBox(NoWheelComboBox): # Caller is responsible for providing the list in the preferred order @@ -869,6 +873,7 @@ class ComboBox(NoWheelComboBox): else: self.setCurrentIndex(0) + class GenericRulesTable(QTableWidget): ''' @@ -1164,6 +1169,7 @@ class GenericRulesTable(QTableWidget): print("%s:values_index_changed(): row %d " % (self.objectName(), row)) + class ExclusionRules(GenericRulesTable): COLUMNS = {'ENABLED':{'ordinal': 0, 'name': ''}, @@ -1255,6 +1261,7 @@ class ExclusionRules(GenericRulesTable): self.blockSignals(False) + class PrefixRules(GenericRulesTable): COLUMNS = {'ENABLED':{'ordinal': 0, 'name': ''}, diff --git a/src/calibre/gui2/comments_editor.py b/src/calibre/gui2/comments_editor.py index b1d9ab0220..db93eefa42 100644 --- a/src/calibre/gui2/comments_editor.py +++ b/src/calibre/gui2/comments_editor.py @@ -24,6 +24,7 @@ from calibre.utils.soupparser import fromstring from calibre.utils.config import tweaks from calibre.utils.imghdr import what + class PageAction(QAction): # {{{ def __init__(self, wac, icon, text, checkable, view): @@ -53,6 +54,7 @@ class PageAction(QAction): # {{{ # }}} + class BlockStyleAction(QAction): # {{{ def __init__(self, text, name, view): @@ -65,6 +67,7 @@ class BlockStyleAction(QAction): # {{{ # }}} + class EditorWidget(QWebView): # {{{ def __init__(self, parent=None): @@ -239,6 +242,7 @@ class EditorWidget(QWebView): # {{{ d.bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel) d.br = b = QPushButton(_('&Browse')) b.setIcon(QIcon(I('document_open.png'))) + def cf(): files = choose_files(d, 'select link file', _('Choose file'), select_only_single_file=True) if files: @@ -419,6 +423,7 @@ State_SingleQuote = 6 State_DoubleQuote = 7 State_AttributeValue = 8 + class Highlighter(QSyntaxHighlighter): def __init__(self, doc): @@ -618,6 +623,7 @@ class Highlighter(QSyntaxHighlighter): # }}} + class Editor(QWidget): # {{{ toolbar_prefs_name = None @@ -719,6 +725,7 @@ class Editor(QWidget): # {{{ def html(self): def fset(self, v): self.editor.html = v + def fget(self): self.tabs.setCurrentIndex(0) return self.editor.html @@ -740,6 +747,7 @@ class Editor(QWidget): # {{{ def tab(self): def fget(self): return 'code' if self.tabs.currentWidget() is self.code_edit else 'wyswyg' + def fset(self, val): self.tabs.setCurrentWidget(self.code_edit if val == 'code' else self.wyswyg) return property(fget=fget, fset=fset) @@ -770,6 +778,7 @@ class Editor(QWidget): # {{{ def toolbars_visible(self): def fget(self): return self.toolbar1.isVisible() or self.toolbar2.isVisible() or self.toolbar3.isVisible() + def fset(self, val): getattr(self, ('show' if val else 'hide') + '_toolbars')() return property(fget=fget, fset=fset) diff --git a/src/calibre/gui2/complete2.py b/src/calibre/gui2/complete2.py index fb52a3246b..db2858bc1e 100644 --- a/src/calibre/gui2/complete2.py +++ b/src/calibre/gui2/complete2.py @@ -20,9 +20,11 @@ from calibre.utils.icu import sort_key, primary_startswith, primary_contains from calibre.gui2.widgets import EnComboBox, LineEditECM from calibre.utils.config import tweaks + def containsq(x, prefix): return primary_contains(prefix, x) + class CompleteModel(QAbstractListModel): # {{{ def __init__(self, parent=None, sort_func=sort_key): @@ -73,6 +75,7 @@ class CompleteModel(QAbstractListModel): # {{{ return self.index(i) # }}} + class Completer(QListView): # {{{ item_selected = pyqtSignal(object) @@ -283,6 +286,7 @@ class Completer(QListView): # {{{ return False # }}} + class LineEdit(QLineEdit, LineEditECM): ''' A line edit that completes on multiple items separated by a @@ -330,6 +334,7 @@ class LineEdit(QLineEdit, LineEditECM): def all_items(self): def fget(self): return self.mcompleter.model().all_items + def fset(self, items): self.mcompleter.model().set_items(items) return property(fget=fget, fset=fset) @@ -338,6 +343,7 @@ class LineEdit(QLineEdit, LineEditECM): def disable_popup(self): def fget(self): return self.mcompleter.disable_popup + def fset(self, val): self.mcompleter.disable_popup = bool(val) return property(fget=fget, fset=fset) @@ -420,6 +426,7 @@ class LineEdit(QLineEdit, LineEditECM): self.setCursorPosition(len(before_text)) self.item_selected.emit(text) + class EditWithComplete(EnComboBox): item_selected = pyqtSignal(object) @@ -462,6 +469,7 @@ class EditWithComplete(EnComboBox): def all_items(self): def fget(self): return self.lineEdit().all_items + def fset(self, val): self.lineEdit().all_items = val return property(fget=fget, fset=fset) @@ -470,6 +478,7 @@ class EditWithComplete(EnComboBox): def disable_popup(self): def fget(self): return self.lineEdit().disable_popup + def fset(self, val): self.lineEdit().disable_popup = bool(val) return property(fget=fget, fset=fset) diff --git a/src/calibre/gui2/convert/__init__.py b/src/calibre/gui2/convert/__init__.py index 4557793c2f..ec534de059 100644 --- a/src/calibre/gui2/convert/__init__.py +++ b/src/calibre/gui2/convert/__init__.py @@ -20,6 +20,7 @@ from calibre import prepare_string_for_xml from calibre.customize.ui import plugin_for_input_format from calibre.gui2.font_family_chooser import FontFamilyChooser + def config_widget_for_input_plugin(plugin): name = plugin.name.lower().replace(' ', '_') try: @@ -36,6 +37,7 @@ def config_widget_for_input_plugin(plugin): if issubclass(ans, Widget): return ans + def bulk_defaults_for_input_format(fmt): plugin = plugin_for_input_format(fmt) if plugin is not None: @@ -44,6 +46,7 @@ def bulk_defaults_for_input_format(fmt): return load_defaults(w.COMMIT_NAME) return {} + class Widget(QWidget): TITLE = _('Unknown') diff --git a/src/calibre/gui2/convert/azw3_output.py b/src/calibre/gui2/convert/azw3_output.py index 9b35f93360..21bbaf0183 100644 --- a/src/calibre/gui2/convert/azw3_output.py +++ b/src/calibre/gui2/convert/azw3_output.py @@ -12,6 +12,7 @@ from calibre.gui2.convert import Widget font_family_model = None + class PluginWidget(Widget, Ui_Form): TITLE = _('AZW3 Output') diff --git a/src/calibre/gui2/convert/bulk.py b/src/calibre/gui2/convert/bulk.py index efb6765398..d95957569d 100644 --- a/src/calibre/gui2/convert/bulk.py +++ b/src/calibre/gui2/convert/bulk.py @@ -21,6 +21,7 @@ from calibre.ebooks.conversion.plumber import Plumber from calibre.utils.config import prefs from calibre.utils.logging import Log + class BulkConfig(Config): def __init__(self, parent, db, preferred_output_format=None, diff --git a/src/calibre/gui2/convert/comic_input.py b/src/calibre/gui2/convert/comic_input.py index 094f3aa06c..ecdb59ae37 100644 --- a/src/calibre/gui2/convert/comic_input.py +++ b/src/calibre/gui2/convert/comic_input.py @@ -10,6 +10,7 @@ __docformat__ = 'restructuredtext en' from calibre.gui2.convert.comic_input_ui import Ui_Form from calibre.gui2.convert import Widget + class PluginWidget(Widget, Ui_Form): TITLE = _('Comic Input') diff --git a/src/calibre/gui2/convert/debug.py b/src/calibre/gui2/convert/debug.py index 3b382de47a..556e2fb7d8 100644 --- a/src/calibre/gui2/convert/debug.py +++ b/src/calibre/gui2/convert/debug.py @@ -12,6 +12,7 @@ from calibre.gui2.convert.debug_ui import Ui_Form from calibre.gui2.convert import Widget from calibre.gui2 import error_dialog, choose_dir + class DebugWidget(Widget, Ui_Form): TITLE = _('Debug') diff --git a/src/calibre/gui2/convert/docx_input.py b/src/calibre/gui2/convert/docx_input.py index 63b302138f..3af6b063dd 100644 --- a/src/calibre/gui2/convert/docx_input.py +++ b/src/calibre/gui2/convert/docx_input.py @@ -9,6 +9,7 @@ __copyright__ = '2013, Kovid Goyal ' from calibre.gui2.convert.docx_input_ui import Ui_Form from calibre.gui2.convert import Widget + class PluginWidget(Widget, Ui_Form): TITLE = _('DOCX Input') diff --git a/src/calibre/gui2/convert/docx_output.py b/src/calibre/gui2/convert/docx_output.py index e57669f161..dbba690f2f 100644 --- a/src/calibre/gui2/convert/docx_output.py +++ b/src/calibre/gui2/convert/docx_output.py @@ -10,6 +10,7 @@ from calibre.gui2.convert import Widget paper_size_model = None orientation_model = None + class PluginWidget(Widget, Ui_Form): TITLE = _('DOCX Output') diff --git a/src/calibre/gui2/convert/epub_output.py b/src/calibre/gui2/convert/epub_output.py index df5896307c..ec378b5e44 100644 --- a/src/calibre/gui2/convert/epub_output.py +++ b/src/calibre/gui2/convert/epub_output.py @@ -10,6 +10,7 @@ __docformat__ = 'restructuredtext en' from calibre.gui2.convert.epub_output_ui import Ui_Form from calibre.gui2.convert import Widget + class PluginWidget(Widget, Ui_Form): TITLE = _('EPUB Output') diff --git a/src/calibre/gui2/convert/fb2_input.py b/src/calibre/gui2/convert/fb2_input.py index ae4093eb07..a587969786 100644 --- a/src/calibre/gui2/convert/fb2_input.py +++ b/src/calibre/gui2/convert/fb2_input.py @@ -7,6 +7,7 @@ __docformat__ = 'restructuredtext en' from calibre.gui2.convert.fb2_input_ui import Ui_Form from calibre.gui2.convert import Widget + class PluginWidget(Widget, Ui_Form): TITLE = _('FB2 Input') diff --git a/src/calibre/gui2/convert/fb2_output.py b/src/calibre/gui2/convert/fb2_output.py index 19d0995fc1..2d7e849cf2 100644 --- a/src/calibre/gui2/convert/fb2_output.py +++ b/src/calibre/gui2/convert/fb2_output.py @@ -9,6 +9,7 @@ from calibre.gui2.convert import Widget format_model = None + class PluginWidget(Widget, Ui_Form): TITLE = _('FB2 Output') diff --git a/src/calibre/gui2/convert/font_key.py b/src/calibre/gui2/convert/font_key.py index 721a17feef..c061310dfb 100644 --- a/src/calibre/gui2/convert/font_key.py +++ b/src/calibre/gui2/convert/font_key.py @@ -11,6 +11,7 @@ from PyQt5.Qt import QDialog from calibre.gui2.convert.font_key_ui import Ui_Dialog from calibre.utils.localization import localize_user_manual_link + class FontKeyChooser(QDialog, Ui_Dialog): def __init__(self, parent=None, base_font_size=0.0, font_key=None): diff --git a/src/calibre/gui2/convert/gui_conversion.py b/src/calibre/gui2/convert/gui_conversion.py index ce51d4ca77..ff47c41c07 100644 --- a/src/calibre/gui2/convert/gui_conversion.py +++ b/src/calibre/gui2/convert/gui_conversion.py @@ -11,6 +11,7 @@ from calibre.ebooks.conversion.plumber import Plumber from calibre.customize.ui import plugin_for_catalog_format from calibre.utils.logging import Log + def gui_convert(input, output, recommendations, notification=DummyReporter(), abort_after_input_dump=False, log=None, override_input_metadata=False): recommendations = list(recommendations) @@ -24,12 +25,14 @@ def gui_convert(input, output, recommendations, notification=DummyReporter(), plumber.run() + def gui_convert_override(input, output, recommendations, notification=DummyReporter(), abort_after_input_dump=False, log=None): gui_convert(input, output, recommendations, notification=notification, abort_after_input_dump=abort_after_input_dump, log=log, override_input_metadata=True) + def gui_catalog(fmt, title, dbspec, ids, out_file_name, sync, fmt_options, connected_device, notification=DummyReporter(), log=None): if log is None: diff --git a/src/calibre/gui2/convert/heuristics.py b/src/calibre/gui2/convert/heuristics.py index 467ecf35f3..c43fda0f89 100644 --- a/src/calibre/gui2/convert/heuristics.py +++ b/src/calibre/gui2/convert/heuristics.py @@ -11,6 +11,7 @@ from calibre.gui2.convert.heuristics_ui import Ui_Form from calibre.gui2.convert import Widget from calibre.utils.localization import localize_user_manual_link + class HeuristicsWidget(Widget, Ui_Form): TITLE = _('Heuristic\nProcessing') diff --git a/src/calibre/gui2/convert/htmlz_output.py b/src/calibre/gui2/convert/htmlz_output.py index 421465429e..1b3a242339 100644 --- a/src/calibre/gui2/convert/htmlz_output.py +++ b/src/calibre/gui2/convert/htmlz_output.py @@ -9,6 +9,7 @@ from calibre.gui2.convert import Widget format_model = None + class PluginWidget(Widget, Ui_Form): TITLE = _('HTMLZ Output') diff --git a/src/calibre/gui2/convert/look_and_feel.py b/src/calibre/gui2/convert/look_and_feel.py index 8460baea3f..c556f99d03 100644 --- a/src/calibre/gui2/convert/look_and_feel.py +++ b/src/calibre/gui2/convert/look_and_feel.py @@ -13,6 +13,7 @@ from PyQt5.Qt import Qt from calibre.gui2.convert.look_and_feel_ui import Ui_Form from calibre.gui2.convert import Widget + class LookAndFeelWidget(Widget, Ui_Form): TITLE = _('Look & Feel') diff --git a/src/calibre/gui2/convert/lrf_output.py b/src/calibre/gui2/convert/lrf_output.py index 13340cf7f6..07bb6451a3 100644 --- a/src/calibre/gui2/convert/lrf_output.py +++ b/src/calibre/gui2/convert/lrf_output.py @@ -11,6 +11,7 @@ from calibre.gui2.convert import Widget font_family_model = None + class PluginWidget(Widget, Ui_Form): TITLE = _('LRF Output') diff --git a/src/calibre/gui2/convert/metadata.py b/src/calibre/gui2/convert/metadata.py index d87310631c..cbf5f7acc3 100644 --- a/src/calibre/gui2/convert/metadata.py +++ b/src/calibre/gui2/convert/metadata.py @@ -21,6 +21,7 @@ from calibre.utils.icu import sort_key from calibre.library.comments import comments_to_html from calibre.utils.config import tweaks + def create_opf_file(db, book_id, opf_file=None): mi = db.get_metadata(book_id, index_is_id=True) old_cover = mi.cover @@ -34,6 +35,7 @@ def create_opf_file(db, book_id, opf_file=None): opf_file.close() return mi, opf_file + def create_cover_file(db, book_id): cover = db.cover(book_id, index_is_id=True) cf = None @@ -43,6 +45,7 @@ def create_cover_file(db, book_id): cf.close() return cf + class MetadataWidget(Widget, Ui_Form): TITLE = _('Metadata') diff --git a/src/calibre/gui2/convert/mobi_output.py b/src/calibre/gui2/convert/mobi_output.py index 7d6312c5a9..c1c152b724 100644 --- a/src/calibre/gui2/convert/mobi_output.py +++ b/src/calibre/gui2/convert/mobi_output.py @@ -12,6 +12,7 @@ from calibre.gui2.convert import Widget font_family_model = None + class PluginWidget(Widget, Ui_Form): TITLE = _('MOBI Output') diff --git a/src/calibre/gui2/convert/page_setup.py b/src/calibre/gui2/convert/page_setup.py index d5029d0e8b..3a3136d2d6 100644 --- a/src/calibre/gui2/convert/page_setup.py +++ b/src/calibre/gui2/convert/page_setup.py @@ -12,6 +12,7 @@ from calibre.gui2.convert.page_setup_ui import Ui_Form from calibre.gui2.convert import Widget from calibre.customize.ui import input_profiles, output_profiles + class ProfileModel(QAbstractListModel): def __init__(self, profiles): @@ -35,6 +36,7 @@ class ProfileModel(QAbstractListModel): return ('%s [%s]' % (profile.description, ss)) return None + class PageSetupWidget(Widget, Ui_Form): TITLE = _('Page Setup') diff --git a/src/calibre/gui2/convert/pdb_output.py b/src/calibre/gui2/convert/pdb_output.py index 506081c4df..be07fd07ba 100644 --- a/src/calibre/gui2/convert/pdb_output.py +++ b/src/calibre/gui2/convert/pdb_output.py @@ -9,6 +9,7 @@ from calibre.gui2.convert import Widget format_model = None + class PluginWidget(Widget, Ui_Form): TITLE = _('PDB Output') diff --git a/src/calibre/gui2/convert/pdf_input.py b/src/calibre/gui2/convert/pdf_input.py index 967a0fe234..109d1f9980 100644 --- a/src/calibre/gui2/convert/pdf_input.py +++ b/src/calibre/gui2/convert/pdf_input.py @@ -7,6 +7,7 @@ __docformat__ = 'restructuredtext en' from calibre.gui2.convert.pdf_input_ui import Ui_Form from calibre.gui2.convert import Widget, QDoubleSpinBox + class PluginWidget(Widget, Ui_Form): TITLE = _('PDF Input') diff --git a/src/calibre/gui2/convert/pdf_output.py b/src/calibre/gui2/convert/pdf_output.py index 2d65b7387f..87c82ba500 100644 --- a/src/calibre/gui2/convert/pdf_output.py +++ b/src/calibre/gui2/convert/pdf_output.py @@ -11,6 +11,7 @@ from calibre.utils.localization import localize_user_manual_link paper_size_model = None orientation_model = None + class PluginWidget(Widget, Ui_Form): TITLE = _('PDF Output') diff --git a/src/calibre/gui2/convert/pml_output.py b/src/calibre/gui2/convert/pml_output.py index 56197ecde0..6d21105a8c 100644 --- a/src/calibre/gui2/convert/pml_output.py +++ b/src/calibre/gui2/convert/pml_output.py @@ -9,6 +9,7 @@ from calibre.gui2.convert import Widget format_model = None + class PluginWidget(Widget, Ui_Form): TITLE = _('PMLZ Output') diff --git a/src/calibre/gui2/convert/rb_output.py b/src/calibre/gui2/convert/rb_output.py index 25d1d8b0e0..b9f6b3bc2b 100644 --- a/src/calibre/gui2/convert/rb_output.py +++ b/src/calibre/gui2/convert/rb_output.py @@ -9,6 +9,7 @@ from calibre.gui2.convert import Widget format_model = None + class PluginWidget(Widget, Ui_Form): TITLE = _('RB Output') diff --git a/src/calibre/gui2/convert/regex_builder.py b/src/calibre/gui2/convert/regex_builder.py index 004a1dbec4..6c3043e822 100644 --- a/src/calibre/gui2/convert/regex_builder.py +++ b/src/calibre/gui2/convert/regex_builder.py @@ -17,6 +17,7 @@ from calibre.constants import iswindows from calibre.utils.ipc.simple_worker import fork_job, WorkerError from calibre.ptempfile import TemporaryFile + class RegexBuilder(QDialog, Ui_RegexBuilder): def __init__(self, db, book_id, regex, doc=None, parent=None): @@ -196,6 +197,7 @@ class RegexBuilder(QDialog, Ui_RegexBuilder): def doc(self): return unicode(self.preview.toPlainText()) + class RegexEdit(QWidget, Ui_Edit): doc_update = pyqtSignal(unicode) diff --git a/src/calibre/gui2/convert/rtf_input.py b/src/calibre/gui2/convert/rtf_input.py index e6ce35b0f1..ab0fae241e 100644 --- a/src/calibre/gui2/convert/rtf_input.py +++ b/src/calibre/gui2/convert/rtf_input.py @@ -9,6 +9,7 @@ __copyright__ = '2013, Kovid Goyal ' from calibre.gui2.convert.rtf_input_ui import Ui_Form from calibre.gui2.convert import Widget + class PluginWidget(Widget, Ui_Form): TITLE = _('RTF Input') diff --git a/src/calibre/gui2/convert/search_and_replace.py b/src/calibre/gui2/convert/search_and_replace.py index f4ddf103b5..86dc1ec2e9 100644 --- a/src/calibre/gui2/convert/search_and_replace.py +++ b/src/calibre/gui2/convert/search_and_replace.py @@ -15,6 +15,7 @@ from calibre.gui2 import (error_dialog, question_dialog, choose_files, from calibre import as_unicode from calibre.utils.localization import localize_user_manual_link + class SearchAndReplaceWidget(Widget, Ui_Form): TITLE = _('Search\n&\nReplace') diff --git a/src/calibre/gui2/convert/single.py b/src/calibre/gui2/convert/single.py index 2fac3a9a13..0240f219ba 100644 --- a/src/calibre/gui2/convert/single.py +++ b/src/calibre/gui2/convert/single.py @@ -32,14 +32,17 @@ from calibre.customize.conversion import OptionRecommendation from calibre.utils.config import prefs, tweaks from calibre.utils.logging import Log + class NoSupportedInputFormats(Exception): def __init__(self, available_formats): Exception.__init__(self) self.available_formats = available_formats + def sort_formats_by_preference(formats, prefs): uprefs = [x.upper() for x in prefs] + def key(x): try: return uprefs.index(x.upper()) @@ -48,6 +51,7 @@ def sort_formats_by_preference(formats, prefs): return len(prefs) return sorted(formats, key=key) + def get_output_formats(preferred_output_format): all_formats = {x.upper() for x in available_output_formats()} all_formats.discard('OEB') @@ -62,6 +66,7 @@ def get_output_formats(preferred_output_format): key=lambda x:{'EPUB':'!A', 'MOBI':'!B'}.get(x.upper(), x))) return fmts + class GroupModel(QAbstractListModel): def __init__(self, widgets): @@ -86,11 +91,13 @@ class GroupModel(QAbstractListModel): return (f) return None + def get_preferred_input_format_for_book(db, book_id): recs = load_specifics(db, book_id) if recs: return recs.get('gui_preferred_input_format', None) + def get_available_formats_for_book(db, book_id): available_formats = db.formats(book_id, index_is_id=True) if not available_formats: @@ -98,6 +105,7 @@ def get_available_formats_for_book(db, book_id): return set([x.lower() for x in available_formats.split(',')]) + def get_supported_input_formats_for_book(db, book_id): available_formats = get_available_formats_for_book(db, book_id) input_formats = set([x.lower() for x in supported_input_formats()]) diff --git a/src/calibre/gui2/convert/snb_output.py b/src/calibre/gui2/convert/snb_output.py index 73527365c2..18e2fee90c 100644 --- a/src/calibre/gui2/convert/snb_output.py +++ b/src/calibre/gui2/convert/snb_output.py @@ -9,6 +9,7 @@ from calibre.gui2.convert import Widget newline_model = None + class PluginWidget(Widget, Ui_Form): TITLE = _('SNB Output') diff --git a/src/calibre/gui2/convert/structure_detection.py b/src/calibre/gui2/convert/structure_detection.py index 0bf5ab7cd7..914f740eef 100644 --- a/src/calibre/gui2/convert/structure_detection.py +++ b/src/calibre/gui2/convert/structure_detection.py @@ -10,6 +10,7 @@ from calibre.gui2.convert.structure_detection_ui import Ui_Form from calibre.gui2.convert import Widget from calibre.gui2 import error_dialog + class StructureDetectionWidget(Widget, Ui_Form): TITLE = _('Structure\nDetection') diff --git a/src/calibre/gui2/convert/toc.py b/src/calibre/gui2/convert/toc.py index 30b8e03b2d..b981481506 100644 --- a/src/calibre/gui2/convert/toc.py +++ b/src/calibre/gui2/convert/toc.py @@ -12,6 +12,7 @@ from calibre.gui2.convert import Widget from calibre.gui2 import error_dialog from calibre.utils.localization import localize_user_manual_link + class TOCWidget(Widget, Ui_Form): TITLE = _('Table of\nContents') diff --git a/src/calibre/gui2/convert/txt_input.py b/src/calibre/gui2/convert/txt_input.py index c3adcbb5d3..0e57943a81 100644 --- a/src/calibre/gui2/convert/txt_input.py +++ b/src/calibre/gui2/convert/txt_input.py @@ -10,6 +10,7 @@ from calibre.gui2.convert.txt_input_ui import Ui_Form from calibre.gui2.convert import Widget from calibre.ebooks.conversion.plugins.txt_input import MD_EXTENSIONS + class PluginWidget(Widget, Ui_Form): TITLE = _('TXT Input') diff --git a/src/calibre/gui2/convert/txt_output.py b/src/calibre/gui2/convert/txt_output.py index 816e8d7785..bc9d44f1e7 100644 --- a/src/calibre/gui2/convert/txt_output.py +++ b/src/calibre/gui2/convert/txt_output.py @@ -8,6 +8,7 @@ __docformat__ = 'restructuredtext en' from calibre.gui2.convert.txt_output_ui import Ui_Form from calibre.gui2.convert import Widget + class PluginWidget(Widget, Ui_Form): TITLE = _('TXT Output') diff --git a/src/calibre/gui2/convert/txtz_output.py b/src/calibre/gui2/convert/txtz_output.py index f22c682044..9bc6c6ff92 100644 --- a/src/calibre/gui2/convert/txtz_output.py +++ b/src/calibre/gui2/convert/txtz_output.py @@ -7,6 +7,7 @@ __docformat__ = 'restructuredtext en' from calibre.gui2.convert.txt_output import PluginWidget as TXTPluginWidget + class PluginWidget(TXTPluginWidget): TITLE = _('TXTZ Output') diff --git a/src/calibre/gui2/convert/xpath_wizard.py b/src/calibre/gui2/convert/xpath_wizard.py index bdf296550e..99bcbc4b7a 100644 --- a/src/calibre/gui2/convert/xpath_wizard.py +++ b/src/calibre/gui2/convert/xpath_wizard.py @@ -42,6 +42,7 @@ class WizardWidget(QWidget, Ui_Form): expr = '//'+tag + q return expr + class Wizard(QDialog): def __init__(self, parent=None): diff --git a/src/calibre/gui2/cover_flow.py b/src/calibre/gui2/cover_flow.py index 726d9e627c..431916f605 100644 --- a/src/calibre/gui2/cover_flow.py +++ b/src/calibre/gui2/cover_flow.py @@ -226,6 +226,7 @@ else: DatabaseImages = None FileSystemImages = None + class CBDialog(QDialog): closed = pyqtSignal() @@ -444,6 +445,7 @@ class CoverFlowMixin(object): def sync_listview_to_cf(self, row): self.cf_last_updated_at = time.time() + def test(): from PyQt5.Qt import QApplication, QMainWindow app = QApplication([]) @@ -460,6 +462,7 @@ def test(): cf.setFocus(Qt.OtherFocusReason) sys.exit(app.exec_()) + def main(args=sys.argv): return 0 diff --git a/src/calibre/gui2/covers.py b/src/calibre/gui2/covers.py index d47727513e..81875781ab 100644 --- a/src/calibre/gui2/covers.py +++ b/src/calibre/gui2/covers.py @@ -21,6 +21,7 @@ from calibre.gui2.font_family_chooser import FontFamilyChooser from calibre.utils.date import now from calibre.utils.icu import sort_key + class Preview(QLabel): def __init__(self, parent=None): @@ -30,6 +31,7 @@ class Preview(QLabel): def sizeHint(self): return QSize(300, 400) + class ColorButton(QToolButton): def __init__(self, color, parent=None): @@ -45,6 +47,7 @@ class ColorButton(QToolButton): def color(self): def fget(self): return self._color.name(QColor.HexRgb)[1:] + def fset(self, val): self._color = QColor('#' + val) return property(fget=fget, fset=fset) @@ -59,6 +62,7 @@ class ColorButton(QToolButton): self._color = c self.update_display() + class CreateColorScheme(QDialog): def __init__(self, scheme_name, scheme, existing_names, edit_scheme=False, parent=None): @@ -96,6 +100,7 @@ class CreateColorScheme(QDialog): 'A color scheme with the name "%s" already exists.') % name, show=True) QDialog.accept(self) + class CoverSettingsWidget(QWidget): changed = pyqtSignal() @@ -170,6 +175,7 @@ class CoverSettingsWidget(QWidget): fp.l = l = QFormLayout() fp.setLayout(l) fp.f = [] + def add_hline(): f = QFrame() fp.f.append(f) @@ -196,6 +202,7 @@ class CoverSettingsWidget(QWidget): add_hline() self.changed_timer = t = QTimer(self) t.setSingleShot(True), t.setInterval(500), t.timeout.connect(self.emit_changed) + def create_sz(label): ans = QSpinBox(self) ans.setSuffix(' px'), ans.setMinimum(100), ans.setMaximum(10000) @@ -499,6 +506,7 @@ class CoverSettingsWidget(QWidget): for k, v in self.current_prefs.iteritems(): self.original_prefs[k] = v + class CoverSettingsDialog(QDialog): def __init__(self, mi=None, prefs=None, parent=None): diff --git a/src/calibre/gui2/css_transform_rules.py b/src/calibre/gui2/css_transform_rules.py index 838c6042ca..e4b0f0bf4a 100644 --- a/src/calibre/gui2/css_transform_rules.py +++ b/src/calibre/gui2/css_transform_rules.py @@ -20,6 +20,7 @@ from calibre.gui2.widgets2 import Dialog from calibre.utils.config import JSONConfig from calibre.utils.localization import localize_user_manual_link + class RuleEdit(QWidget): # {{{ MSG = _('Create the rule below, the rule can be used to transform style properties') @@ -151,6 +152,7 @@ class RuleEdit(QWidget): # {{{ return True # }}} + class RuleEditDialog(RuleEditDialogBase): # {{{ PREFS_NAME = 'edit-css-transform-rule' @@ -158,6 +160,7 @@ class RuleEditDialog(RuleEditDialogBase): # {{{ RuleEditClass = RuleEdit # }}} + class RuleItem(RuleItemBase): # {{{ @staticmethod @@ -178,6 +181,7 @@ class RuleItem(RuleItemBase): # {{{ return text # }}} + class Rules(RulesBase): # {{{ RuleItemClass = RuleItem @@ -187,6 +191,7 @@ class Rules(RulesBase): # {{{ ' below to get started.') # }}} + class Tester(Dialog): # {{{ DIALOG_TITLE = _('Test style transform rules') @@ -233,6 +238,7 @@ class Tester(Dialog): # {{{ return QSize(800, 600) # }}} + class RulesDialog(RulesDialogBase): # {{{ DIALOG_TITLE = _('Edit style transform rules') @@ -247,6 +253,7 @@ class RulesDialog(RulesDialogBase): # {{{ RulesDialogBase.__init__(self, *args, **kw) # }}} + class RulesWidget(QWidget, SaveLoadMixin): # {{{ changed = pyqtSignal() diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py index ff7661ed38..96f12b293c 100644 --- a/src/calibre/gui2/custom_column_widgets.py +++ b/src/calibre/gui2/custom_column_widgets.py @@ -23,6 +23,7 @@ from calibre.library.comments import comments_to_html from calibre.gui2.library.delegates import ClearingDoubleSpinBox, ClearingSpinBox from calibre.gui2.widgets2 import RatingEditor + class Base(object): def __init__(self, db, col_id, parent=None): @@ -65,6 +66,7 @@ class Base(object): def break_cycles(self): self.db = self.widgets = self.initial_val = None + class SimpleText(Base): def setup_ui(self, parent): @@ -120,6 +122,7 @@ class Bool(Base): val = self.widgets[1].currentIndex() return {2: None, 1: False, 0: True}[val] + class Int(Base): def setup_ui(self, parent): @@ -149,6 +152,7 @@ class Int(Base): self.setter(0) self.was_none = to_what == self.widgets[1].minimum() + class Float(Int): def setup_ui(self, parent): @@ -162,6 +166,7 @@ class Float(Int): self.was_none = False w.valueChanged.connect(self.valueChanged) + class Rating(Base): def setup_ui(self, parent): @@ -175,6 +180,7 @@ class Rating(Base): def getter(self): return self.widgets[1].rating_value or None + class DateTimeEdit(QDateTimeEdit): def focusInEvent(self, x): @@ -253,6 +259,7 @@ class DateTime(Base): def normalize_ui_val(self, val): return as_utc(val) if val is not None else None + class Comments(Base): def setup_ui(self, parent): @@ -284,10 +291,12 @@ class Comments(Base): def tab(self): def fget(self): return self._tb.tab + def fset(self, val): self._tb.tab = val return property(fget=fget, fset=fset) + class MultipleWidget(QWidget): def __init__(self, parent): @@ -337,6 +346,7 @@ class MultipleWidget(QWidget): def text(self): return self.tags_box.text() + class Text(Base): def setup_ui(self, parent): @@ -422,6 +432,7 @@ class Text(Base): if d.exec_() == TagEditor.Accepted: self.setter(d.tags) + class Series(Base): def setup_ui(self, parent): @@ -494,6 +505,7 @@ class Series(Base): val, s_index = self.current_val mi.set('#' + self.col_metadata['label'], val, extra=s_index) + class Enumeration(Base): def setup_ui(self, parent): @@ -537,6 +549,7 @@ class Enumeration(Base): val = None return val + def comments_factory(db, key, parent): fm = db.custom_column_num_map[key] ctype = fm.get('display', {}).get('interpret_as', 'html') @@ -558,12 +571,14 @@ widgets = { 'enumeration': Enumeration } + def field_sort_key(y, fm=None): m1 = fm[y] name = icu_lower(m1['name']) n1 = 'zzzzz' + name if m1['datatype'] == 'comments' and m1.get('display', {}).get('interpret_as') != 'short-text' else name return sort_key(n1) + def populate_metadata_page(layout, db, book_id, bulk=False, two_column=False, parent=None): def widget_factory(typ, key): if bulk: @@ -667,6 +682,7 @@ def populate_metadata_page(layout, db, book_id, bulk=False, two_column=False, pa layout.setRowStretch(layout.rowCount()-1, 100) return ans, items + class BulkBase(Base): @property @@ -738,6 +754,7 @@ class BulkBase(Base): if not self.ignore_change_signals: self.a_c_checkbox.setChecked(True) + class BulkBool(BulkBase, Bool): def get_initial_value(self, book_ids): @@ -793,6 +810,7 @@ class BulkBool(BulkBase, Bool): else: self.a_c_checkbox.setChecked(True) + class BulkInt(BulkBase): def setup_ui(self, parent): @@ -821,6 +839,7 @@ class BulkInt(BulkBase): self.setter(0) self.was_none = to_what == self.main_widget.minimum() + class BulkFloat(BulkInt): def setup_ui(self, parent): @@ -832,6 +851,7 @@ class BulkFloat(BulkInt): self.was_none = False self.main_widget.valueChanged.connect(self.valueChanged) + class BulkRating(BulkBase): def setup_ui(self, parent): @@ -846,6 +866,7 @@ class BulkRating(BulkBase): def getter(self): return self.main_widget.rating_value or None + class BulkDateTime(BulkBase): def setup_ui(self, parent): @@ -897,6 +918,7 @@ class BulkDateTime(BulkBase): def normalize_ui_val(self, val): return as_utc(val) if val is not None else None + class BulkSeries(BulkBase): def setup_ui(self, parent): @@ -975,6 +997,7 @@ class BulkSeries(BulkBase): self.db.set_custom_bulk(book_ids, val, extras=extras, num=self.col_id, notify=notify) + class BulkEnumeration(BulkBase, Enumeration): def get_initial_value(self, book_ids): @@ -1019,6 +1042,7 @@ class BulkEnumeration(BulkBase, Enumeration): self.main_widget.setCurrentIndex(self.main_widget.findText(val)) self.ignore_change_signals = False + class RemoveTags(QWidget): def __init__(self, parent, values): @@ -1043,6 +1067,7 @@ class RemoveTags(QWidget): else: self.tags_box.setEnabled(True) + class BulkText(BulkBase): def setup_ui(self, parent): diff --git a/src/calibre/gui2/dbus_export/demo.py b/src/calibre/gui2/dbus_export/demo.py index 808419a3a3..0efc6ded9a 100644 --- a/src/calibre/gui2/dbus_export/demo.py +++ b/src/calibre/gui2/dbus_export/demo.py @@ -17,9 +17,11 @@ from calibre.gui2.dbus_export.widgets import factory setup_for_cli_run() + def make_checkable(ac, checked=True): ac.setCheckable(True), ac.setChecked(checked) + class MainWindow(QMainWindow): window_blocked = pyqtSignal() diff --git a/src/calibre/gui2/dbus_export/gtk.py b/src/calibre/gui2/dbus_export/gtk.py index 6b361a3994..c1bf4bb758 100644 --- a/src/calibre/gui2/dbus_export/gtk.py +++ b/src/calibre/gui2/dbus_export/gtk.py @@ -56,6 +56,7 @@ UI_INFO = """ """ + class MenuExampleWindow(Gtk.ApplicationWindow): def __init__(self, app): @@ -196,6 +197,7 @@ class MenuExampleWindow(Gtk.ApplicationWindow): self.popup.popup(None, None, None, None, event.button, event.time) return True # event has been handled + def convert(v): if isinstance(v, basestring): return unicode(v) @@ -211,6 +213,7 @@ def convert(v): return int(v) return v + class MyApplication(Gtk.Application): def do_activate(self): diff --git a/src/calibre/gui2/dbus_export/menu.py b/src/calibre/gui2/dbus_export/menu.py index 5a99a9688e..6b682625a2 100644 --- a/src/calibre/gui2/dbus_export/menu.py +++ b/src/calibre/gui2/dbus_export/menu.py @@ -19,9 +19,11 @@ from calibre.gui2.dbus_export.utils import ( null = object() + def PropDict(mapping=()): return dbus.Dictionary(mapping, signature='sv') + def create_properties_for_action(ac, previous=None): ans = PropDict() if ac.isSeparator(): @@ -63,6 +65,7 @@ def create_properties_for_action(ac, previous=None): ans['x-qt-icon-cache-key'] = icon.cacheKey() return ans + def menu_actions(menu): try: return menu.actions() @@ -71,6 +74,7 @@ def menu_actions(menu): return QMenu.actions(menu) raise + class DBusMenu(QObject): handle_event_signal = pyqtSignal(object, object, object, object) @@ -266,6 +270,7 @@ class DBusMenu(QObject): return True return False + class DBusMenuAPI(Object): IFACE = 'com.canonical.dbusmenu' @@ -369,6 +374,7 @@ class DBusMenuAPI(Object): def ItemActivationRequested(self, id, timestamp): pass + def test(): setup_for_cli_run() app = QApplication([]) diff --git a/src/calibre/gui2/dbus_export/menu2.py b/src/calibre/gui2/dbus_export/menu2.py index bf52d9aff8..a3df7ab9dc 100644 --- a/src/calibre/gui2/dbus_export/menu2.py +++ b/src/calibre/gui2/dbus_export/menu2.py @@ -19,6 +19,7 @@ from PyQt5.Qt import QObject, pyqtSignal, QTimer, Qt from calibre.utils.dbus_service import Object, method as dbus_method, signal as dbus_signal from calibre.gui2.dbus_export.utils import set_X_window_properties + def add_window_properties_for_menu(widget, object_path, bus): op = unicode(object_path) set_X_window_properties(widget.effectiveWinId(), _UNITY_OBJECT_PATH=op, _GTK_UNIQUE_BUS_NAME=unicode(bus.get_unique_name()), _GTK_MENUBAR_OBJECT_PATH=op) diff --git a/src/calibre/gui2/dbus_export/tray.py b/src/calibre/gui2/dbus_export/tray.py index 4fd26bb187..7fda0f204c 100644 --- a/src/calibre/gui2/dbus_export/tray.py +++ b/src/calibre/gui2/dbus_export/tray.py @@ -25,6 +25,7 @@ from calibre.utils.dbus_service import ( _sni_count = 0 + class StatusNotifierItem(QObject): IFACE = 'org.kde.StatusNotifierItem' @@ -98,6 +99,7 @@ class StatusNotifierItem(QObject): _status_item_menu_count = 0 + class StatusNotifierItemAPI(Object): 'See http://www.notmart.org/misc/statusnotifieritem/statusnotifieritem.html' diff --git a/src/calibre/gui2/dbus_export/utils.py b/src/calibre/gui2/dbus_export/utils.py index ea5f442279..625b870eeb 100644 --- a/src/calibre/gui2/dbus_export/utils.py +++ b/src/calibre/gui2/dbus_export/utils.py @@ -12,6 +12,7 @@ import dbus from PyQt5.Qt import QSize, QImage, Qt, QKeySequence, QBuffer, QByteArray + def log(*args, **kw): kw['file'] = sys.stderr print('DBusExport:', *args, **kw) @@ -19,6 +20,7 @@ def log(*args, **kw): from calibre.ptempfile import PersistentTemporaryDirectory + class IconCache(object): # Avoiding sending status notifier icon data over DBus, makes dbus-monitor @@ -58,6 +60,7 @@ class IconCache(object): _icon_cache = None + def icon_cache(): global _icon_cache if _icon_cache is None: @@ -86,6 +89,7 @@ def qicon_to_sni_image_list(qicon): ans.append((w, h, dbus.ByteArray(data))) return ans + def swap_mnemonic_char(text, from_char='&', to_char='_'): text = text.replace(to_char, to_char * 2) # Escape to_char # Replace the first occurence of an unescaped from_char with to_char @@ -96,6 +100,7 @@ def swap_mnemonic_char(text, from_char='&', to_char='_'): text = text.replace(from_char * 2, from_char) return text + def key_sequence_to_dbus_shortcut(qks): for key in qks: if key == -1 or key == Qt.Key_unknown: @@ -112,6 +117,7 @@ def key_sequence_to_dbus_shortcut(qks): if items: yield items + def icon_to_dbus_menu_icon(icon, size=32): if icon.isNull(): return None @@ -121,6 +127,7 @@ def icon_to_dbus_menu_icon(icon, size=32): icon.pixmap(32).save(buf, 'PNG') return dbus.ByteArray(bytes((ba.data()))) + def setup_for_cli_run(): import signal from dbus.mainloop.glib import DBusGMainLoop, threads_init @@ -128,6 +135,7 @@ def setup_for_cli_run(): DBusGMainLoop(set_as_default=True) signal.signal(signal.SIGINT, signal.SIG_DFL) # quit on Ctrl-C + def set_X_window_properties(win_id, **properties): ' Set X Window properties on the window with the specified id. Only string values are supported. ' import xcb, xcb.xproto diff --git a/src/calibre/gui2/dbus_export/widgets.py b/src/calibre/gui2/dbus_export/widgets.py index f331571d93..958cd9baf5 100644 --- a/src/calibre/gui2/dbus_export/widgets.py +++ b/src/calibre/gui2/dbus_export/widgets.py @@ -16,11 +16,13 @@ from calibre.constants import iswindows, isosx UNITY_WINDOW_REGISTRAR = ('com.canonical.AppMenu.Registrar', '/com/canonical/AppMenu/Registrar', 'com.canonical.AppMenu.Registrar') STATUS_NOTIFIER = ("org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher", "org.kde.StatusNotifierWatcher") + def log(*args, **kw): kw['file'] = sys.stderr print('DBusExport:', *args, **kw) kw['file'].flush() + class MenuBarAction(QAction): def __init__(self, mb): @@ -31,6 +33,7 @@ class MenuBarAction(QAction): menu_counter = 0 + class ExportedMenuBar(QMenuBar): # {{{ is_native_menubar = True @@ -118,6 +121,7 @@ class ExportedMenuBar(QMenuBar): # {{{ # }}} + class Factory(QObject): def __init__(self, app_id=None): @@ -251,6 +255,8 @@ class Factory(QObject): # TODO: have the created widgets also handle bus disconnection _factory = None + + def factory(app_id=None): global _factory if _factory is None: diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index dba9ffa739..24017bbfa5 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -37,6 +37,7 @@ from calibre.library.save_to_disk import find_plugboard from calibre.ptempfile import PersistentTemporaryFile, force_unicode as filename_to_unicode # }}} + class DeviceJob(BaseJob): # {{{ def __init__(self, func, done, job_manager, args=[], kwargs={}, @@ -116,11 +117,13 @@ class DeviceJob(BaseJob): # {{{ # }}} + def device_name_for_plugboards(device_class): if hasattr(device_class, 'DEVICE_PLUGBOARD_NAME'): return device_class.DEVICE_PLUGBOARD_NAME return device_class.__class__.__name__ + class BusyCursor(object): def __enter__(self): @@ -685,6 +688,7 @@ class DeviceManager(Thread): # {{{ # }}} + class DeviceAction(QAction): # {{{ a_s = pyqtSignal(object) @@ -704,6 +708,7 @@ class DeviceAction(QAction): # {{{ self.specific) # }}} + class DeviceMenu(QMenu): # {{{ fetch_annotations = pyqtSignal() @@ -853,6 +858,7 @@ class DeviceMenu(QMenu): # {{{ # }}} + class DeviceSignals(QObject): # {{{ #: This signal is emitted once, after metadata is downloaded from the #: connected device. @@ -872,6 +878,7 @@ class DeviceSignals(QObject): # {{{ device_signals = DeviceSignals() # }}} + class DeviceMixin(object): # {{{ def __init__(self, *args, **kwargs): @@ -1364,6 +1371,7 @@ class DeviceMixin(object): # {{{ @dynamic_property def news_to_be_synced(self): doc = 'Set of ids to be sent to device' + def fget(self): ans = [] try: @@ -1764,6 +1772,7 @@ class DeviceMixin(object): # {{{ return False string_pat = re.compile('(?u)\W|[_]') + def clean_string(x): x = x.lower() if x else '' return string_pat.sub('', x) diff --git a/src/calibre/gui2/device_drivers/configwidget.py b/src/calibre/gui2/device_drivers/configwidget.py index 4ddafd8682..6db50ccfb3 100644 --- a/src/calibre/gui2/device_drivers/configwidget.py +++ b/src/calibre/gui2/device_drivers/configwidget.py @@ -14,6 +14,7 @@ from calibre.gui2.device_drivers.configwidget_ui import Ui_ConfigWidget from calibre.utils.formatter import validation_formatter from calibre.ebooks import BOOK_EXTENSIONS + class ConfigWidget(QWidget, Ui_ConfigWidget): def __init__(self, settings, all_formats, supports_subdirs, @@ -63,6 +64,7 @@ class ConfigWidget(QWidget, Ui_ConfigWidget): self.opt_use_author_sort.hide() if extra_customization_message: extra_customization_choices = extra_customization_choices or {} + def parse_msg(m): msg, _, tt = m.partition(':::') if m else ('', '', '') return msg.strip(), textwrap.fill(tt.strip(), 100) diff --git a/src/calibre/gui2/device_drivers/mtp_config.py b/src/calibre/gui2/device_drivers/mtp_config.py index ab2ec98895..fbee0d4e3a 100644 --- a/src/calibre/gui2/device_drivers/mtp_config.py +++ b/src/calibre/gui2/device_drivers/mtp_config.py @@ -21,6 +21,7 @@ from calibre.gui2.dialogs.template_dialog import TemplateDialog from calibre.utils.date import parse_date from calibre.gui2.device_drivers.mtp_folder_browser import Browser, IgnoredFolders + class FormatsConfig(QWidget): # {{{ def __init__(self, all_formats, format_map): @@ -73,6 +74,7 @@ class FormatsConfig(QWidget): # {{{ self.f.setCurrentRow(idx+1) # }}} + class TemplateConfig(QWidget): # {{{ def __init__(self, val): @@ -117,6 +119,7 @@ class TemplateConfig(QWidget): # {{{ return False # }}} + class SendToConfig(QWidget): # {{{ def __init__(self, val, device): @@ -157,6 +160,7 @@ class SendToConfig(QWidget): # {{{ # }}} + class IgnoredDevices(QWidget): # {{{ def __init__(self, devs, blacklist): @@ -198,6 +202,7 @@ class IgnoredDevices(QWidget): # {{{ # Rules {{{ + class Rule(QWidget): remove = pyqtSignal(object) @@ -267,6 +272,7 @@ class Rule(QWidget): ) return None + class FormatRules(QGroupBox): def __init__(self, device, rules): @@ -326,6 +332,7 @@ class FormatRules(QGroupBox): yield r # }}} + class MTPConfig(QTabWidget): def __init__(self, device, parent=None, highlight_ignored_folders=False): @@ -485,6 +492,7 @@ class MTPConfig(QTabWidget): self.device.prefs[self.current_device_key] = p + class SendError(QDialog): def __init__(self, gui, error): diff --git a/src/calibre/gui2/device_drivers/mtp_folder_browser.py b/src/calibre/gui2/device_drivers/mtp_folder_browser.py index 98ad08b072..9492d6411c 100644 --- a/src/calibre/gui2/device_drivers/mtp_folder_browser.py +++ b/src/calibre/gui2/device_drivers/mtp_folder_browser.py @@ -14,6 +14,7 @@ from PyQt5.Qt import (QTabWidget, QTreeWidget, QTreeWidgetItem, Qt, QDialog, from calibre.gui2 import file_icon_provider + def browser_item(f, parent): name = f.name if not f.is_folder: @@ -28,6 +29,7 @@ def browser_item(f, parent): return ans + class Storage(QTreeWidget): def __init__(self, storage, show_files=False, item_func=browser_item): @@ -56,6 +58,7 @@ class Storage(QTreeWidget): return (self.object_id, item.data(0, Qt.UserRole)) return None + class Folders(QTabWidget): selected = pyqtSignal() @@ -76,6 +79,7 @@ class Folders(QTabWidget): if w is not None: return w.current_item + class Browser(QDialog): def __init__(self, filesystem_cache, show_files=True, parent=None): @@ -97,6 +101,7 @@ class Browser(QDialog): def current_item(self): return self.folders.current_item + class IgnoredFolders(QDialog): def __init__(self, dev, ignored_folders=None, parent=None): @@ -202,6 +207,7 @@ class IgnoredFolders(QDialog): ans[unicode(w.storage.storage_id)] = list(folders) return ans + def setup_device(): from calibre.devices.mtp.driver import MTP_DEVICE from calibre.devices.scanner import DeviceScanner @@ -215,6 +221,7 @@ def setup_device(): dev.open(cd, 'test') return dev + def browse(): from calibre.gui2 import Application app = Application([]) @@ -225,6 +232,7 @@ def browse(): dev.shutdown() return d.current_item + def ignored_folders(): from calibre.gui2 import Application app = Application([]) diff --git a/src/calibre/gui2/device_drivers/tabbed_device_config.py b/src/calibre/gui2/device_drivers/tabbed_device_config.py index 4506631160..c8af7da14e 100644 --- a/src/calibre/gui2/device_drivers/tabbed_device_config.py +++ b/src/calibre/gui2/device_drivers/tabbed_device_config.py @@ -18,12 +18,15 @@ from calibre.ebooks import BOOK_EXTENSIONS from calibre.gui2.device_drivers.mtp_config import (FormatsConfig, TemplateConfig) from calibre.devices.usbms.driver import debug_print + def wrap_msg(msg): return textwrap.fill(msg.strip(), 100) + def setToolTipFor(widget, tt): widget.setToolTip(wrap_msg(tt)) + def create_checkbox(title, tt, state): cb = QCheckBox(title) cb.setToolTip(wrap_msg(tt)) @@ -52,6 +55,7 @@ class TabbedDeviceConfig(QTabWidget): from DeviceOptionsGroupBox, are created to further group the options. The group boxes can be coded to support any control type and dependencies between them. """ + def __init__(self, device_settings, all_formats, supports_subdirs, must_read_metadata, supports_use_author_sort, extra_customization_message, device, @@ -221,6 +225,7 @@ class DeviceConfigTab(QWidget): # {{{ abstract the properties of the configuration tab. When a property is accessed, it will iterate over all known widgets looking for the property. ''' + def __init__(self, parent=None): QWidget.__init__(self) self.parent = parent @@ -264,6 +269,7 @@ class ExtraCustomization(DeviceConfigTab): # {{{ if extra_customization_message: extra_customization_choices = extra_customization_choices or {} + def parse_msg(m): msg, _, tt = m.partition(':::') if m else ('', '', '') return msg.strip(), textwrap.fill(tt.strip(), 100) @@ -351,10 +357,12 @@ class ExtraCustomization(DeviceConfigTab): # {{{ # }}} + class DeviceOptionsGroupBox(QGroupBox): """ This is a container for the individual options for a device driver. """ + def __init__(self, parent, device=None, title=_("Unknown")): QGroupBox.__init__(self, parent) diff --git a/src/calibre/gui2/dialogs/add_from_isbn.py b/src/calibre/gui2/dialogs/add_from_isbn.py index 479e710d5b..d65ca7754c 100644 --- a/src/calibre/gui2/dialogs/add_from_isbn.py +++ b/src/calibre/gui2/dialogs/add_from_isbn.py @@ -16,6 +16,7 @@ from calibre.ebooks.metadata import check_isbn from calibre.constants import iswindows from calibre.gui2 import gprefs, question_dialog, error_dialog + class AddFromISBN(QDialog): def __init__(self, parent=None): diff --git a/src/calibre/gui2/dialogs/authors_edit.py b/src/calibre/gui2/dialogs/authors_edit.py index 62e53327a1..34ce34722c 100644 --- a/src/calibre/gui2/dialogs/authors_edit.py +++ b/src/calibre/gui2/dialogs/authors_edit.py @@ -17,6 +17,7 @@ from calibre.gui2 import gprefs from calibre.gui2.complete2 import EditWithComplete from calibre.ebooks.metadata import string_to_authors + class ItemDelegate(QStyledItemDelegate): edited = pyqtSignal(object) @@ -44,6 +45,7 @@ class ItemDelegate(QStyledItemDelegate): init_line_edit(self.ed, self.all_authors) return self.ed + class List(QListWidget): def __init__(self, all_authors, parent): @@ -95,6 +97,7 @@ class List(QListWidget): for x in sorted(remove, reverse=True): self.takeItem(x) + class Edit(EditWithComplete): returnPressed = pyqtSignal() @@ -106,12 +109,14 @@ class Edit(EditWithComplete): return return EditWithComplete.keyPressEvent(self, ev) + def init_line_edit(a, all_authors): a.set_separator('&') a.set_space_before_sep(True) a.set_add_separator(tweaks['authors_completer_append_separator']) a.update_items_cache(all_authors) + class AuthorsEdit(QDialog): def __init__(self, all_authors, current_authors, parent=None): diff --git a/src/calibre/gui2/dialogs/book_info.py b/src/calibre/gui2/dialogs/book_info.py index 520b4edd51..fba1eca5bb 100644 --- a/src/calibre/gui2/dialogs/book_info.py +++ b/src/calibre/gui2/dialogs/book_info.py @@ -17,6 +17,7 @@ from calibre import fit_image from calibre.gui2.book_details import render_html, details_context_menu_event, css from calibre.gui2.widgets import CoverView + class Details(QWebView): def __init__(self, book_info, parent=None): @@ -29,6 +30,7 @@ class Details(QWebView): def contextMenuEvent(self, ev): details_context_menu_event(self, ev, self.book_info) + class BookInfo(QDialog): closed = pyqtSignal(object) diff --git a/src/calibre/gui2/dialogs/catalog.py b/src/calibre/gui2/dialogs/catalog.py index e6ba7e68e1..a3987e90c9 100644 --- a/src/calibre/gui2/dialogs/catalog.py +++ b/src/calibre/gui2/dialogs/catalog.py @@ -15,6 +15,7 @@ from calibre.gui2.dialogs.catalog_ui import Ui_Dialog from calibre.gui2 import dynamic, info_dialog from calibre.customize.ui import catalog_plugins + class Catalog(QDialog, Ui_Dialog): ''' Catalog Dialog builder''' diff --git a/src/calibre/gui2/dialogs/check_library.py b/src/calibre/gui2/dialogs/check_library.py index 6a84e9726c..5e6c4999ba 100644 --- a/src/calibre/gui2/dialogs/check_library.py +++ b/src/calibre/gui2/dialogs/check_library.py @@ -16,6 +16,7 @@ from calibre.library.check_library import CheckLibrary, CHECKS from calibre.utils.recycle_bin import delete_file, delete_tree from calibre import prints, as_unicode + class DBCheck(QDialog): # {{{ update_msg = pyqtSignal(object) @@ -70,9 +71,11 @@ class DBCheck(QDialog): # {{{ # }}} + class Item(QTreeWidgetItem): pass + class CheckLibraryDialog(QDialog): def __init__(self, parent, db): diff --git a/src/calibre/gui2/dialogs/choose_format.py b/src/calibre/gui2/dialogs/choose_format.py index 86c4cf76a1..094ac09d2d 100644 --- a/src/calibre/gui2/dialogs/choose_format.py +++ b/src/calibre/gui2/dialogs/choose_format.py @@ -9,6 +9,7 @@ from PyQt5.Qt import ( from calibre.gui2 import file_icon_provider + class ChooseFormatDialog(QDialog): def __init__(self, window, msg, formats, show_open_with=False): diff --git a/src/calibre/gui2/dialogs/choose_format_device.py b/src/calibre/gui2/dialogs/choose_format_device.py index 47bc536671..775af1dad5 100644 --- a/src/calibre/gui2/dialogs/choose_format_device.py +++ b/src/calibre/gui2/dialogs/choose_format_device.py @@ -6,6 +6,7 @@ from PyQt5.Qt import QDialog, QTreeWidgetItem, QIcon, QModelIndex from calibre.gui2 import file_icon_provider from calibre.gui2.dialogs.choose_format_device_ui import Ui_ChooseFormatDeviceDialog + class ChooseFormatDeviceDialog(QDialog, Ui_ChooseFormatDeviceDialog): def __init__(self, window, msg, formats): diff --git a/src/calibre/gui2/dialogs/choose_library.py b/src/calibre/gui2/dialogs/choose_library.py index f331bb68c7..c64cf8d59b 100644 --- a/src/calibre/gui2/dialogs/choose_library.py +++ b/src/calibre/gui2/dialogs/choose_library.py @@ -18,6 +18,7 @@ from calibre.constants import (filesystem_encoding, iswindows, get_portable_base) from calibre import isbytestring, patheq, force_unicode + class ProgressDialog(PD): on_progress_update = pyqtSignal(object, object, object) @@ -39,6 +40,7 @@ class ProgressDialog(PD): def show_new_progress(self, *args): self.on_progress_update.emit(*args) + class ChooseLibrary(QDialog, Ui_Dialog): def __init__(self, db, callback, parent): @@ -136,6 +138,7 @@ class ChooseLibrary(QDialog, Ui_Dialog): pd.canceled_signal.connect(abort_move.set) self.parent().library_view.model().stop_metadata_backup() move_error = [] + def do_move(): try: self.db.new_api.move_library_to(loc, abort=abort_move, progress=pd.show_new_progress) diff --git a/src/calibre/gui2/dialogs/choose_plugin_toolbars.py b/src/calibre/gui2/dialogs/choose_plugin_toolbars.py index 7f730b880c..e5d903a626 100644 --- a/src/calibre/gui2/dialogs/choose_plugin_toolbars.py +++ b/src/calibre/gui2/dialogs/choose_plugin_toolbars.py @@ -12,6 +12,7 @@ __license__ = 'GPL v3' from PyQt5.Qt import (QDialog, QVBoxLayout, QLabel, QDialogButtonBox, QListWidget, QAbstractItemView, QSizePolicy) + class ChoosePluginToolbarsDialog(QDialog): def __init__(self, parent, plugin, locations): diff --git a/src/calibre/gui2/dialogs/comicconf.py b/src/calibre/gui2/dialogs/comicconf.py index 692885c4d1..07931322aa 100644 --- a/src/calibre/gui2/dialogs/comicconf.py +++ b/src/calibre/gui2/dialogs/comicconf.py @@ -8,15 +8,18 @@ from PyQt5.Qt import QDialog from calibre.gui2.dialogs.comicconf_ui import Ui_Dialog from calibre.ebooks.lrf.comic.convert_from import config, PROFILES + def set_conversion_defaults(window): d = ComicConf(window) d.exec_() + def get_bulk_conversion_options(window): d = ComicConf(window, config_defaults=config(None).as_string()) if d.exec_() == QDialog.Accepted: return d.config.parse() + def get_conversion_options(window, defaults, title, author): if defaults is None: defaults = config(None).as_string() diff --git a/src/calibre/gui2/dialogs/comments_dialog.py b/src/calibre/gui2/dialogs/comments_dialog.py index f251a9389d..516e3e7c98 100644 --- a/src/calibre/gui2/dialogs/comments_dialog.py +++ b/src/calibre/gui2/dialogs/comments_dialog.py @@ -10,6 +10,7 @@ from calibre.gui2.dialogs.comments_dialog_ui import Ui_CommentsDialog from calibre.library.comments import comments_to_html from calibre.gui2.widgets2 import Dialog + class CommentsDialog(QDialog, Ui_CommentsDialog): def __init__(self, parent, text, column_name=None): diff --git a/src/calibre/gui2/dialogs/confirm_delete.py b/src/calibre/gui2/dialogs/confirm_delete.py index 0e11d5d562..9681064398 100644 --- a/src/calibre/gui2/dialogs/confirm_delete.py +++ b/src/calibre/gui2/dialogs/confirm_delete.py @@ -10,6 +10,7 @@ from PyQt5.Qt import ( from calibre import confirm_config_name from calibre.gui2 import dynamic + class Dialog(QDialog): def __init__(self, msg, name, parent, config_set=dynamic, icon='dialog_warning.png', diff --git a/src/calibre/gui2/dialogs/confirm_delete_location.py b/src/calibre/gui2/dialogs/confirm_delete_location.py index 5383179afb..b2849d41ea 100644 --- a/src/calibre/gui2/dialogs/confirm_delete_location.py +++ b/src/calibre/gui2/dialogs/confirm_delete_location.py @@ -9,6 +9,7 @@ from functools import partial from calibre.gui2.dialogs.confirm_delete_location_ui import Ui_Dialog from PyQt5.Qt import QDialog, Qt, QPixmap, QIcon + class Dialog(QDialog, Ui_Dialog): def __init__(self, msg, name, parent): diff --git a/src/calibre/gui2/dialogs/confirm_merge.py b/src/calibre/gui2/dialogs/confirm_merge.py index ad2957c468..6984c57ba1 100644 --- a/src/calibre/gui2/dialogs/confirm_merge.py +++ b/src/calibre/gui2/dialogs/confirm_merge.py @@ -18,6 +18,7 @@ from calibre.gui2.dialogs.confirm_delete import confirm_config_name from calibre.utils.config import tweaks from calibre.utils.date import format_date + class ConfirmMerge(Dialog): def __init__(self, msg, name, parent, mi): diff --git a/src/calibre/gui2/dialogs/conversion_error.py b/src/calibre/gui2/dialogs/conversion_error.py index 47447a1268..d1fb40e096 100644 --- a/src/calibre/gui2/dialogs/conversion_error.py +++ b/src/calibre/gui2/dialogs/conversion_error.py @@ -5,6 +5,7 @@ from PyQt5.Qt import QDialog from calibre.gui2.dialogs.conversion_error_ui import Ui_ConversionErrorDialog + class ConversionErrorDialog(QDialog, Ui_ConversionErrorDialog): def __init__(self, window, title, html, show=False): diff --git a/src/calibre/gui2/dialogs/custom_recipes.py b/src/calibre/gui2/dialogs/custom_recipes.py index 314d0b777e..03ae4c5677 100644 --- a/src/calibre/gui2/dialogs/custom_recipes.py +++ b/src/calibre/gui2/dialogs/custom_recipes.py @@ -22,9 +22,11 @@ from calibre.utils.icu import sort_key from calibre.web.feeds.recipes.collection import get_builtin_recipe_collection, get_builtin_recipe_by_id from calibre.utils.localization import localize_user_manual_link + def is_basic_recipe(src): return re.search(r'^class BasicUserRecipe', src, flags=re.MULTILINE) is not None + class CustomRecipeModel(QAbstractListModel): # {{{ def __init__(self, recipe_model): @@ -111,6 +113,7 @@ class CustomRecipeModel(QAbstractListModel): # {{{ self.endResetModel() # }}} + def py3_repr(x): ans = repr(x) if isinstance(x, bytes) and not ans.startswith('b'): @@ -119,6 +122,7 @@ def py3_repr(x): ans = ans[1:] return ans + def options_to_recipe_source(title, oldest_article, max_articles_per_feed, feeds): classname = 'BasicUserRecipe%d' % int(time.time()) title = unicode(title).strip() or classname @@ -149,6 +153,7 @@ def options_to_recipe_source(title, oldest_article, max_articles_per_feed, feeds max_articles_per_feed=max_articles_per_feed, base='AutomaticNewsRecipe') return src + class RecipeList(QWidget): # {{{ edit_recipe = pyqtSignal(object, object) @@ -254,6 +259,7 @@ class RecipeList(QWidget): # {{{ self.select_row() # }}} + class BasicRecipe(QWidget): # {{{ def __init__(self, parent): @@ -399,6 +405,7 @@ class BasicRecipe(QWidget): # {{{ return property(fget=fget, fset=fset) # }}} + class AdvancedRecipe(QWidget): # {{{ def __init__(self, parent): @@ -438,6 +445,7 @@ class AdvancedRecipe(QWidget): # {{{ return QSize(800, 500) # }}} + class CustomRecipes(Dialog): def __init__(self, recipe_model, parent=None): diff --git a/src/calibre/gui2/dialogs/delete_matching_from_device.py b/src/calibre/gui2/dialogs/delete_matching_from_device.py index 8d67140ddf..09f7707449 100644 --- a/src/calibre/gui2/dialogs/delete_matching_from_device.py +++ b/src/calibre/gui2/dialogs/delete_matching_from_device.py @@ -12,6 +12,7 @@ from calibre.gui2.dialogs.delete_matching_from_device_ui import \ Ui_DeleteMatchingFromDeviceDialog from calibre.utils.date import UNDEFINED_DATE + class tableItem(QTableWidgetItem): def __init__(self, text): @@ -25,18 +26,21 @@ class tableItem(QTableWidgetItem): def __lt__(self, other): return self.sort < other.sort + class centeredTableItem(tableItem): def __init__(self, text): tableItem.__init__(self, text) self.setTextAlignment(Qt.AlignCenter) + class titleTableItem(tableItem): def __init__(self, text): tableItem.__init__(self, text) self.sort = title_sort(text.lower()) + class authorTableItem(tableItem): def __init__(self, book): @@ -46,6 +50,7 @@ class authorTableItem(tableItem): else: self.sort = authors_to_sort_string(book.authors).lower() + class dateTableItem(tableItem): def __init__(self, date): diff --git a/src/calibre/gui2/dialogs/device_category_editor.py b/src/calibre/gui2/dialogs/device_category_editor.py index 4f4758ad98..3923c3dd93 100644 --- a/src/calibre/gui2/dialogs/device_category_editor.py +++ b/src/calibre/gui2/dialogs/device_category_editor.py @@ -6,6 +6,7 @@ from PyQt5.Qt import Qt, QDialog, QListWidgetItem from calibre.gui2.dialogs.device_category_editor_ui import Ui_DeviceCategoryEditor from calibre.gui2 import question_dialog, error_dialog + class ListWidgetItem(QListWidgetItem): def __init__(self, txt): @@ -45,6 +46,7 @@ class ListWidgetItem(QListWidgetItem): self.current_value = txt QListWidgetItem.setText(txt) + class DeviceCategoryEditor(QDialog, Ui_DeviceCategoryEditor): def __init__(self, window, tag_to_match, data, key): diff --git a/src/calibre/gui2/dialogs/drm_error.py b/src/calibre/gui2/dialogs/drm_error.py index 81ee082945..1a70ad0458 100644 --- a/src/calibre/gui2/dialogs/drm_error.py +++ b/src/calibre/gui2/dialogs/drm_error.py @@ -9,6 +9,7 @@ __docformat__ = 'restructuredtext en' from PyQt5.Qt import QDialog from calibre.gui2.dialogs.drm_error_ui import Ui_Dialog + class DRMErrorMessage(QDialog, Ui_Dialog): def __init__(self, parent=None, title=None): diff --git a/src/calibre/gui2/dialogs/duplicates.py b/src/calibre/gui2/dialogs/duplicates.py index 7cbc3fb003..3f06378e15 100644 --- a/src/calibre/gui2/dialogs/duplicates.py +++ b/src/calibre/gui2/dialogs/duplicates.py @@ -16,6 +16,7 @@ from PyQt5.Qt import ( from calibre.gui2 import gprefs from calibre.ebooks.metadata import authors_to_string + class DuplicatesQuestion(QDialog): def __init__(self, db, duplicates, parent=None): diff --git a/src/calibre/gui2/dialogs/edit_authors_dialog.py b/src/calibre/gui2/dialogs/edit_authors_dialog.py index e0cefbd2c2..524a9a174e 100644 --- a/src/calibre/gui2/dialogs/edit_authors_dialog.py +++ b/src/calibre/gui2/dialogs/edit_authors_dialog.py @@ -12,13 +12,16 @@ from calibre.gui2 import error_dialog, gprefs from calibre.gui2.dialogs.edit_authors_dialog_ui import Ui_EditAuthorsDialog from calibre.utils.icu import sort_key + class tableItem(QTableWidgetItem): + def __ge__(self, other): return sort_key(unicode(self.text())) >= sort_key(unicode(other.text())) def __lt__(self, other): return sort_key(unicode(self.text())) < sort_key(unicode(other.text())) + class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog): def __init__(self, parent, db, id_to_select, select_sort, select_link): diff --git a/src/calibre/gui2/dialogs/exim.py b/src/calibre/gui2/dialogs/exim.py index d82c578f80..80eddeda03 100644 --- a/src/calibre/gui2/dialogs/exim.py +++ b/src/calibre/gui2/dialogs/exim.py @@ -22,6 +22,7 @@ from calibre.gui2.widgets2 import Dialog from calibre.utils.exim import all_known_libraries, export, Importer, import_data from calibre.utils.icu import numeric_sort_key + def disk_usage(path_to_dir, abort=None): stack = [path_to_dir] ans = 0 @@ -40,6 +41,7 @@ def disk_usage(path_to_dir, abort=None): pass return ans + class ImportLocation(QWidget): def __init__(self, lpath, parent=None): @@ -66,6 +68,7 @@ class ImportLocation(QWidget): def path(self): return self.le.text().strip() + class RunAction(QDialog): update_current_signal = pyqtSignal(object, object, object) @@ -140,6 +143,7 @@ class RunAction(QDialog): self.tb = traceback.format_exc() self.finish_signal.emit() + class EximDialog(Dialog): update_disk_usage = pyqtSignal(object, object) diff --git a/src/calibre/gui2/dialogs/match_books.py b/src/calibre/gui2/dialogs/match_books.py index bc9e543e94..35a440be25 100644 --- a/src/calibre/gui2/dialogs/match_books.py +++ b/src/calibre/gui2/dialogs/match_books.py @@ -15,6 +15,7 @@ from calibre.gui2 import gprefs, error_dialog from calibre.gui2.dialogs.match_books_ui import Ui_MatchBooks from calibre.utils.icu import sort_key + class TableItem(QTableWidgetItem): ''' A QTableWidgetItem that sorts on a separate string and uses ICU rules @@ -44,6 +45,7 @@ class TableItem(QTableWidgetItem): return self.sort_idx < other.sort_idx return 0 + class MatchBooks(QDialog, Ui_MatchBooks): def __init__(self, gui, view, id_, row_index): diff --git a/src/calibre/gui2/dialogs/message_box.py b/src/calibre/gui2/dialogs/message_box.py index 8a40eceff4..4e2b56ab53 100644 --- a/src/calibre/gui2/dialogs/message_box.py +++ b/src/calibre/gui2/dialogs/message_box.py @@ -14,6 +14,7 @@ from PyQt5.Qt import (QDialog, QIcon, QApplication, QSize, QKeySequence, from calibre.constants import __version__, isfrozen from calibre.gui2 import gprefs + class MessageBox(QDialog): # {{{ ERROR = 0 @@ -166,6 +167,7 @@ class MessageBox(QDialog): # {{{ self.resize_needed.emit() # }}} + class ViewLog(QDialog): # {{{ def __init__(self, title, html, parent=None, unique_name=None): @@ -208,6 +210,7 @@ class ViewLog(QDialog): # {{{ _proceed_memory = [] + class ProceedNotification(MessageBox): # {{{ ''' @@ -279,6 +282,7 @@ class ProceedNotification(MessageBox): # {{{ # }}} + class ErrorNotification(MessageBox): # {{{ def __init__(self, html_log, log_viewer_title, title, msg, @@ -319,6 +323,7 @@ class ErrorNotification(MessageBox): # {{{ _proceed_memory.remove(self) # }}} + class JobError(QDialog): # {{{ WIDTH = 600 diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index 2ca4b24da4..ee9a6646aa 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -28,6 +28,7 @@ from calibre.utils.imghdr import identify from calibre.utils.date import qt_to_dt from calibre.db import _get_next_series_num_for_list + def get_cover_data(stream, ext): # {{{ from calibre.ebooks.metadata.meta import get_metadata old = prefs['read_file_metadata'] @@ -62,6 +63,7 @@ Settings = namedtuple('Settings', null = object() + class MyBlockingBusy(QDialog): # {{{ all_done = pyqtSignal() @@ -138,6 +140,7 @@ class MyBlockingBusy(QDialog): # {{{ if args.do_swap_ta: title_map = cache.all_field_for('title', self.ids) authors_map = cache.all_field_for('authors', self.ids) + def new_title(authors): ans = authors_to_string(authors) return titlecase(ans) if args.do_title_case else ans @@ -153,6 +156,7 @@ class MyBlockingBusy(QDialog): # {{{ if args.do_title_sort: lang_map = cache.all_field_for('languages', self.ids) title_map = cache.all_field_for('title', self.ids) + def get_sort(book_id): if args.languages: lang = args.languages[0] @@ -292,6 +296,7 @@ class MyBlockingBusy(QDialog): # {{{ # }}} + class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): s_r_functions = {'' : lambda x: x, diff --git a/src/calibre/gui2/dialogs/opml.py b/src/calibre/gui2/dialogs/opml.py index 8e4debfa9c..9235eea48d 100644 --- a/src/calibre/gui2/dialogs/opml.py +++ b/src/calibre/gui2/dialogs/opml.py @@ -20,6 +20,7 @@ from calibre.utils.icu import sort_key Group = namedtuple('Group', 'title feeds') + def uniq(vals, kmap=lambda x:x): ''' Remove all duplicates from vals, while preserving order. kmap must be a callable that returns a hashable value for every item in vals ''' @@ -29,6 +30,7 @@ def uniq(vals, kmap=lambda x:x): seen_add = seen.add return tuple(x for x, k in zip(vals, lvals) if k not in seen and not seen_add(k)) + def import_opml(raw, preserve_groups=True): root = etree.fromstring(raw) groups = defaultdict(list) diff --git a/src/calibre/gui2/dialogs/password.py b/src/calibre/gui2/dialogs/password.py index 062d2becb1..08a7ca6251 100644 --- a/src/calibre/gui2/dialogs/password.py +++ b/src/calibre/gui2/dialogs/password.py @@ -6,6 +6,7 @@ from PyQt5.Qt import QDialog, QLineEdit, Qt from calibre.gui2.dialogs.password_ui import Ui_Dialog from calibre.gui2 import dynamic + class PasswordDialog(QDialog, Ui_Dialog): def __init__(self, window, name, msg): diff --git a/src/calibre/gui2/dialogs/plugin_updater.py b/src/calibre/gui2/dialogs/plugin_updater.py index 7b545e9f92..64ed0c1d2e 100644 --- a/src/calibre/gui2/dialogs/plugin_updater.py +++ b/src/calibre/gui2/dialogs/plugin_updater.py @@ -32,6 +32,7 @@ FILTER_INSTALLED = 1 FILTER_UPDATE_AVAILABLE = 2 FILTER_NOT_INSTALLED = 3 + def get_plugin_updates_available(raise_error=False): ''' API exposed to read whether there are updates available for any @@ -48,12 +49,15 @@ def get_plugin_updates_available(raise_error=False): return update_plugins return None + def filter_upgradeable_plugins(display_plugin): return display_plugin.is_upgrade_available() + def filter_not_installed_plugins(display_plugin): return not display_plugin.is_installed() + def read_available_plugins(raise_error=False): import json, bz2 display_plugins = [] @@ -81,6 +85,7 @@ def read_available_plugins(raise_error=False): display_plugins = sorted(display_plugins, key=lambda k: k.name) return display_plugins + def get_installed_plugin_status(display_plugin): display_plugin.installed_version = None display_plugin.plugin = None @@ -110,6 +115,7 @@ class ImageTitleLayout(QHBoxLayout): ''' A reusable layout widget displaying an image followed by a title ''' + def __init__(self, parent, icon_name, title): QHBoxLayout.__init__(self) title_font = QFont() @@ -729,6 +735,7 @@ class PluginUpdaterDialog(SizePersistedDialog): b = d.bb.addButton(_('Restart calibre now'), d.bb.AcceptRole) b.setIcon(QIcon(I('lt.png'))) d.do_restart = False + def rf(): d.do_restart = True b.clicked.connect(rf) diff --git a/src/calibre/gui2/dialogs/progress.py b/src/calibre/gui2/dialogs/progress.py index 1e19c41235..e36bcaf5bb 100644 --- a/src/calibre/gui2/dialogs/progress.py +++ b/src/calibre/gui2/dialogs/progress.py @@ -10,6 +10,7 @@ from PyQt5.Qt import ( from calibre.gui2 import elided_text from calibre.gui2.progress_indicator import ProgressIndicator + class ProgressDialog(QDialog): canceled_signal = pyqtSignal() @@ -66,6 +67,7 @@ class ProgressDialog(QDialog): def value(self): def fset(self, val): return self.bar.setValue(val) + def fget(self): return self.bar.value() return property(fget=fget, fset=fset) @@ -80,6 +82,7 @@ class ProgressDialog(QDialog): def max(self): def fget(self): return self.bar.maximum() + def fset(self, val): self.bar.setMaximum(val) return property(fget=fget, fset=fset) @@ -88,6 +91,7 @@ class ProgressDialog(QDialog): def min(self): def fget(self): return self.bar.minimum() + def fset(self, val): self.bar.setMinimum(val) return property(fget=fget, fset=fset) @@ -96,6 +100,7 @@ class ProgressDialog(QDialog): def title(self): def fget(self): return self.title_label.text() + def fset(self, val): self.title_label.setText(unicode(val or '')) return property(fget=fget, fset=fset) @@ -104,6 +109,7 @@ class ProgressDialog(QDialog): def msg(self): def fget(self): return self.message.text() + def fset(self, val): val = unicode(val or '') self.message.setText(elided_text(val, self.font(), self.message.minimumWidth()-10)) @@ -127,6 +133,7 @@ class ProgressDialog(QDialog): else: QDialog.keyPressEvent(self, ev) + class BlockingBusy(QDialog): def __init__(self, msg, parent=None, window_title=_('Working')): diff --git a/src/calibre/gui2/dialogs/quickview.py b/src/calibre/gui2/dialogs/quickview.py index 0818f05dc2..c2267b4d03 100644 --- a/src/calibre/gui2/dialogs/quickview.py +++ b/src/calibre/gui2/dialogs/quickview.py @@ -14,6 +14,7 @@ from calibre.gui2 import gprefs from calibre.gui2.dialogs.quickview_ui import Ui_Quickview from calibre.utils.icu import sort_key + class TableItem(QTableWidgetItem): ''' @@ -51,6 +52,7 @@ IN_WIDGET_DOCK = 3 IN_WIDGET_SEARCH = 4 IN_WIDGET_CLOSE = 5 + class BooksTableFilter(QObject): return_pressed_signal = pyqtSignal() @@ -61,6 +63,7 @@ class BooksTableFilter(QObject): return True return False + class WidgetFocusFilter(QObject): focus_entered_signal = pyqtSignal(object) @@ -70,6 +73,7 @@ class WidgetFocusFilter(QObject): self.focus_entered_signal.emit(obj) return False + class WidgetTabFilter(QObject): def __init__(self, attach_to_Class, which_widget, tab_signal): @@ -87,6 +91,7 @@ class WidgetTabFilter(QObject): return True return False + class Quickview(QDialog, Ui_Quickview): change_quickview_column = pyqtSignal(object) diff --git a/src/calibre/gui2/dialogs/restore_library.py b/src/calibre/gui2/dialogs/restore_library.py index dbdeae8523..849feb0065 100644 --- a/src/calibre/gui2/dialogs/restore_library.py +++ b/src/calibre/gui2/dialogs/restore_library.py @@ -13,6 +13,7 @@ from calibre.gui2 import (error_dialog, question_dialog, warning_dialog, from calibre import force_unicode from calibre.constants import filesystem_encoding + class DBRestore(QDialog): update_signal = pyqtSignal(object, object) @@ -74,6 +75,7 @@ class DBRestore(QDialog): self.msg.setText(msg) self.pb.setValue(step) + def _show_success_msg(restorer, parent=None): r = restorer olddb = _('The old database was saved as: %s')%force_unicode(r.olddb, @@ -88,6 +90,7 @@ def _show_success_msg(restorer, parent=None): _('Restoring database was successful. %s')%olddb, show=True, show_copy_button=False) + def restore_database(db, parent=None): if not question_dialog(parent, _('Are you sure?'), '

'+ _('Your list of books, with all their metadata is ' @@ -117,6 +120,7 @@ def restore_database(db, parent=None): _show_success_msg(r, parent=parent) return True + def repair_library_at(library_path, parent=None, wait_time=2): d = DBRestore(parent, library_path, wait_time=wait_time) d.exec_() diff --git a/src/calibre/gui2/dialogs/saved_search_editor.py b/src/calibre/gui2/dialogs/saved_search_editor.py index 688c81800c..11864656c0 100644 --- a/src/calibre/gui2/dialogs/saved_search_editor.py +++ b/src/calibre/gui2/dialogs/saved_search_editor.py @@ -10,6 +10,7 @@ from calibre.utils.icu import sort_key from calibre.gui2 import error_dialog from calibre.gui2.dialogs.confirm_delete import confirm + class SavedSearchEditor(QDialog, Ui_SavedSearchEditor): def __init__(self, parent, initial_search=None): diff --git a/src/calibre/gui2/dialogs/scheduler.py b/src/calibre/gui2/dialogs/scheduler.py index 646e85a912..f892a4cc5e 100644 --- a/src/calibre/gui2/dialogs/scheduler.py +++ b/src/calibre/gui2/dialogs/scheduler.py @@ -28,12 +28,14 @@ from calibre.utils.network import internet_connected from calibre import force_unicode from calibre.utils.localization import get_lang, canonicalize_lang + def convert_day_time_schedule(val): day_of_week, hour, minute = val if day_of_week == -1: return (tuple(xrange(7)), hour, minute) return ((day_of_week,), hour, minute) + class RecipesView(QTreeView): def __init__(self, parent): @@ -48,6 +50,8 @@ class RecipesView(QTreeView): self.parent().current_changed(current, previous) # Time/date widgets {{{ + + class Base(QWidget): def __init__(self, parent=None): @@ -56,6 +60,7 @@ class Base(QWidget): self.setLayout(self.l) self.setToolTip(textwrap.dedent(self.HELP)) + class DaysOfWeek(Base): HELP = _('''\ @@ -107,6 +112,7 @@ class DaysOfWeek(Base): hour, minute = t.hour(), t.minute() return 'days_of_week', (days_of_week, int(hour), int(minute)) + class DaysOfMonth(Base): HELP = _('''\ @@ -158,6 +164,7 @@ class DaysOfMonth(Base): hour, minute = t.hour(), t.minute() return 'days_of_month', (days_of_month, int(hour), int(minute)) + class EveryXDays(Base): HELP = _('''\ @@ -533,6 +540,7 @@ class SchedulerDialog(QDialog): if scheduled: typ, sch, last_downloaded = schedule_info d = utcnow() - last_downloaded + def hm(x): return (x-x%3600)//3600, (x%3600 - (x%3600)%60)//60 hours, minutes = hm(d.seconds) diff --git a/src/calibre/gui2/dialogs/search.py b/src/calibre/gui2/dialogs/search.py index 58d77cd2c6..5b010ee046 100644 --- a/src/calibre/gui2/dialogs/search.py +++ b/src/calibre/gui2/dialogs/search.py @@ -18,6 +18,7 @@ from calibre.utils.localization import localize_user_manual_link box_values = {} last_matchkind = CONTAINS_MATCH + def init_dateop(cb): for op, desc in [ ('=', _('equal to')), @@ -28,9 +29,11 @@ def init_dateop(cb): ]: cb.addItem(desc, op) + def current_dateop(cb): return unicode(cb.itemData(cb.currentIndex()) or '') + class SearchDialog(QDialog, Ui_Dialog): def __init__(self, parent, db): diff --git a/src/calibre/gui2/dialogs/select_formats.py b/src/calibre/gui2/dialogs/select_formats.py index 61bb0b40f1..baae2f50f4 100644 --- a/src/calibre/gui2/dialogs/select_formats.py +++ b/src/calibre/gui2/dialogs/select_formats.py @@ -12,6 +12,7 @@ from PyQt5.Qt import QVBoxLayout, QDialog, QLabel, QDialogButtonBox, Qt, \ from calibre.gui2 import file_icon_provider + class Formats(QAbstractListModel): def __init__(self, fmt_count): @@ -45,6 +46,7 @@ class Formats(QAbstractListModel): def fmt(self, idx): return self.fmts[idx.row()] + class SelectFormats(QDialog): def __init__(self, fmt_count, msg, single=False, parent=None, exclude=False): diff --git a/src/calibre/gui2/dialogs/smartdevice.py b/src/calibre/gui2/dialogs/smartdevice.py index 16c391f977..00323c5ef4 100644 --- a/src/calibre/gui2/dialogs/smartdevice.py +++ b/src/calibre/gui2/dialogs/smartdevice.py @@ -11,6 +11,7 @@ from calibre.gui2 import error_dialog from calibre.gui2.dialogs.smartdevice_ui import Ui_Dialog from calibre.utils.mdns import get_all_ips + def _cmp_ipaddr(l, r): lparts = ['%3s'%x for x in l.split('.')] rparts = ['%3s'%x for x in r.split('.')] @@ -25,6 +26,7 @@ def _cmp_ipaddr(l, r): return cmp(lparts, rparts) + def get_all_ip_addresses(): ipaddrs = list() for iface in get_all_ips().itervalues(): @@ -34,6 +36,7 @@ def get_all_ip_addresses(): ipaddrs.sort(cmp=_cmp_ipaddr) return ipaddrs + class SmartdeviceDialog(QDialog, Ui_Dialog): def __init__(self, parent): diff --git a/src/calibre/gui2/dialogs/tag_categories.py b/src/calibre/gui2/dialogs/tag_categories.py index 8d3db9c664..2446ddd9fb 100644 --- a/src/calibre/gui2/dialogs/tag_categories.py +++ b/src/calibre/gui2/dialogs/tag_categories.py @@ -11,6 +11,7 @@ from calibre.gui2 import error_dialog from calibre.constants import islinux from calibre.utils.icu import sort_key, strcmp + class Item: def __init__(self, name, label, index, icon, exists): @@ -19,9 +20,11 @@ class Item: self.index = index self.icon = icon self.exists = exists + def __str__(self): return 'name=%s, label=%s, index=%s, exists='%(self.name, self.label, self.index, self.exists) + class TagCategories(QDialog, Ui_TagCategories): ''' diff --git a/src/calibre/gui2/dialogs/tag_editor.py b/src/calibre/gui2/dialogs/tag_editor.py index 0153ffb10c..7f4cbf21ae 100644 --- a/src/calibre/gui2/dialogs/tag_editor.py +++ b/src/calibre/gui2/dialogs/tag_editor.py @@ -10,6 +10,7 @@ from calibre.gui2 import question_dialog, error_dialog, gprefs from calibre.constants import islinux from calibre.utils.icu import sort_key, primary_contains + class TagEditor(QDialog, Ui_TagEditor): def __init__(self, window, db, id_=None, key=None, current_tags=None): diff --git a/src/calibre/gui2/dialogs/tag_list_editor.py b/src/calibre/gui2/dialogs/tag_list_editor.py index f9c527b1cf..7243894fc9 100644 --- a/src/calibre/gui2/dialogs/tag_list_editor.py +++ b/src/calibre/gui2/dialogs/tag_list_editor.py @@ -9,6 +9,7 @@ from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2 import question_dialog, error_dialog, info_dialog, gprefs from calibre.utils.icu import sort_key + class NameTableWidgetItem(QTableWidgetItem): def __init__(self, txt): @@ -61,6 +62,7 @@ class NameTableWidgetItem(QTableWidgetItem): def __lt__(self, other): return sort_key(unicode(self.text())) < sort_key(unicode(other.text())) + class CountTableWidgetItem(QTableWidgetItem): def __init__(self, count): @@ -73,6 +75,7 @@ class CountTableWidgetItem(QTableWidgetItem): def __lt__(self, other): return self._count < other._count + class EditColumnDelegate(QItemDelegate): def __init__(self, table): @@ -95,6 +98,7 @@ class EditColumnDelegate(QItemDelegate): self.table.item(index.row(), 2).setData(Qt.DisplayRole, '') self.table.blockSignals(False) + class TagListEditor(QDialog, Ui_TagListEditor): def __init__(self, window, cat_name, tag_to_match, data, sorter): diff --git a/src/calibre/gui2/dialogs/template_dialog.py b/src/calibre/gui2/dialogs/template_dialog.py index 637b4b54ba..9c60af4ddc 100644 --- a/src/calibre/gui2/dialogs/template_dialog.py +++ b/src/calibre/gui2/dialogs/template_dialog.py @@ -21,6 +21,7 @@ from calibre.library.coloring import (displayable_columns, color_row_key) from calibre.gui2 import error_dialog, choose_files, pixmap_to_data from calibre.utils.localization import localize_user_manual_link + class ParenPosition: def __init__(self, block, pos, paren): @@ -32,6 +33,7 @@ class ParenPosition: def set_highlight(self, to_what): self.highlight = to_what + class TemplateHighlighter(QSyntaxHighlighter): Config = {} @@ -200,6 +202,7 @@ class TemplateHighlighter(QSyntaxHighlighter): self.rehighlight() self.generate_paren_positions = False + class TemplateDialog(QDialog, Ui_TemplateDialog): def __init__(self, parent, text, mi=None, fm=None, color_field=None, diff --git a/src/calibre/gui2/dialogs/template_line_editor.py b/src/calibre/gui2/dialogs/template_line_editor.py index 7c31394842..8b7ce5a467 100644 --- a/src/calibre/gui2/dialogs/template_line_editor.py +++ b/src/calibre/gui2/dialogs/template_line_editor.py @@ -10,6 +10,7 @@ from PyQt5.Qt import QLineEdit from calibre.gui2.dialogs.template_dialog import TemplateDialog + class TemplateLineEditor(QLineEdit): ''' diff --git a/src/calibre/gui2/dialogs/trim_image.py b/src/calibre/gui2/dialogs/trim_image.py index eeea3e8a23..dde1ff5c72 100644 --- a/src/calibre/gui2/dialogs/trim_image.py +++ b/src/calibre/gui2/dialogs/trim_image.py @@ -15,6 +15,7 @@ from PyQt5.Qt import ( from calibre.gui2 import gprefs from calibre.gui2.tweak_book.editor.canvas import Canvas + class TrimImage(QDialog): def __init__(self, img_data, parent=None): diff --git a/src/calibre/gui2/dnd.py b/src/calibre/gui2/dnd.py index cde3b52964..a46190351a 100644 --- a/src/calibre/gui2/dnd.py +++ b/src/calibre/gui2/dnd.py @@ -21,6 +21,7 @@ from calibre import browser, as_unicode, prints from calibre.gui2 import error_dialog from calibre.utils.imghdr import what + def image_extensions(): if not hasattr(image_extensions, 'ans'): image_extensions.ans = [bytes(x).decode('utf-8') for x in QImageReader.supportedImageFormats()] @@ -29,6 +30,7 @@ def image_extensions(): # This is present for compatibility with old plugins, do not use IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'gif', 'png', 'bmp'] + class Worker(Thread): # {{{ def __init__(self, url, fpath, rq): @@ -51,6 +53,7 @@ class Worker(Thread): # {{{ self.rq.put((a, b, c)) # }}} + class DownloadDialog(QDialog): # {{{ def __init__(self, url, fname, parent): @@ -125,10 +128,12 @@ class DownloadDialog(QDialog): # {{{ # }}} + def dnd_has_image(md): # Chromium puts image data into application/octet-stream return md.hasImage() or md.hasFormat('application/octet-stream') and what(None, bytes(md.data('application/octet-stream'))) in image_extensions() + def data_as_string(f, md): raw = bytes(md.data(f)) if '/x-moz' in f: @@ -138,6 +143,7 @@ def data_as_string(f, md): pass return raw + def urls_from_md(md): ans = list(md.urls()) if md.hasText(): @@ -149,6 +155,7 @@ def urls_from_md(md): ans.append(u) return ans + def path_from_qurl(qurl): raw = bytes(qurl.toEncoded( QUrl.PreferLocalFile | QUrl.RemoveScheme | QUrl.RemovePassword | QUrl.RemoveUserInfo | @@ -158,12 +165,14 @@ def path_from_qurl(qurl): ans = ans[1:] return ans + def remote_urls_from_qurl(qurls, allowed_exts): for qurl in qurls: if qurl.scheme() in {'http', 'https', 'ftp'} and posixpath.splitext( qurl.path())[1][1:].lower() in allowed_exts: yield bytes(qurl.toEncoded()), posixpath.basename(qurl.path()) + def dnd_has_extension(md, extensions, allow_all_extensions=False): if DEBUG: prints('\nDebugging DND event') @@ -187,6 +196,7 @@ def dnd_has_extension(md, extensions, allow_all_extensions=False): return bool(exts) return bool(exts.intersection(frozenset(extensions))) + def dnd_get_image(md, image_exts=None): ''' Get the image in the QMimeData object md. @@ -246,6 +256,7 @@ def dnd_get_image(md, image_exts=None): return None, None + def dnd_get_files(md, exts, allow_all_extensions=False, filter_exts=()): ''' Get the file in the QMimeData object md with an extension that is one of @@ -259,6 +270,7 @@ def dnd_get_files(md, exts, allow_all_extensions=False, filter_exts=()): urls = urls_from_md(md) # First look for a local file local_files = [path_from_qurl(x) for x in urls] + def is_ok(path): ext = posixpath.splitext(path)[1][1:].lower() if allow_all_extensions and ext and ext not in filter_exts: @@ -285,6 +297,7 @@ def dnd_get_files(md, exts, allow_all_extensions=False, filter_exts=()): return None, None + def _get_firefox_pair(md, exts, url, fname): url = bytes(md.data(url)).decode('utf-16') fname = bytes(md.data(fname)).decode('utf-16') @@ -346,6 +359,7 @@ def get_firefox_rurl(md, exts): prints('Firefox rurl:', url, fname) return url, fname + def has_firefox_ext(md, exts): return bool(get_firefox_rurl(md, exts)[0]) diff --git a/src/calibre/gui2/ebook_download.py b/src/calibre/gui2/ebook_download.py index 82bb2cd45f..0d7b3da313 100644 --- a/src/calibre/gui2/ebook_download.py +++ b/src/calibre/gui2/ebook_download.py @@ -39,11 +39,13 @@ class DownloadInfo(MessageBox): def show_again_changed(self): gprefs.set('show_get_books_download_info', self.toggle_checkbox.isChecked()) + def show_download_info(filename, parent=None): if not gprefs.get('show_get_books_download_info', True): return DownloadInfo(filename, parent).exec_() + def get_download_filename(response): filename = get_download_filename_from_response(response) filename, ext = os.path.splitext(filename) @@ -51,6 +53,7 @@ def get_download_filename(response): filename = ascii_filename(filename) return filename + def download_file(url, cookie_file=None, filename=None, create_browser=None): if url.startswith('//'): url = 'http:' + url @@ -122,6 +125,7 @@ class EbookDownload(object): gui_ebook_download = EbookDownload() + def start_ebook_download(callback, job_manager, gui, cookie_file=None, url='', filename='', save_loc='', add_to_lib=True, tags=[], create_browser=None): description = _('Downloading %s') % filename.decode('utf-8', 'ignore') if filename else url.decode('utf-8', 'ignore') job = ThreadedJob('ebook_download', description, gui_ebook_download, ( diff --git a/src/calibre/gui2/email.py b/src/calibre/gui2/email.py index cc9e9fd91a..e7db20bc6e 100644 --- a/src/calibre/gui2/email.py +++ b/src/calibre/gui2/email.py @@ -29,6 +29,7 @@ from calibre.utils.config import tweaks, prefs from calibre.utils.icu import sort_key from calibre.gui2.threaded_jobs import ThreadedJob + class Worker(Thread): def __init__(self, func, args): @@ -117,6 +118,7 @@ class Sendmail(object): eto = [] for x in to.split(','): eto.append(extract_email_address(x.strip())) + def safe_debug(*args, **kwargs): try: return log.debug(*args, **kwargs) @@ -180,6 +182,7 @@ def email_news(mi, remove, get_fmts, done, job_manager): plugboard_email_value = 'email' plugboard_email_formats = ['epub', 'mobi', 'azw3'] + class SelectRecipients(QDialog): # {{{ def __init__(self, parent=None): @@ -284,6 +287,7 @@ class SelectRecipients(QDialog): # {{{ ans.append((to, fmts, subject)) return ans + def select_recipients(parent=None): d = SelectRecipients(parent) if d.exec_() == d.Accepted: @@ -291,6 +295,7 @@ def select_recipients(parent=None): return () # }}} + class EmailMixin(object): # {{{ def __init__(self, *args, **kwargs): @@ -474,6 +479,7 @@ class EmailMixin(object): # {{{ index_is_id=True) remove = [id_] if config['delete_news_from_library_on_upload'] \ else [] + def get_fmts(fmts): files, auto = self.library_view.model().\ get_preferred_formats_from_ids([id_], fmts, diff --git a/src/calibre/gui2/font_family_chooser.py b/src/calibre/gui2/font_family_chooser.py index b4ad3331c2..7e01474822 100644 --- a/src/calibre/gui2/font_family_chooser.py +++ b/src/calibre/gui2/font_family_chooser.py @@ -46,6 +46,7 @@ def add_fonts(parent): return families + def writing_system_for_font(font): has_latin = True systems = QFontDatabase().writingSystems(font.family()) @@ -85,6 +86,7 @@ def writing_system_for_font(font): return system, has_latin + class FontFamilyDelegate(QStyledItemDelegate): def sizeHint(self, option, index): @@ -143,6 +145,7 @@ class FontFamilyDelegate(QStyledItemDelegate): r.setLeft(r.left() + w) painter.drawText(r, Qt.AlignVCenter|Qt.AlignLeading|Qt.TextSingleLine, sample) + class Typefaces(QLabel): def __init__(self, parent=None): @@ -176,6 +179,7 @@ class Typefaces(QLabel): msg = msg.format('\n\n'.join(entries)) self.setText(msg) + class FontsView(QListView): changed = pyqtSignal() @@ -319,6 +323,7 @@ class FontFamilyDialog(QDialog): self.faces.show_family(fam, self.font_scanner.fonts_for_family(fam) if fam else None) + class FontFamilyChooser(QWidget): family_changed = pyqtSignal(object) @@ -351,6 +356,7 @@ class FontFamilyChooser(QWidget): def font_family(self): def fget(self): return self._current_family + def fset(self, val): if not val: val = None @@ -364,6 +370,7 @@ class FontFamilyChooser(QWidget): if d.exec_() == d.Accepted: self.font_family = d.font_family + def test(): app = QApplication([]) app diff --git a/src/calibre/gui2/icon_theme.py b/src/calibre/gui2/icon_theme.py index c486e688bf..299bc225fe 100644 --- a/src/calibre/gui2/icon_theme.py +++ b/src/calibre/gui2/icon_theme.py @@ -47,12 +47,14 @@ BASE_URL = 'https://code.calibre-ebook.com/icon-themes/' COVER_SIZE = (340, 272) + def render_svg(filepath): must_use_qt(headless=False) pngpath = filepath[:-4] + '.png' i = QImage(filepath) i.save(pngpath) + def read_images_from_folder(path): name_map = {} path = os.path.abspath(path) @@ -71,12 +73,14 @@ def read_images_from_folder(path): name_map[name] = filepath return name_map + class Theme(object): def __init__(self, title='', author='', version=-1, description='', license='Unknown', url=None, cover=None): self.title, self.author, self.version, self.description = title, author, version, description self.license, self.cover, self.url = license, cover, url + class Report(object): def __init__(self, path, name_map, extra, missing, theme): @@ -87,6 +91,7 @@ class Report(object): def name(self): return ascii_filename(self.theme.title).replace(' ', '_').replace('.', '_').lower() + def read_theme_from_folder(path): path = os.path.abspath(path) current_image_map = read_images_from_folder(P('images', allow_user_override=False)) @@ -107,6 +112,7 @@ def read_theme_from_folder(path): except ValueError: # Corrupted metadata file metadata = {} + def safe_int(x): try: return int(x) @@ -125,6 +131,7 @@ def read_theme_from_folder(path): theme.cover = create_cover(ans) return ans + def icon_for_action(name): for plugin in interface_actions(): if plugin.name == name: @@ -135,6 +142,7 @@ def icon_for_action(name): if icon: return icon + def default_cover_icons(cols=5): count = 0 for ac in gprefs.defaults['action-layout-toolbar']: @@ -152,6 +160,7 @@ def default_cover_icons(cols=5): del extra[0] count += 1 + def create_cover(report, icons=(), cols=5, size=120, padding=16): icons = icons or tuple(default_cover_icons(cols)) rows = int(math.ceil(len(icons) / cols)) @@ -176,6 +185,7 @@ def create_cover(report, icons=(), cols=5, size=120, padding=16): canvas.compose(img, x + dx, y) return canvas.export() + def verify_theme(report): must_use_qt() report.bad = bad = {} @@ -186,6 +196,7 @@ def verify_theme(report): bad[name] = reader.errorString() return bool(bad) + class ThemeCreateDialog(Dialog): def __init__(self, parent, report): @@ -284,6 +295,7 @@ class ThemeCreateDialog(Dialog): 'You must specify an author for this icon theme'), show=True) return Dialog.accept(self) + class Compress(QProgressDialog): update_signal = pyqtSignal(object, object) @@ -325,6 +337,7 @@ class Compress(QProgressDialog): else: self.update_signal.emit(self.maximum(), '') + def create_themeball(report, progress=None, abort=None): pool = ThreadPool(processes=cpu_count()) buf = BytesIO() @@ -412,6 +425,7 @@ def create_theme(folder=None, parent=None): # Choose Theme {{{ + def download_cover(cover_url, etag=None, cached=b''): url = BASE_URL + cover_url headers = {} @@ -429,6 +443,7 @@ def download_cover(cover_url, etag=None, cached=b''): return cached, etag raise + def get_cover(metadata): cdir = os.path.join(cache_dir(), 'icon-theme-covers') try: @@ -436,9 +451,11 @@ def get_cover(metadata): except EnvironmentError as e: if e.errno != errno.EEXIST: raise + def path(ext): return os.path.join(cdir, metadata['name'] + '.' + ext) etag_file, cover_file = map(path, 'etag jpg'.split()) + def safe_read(path): try: with open(path, 'rb') as f: @@ -457,6 +474,7 @@ def get_cover(metadata): f.write(etag) return cached or b'' + def get_covers(themes, callback, num_of_workers=8): items = Queue() tuple(map(items.put, themes)) @@ -481,6 +499,7 @@ def get_covers(themes, callback, num_of_workers=8): t.daemon = True t.start() + class Delegate(QStyledItemDelegate): SPACING = 10 @@ -517,6 +536,7 @@ class Delegate(QStyledItemDelegate): painter.drawStaticText(COVER_SIZE[0] + self.SPACING, option.rect.top() + self.SPACING, theme['static-text']) painter.restore() + class DownloadProgress(ProgressDialog): ds = pyqtSignal(object) @@ -539,6 +559,7 @@ class DownloadProgress(ProgressDialog): def queue_reject(self): self.rej.emit() + class ChooseTheme(Dialog): cover_downloaded = pyqtSignal(object, object) @@ -581,6 +602,7 @@ class ChooseTheme(Dialog): self.w = w = QWidget(self) l.addWidget(w) w.l = l = QGridLayout(w) + def add_row(x, y=None): if isinstance(x, type('')): x = QLabel(x) @@ -749,6 +771,7 @@ class ChooseTheme(Dialog): if ret == d.Rejected or not self.keep_downloading or d.canceled or self.downloaded_theme is None: return dt = self.downloaded_theme + def commit_changes(): dt.seek(0) f = decompress(dt) @@ -761,6 +784,7 @@ class ChooseTheme(Dialog): # }}} + def remove_icon_theme(): icdir = os.path.join(config_dir, 'resources', 'images') metadata_file = os.path.join(icdir, 'icon-theme.json') @@ -779,12 +803,14 @@ def remove_icon_theme(): raise os.remove(metadata_file) + def safe_copy(src, destpath): tpath = destpath + '-temp' with open(tpath, 'wb') as dest: shutil.copyfileobj(src, dest) atomic_rename(tpath, destpath) + def install_icon_theme(theme, f): icdir = os.path.abspath(os.path.join(config_dir, 'resources', 'images')) if not os.path.exists(icdir): diff --git a/src/calibre/gui2/init.py b/src/calibre/gui2/init.py index 0aad9cadee..a087dbdfa5 100644 --- a/src/calibre/gui2/init.py +++ b/src/calibre/gui2/init.py @@ -25,11 +25,13 @@ from calibre.gui2.notify import get_notifier _keep_refs = [] + def partial(*args, **kwargs): ans = functools.partial(*args, **kwargs) _keep_refs.append(ans) return ans + class LibraryViewMixin(object): # {{{ def __init__(self, *args, **kwargs): @@ -100,6 +102,7 @@ class LibraryViewMixin(object): # {{{ # }}} + class QuickviewSplitter(QSplitter): # {{{ def __init__(self, parent=None, orientation=Qt.Vertical, qv_widget=None): @@ -131,6 +134,7 @@ class QuickviewSplitter(QSplitter): # {{{ self.qv_widget.hide() # }}} + class LibraryWidget(Splitter): # {{{ def __init__(self, parent): @@ -166,6 +170,7 @@ class LibraryWidget(Splitter): # {{{ self.addWidget(parent.quickview_splitter) # }}} + class Stack(QStackedWidget): # {{{ def __init__(self, parent): @@ -204,6 +209,7 @@ class UpdateLabel(QLabel): # {{{ pass # }}} + class StatusBar(QStatusBar): # {{{ def __init__(self, parent=None): @@ -281,6 +287,7 @@ class StatusBar(QStatusBar): # {{{ # }}} + class GridViewButton(LayoutButton): # {{{ def __init__(self, gui): @@ -453,6 +460,7 @@ class VLTabs(QTabBar): # {{{ # }}} + class LayoutMixin(object): # {{{ def __init__(self, *args, **kwargs): diff --git a/src/calibre/gui2/job_indicator.py b/src/calibre/gui2/job_indicator.py index fab90baaf6..24ccff629b 100644 --- a/src/calibre/gui2/job_indicator.py +++ b/src/calibre/gui2/job_indicator.py @@ -13,6 +13,7 @@ from PyQt5.Qt import (QPainter, Qt, QWidget, QPropertyAnimation, QRect, QPoint, from calibre.gui2 import config + class Pointer(QWidget): def __init__(self, gui): diff --git a/src/calibre/gui2/jobs.py b/src/calibre/gui2/jobs.py index 76af5b6d9d..8a6be2a29a 100644 --- a/src/calibre/gui2/jobs.py +++ b/src/calibre/gui2/jobs.py @@ -31,6 +31,7 @@ from calibre.gui2.widgets2 import Dialog from calibre.utils.search_query_parser import SearchQueryParser, ParseException from calibre.utils.icu import lower + class AdaptSQP(SearchQueryParser): def __init__(self, *args, **kwargs): @@ -353,6 +354,7 @@ class JobManager(QAbstractTableModel, AdaptSQP): # {{{ # }}} + class FilterModel(QSortFilterProxyModel): # {{{ search_done = pyqtSignal(object) @@ -389,6 +391,7 @@ class FilterModel(QSortFilterProxyModel): # {{{ # Jobs UI {{{ + class ProgressBarDelegate(QAbstractItemDelegate): # {{{ def sizeHint(self, option, index): @@ -409,6 +412,7 @@ class ProgressBarDelegate(QAbstractItemDelegate): # {{{ QApplication.style().drawControl(QStyle.CE_ProgressBar, opts, painter) # }}} + class DetailView(Dialog): # {{{ def __init__(self, parent, job): @@ -454,6 +458,7 @@ class DetailView(Dialog): # {{{ self.log.appendPlainText(more.decode('utf-8', 'replace')) # }}} + class JobsButton(QFrame): # {{{ tray_tooltip_updated = pyqtSignal(object) @@ -556,6 +561,7 @@ class JobsButton(QFrame): # {{{ # }}} + class JobsDialog(QDialog, Ui_JobsDialog): def __init__(self, window, model): diff --git a/src/calibre/gui2/keyboard.py b/src/calibre/gui2/keyboard.py index cc4f258100..fa63545cfb 100644 --- a/src/calibre/gui2/keyboard.py +++ b/src/calibre/gui2/keyboard.py @@ -26,9 +26,11 @@ from calibre.gui2.search_box import SearchBox2 ROOT = QModelIndex() + class NameConflict(ValueError): pass + def keysequence_from_event(ev): # {{{ k, mods = ev.key(), int(ev.modifiers()) if k in ( @@ -44,6 +46,7 @@ def keysequence_from_event(ev): # {{{ return QKeySequence(k | mods) # }}} + def finalize(shortcuts, custom_keys_map={}): # {{{ ''' Resolve conflicts and assign keys to every action in shortcuts, which must @@ -87,6 +90,7 @@ def finalize(shortcuts, custom_keys_map={}): # {{{ # }}} + class Manager(QObject): # {{{ def __init__(self, parent=None, config_name='shortcuts/main'): @@ -159,6 +163,7 @@ class Manager(QObject): # {{{ # Model {{{ + class Node(object): def __init__(self, group_map, shortcut_map, name=None, shortcut=None): @@ -179,6 +184,7 @@ class Node(object): for child in self.children: yield child + class ConfigModel(SearchQueryParser, QAbstractItemModel): def __init__(self, keyboard, parent=None): @@ -360,6 +366,7 @@ class ConfigModel(SearchQueryParser, QAbstractItemModel): # }}} + class Editor(QFrame): # {{{ editing_done = pyqtSignal(object) @@ -614,6 +621,7 @@ class Delegate(QStyledItemDelegate): # {{{ # }}} + class ShortcutConfig(QWidget): # {{{ changed_signal = pyqtSignal() diff --git a/src/calibre/gui2/languages.py b/src/calibre/gui2/languages.py index a2397e8ba3..b5a954096a 100644 --- a/src/calibre/gui2/languages.py +++ b/src/calibre/gui2/languages.py @@ -14,6 +14,7 @@ from calibre.utils.icu import sort_key, lower _lang_map = None + def get_lang_map(): global _lang_map if _lang_map is None: @@ -22,6 +23,7 @@ def get_lang_map(): _lang_map.pop(x, None) return _lang_map + class LanguagesEdit(EditWithComplete): def __init__(self, parent=None, db=None, prefs=None): diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index ae17c8a30f..3218417be0 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -20,6 +20,7 @@ from calibre.gui2.widgets2 import RightClickButton from calibre.utils.config_base import tweaks from calibre import human_readable + class LocationManager(QObject): # {{{ locations_changed = pyqtSignal() @@ -169,6 +170,7 @@ class LocationManager(QObject): # {{{ # }}} + class SearchBar(QWidget): # {{{ def __init__(self, parent): @@ -257,6 +259,7 @@ class SearchBar(QWidget): # {{{ # }}} + class Spacer(QWidget): # {{{ def __init__(self, parent): @@ -266,6 +269,7 @@ class Spacer(QWidget): # {{{ self.l.addStretch(10) # }}} + class MainWindowMixin(object): # {{{ def __init__(self, *args, **kwargs): diff --git a/src/calibre/gui2/library/alternate_views.py b/src/calibre/gui2/library/alternate_views.py index e269b598c7..826c5d9e1d 100644 --- a/src/calibre/gui2/library/alternate_views.py +++ b/src/calibre/gui2/library/alternate_views.py @@ -32,12 +32,15 @@ from calibre.utils.config import prefs, tweaks CM_TO_INCH = 0.393701 CACHE_FORMAT = 'PPM' + def auto_height(widget): return max(185, QApplication.instance().desktop().availableGeometry(widget).height() / 5.0) + class EncodeError(ValueError): pass + def image_to_data(image): # {{{ ba = QByteArray() buf = QBuffer(ba) @@ -50,14 +53,18 @@ def image_to_data(image): # {{{ # }}} # Drag 'n Drop {{{ + + def dragMoveEvent(self, event): event.acceptProposedAction() + def event_has_mods(self, event=None): mods = event.modifiers() if event is not None else \ QApplication.keyboardModifiers() return mods & Qt.ControlModifier or mods & Qt.ShiftModifier + def mousePressEvent(base_class, self, event): ep = event.pos() if self.indexAt(ep) in self.selectionModel().selectedIndexes() and \ @@ -67,6 +74,7 @@ def mousePressEvent(base_class, self, event): return self.handle_mouse_press_event(event) return base_class.mousePressEvent(self, event) + def drag_icon(self, cover, multiple): cover = cover.scaledToHeight(120, Qt.SmoothTransformation) if multiple: @@ -95,6 +103,7 @@ def drag_icon(self, cover, multiple): cover = base return QPixmap.fromImage(cover) + def drag_data(self): m = self.model() db = m.db @@ -138,6 +147,7 @@ def drag_data(self): drag.setPixmap(cover) return drag + def mouseMoveEvent(base_class, self, event): if not self.drag_allowed: return @@ -160,6 +170,7 @@ def mouseMoveEvent(base_class, self, event): drag.exec_(Qt.CopyAction) self.drag_start_pos = None + def dragEnterEvent(self, event): if int(event.possibleActions() & Qt.CopyAction) + \ int(event.possibleActions() & Qt.MoveAction) == 0: @@ -169,12 +180,14 @@ def dragEnterEvent(self, event): if paths: event.acceptProposedAction() + def dropEvent(self, event): paths = self.paths_from_event(event) event.setDropAction(Qt.CopyAction) event.accept() self.files_dropped.emit(paths) + def paths_from_event(self, event): ''' Accept a drop event and return a list of paths that can be read from @@ -187,6 +200,7 @@ def paths_from_event(self, event): return [u for u in urls if os.path.splitext(u)[1] and os.path.exists(u)] + def setup_dnd_interface(cls_or_self): if isinstance(cls_or_self, type): cls = cls_or_self @@ -210,6 +224,8 @@ def setup_dnd_interface(cls_or_self): # }}} # Manage slave views {{{ + + def sync(func): @wraps(func) def ans(self, *args, **kwargs): @@ -219,6 +235,7 @@ def sync(func): return func(self, *args, **kwargs) return ans + class AlternateViews(object): def __init__(self, main_view): @@ -309,6 +326,8 @@ class AlternateViews(object): # }}} # Rendering of covers {{{ + + class CoverDelegate(QStyledItemDelegate): MARGIN = 4 @@ -606,6 +625,8 @@ class CoverDelegate(QStyledItemDelegate): # }}} # The View {{{ + + @setup_dnd_interface class GridView(QListView): diff --git a/src/calibre/gui2/library/caches.py b/src/calibre/gui2/library/caches.py index a7a5c082d5..6f5034ef95 100644 --- a/src/calibre/gui2/library/caches.py +++ b/src/calibre/gui2/library/caches.py @@ -13,13 +13,16 @@ from PyQt5.Qt import QImage, QPixmap from calibre.db.utils import ThumbnailCache as TC + class ThumbnailCache(TC): + def __init__(self, max_size=1024, thumbnail_size=(100, 100)): TC.__init__(self, name='gui-thumbnail-cache', min_disk_cache=100, max_size=max_size, thumbnail_size=thumbnail_size) def set_database(self, db): TC.set_group_id(self, db.library_id) + class CoverCache(dict): ' This is a RAM cache to speed up rendering of covers by storing them as QPixmaps ' diff --git a/src/calibre/gui2/library/delegates.py b/src/calibre/gui2/library/delegates.py index b655a393a5..7511e739b4 100644 --- a/src/calibre/gui2/library/delegates.py +++ b/src/calibre/gui2/library/delegates.py @@ -26,6 +26,7 @@ from calibre.gui2.dialogs.template_dialog import TemplateDialog from calibre.gui2.dialogs.tag_editor import TagEditor from calibre.gui2.languages import LanguagesEdit + class UpdateEditorGeometry(object): def updateEditorGeometry(self, editor, option, index): @@ -97,6 +98,7 @@ class UpdateEditorGeometry(object): initial_geometry.adjust(delta_x, 0, delta_width, 0) editor.setGeometry(initial_geometry) + class DateTimeEdit(QDateTimeEdit): # {{{ def __init__(self, parent, format): @@ -131,6 +133,7 @@ class DateTimeEdit(QDateTimeEdit): # {{{ # Number Editor {{{ + def make_clearing_spinbox(spinbox): class SpinBox(spinbox): @@ -160,10 +163,12 @@ ClearingDoubleSpinBox = make_clearing_spinbox(QDoubleSpinBox) # setter for text-like delegates. Return '' if CTRL is pushed {{{ + def check_key_modifier(which_modifier): v = int(QApplication.keyboardModifiers() & (Qt.ControlModifier + Qt.ShiftModifier)) return v == which_modifier + def get_val_for_textlike_columns(index_): if check_key_modifier(Qt.ControlModifier): ct = '' @@ -173,6 +178,7 @@ def get_val_for_textlike_columns(index_): # }}} + class RatingDelegate(QStyledItemDelegate, UpdateEditorGeometry): # {{{ def __init__(self, *args, **kwargs): @@ -250,6 +256,7 @@ class DateDelegate(QStyledItemDelegate, UpdateEditorGeometry): # {{{ # }}} + class PubDateDelegate(QStyledItemDelegate, UpdateEditorGeometry): # {{{ def __init__(self, *args, **kwargs): @@ -282,6 +289,7 @@ class PubDateDelegate(QStyledItemDelegate, UpdateEditorGeometry): # {{{ # }}} + class TextDelegate(QStyledItemDelegate, UpdateEditorGeometry): # {{{ def __init__(self, parent): @@ -320,6 +328,7 @@ class TextDelegate(QStyledItemDelegate, UpdateEditorGeometry): # {{{ # }}} + class CompleteDelegate(QStyledItemDelegate, UpdateEditorGeometry): # {{{ def __init__(self, parent, sep, items_func_name, space_before_sep=False): @@ -370,6 +379,7 @@ class CompleteDelegate(QStyledItemDelegate, UpdateEditorGeometry): # {{{ QStyledItemDelegate.setModelData(self, editor, model, index) # }}} + class LanguagesDelegate(QStyledItemDelegate, UpdateEditorGeometry): # {{{ def __init__(self, parent): @@ -390,6 +400,7 @@ class LanguagesDelegate(QStyledItemDelegate, UpdateEditorGeometry): # {{{ model.setData(index, (val), Qt.EditRole) # }}} + class CcDateDelegate(QStyledItemDelegate, UpdateEditorGeometry): # {{{ ''' @@ -436,6 +447,7 @@ class CcDateDelegate(QStyledItemDelegate, UpdateEditorGeometry): # {{{ # }}} + class CcTextDelegate(QStyledItemDelegate, UpdateEditorGeometry): # {{{ ''' @@ -474,6 +486,7 @@ class CcTextDelegate(QStyledItemDelegate, UpdateEditorGeometry): # {{{ model.setData(index, val, Qt.EditRole) # }}} + class CcLongTextDelegate(QStyledItemDelegate): # {{{ ''' @@ -500,6 +513,7 @@ class CcLongTextDelegate(QStyledItemDelegate): # {{{ model.setData(index, (editor.textbox.html), Qt.EditRole) # }}} + class CcNumberDelegate(QStyledItemDelegate, UpdateEditorGeometry): # {{{ ''' @@ -550,6 +564,7 @@ class CcNumberDelegate(QStyledItemDelegate, UpdateEditorGeometry): # {{{ # }}} + class CcEnumDelegate(QStyledItemDelegate, UpdateEditorGeometry): # {{{ ''' @@ -597,6 +612,7 @@ class CcEnumDelegate(QStyledItemDelegate, UpdateEditorGeometry): # {{{ editor.setCurrentIndex(idx) # }}} + class CcCommentsDelegate(QStyledItemDelegate): # {{{ ''' @@ -646,6 +662,7 @@ class CcCommentsDelegate(QStyledItemDelegate): # {{{ model.setData(index, (editor.textbox.html), Qt.EditRole) # }}} + class DelegateCB(QComboBox): # {{{ def __init__(self, parent): @@ -657,6 +674,7 @@ class DelegateCB(QComboBox): # {{{ return QComboBox.event(self, e) # }}} + class CcBoolDelegate(QStyledItemDelegate, UpdateEditorGeometry): # {{{ def __init__(self, parent): @@ -701,6 +719,7 @@ class CcBoolDelegate(QStyledItemDelegate, UpdateEditorGeometry): # {{{ # }}} + class CcTemplateDelegate(QStyledItemDelegate): # {{{ def __init__(self, parent): diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index 48a4e1ece6..4b5df4d163 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -33,6 +33,7 @@ from calibre.library.coloring import color_row_key Counts = namedtuple('Counts', 'library_total total current') + def human_readable(size, precision=1): """ Convert a size in bytes into megabytes """ return ('%.'+str(precision)+'f') % ((size/(1024.*1024.)),) @@ -44,12 +45,14 @@ ALIGNMENT_MAP = {'left': Qt.AlignLeft, 'right': Qt.AlignRight, 'center': _default_image = None + def default_image(): global _default_image if _default_image is None: _default_image = QImage(I('default_cover.png')) return _default_image + def group_numbers(numbers): for k, g in groupby(enumerate(sorted(numbers)), lambda (i, x):i - x): first = None @@ -58,6 +61,7 @@ def group_numbers(numbers): first = last[1] yield first, last[1] + class ColumnColor(object): # {{{ def __init__(self, formatter): @@ -86,6 +90,7 @@ class ColumnColor(object): # {{{ pass # }}} + class ColumnIcon(object): # {{{ def __init__(self, formatter, model): @@ -156,6 +161,7 @@ class ColumnIcon(object): # {{{ pass # }}} + class BooksModel(QAbstractTableModel): # {{{ about_to_be_sorted = pyqtSignal(object, name='aboutToBeSorted') @@ -280,6 +286,7 @@ class BooksModel(QAbstractTableModel): # {{{ self.custom_columns = self.db.field_metadata.custom_field_metadata() self.column_map = list(self.orig_headers.keys()) + \ list(self.custom_columns) + def col_idx(name): if name == 'ondevice': return -1 @@ -632,6 +639,7 @@ class BooksModel(QAbstractTableModel): # {{{ except: traceback.print_exc() pt.close() + def to_uni(x): if isbytestring(x): x = x.decode(filesystem_encoding) @@ -726,6 +734,7 @@ class BooksModel(QAbstractTableModel): # {{{ bt = self.db.new_api.pref('bools_are_tristate') bn = self.bool_no_icon by = self.bool_yes_icon + def func(idx): val = force_to_bool(fffunc(field_obj, idfunc(idx))) if val is None: @@ -733,6 +742,7 @@ class BooksModel(QAbstractTableModel): # {{{ return by if val else bn elif field == 'size': sz_mult = 1.0/(1024**2) + def func(idx): val = fffunc(field_obj, idfunc(idx), default_value=0) or 0 if val is 0: @@ -745,6 +755,7 @@ class BooksModel(QAbstractTableModel): # {{{ elif field == 'ondevice' and decorator: by = self.bool_yes_icon bb = self.bool_blank_icon + def func(idx): return by if fffunc(field_obj, idfunc(idx)) else bb elif dt in {'text', 'comments', 'composite', 'enumeration'}: @@ -754,6 +765,7 @@ class BooksModel(QAbstractTableModel): # {{{ if field_obj.is_composite: if do_sort: sv = m['is_multiple']['cache_to_list'] + def func(idx): val = fffunc(field_obj, idfunc(idx), default_value='') or '' return (jv.join(sorted((x.strip() for x in val.split(sv)), key=sort_key))) @@ -780,10 +792,12 @@ class BooksModel(QAbstractTableModel): # {{{ return (QDateTime(as_local_time(fffunc(field_obj, idfunc(idx), default_value=UNDEFINED_DATE)))) elif dt == 'rating': rating_fields[field] = m['display'].get('allow_half_stars', False) + def func(idx): return int(fffunc(field_obj, idfunc(idx), default_value=0)) elif dt == 'series': sidx_field = self.db.new_api.fields[field + '_index'] + def func(idx): book_id = idfunc(idx) series = fffunc(field_obj, book_id, default_value=False) @@ -792,6 +806,7 @@ class BooksModel(QAbstractTableModel): # {{{ return None elif dt in {'int', 'float'}: fmt = m['display'].get('number_format', None) + def func(idx): val = fffunc(field_obj, idfunc(idx)) if val is None: @@ -822,6 +837,7 @@ class BooksModel(QAbstractTableModel): # {{{ self.dc_decorator[col] = renderer(col, 'bool') tc = self.dc.copy() + def stars_tooltip(func, allow_half=True): def f(idx): ans = val = int(func(idx)) @@ -1148,6 +1164,7 @@ class BooksModel(QAbstractTableModel): # {{{ # }}} + class OnDeviceSearch(SearchQueryParser): # {{{ USABLE_LOCATIONS = [ @@ -1239,6 +1256,7 @@ class OnDeviceSearch(SearchQueryParser): # {{{ # }}} + class DeviceDBSortKeyGen(object): # {{{ def __init__(self, attr, keyfunc, db): @@ -1254,6 +1272,7 @@ class DeviceDBSortKeyGen(object): # {{{ return ans # }}} + class DeviceBooksModel(BooksModel): # {{{ booklist_dirtied = pyqtSignal() @@ -1406,6 +1425,7 @@ class DeviceBooksModel(BooksModel): # {{{ def sort(self, col, order, reset=True): descending = order != Qt.AscendingOrder cname = self.column_map[col] + def author_key(x): try: ax = self.db[x].author_sort diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py index 06c7c1562d..685d588629 100644 --- a/src/calibre/gui2/library/views.py +++ b/src/calibre/gui2/library/views.py @@ -28,6 +28,7 @@ from calibre.gui2.library import DEFAULT_SORT from calibre.constants import filesystem_encoding from calibre import force_unicode + class HeaderView(QHeaderView): # {{{ def __init__(self, *args): @@ -110,6 +111,7 @@ class HeaderView(QHeaderView): # {{{ painter.restore() # }}} + class PreserveViewState(object): # {{{ ''' @@ -171,6 +173,7 @@ class PreserveViewState(object): # {{{ self.__enter__() return {x:getattr(self, x) for x in ('selected_ids', 'current_id', 'vscroll', 'hscroll')} + def fset(self, state): for k, v in state.iteritems(): setattr(self, k, v) @@ -179,6 +182,7 @@ class PreserveViewState(object): # {{{ # }}} + @setup_dnd_interface class BooksView(QTableView): # {{{ @@ -741,6 +745,7 @@ class BooksView(QTableView): # {{{ if bool(old_marked) == bool(current_marked): changed = old_marked | current_marked i = self.model().db.data.id_to_index + def f(x): try: return i(x) @@ -1026,6 +1031,7 @@ class BooksView(QTableView): # {{{ except: pass return None + def fset(self, val): if val is None: return @@ -1116,6 +1122,7 @@ class BooksView(QTableView): # {{{ # }}} + class DeviceBooksView(BooksView): # {{{ is_library_view = False diff --git a/src/calibre/gui2/lrf_renderer/bookview.py b/src/calibre/gui2/lrf_renderer/bookview.py index 2c1d6628fb..c826004fef 100644 --- a/src/calibre/gui2/lrf_renderer/bookview.py +++ b/src/calibre/gui2/lrf_renderer/bookview.py @@ -3,6 +3,7 @@ __copyright__ = '2008, Kovid Goyal ' from PyQt5.Qt import QGraphicsView, QSize + class BookView(QGraphicsView): MINIMUM_SIZE = QSize(400, 500) diff --git a/src/calibre/gui2/lrf_renderer/document.py b/src/calibre/gui2/lrf_renderer/document.py index b29147ea22..81983e7bc3 100644 --- a/src/calibre/gui2/lrf_renderer/document.py +++ b/src/calibre/gui2/lrf_renderer/document.py @@ -18,6 +18,7 @@ class Color(QColor): def __init__(self, color): QColor.__init__(self, color.r, color.g, color.b, 0xff-color.a) + class Pen(QPen): def __init__(self, color, width): @@ -70,6 +71,7 @@ def object_factory(container, obj, respect_max_y=False): container.ruby_tags, container.link_activated) return None + class _Canvas(QGraphicsRectItem): def __init__(self, font_loader, logger, opts, width=0, height=0, parent=None, x=0, y=0): @@ -209,6 +211,7 @@ class Canvas(_Canvas, ContentObject): block.reset() _Canvas.layout_block(self, block, x, y) + class Header(Canvas): def __init__(self, font_loader, header, page_style, logger, opts, ruby_tags, link_activated): @@ -217,6 +220,7 @@ class Header(Canvas): if opts.visual_debug: self.setPen(QPen(Qt.blue, 1, Qt.DashLine)) + class Footer(Canvas): def __init__(self, font_loader, footer, page_style, logger, opts, ruby_tags, link_activated): @@ -225,6 +229,7 @@ class Footer(Canvas): if opts.visual_debug: self.setPen(QPen(Qt.blue, 1, Qt.DashLine)) + class Screen(_Canvas): def __init__(self, font_loader, chapter, odd, logger, opts, ruby_tags, link_activated): diff --git a/src/calibre/gui2/lrf_renderer/main.py b/src/calibre/gui2/lrf_renderer/main.py index 76ee8ef164..8c0d1c1d09 100644 --- a/src/calibre/gui2/lrf_renderer/main.py +++ b/src/calibre/gui2/lrf_renderer/main.py @@ -17,6 +17,7 @@ from calibre.gui2.main_window import MainWindow from calibre.gui2.lrf_renderer.document import Document from calibre.gui2.search_box import SearchBox2 + class RenderWorker(QThread): def __init__(self, parent, lrf_stream, logger, opts): @@ -45,6 +46,7 @@ class RenderWorker(QThread): self.aborted = True self.lrf.keep_parsing = False + class Config(QDialog, Ui_ViewerConfig): def __init__(self, parent, opts): @@ -54,6 +56,7 @@ class Config(QDialog, Ui_ViewerConfig): self.white_background.setChecked(opts.white_background) self.hyphenate.setChecked(opts.hyphenate) + class Main(MainWindow, Ui_MainWindow): def create_document(self): @@ -283,6 +286,7 @@ Read the LRF ebook book.lrf help=_('Profile the LRF renderer')) return parser + def normalize_settings(parser, opts): saved_opts = config['LRF_ebook_viewer_options'] if not saved_opts: diff --git a/src/calibre/gui2/lrf_renderer/text.py b/src/calibre/gui2/lrf_renderer/text.py index 018063fbbf..42b0f65616 100644 --- a/src/calibre/gui2/lrf_renderer/text.py +++ b/src/calibre/gui2/lrf_renderer/text.py @@ -15,6 +15,7 @@ NULL = lambda a, b: a COLOR = lambda a, b: QColor(*a) WEIGHT = lambda a, b: WEIGHT_MAP(a) + class PixmapItem(QGraphicsPixmapItem): def __init__(self, data, encoding, x0, y0, x1, y1, xsize, ysize): @@ -88,6 +89,7 @@ class FontLoader(object): qfont.setUnderline(text_style.emplineposition == 'after') return qfont + class Style(object): map = collections.defaultdict(lambda : NULL) @@ -144,6 +146,7 @@ class BlockStyle(Style): framecolor=COLOR, ) + class ParSkip(object): def __init__(self, parskip): @@ -317,6 +320,7 @@ class TextBlock(object): s += str(line) + '\n' return s + class Link(QGraphicsRectItem): inactive_brush = QBrush(QColor(0xff, 0xff, 0xff, 0xff)) active_brush = QBrush(QColor(0x00, 0x00, 0x00, 0x59)) @@ -342,6 +346,7 @@ class Link(QGraphicsRectItem): self.hoverLeaveEvent(None) self.slot(self.refobj) + class Line(QGraphicsItem): whitespace = re.compile(r'\s+') @@ -571,6 +576,7 @@ class Word(object): self.highlight = False self.valign = valign + def main(args=sys.argv): return 0 diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py index a068bfdde8..7bf06b28eb 100644 --- a/src/calibre/gui2/main.py +++ b/src/calibre/gui2/main.py @@ -22,9 +22,11 @@ from calibre.utils.config import prefs, dynamic if iswindows: winutil = plugins['winutil'][0] + class AbortInit(Exception): pass + def option_parser(): parser = _option_parser(_('''\ %prog [options] [path_to_ebook] @@ -51,6 +53,7 @@ path_to_ebook to the database. setup_gui_option_parser(parser) return parser + def find_portable_library(): base = get_portable_base() if base is None: @@ -84,6 +87,7 @@ def find_portable_library(): if not os.path.exists(lib): os.mkdir(lib) + def init_qt(args): parser = option_parser() opts, args = parser.parse_args(args) @@ -155,10 +159,12 @@ def get_library_path(gui_runner): library_path = gui_runner.choose_dir(get_default_library_path()) return library_path + def repair_library(library_path): from calibre.gui2.dialogs.restore_library import repair_library_at return repair_library_at(library_path) + def windows_repair(library_path=None): from binascii import hexlify, unhexlify import cPickle, subprocess @@ -190,6 +196,7 @@ class EventAccumulator(object): def __call__(self, ev): self.events.append(ev) + class GuiRunner(QObject): '''Make sure an event loop is running before starting the main work of initialization''' @@ -333,6 +340,7 @@ class GuiRunner(QObject): self.initialize_db() + def get_debug_executable(): e = sys.executable if getattr(sys, 'frozen', False) else sys.argv[0] if hasattr(sys, 'frameworks_dir'): @@ -350,6 +358,7 @@ def get_debug_executable(): exe = base + '-debug' + ext return exe + def run_in_debug_mode(logpath=None): import tempfile, subprocess fd, logpath = tempfile.mkstemp('.txt') @@ -365,9 +374,11 @@ def run_in_debug_mode(logpath=None): stderr=subprocess.STDOUT, stdin=open(os.devnull, 'r'), creationflags=creationflags) + def shellquote(s): return "'" + s.replace("'", "'\\''") + "'" + def run_gui(opts, args, listener, app, gui_debug=None): initialize_file_icon_provider() app.load_builtin_fonts(scan_for_fonts=True) @@ -423,6 +434,7 @@ def run_gui(opts, args, listener, app, gui_debug=None): singleinstance_name = 'calibre_GUI' + def cant_start(msg=_('If you are sure it is not running')+', ', det_msg=_('Timed out waiting for response from running calibre'), listener_failed=False): @@ -448,6 +460,7 @@ def cant_start(msg=_('If you are sure it is not running')+', ', raise SystemExit(1) + def build_pipe(print_error=True): t = RC(print_error=print_error) t.start() @@ -457,6 +470,7 @@ def build_pipe(print_error=True): raise SystemExit(1) return t + def shutdown_other(rc=None): if rc is None: rc = build_pipe(print_error=False) @@ -473,6 +487,7 @@ def shutdown_other(rc=None): prints(_('Failed to shutdown running calibre instance')) raise SystemExit(1) + def communicate(opts, args): t = build_pipe() if opts.shutdown_running_calibre: @@ -485,6 +500,7 @@ def communicate(opts, args): t.conn.close() raise SystemExit(0) + def create_listener(): if islinux: from calibre.utils.ipc.server import LinuxListener as Listener @@ -492,6 +508,7 @@ def create_listener(): from multiprocessing.connection import Listener return Listener(address=gui_socket_address()) + def main(args=sys.argv): if iswindows and 'CALIBRE_REPAIR_CORRUPTED_DB' in os.environ: windows_repair() diff --git a/src/calibre/gui2/main_window.py b/src/calibre/gui2/main_window.py index ee05563e7d..c4e177631e 100644 --- a/src/calibre/gui2/main_window.py +++ b/src/calibre/gui2/main_window.py @@ -13,6 +13,7 @@ from calibre.utils.config import OptionParser from calibre.gui2 import error_dialog from calibre import prints + def option_parser(usage='''\ Usage: %prog [options] @@ -21,6 +22,7 @@ Launch the Graphical User Interface parser = OptionParser(usage) return parser + class GarbageCollector(QObject): ''' @@ -68,6 +70,7 @@ class GarbageCollector(QObject): for obj in gc.garbage: print (obj, repr(obj), type(obj)) + class ExceptionHandler(object): def __init__(self, main_window): @@ -80,6 +83,7 @@ class ExceptionHandler(object): else: sys.__excepthook__(type, value, tb) + class MainWindow(QMainWindow): ___menu_bar = None @@ -161,6 +165,7 @@ class MainWindow(QMainWindow): self.window_unblocked.emit() return QMainWindow.event(self, ev) + def clone_menu(menu): # This is needed to workaround a bug in Qt 5.5+ and Unity. When the same # QAction object is used in both a QMenuBar and a QMenu, sub-menus of the diff --git a/src/calibre/gui2/metadata/basic_widgets.py b/src/calibre/gui2/metadata/basic_widgets.py index 43cfd29119..8532ca9893 100644 --- a/src/calibre/gui2/metadata/basic_widgets.py +++ b/src/calibre/gui2/metadata/basic_widgets.py @@ -45,6 +45,7 @@ OK_COLOR = 'rgba(0, 255, 0, 12%)' ERR_COLOR = 'rgba(255, 0, 0, 12%)' INDICATOR_SHEET = 'QLineEdit { color: black; background-color: %s }' + def save_dialog(parent, title, msg, det_msg=''): d = QMessageBox(parent) d.setWindowTitle(title) @@ -52,6 +53,7 @@ def save_dialog(parent, title, msg, det_msg=''): d.setStandardButtons(QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel) return d.exec_() + def clean_text(x): return re.sub(r'\s', ' ', x.strip()) @@ -77,6 +79,7 @@ class BasicMetadataWidget(object): return property(fget=fget, fset=fset) ''' + class ToMetadataMixin(object): FIELD_NAME = None @@ -108,6 +111,7 @@ class ToMetadataMixin(object): else: self.setEditText(text) + def make_undoable(spinbox): 'Add a proper undo/redo capability to spinbox which must be a sub-class of QAbstractSpinBox' @@ -181,6 +185,8 @@ def make_undoable(spinbox): return UndoableSpinbox # Title {{{ + + class TitleEdit(EnLineEdit, ToMetadataMixin): TITLE_ATTR = FIELD_NAME = 'title' @@ -234,6 +240,7 @@ class TitleEdit(EnLineEdit, ToMetadataMixin): def break_cycles(self): self.dialog = None + class TitleSortEdit(TitleEdit, ToMetadataMixin): TITLE_ATTR = FIELD_NAME = 'title_sort' @@ -314,6 +321,8 @@ class TitleSortEdit(TitleEdit, ToMetadataMixin): # }}} # Authors {{{ + + class AuthorsEdit(EditWithComplete, ToMetadataMixin): TOOLTIP = '' @@ -422,6 +431,7 @@ class AuthorsEdit(EditWithComplete, ToMetadataMixin): except: pass + class AuthorSortEdit(EnLineEdit, ToMetadataMixin): TOOLTIP = _('Specify how the author(s) of this book should be sorted. ' @@ -571,6 +581,8 @@ class AuthorSortEdit(EnLineEdit, ToMetadataMixin): # }}} # Series {{{ + + class SeriesEdit(EditWithComplete, ToMetadataMixin): TOOLTIP = _('List of known series. You can add new series.') @@ -707,6 +719,7 @@ class SeriesIndexEdit(make_undoable(QDoubleSpinBox), ToMetadataMixin): # }}} + class BuddyLabel(QLabel): # {{{ def __init__(self, buddy): @@ -717,6 +730,7 @@ class BuddyLabel(QLabel): # {{{ # Formats {{{ + class Format(QListWidgetItem): def __init__(self, parent, ext, size, path=None, timestamp=None): @@ -733,6 +747,7 @@ class Format(QListWidgetItem): self.setToolTip(text) self.setStatusTip(text) + class OrigAction(QAction): restore_fmt = pyqtSignal(object) @@ -745,6 +760,7 @@ class OrigAction(QAction): def _triggered(self): self.restore_fmt.emit(self.fmt) + class FormatList(_FormatList): restore_fmt = pyqtSignal(object) @@ -773,6 +789,7 @@ class FormatList(_FormatList): self.takeItem(i) break + class FormatsManager(QWidget): def __init__(self, parent, copy_fmt): @@ -1008,6 +1025,7 @@ class FormatsManager(QWidget): self.temp_files = [] # }}} + class Cover(ImageView): # {{{ download_cover = pyqtSignal() @@ -1164,6 +1182,7 @@ class Cover(ImageView): # {{{ def current_val(self): def fget(self): return self._cdata + def fset(self, cdata): self._cdata = None self.cdata_before_trim = None @@ -1206,6 +1225,7 @@ class Cover(ImageView): # {{{ # }}} + class CommentsEdit(Editor, ToMetadataMixin): # {{{ FIELD_NAME = 'comments' @@ -1215,6 +1235,7 @@ class CommentsEdit(Editor, ToMetadataMixin): # {{{ def current_val(self): def fget(self): return self.html + def fset(self, val): if not val or not val.strip(): val = '' @@ -1234,6 +1255,7 @@ class CommentsEdit(Editor, ToMetadataMixin): # {{{ db.set_comment(id_, self.current_val, notify=False, commit=False) # }}} + class RatingEdit(RatingEditor, ToMetadataMixin): # {{{ LABEL = _('&Rating:') TOOLTIP = _('Rating of this book. 0-5 stars') @@ -1248,6 +1270,7 @@ class RatingEdit(RatingEditor, ToMetadataMixin): # {{{ def current_val(self): def fget(self): return self.rating_value + def fset(self, val): self.rating_value = val return property(fget=fget, fset=fset) @@ -1266,6 +1289,7 @@ class RatingEdit(RatingEditor, ToMetadataMixin): # {{{ # }}} + class TagsEdit(EditWithComplete, ToMetadataMixin): # {{{ LABEL = _('Ta&gs:') TOOLTIP = '

'+_('Tags categorize the book. This is particularly ' @@ -1284,6 +1308,7 @@ class TagsEdit(EditWithComplete, ToMetadataMixin): # {{{ def current_val(self): def fget(self): return [clean_text(x) for x in unicode(self.text()).split(',')] + def fset(self, val): if not val: val = [] @@ -1330,6 +1355,7 @@ class TagsEdit(EditWithComplete, ToMetadataMixin): # {{{ # }}} + class LanguagesEdit(LE, ToMetadataMixin): # {{{ LABEL = _('&Languages:') @@ -1344,6 +1370,7 @@ class LanguagesEdit(LE, ToMetadataMixin): # {{{ def current_val(self): def fget(self): return self.lang_codes + def fset(self, val): self.set_lang_codes(val, self.allow_undo) return property(fget=fget, fset=fset) @@ -1373,6 +1400,7 @@ class LanguagesEdit(LE, ToMetadataMixin): # {{{ # Identifiers {{{ + class Identifiers(Dialog): def __init__(self, identifiers, parent=None): @@ -1418,6 +1446,7 @@ class Identifiers(Dialog): return Dialog.accept(self) + class IdentifiersEdit(QLineEdit, ToMetadataMixin): LABEL = _('I&ds:') BASE_TT = _('Edit the identifiers for this book. ' @@ -1460,9 +1489,11 @@ class IdentifiersEdit(QLineEdit, ToMetadataMixin): c = v ans[itype] = c return ans + def fset(self, val): if not val: val = {} + def keygen(x): x = x[0] if x == 'isbn': @@ -1521,6 +1552,7 @@ class IdentifiersEdit(QLineEdit, ToMetadataMixin): # }}} + class ISBNDialog(QDialog): # {{{ def __init__(self, parent, txt): @@ -1573,6 +1605,7 @@ class ISBNDialog(QDialog): # {{{ # }}} + class PublisherEdit(EditWithComplete, ToMetadataMixin): # {{{ LABEL = _('&Publisher:') FIELD_NAME = 'publisher' @@ -1624,12 +1657,14 @@ class PublisherEdit(EditWithComplete, ToMetadataMixin): # {{{ # DateEdit {{{ + class CalendarWidget(QCalendarWidget): def showEvent(self, ev): if self.selectedDate().year() == UNDEFINED_DATE.year: self.setSelectedDate(QDate.currentDate()) + class DateEdit(make_undoable(QDateTimeEdit), ToMetadataMixin): TOOLTIP = '' @@ -1665,6 +1700,7 @@ class DateEdit(make_undoable(QDateTimeEdit), ToMetadataMixin): def current_val(self): def fget(self): return qt_to_dt(self.dateTime(), as_utc=False) + def fset(self, val): if val is None or is_date_undefined(val): val = UNDEFINED_DATE @@ -1698,6 +1734,7 @@ class DateEdit(make_undoable(QDateTimeEdit), ToMetadataMixin): else: return super(DateEdit, self).keyPressEvent(ev) + class PubdateEdit(DateEdit): LABEL = _('Publishe&d:') FMT = 'MMM yyyy' diff --git a/src/calibre/gui2/metadata/bulk_download.py b/src/calibre/gui2/metadata/bulk_download.py index 0dec25ed10..5693435134 100644 --- a/src/calibre/gui2/metadata/bulk_download.py +++ b/src/calibre/gui2/metadata/bulk_download.py @@ -22,6 +22,7 @@ from calibre.ptempfile import (PersistentTemporaryDirectory, # Start download {{{ + class Job(ThreadedJob): ignore_html_details = True @@ -43,11 +44,13 @@ class Job(ThreadedJob): def log_file(self): return open(self.download_debug_log, 'rb') + def show_config(gui, parent): from calibre.gui2.preferences import show_config_widget show_config_widget('Sharing', 'Metadata download', parent=parent, gui=gui, never_shutdown=True) + class ConfirmDialog(QDialog): def __init__(self, ids, parent): @@ -109,6 +112,7 @@ class ConfirmDialog(QDialog): self.identify = False self.accept() + def split_jobs(ids, batch_size=100): ans = [] ids = list(ids) @@ -118,6 +122,7 @@ def split_jobs(ids, batch_size=100): ids = ids[batch_size:] return ans + def start_download(gui, ids, callback, ensure_fields=None): d = ConfirmDialog(ids, gui) ret = d.exec_() @@ -138,6 +143,7 @@ def start_download(gui, ids, callback, ensure_fields=None): # }}} + def get_job_details(job): (aborted, good_ids, tdir, log_file, failed_ids, failed_covers, title_map, lm_map, all_failed) = job.result @@ -153,6 +159,7 @@ def get_job_details(job): return (aborted, good_ids, tdir, log_file, failed_ids, failed_covers, all_failed, det_msg, lm_map) + class HeartBeat(object): CHECK_INTERVAL = 300 # seconds ''' Check that the file count in tdir changes every five minutes ''' @@ -171,6 +178,7 @@ class HeartBeat(object): self.last_time = time.time() return True + class Notifier(Thread): def __init__(self, notifications, title_map, tdir, total): @@ -201,6 +209,7 @@ class Notifier(Thread): _('Processed %s')%self.title_map[book_id])) time.sleep(1) + def download(all_ids, tf, db, do_identify, covers, ensure_fields, log=None, abort=None, notifications=None): batch_size = 10 diff --git a/src/calibre/gui2/metadata/config.py b/src/calibre/gui2/metadata/config.py index ce3ab4e56a..379a2985d9 100644 --- a/src/calibre/gui2/metadata/config.py +++ b/src/calibre/gui2/metadata/config.py @@ -14,6 +14,7 @@ from PyQt5.Qt import (QWidget, QGridLayout, QGroupBox, QListView, Qt, QSpinBox, from calibre.gui2.preferences.metadata_sources import FieldsModel as FM + class FieldsModel(FM): # {{{ def __init__(self, plugin): @@ -51,6 +52,7 @@ class FieldsModel(FM): # {{{ # }}} + class ConfigWidget(QWidget): def __init__(self, plugin): diff --git a/src/calibre/gui2/metadata/diff.py b/src/calibre/gui2/metadata/diff.py index 78ddb8bc73..49b95ba4c2 100644 --- a/src/calibre/gui2/metadata/diff.py +++ b/src/calibre/gui2/metadata/diff.py @@ -32,6 +32,7 @@ Widgets = namedtuple('Widgets', 'new old label button') # Widgets {{{ + class LineEdit(EditWithComplete): changed = pyqtSignal() @@ -60,6 +61,7 @@ class LineEdit(EditWithComplete): val = val.strip(ism['list_to_ui'].strip()) val = [x.strip() for x in val.split(ism['list_to_ui']) if x.strip()] return val + def fset(self, val): ism = self.metadata['is_multiple'] if ism: @@ -87,6 +89,7 @@ class LineEdit(EditWithComplete): def current_val(self): def fget(self): return unicode(self.text()) + def fset(self, val): self.setText(val) self.setCursorPosition(0) @@ -120,6 +123,7 @@ class LanguagesEdit(LE): def current_val(self): def fget(self): return self.lang_codes + def fset(self, val): self.lang_codes = val return property(fget=fget, fset=fset) @@ -137,6 +141,7 @@ class LanguagesEdit(LE): def same_as(self, other): return self.current_val == other.current_val + class RatingsEdit(RatingEdit): changed = pyqtSignal() @@ -161,6 +166,7 @@ class RatingsEdit(RatingEdit): def same_as(self, other): return self.current_val == other.current_val + class DateEdit(PubdateEdit): changed = pyqtSignal() @@ -188,6 +194,7 @@ class DateEdit(PubdateEdit): def same_as(self, other): return self.text() == other.text() + class SeriesEdit(LineEdit): def __init__(self, *args, **kwargs): @@ -225,6 +232,7 @@ class SeriesEdit(LineEdit): sidx = fmt_sidx(num) self.setText(self.text() + ' [%s]' % sidx) + class IdentifiersEdit(LineEdit): def from_mi(self, mi): @@ -238,12 +246,14 @@ class IdentifiersEdit(LineEdit): def fget(self): parts = (x.strip() for x in self.current_val.split(',') if x.strip()) return {k:v for k, v in {x.partition(':')[0].strip():x.partition(':')[-1].strip() for x in parts}.iteritems() if k and v} + def fset(self, val): val = ('%s:%s' % (k, v) for k, v in val.iteritems()) self.setText(', '.join(val)) self.setCursorPosition(0) return property(fget=fget, fset=fset) + class CommentsEdit(Editor): changed = pyqtSignal() @@ -263,6 +273,7 @@ class CommentsEdit(Editor): def current_val(self): def fget(self): return self.html + def fset(self, val): self.html = val or '' self.changed.emit() @@ -285,6 +296,7 @@ class CommentsEdit(Editor): def same_as(self, other): return self.current_val == other.current_val + class CoverView(QWidget): changed = pyqtSignal() @@ -307,6 +319,7 @@ class CoverView(QWidget): def current_val(self): def fget(self): return self.pixmap + def fset(self, val): self.pixmap = val self.changed.emit() @@ -373,6 +386,7 @@ class CoverView(QWidget): p.end() # }}} + class CompareSingle(QWidget): def __init__( @@ -517,6 +531,7 @@ class CompareSingle(QWidget): changed = True return changed + class CompareMany(QDialog): def __init__(self, ids, get_metadata, field_metadata, parent=None, diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index 86a86b9366..866cad3372 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -35,6 +35,7 @@ fetched_fields = ('title', 'title_sort', 'authors', 'author_sort', 'series', 'series_index', 'languages', 'publisher', 'tags', 'rating', 'comments', 'pubdate') + class ScrollArea(QScrollArea): def __init__(self, widget=None, parent=None): @@ -44,6 +45,7 @@ class ScrollArea(QScrollArea): if widget is not None: self.setWidget(widget) + class MetadataSingleDialogBase(QDialog): view_format = pyqtSignal(object, object) @@ -658,6 +660,7 @@ class MetadataSingleDialogBase(QDialog): # from garbage collecting this dialog self.set_current_callback = self.db = None self.metadata_before_fetch = None + def disconnect(signal): try: signal.disconnect() @@ -677,6 +680,7 @@ class MetadataSingleDialogBase(QDialog): # }}} + class Splitter(QSplitter): frame_resized = pyqtSignal(object) @@ -685,6 +689,7 @@ class Splitter(QSplitter): self.frame_resized.emit(ev) return QSplitter.resizeEvent(self, ev) + class MetadataSingleDialog(MetadataSingleDialogBase): # {{{ def do_layout(self): @@ -760,6 +765,7 @@ class MetadataSingleDialog(MetadataSingleDialogBase): # {{{ w.l = l = QGridLayout() w.setLayout(w.l) self.splitter.addWidget(w) + def create_row2(row, widget, button=None, front_button=None): row += 1 ql = BuddyLabel(widget) @@ -814,6 +820,7 @@ class MetadataSingleDialog(MetadataSingleDialogBase): # {{{ # }}} + class DragTrackingWidget(QWidget): # {{{ def __init__(self, parent, on_drag_enter): @@ -825,6 +832,7 @@ class DragTrackingWidget(QWidget): # {{{ # }}} + class MetadataSingleDialogAlt1(MetadataSingleDialogBase): # {{{ cc_two_column = False @@ -979,6 +987,7 @@ class MetadataSingleDialogAlt1(MetadataSingleDialogBase): # {{{ # }}} + class MetadataSingleDialogAlt2(MetadataSingleDialogBase): # {{{ cc_two_column = False @@ -1116,6 +1125,7 @@ class MetadataSingleDialogAlt2(MetadataSingleDialogBase): # {{{ editors = {'default': MetadataSingleDialog, 'alt1': MetadataSingleDialogAlt1, 'alt2': MetadataSingleDialogAlt2} + def edit_metadata(db, row_list, current_row, parent=None, view_slot=None, set_current_callback=None, editing_multiple=False): cls = gprefs.get('edit_metadata_single_layout', '') diff --git a/src/calibre/gui2/metadata/single_download.py b/src/calibre/gui2/metadata/single_download.py index a2db89f5a1..334d2bf676 100644 --- a/src/calibre/gui2/metadata/single_download.py +++ b/src/calibre/gui2/metadata/single_download.py @@ -42,6 +42,7 @@ from calibre.utils.ipc.simple_worker import fork_job, WorkerError from calibre.ptempfile import TemporaryDirectory # }}} + class RichTextDelegate(QStyledItemDelegate): # {{{ def __init__(self, parent=None, max_width=160): @@ -79,6 +80,7 @@ class RichTextDelegate(QStyledItemDelegate): # {{{ painter.restore() # }}} + class CoverDelegate(QStyledItemDelegate): # {{{ ICON_SIZE = 150, 200 @@ -121,6 +123,7 @@ class CoverDelegate(QStyledItemDelegate): # {{{ # }}} + class ResultsModel(QAbstractTableModel): # {{{ COLUMNS = ( @@ -211,6 +214,7 @@ class ResultsModel(QAbstractTableModel): # {{{ # }}} + class ResultsView(QTableView): # {{{ show_details_signal = pyqtSignal(object) @@ -308,6 +312,7 @@ class ResultsView(QTableView): # {{{ # }}} + class Comments(QWebView): # {{{ def __init__(self, parent=None): @@ -376,6 +381,7 @@ class Comments(QWebView): # {{{ return QSize(800, 300) # }}} + class IdentifyWorker(Thread): # {{{ def __init__(self, log, abort, title, authors, identifiers, caches): @@ -433,6 +439,7 @@ class IdentifyWorker(Thread): # {{{ # }}} + class IdentifyWidget(QWidget): # {{{ rejected = pyqtSignal() @@ -549,6 +556,7 @@ class IdentifyWidget(QWidget): # {{{ self.abort.set() # }}} + class CoverWorker(Thread): # {{{ def __init__(self, log, abort, title, authors, identifiers, caches): @@ -628,6 +636,7 @@ class CoverWorker(Thread): # {{{ # }}} + class CoversModel(QAbstractListModel): # {{{ def __init__(self, current_cover, parent=None): @@ -687,6 +696,7 @@ class CoversModel(QAbstractListModel): # {{{ # Remove entries that are still waiting good = [] pmap = {} + def keygen(x): pmap = x[2] if pmap is None: @@ -770,6 +780,7 @@ class CoversModel(QAbstractListModel): # {{{ # }}} + class CoversView(QListView): # {{{ chosen = pyqtSignal() @@ -851,6 +862,7 @@ class CoversView(QListView): # {{{ # }}} + class CoversWidget(QWidget): # {{{ chosen = pyqtSignal() @@ -957,6 +969,7 @@ class CoversWidget(QWidget): # {{{ # }}} + class LogViewer(QDialog): # {{{ def __init__(self, log, parent=None): @@ -1005,6 +1018,7 @@ class LogViewer(QDialog): # {{{ # }}} + class FullFetch(QDialog): # {{{ def __init__(self, current_cover=None, parent=None): @@ -1120,6 +1134,7 @@ class FullFetch(QDialog): # {{{ return self.exec_() # }}} + class CoverFetch(QDialog): # {{{ def __init__(self, current_cover=None, parent=None): diff --git a/src/calibre/gui2/notify.py b/src/calibre/gui2/notify.py index d98672ab14..8768957866 100644 --- a/src/calibre/gui2/notify.py +++ b/src/calibre/gui2/notify.py @@ -9,6 +9,7 @@ __docformat__ = 'restructuredtext en' from calibre.constants import islinux, isosx, get_osx_version + class Notifier(object): DEFAULT_TIMEOUT = 5000 @@ -58,6 +59,7 @@ class KDENotifier(DBUSNotifier): import traceback traceback.print_exc() + class FDONotifier(DBUSNotifier): def __init__(self): @@ -76,6 +78,7 @@ class FDONotifier(DBUSNotifier): import traceback traceback.print_exc() + class QtNotifier(Notifier): def __init__(self, systray=None): @@ -101,6 +104,7 @@ class QtNotifier(Notifier): except: pass + class DummyNotifier(Notifier): ok = True @@ -108,6 +112,7 @@ class DummyNotifier(Notifier): def __call__(self, body, summary=None, replaces_id=None, timeout=0): pass + class AppleNotifier(Notifier): def __init__(self): diff --git a/src/calibre/gui2/open_with.py b/src/calibre/gui2/open_with.py index 33d0c162b4..fa57141ef6 100644 --- a/src/calibre/gui2/open_with.py +++ b/src/calibre/gui2/open_with.py @@ -25,6 +25,7 @@ from calibre.utils.icu import numeric_sort_key as sort_key ENTRY_ROLE = Qt.UserRole + def pixmap_to_data(pixmap): ba = QByteArray() buf = QBuffer(ba) @@ -32,6 +33,7 @@ def pixmap_to_data(pixmap): pixmap.save(buf, 'PNG') return bytearray(ba.data()) + def run_program(entry, path, parent): import subprocess cmdline = entry_to_cmdline(entry, path) @@ -48,6 +50,7 @@ def run_program(entry, path, parent): t.daemon = True t.start() + def entry_to_icon_text(entry, only_text=False): if only_text: return entry.get('name', entry.get('Name')) or _('Unknown') @@ -117,6 +120,7 @@ if iswindows: return cmdline.replace('%1', qpath) del run_program + def run_program(entry, path, parent): # noqa cmdline = entry_to_cmdline(entry, path) print('Running Open With commandline:', repr(entry['cmdline']), ' |==> ', repr(cmdline)) @@ -217,6 +221,7 @@ else: return entry # }}} + class ChooseProgram(Dialog): # {{{ found = pyqtSignal() @@ -296,6 +301,7 @@ class ChooseProgram(Dialog): # {{{ oprefs.defaults['entries'] = {} + def choose_program(file_type='jpeg', parent=None, prefs=oprefs): oft = file_type = file_type.lower() file_type = {'cover_image':'jpeg'}.get(oft, oft) @@ -314,6 +320,7 @@ def choose_program(file_type='jpeg', parent=None, prefs=oprefs): register_keyboard_shortcuts(finalize=True) return entry + def populate_menu(menu, receiver, file_type): file_type = file_type.lower() for entry in oprefs['entries'].get(file_type, ()): @@ -329,6 +336,7 @@ def populate_menu(menu, receiver, file_type): # }}} + class EditPrograms(Dialog): # {{{ def __init__(self, file_type='jpeg', parent=None): @@ -393,6 +401,7 @@ class EditPrograms(Dialog): # {{{ oprefs['entries'][self.file_type] = entries oprefs['entries'] = oprefs['entries'] + def edit_programs(file_type, parent): d = EditPrograms(file_type, parent) d.exec_() @@ -400,6 +409,7 @@ def edit_programs(file_type, parent): registered_shortcuts = {} + def register_keyboard_shortcuts(gui=None, finalize=False): if gui is None: from calibre.gui2.ui import get_gui diff --git a/src/calibre/gui2/preferences/__init__.py b/src/calibre/gui2/preferences/__init__.py index ffffed091d..5fd96ec878 100644 --- a/src/calibre/gui2/preferences/__init__.py +++ b/src/calibre/gui2/preferences/__init__.py @@ -15,9 +15,11 @@ from calibre.customize.ui import preferences_plugins from calibre.utils.config import ConfigProxy from calibre.gui2.complete2 import EditWithComplete + class AbortCommit(Exception): pass + class ConfigWidgetInterface(object): ''' @@ -82,6 +84,7 @@ class ConfigWidgetInterface(object): ''' pass + class Setting(object): CHOICES_SEARCH_FLAGS = Qt.MatchExactly | Qt.MatchCaseSensitive @@ -204,6 +207,7 @@ class Setting(object): val = unicode(self.gui_obj.itemData(idx) or '') return val + class CommaSeparatedList(Setting): def set_gui_val(self, val): @@ -220,6 +224,7 @@ class CommaSeparatedList(Setting): ans = [x for x in ans if x] return ans + class ConfigWidgetBase(QWidget, ConfigWidgetInterface): ''' @@ -298,10 +303,12 @@ def get_plugin(category, name): 'No Preferences Plugin with category: %s and name: %s found' % (category, name)) + class ConfigDialog(QDialog): def set_widget(self, w): self.w = w + def accept(self): try: self.restart_required = self.w.commit() @@ -309,6 +316,7 @@ class ConfigDialog(QDialog): return QDialog.accept(self) + def init_gui(): from calibre.gui2.ui import Main from calibre.gui2.main import option_parser @@ -321,6 +329,7 @@ def init_gui(): gui.initialize(db.library_path, db, None, actions, show_gui=False) return gui + def show_config_widget(category, name, gui=None, show_restart_msg=False, parent=None, never_shutdown=False): ''' @@ -352,6 +361,7 @@ def show_config_widget(category, name, gui=None, show_restart_msg=False, bb.button(bb.RestoreDefaults).setEnabled(w.supports_restoring_to_defaults) bb.button(bb.Apply).setEnabled(False) bb.button(bb.Apply).clicked.connect(d.accept) + def onchange(): b = bb.button(bb.Apply) b.setEnabled(True) @@ -384,9 +394,11 @@ def show_config_widget(category, name, gui=None, show_restart_msg=False, # Testing {{{ + def test_widget(category, name, gui=None): show_config_widget(category, name, gui=gui, show_restart_msg=True) + def test_all(): from PyQt5.Qt import QApplication app = QApplication([]) diff --git a/src/calibre/gui2/preferences/adding.py b/src/calibre/gui2/preferences/adding.py index cf659fcd9b..6adc178dbb 100644 --- a/src/calibre/gui2/preferences/adding.py +++ b/src/calibre/gui2/preferences/adding.py @@ -17,6 +17,7 @@ from calibre.gui2.widgets import FilenamePattern from calibre.gui2.auto_add import AUTO_ADDED from calibre.gui2 import gprefs, choose_dir, error_dialog, question_dialog + class ConfigWidget(ConfigWidgetBase, Ui_Form): def genesis(self, gui): diff --git a/src/calibre/gui2/preferences/behavior.py b/src/calibre/gui2/preferences/behavior.py index 82247774df..6db4cb1f3a 100644 --- a/src/calibre/gui2/preferences/behavior.py +++ b/src/calibre/gui2/preferences/behavior.py @@ -19,10 +19,12 @@ from calibre.ebooks.oeb.iterator import is_supported from calibre.constants import iswindows from calibre.utils.icu import sort_key + class OutputFormatSetting(Setting): CHOICES_SEARCH_FLAGS = Qt.MatchFixedString + class ConfigWidget(ConfigWidgetBase, Ui_Form): def genesis(self, gui): diff --git a/src/calibre/gui2/preferences/coloring.py b/src/calibre/gui2/preferences/coloring.py index 13ee9c3439..e340850e6b 100644 --- a/src/calibre/gui2/preferences/coloring.py +++ b/src/calibre/gui2/preferences/coloring.py @@ -35,6 +35,7 @@ icon_rule_kinds = [(_('icon with text'), 'icon'), (_('composed icons w/text'), 'icon_composed'), (_('composed icons w/no text'), 'icon_only_composed'),] + class ConditionEditor(QWidget): # {{{ ACTION_MAP = { @@ -140,6 +141,7 @@ class ConditionEditor(QWidget): # {{{ def fget(self): idx = self.column_box.currentIndex() return unicode(self.column_box.itemData(idx) or '') + def fset(self, val): for idx in range(self.column_box.count()): c = unicode(self.column_box.itemData(idx) or '') @@ -154,6 +156,7 @@ class ConditionEditor(QWidget): # {{{ def fget(self): idx = self.action_box.currentIndex() return unicode(self.action_box.itemData(idx) or '') + def fset(self, val): for idx in range(self.action_box.count()): c = unicode(self.action_box.itemData(idx) or '') @@ -276,6 +279,7 @@ class ConditionEditor(QWidget): # {{{ self.value_box.setEnabled(False) # }}} + class RuleEditor(QDialog): # {{{ @property @@ -672,6 +676,7 @@ class RuleEditor(QDialog): # {{{ return kind, col, r # }}} + class RulesModel(QAbstractListModel): # {{{ def __init__(self, prefs, fm, pref_name, parent=None): @@ -846,6 +851,7 @@ class RulesModel(QAbstractListModel): # {{{ # }}} + class EditRules(QWidget): # {{{ changed = pyqtSignal() diff --git a/src/calibre/gui2/preferences/columns.py b/src/calibre/gui2/preferences/columns.py index 519d1ba588..41e7df0e09 100644 --- a/src/calibre/gui2/preferences/columns.py +++ b/src/calibre/gui2/preferences/columns.py @@ -15,6 +15,7 @@ from calibre.gui2.preferences.columns_ui import Ui_Form from calibre.gui2.preferences.create_custom_column import CreateCustomColumn from calibre.gui2 import error_dialog, question_dialog, ALL_COLUMNS + class ConfigWidget(ConfigWidgetBase, Ui_Form): restart_critical = True @@ -245,6 +246,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): hidden_cols = list(hidden_cols.intersection(set(model.column_map))) if 'ondevice' in hidden_cols: hidden_cols.remove('ondevice') + def col_pos(x, y): xidx = config_cols.index(x) if x in config_cols else sys.maxint yidx = config_cols.index(y) if y in config_cols else sys.maxint diff --git a/src/calibre/gui2/preferences/conversion.py b/src/calibre/gui2/preferences/conversion.py index 3b80155b11..fb97c7d6c5 100644 --- a/src/calibre/gui2/preferences/conversion.py +++ b/src/calibre/gui2/preferences/conversion.py @@ -23,6 +23,7 @@ from calibre.gui2.convert.toc import TOCWidget from calibre.customize.ui import input_format_plugins, output_format_plugins from calibre.gui2.convert import config_widget_for_input_plugin + class Model(QStringListModel): def __init__(self, widgets): @@ -56,6 +57,7 @@ class ListView(QListView): QListView.currentChanged(self, cur, prev) self.current_changed.emit(cur, prev) + class Base(ConfigWidgetBase): restore_defaults_desc = _('Restore settings to default values. ' @@ -113,6 +115,7 @@ class Base(ConfigWidgetBase): def category_current_changed(self, n, p): self.stack.setCurrentIndex(n.row()) + class CommonOptions(Base): def load_conversion_widgets(self): @@ -120,6 +123,7 @@ class CommonOptions(Base): PageSetupWidget, StructureDetectionWidget, TOCWidget, SearchAndReplaceWidget,] + class InputOptions(Base): def load_conversion_widgets(self): @@ -129,6 +133,7 @@ class InputOptions(Base): if pw is not None: self.conversion_widgets.append(pw) + class OutputOptions(Base): def load_conversion_widgets(self): diff --git a/src/calibre/gui2/preferences/create_custom_column.py b/src/calibre/gui2/preferences/create_custom_column.py index e461a5745e..821accd35d 100644 --- a/src/calibre/gui2/preferences/create_custom_column.py +++ b/src/calibre/gui2/preferences/create_custom_column.py @@ -17,6 +17,7 @@ from PyQt5.Qt import ( from calibre.gui2 import error_dialog + class CreateCustomColumn(QDialog): # Note: in this class, we are treating is_multiple as the boolean that diff --git a/src/calibre/gui2/preferences/device_debug.py b/src/calibre/gui2/preferences/device_debug.py index ed53b1395c..9d374d8d1a 100644 --- a/src/calibre/gui2/preferences/device_debug.py +++ b/src/calibre/gui2/preferences/device_debug.py @@ -12,6 +12,7 @@ from PyQt5.Qt import QDialog, QVBoxLayout, QPlainTextEdit, QTimer, \ from calibre.gui2 import error_dialog + class DebugDevice(QDialog): def __init__(self, gui, parent=None): diff --git a/src/calibre/gui2/preferences/device_user_defined.py b/src/calibre/gui2/preferences/device_user_defined.py index 7e8bc890f1..d57c5ff9ce 100644 --- a/src/calibre/gui2/preferences/device_user_defined.py +++ b/src/calibre/gui2/preferences/device_user_defined.py @@ -10,6 +10,7 @@ __docformat__ = 'restructuredtext en' from PyQt5.Qt import QDialog, QVBoxLayout, QPlainTextEdit, QTimer, \ QDialogButtonBox, QPushButton, QApplication, QIcon, QMessageBox + def step_dialog(parent, title, msg, det_msg=''): d = QMessageBox(parent) d.setWindowTitle(title) diff --git a/src/calibre/gui2/preferences/emailp.py b/src/calibre/gui2/preferences/emailp.py index 602aa4e659..a79e12950d 100644 --- a/src/calibre/gui2/preferences/emailp.py +++ b/src/calibre/gui2/preferences/emailp.py @@ -18,6 +18,7 @@ from calibre.utils.icu import numeric_sort_key from calibre.gui2 import gprefs from calibre.utils.smtp import config as smtp_prefs + class EmailAccounts(QAbstractTableModel): # {{{ def __init__(self, accounts, subjects, aliases={}, tags={}): @@ -207,6 +208,7 @@ class EmailAccounts(QAbstractTableModel): # {{{ # }}} + class ConfigWidget(ConfigWidgetBase, Ui_Form): supports_restoring_to_defaults = False diff --git a/src/calibre/gui2/preferences/history.py b/src/calibre/gui2/preferences/history.py index 975f3f838e..96a9519b6d 100644 --- a/src/calibre/gui2/preferences/history.py +++ b/src/calibre/gui2/preferences/history.py @@ -12,6 +12,7 @@ from PyQt5.Qt import QComboBox, Qt from calibre.gui2 import config as gui_conf + class HistoryBox(QComboBox): def __init__(self, parent=None): diff --git a/src/calibre/gui2/preferences/ignored_devices.py b/src/calibre/gui2/preferences/ignored_devices.py index f8c2411b4c..5cb9820259 100644 --- a/src/calibre/gui2/preferences/ignored_devices.py +++ b/src/calibre/gui2/preferences/ignored_devices.py @@ -13,6 +13,7 @@ from PyQt5.Qt import (QLabel, QVBoxLayout, QListWidget, QListWidgetItem, Qt, from calibre.customize.ui import enable_plugin from calibre.gui2.preferences import ConfigWidgetBase, test_widget + class ConfigWidget(ConfigWidgetBase): restart_critical = False diff --git a/src/calibre/gui2/preferences/keyboard.py b/src/calibre/gui2/preferences/keyboard.py index 22c22d1a6c..81d450bc42 100644 --- a/src/calibre/gui2/preferences/keyboard.py +++ b/src/calibre/gui2/preferences/keyboard.py @@ -12,6 +12,7 @@ from PyQt5.Qt import QVBoxLayout from calibre.gui2.preferences import (ConfigWidgetBase, test_widget) from calibre.gui2.keyboard import ShortcutConfig + class ConfigWidget(ConfigWidgetBase): def genesis(self, gui): diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py index cf0d93b93f..cf0330bbbb 100644 --- a/src/calibre/gui2/preferences/look_feel.py +++ b/src/calibre/gui2/preferences/look_feel.py @@ -33,6 +33,7 @@ from calibre.gui2.preferences.coloring import EditRules from calibre.gui2.library.alternate_views import auto_height, CM_TO_INCH from calibre.gui2.widgets2 import Dialog + class BusyCursor(object): def __enter__(self): @@ -43,6 +44,7 @@ class BusyCursor(object): # IdLinksEditor {{{ + class IdLinksRuleEdit(Dialog): def __init__(self, key='', name='', template='', parent=None): @@ -83,6 +85,7 @@ class IdLinksRuleEdit(Dialog): 'The %s field cannot be empty') % which, show=True) Dialog.accept(self) + class IdLinksEditor(Dialog): def __init__(self, parent=None): @@ -150,6 +153,7 @@ class IdLinksEditor(Dialog): self.table.removeRow(r) # }}} + class DisplayedFields(QAbstractListModel): # {{{ def __init__(self, db, parent=None): @@ -224,6 +228,7 @@ class DisplayedFields(QAbstractListModel): # {{{ # }}} + class Background(QWidget): # {{{ def __init__(self, parent): @@ -257,6 +262,7 @@ class Background(QWidget): # {{{ painter.end() # }}} + class ConfigWidget(ConfigWidgetBase, Ui_Form): size_calculated = pyqtSignal(object) diff --git a/src/calibre/gui2/preferences/main.py b/src/calibre/gui2/preferences/main.py index f0e8cddc25..b5ece35531 100644 --- a/src/calibre/gui2/preferences/main.py +++ b/src/calibre/gui2/preferences/main.py @@ -24,6 +24,8 @@ from calibre.customize.ui import preferences_plugins ICON_SIZE = 32 # Title Bar {{{ + + class Message(QWidget): def __init__(self, parent): @@ -68,6 +70,7 @@ class Message(QWidget): y = (self.height() - br.height()) / 2 self.layout.draw(p, QPointF(0, y)) + class TitleBar(QWidget): def __init__(self, parent=None): @@ -98,6 +101,7 @@ class TitleBar(QWidget): # }}} + class Category(QWidget): # {{{ plugin_activated = pyqtSignal(object) @@ -144,6 +148,7 @@ class Category(QWidget): # {{{ # }}} + class Browser(QScrollArea): # {{{ show_plugin = pyqtSignal(object) diff --git a/src/calibre/gui2/preferences/metadata_sources.py b/src/calibre/gui2/preferences/metadata_sources.py index c0be5fe703..d23b83021b 100644 --- a/src/calibre/gui2/preferences/metadata_sources.py +++ b/src/calibre/gui2/preferences/metadata_sources.py @@ -19,6 +19,7 @@ from calibre.customize.ui import (all_metadata_plugins, is_disabled, enable_plugin, disable_plugin, default_disabled_plugins) from calibre.gui2 import error_dialog, question_dialog + class SourcesModel(QAbstractTableModel): # {{{ def __init__(self, parent=None): @@ -145,6 +146,7 @@ class SourcesModel(QAbstractTableModel): # {{{ # }}} + class FieldsModel(QAbstractListModel): # {{{ def __init__(self, parent=None): @@ -255,6 +257,7 @@ class FieldsModel(QAbstractListModel): # {{{ # }}} + class PluginConfig(QWidget): # {{{ finished = pyqtSignal() @@ -290,6 +293,7 @@ class PluginConfig(QWidget): # {{{ self.plugin.save_settings(self.config_widget) # }}} + class ConfigWidget(ConfigWidgetBase, Ui_Form): def genesis(self, gui): diff --git a/src/calibre/gui2/preferences/misc.py b/src/calibre/gui2/preferences/misc.py index 7aca02c2c5..e6f891d75d 100644 --- a/src/calibre/gui2/preferences/misc.py +++ b/src/calibre/gui2/preferences/misc.py @@ -12,6 +12,7 @@ from calibre.gui2.preferences.misc_ui import Ui_Form from calibre.gui2 import (config, open_local_file, gprefs) from calibre import get_proxies + class WorkersSetting(Setting): def set_gui_val(self, val): @@ -22,6 +23,7 @@ class WorkersSetting(Setting): val = Setting.get_gui_val(self) return val * 2 + class ConfigWidget(ConfigWidgetBase, Ui_Form): def genesis(self, gui): diff --git a/src/calibre/gui2/preferences/plugins.py b/src/calibre/gui2/preferences/plugins.py index 43478e759e..e0eedcb4a9 100644 --- a/src/calibre/gui2/preferences/plugins.py +++ b/src/calibre/gui2/preferences/plugins.py @@ -23,11 +23,13 @@ from calibre.utils.search_query_parser import SearchQueryParser from calibre.utils.icu import lower from calibre.constants import iswindows + class AdaptSQP(SearchQueryParser): def __init__(self, *args, **kwargs): pass + class PluginModel(QAbstractItemModel, AdaptSQP): # {{{ def __init__(self, show_only_user_plugins=False): diff --git a/src/calibre/gui2/preferences/saving.py b/src/calibre/gui2/preferences/saving.py index ab61710f95..e44c3f5dd6 100644 --- a/src/calibre/gui2/preferences/saving.py +++ b/src/calibre/gui2/preferences/saving.py @@ -13,6 +13,7 @@ from calibre.utils.config import ConfigProxy from calibre.library.save_to_disk import config from calibre.gui2 import gprefs + class ConfigWidget(ConfigWidgetBase, Ui_Form): def genesis(self, gui): diff --git a/src/calibre/gui2/preferences/search.py b/src/calibre/gui2/preferences/search.py index 092eddd02f..bbc42a2ba8 100644 --- a/src/calibre/gui2/preferences/search.py +++ b/src/calibre/gui2/preferences/search.py @@ -15,6 +15,7 @@ from calibre.utils.config import prefs from calibre.utils.icu import sort_key from calibre.library.caches import set_use_primary_find_in_search + class ConfigWidget(ConfigWidgetBase, Ui_Form): def genesis(self, gui): diff --git a/src/calibre/gui2/preferences/sending.py b/src/calibre/gui2/preferences/sending.py index 5eaaacb230..3cb6458c54 100644 --- a/src/calibre/gui2/preferences/sending.py +++ b/src/calibre/gui2/preferences/sending.py @@ -13,6 +13,7 @@ from calibre.utils.config import ConfigProxy from calibre.library.save_to_disk import config from calibre.utils.config import prefs + class ConfigWidget(ConfigWidgetBase, Ui_Form): def genesis(self, gui): diff --git a/src/calibre/gui2/preferences/server.py b/src/calibre/gui2/preferences/server.py index 2ae5cddb38..16e0375d1b 100644 --- a/src/calibre/gui2/preferences/server.py +++ b/src/calibre/gui2/preferences/server.py @@ -19,6 +19,7 @@ from calibre.gui2 import error_dialog, config, open_url, warning_dialog, \ from calibre import as_unicode from calibre.utils.icu import sort_key + class ConfigWidget(ConfigWidgetBase, Ui_Form): def genesis(self, gui): diff --git a/src/calibre/gui2/preferences/template_functions.py b/src/calibre/gui2/preferences/template_functions.py index 9613d1c8bb..804f5d0e1d 100644 --- a/src/calibre/gui2/preferences/template_functions.py +++ b/src/calibre/gui2/preferences/template_functions.py @@ -214,6 +214,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): def replace_button_clicked(self): self.delete_button_clicked() self.create_button_clicked() + def refresh_gui(self, gui): pass diff --git a/src/calibre/gui2/preferences/texture_chooser.py b/src/calibre/gui2/preferences/texture_chooser.py index a0d4a97b36..8f8f777634 100644 --- a/src/calibre/gui2/preferences/texture_chooser.py +++ b/src/calibre/gui2/preferences/texture_chooser.py @@ -16,12 +16,14 @@ from calibre.constants import config_dir from calibre.gui2 import choose_files, error_dialog from calibre.utils.icu import sort_key + def texture_dir(): ans = os.path.join(config_dir, 'textures') if not os.path.exists(ans): os.makedirs(ans) return ans + def texture_path(fname): if not fname: return @@ -29,6 +31,7 @@ def texture_path(fname): return I('textures/%s' % fname[1:]) return os.path.join(texture_dir(), fname) + class TextureChooser(QDialog): def __init__(self, parent=None, initial=None): diff --git a/src/calibre/gui2/preferences/toolbar.py b/src/calibre/gui2/preferences/toolbar.py index 8564b00dae..fa11ad9390 100644 --- a/src/calibre/gui2/preferences/toolbar.py +++ b/src/calibre/gui2/preferences/toolbar.py @@ -24,6 +24,7 @@ class FakeAction(object): self.dont_remove_from = dont_remove_from self.dont_add_to = dont_add_to + class BaseModel(QAbstractListModel): def name_to_action(self, name, gui): @@ -140,6 +141,7 @@ class AllModel(BaseModel): self._data = self.get_all_actions(current) self.endResetModel() + class CurrentModel(BaseModel): def __init__(self, key, gui): diff --git a/src/calibre/gui2/preferences/tweaks.py b/src/calibre/gui2/preferences/tweaks.py index dd4ac84462..ff4db80b6e 100644 --- a/src/calibre/gui2/preferences/tweaks.py +++ b/src/calibre/gui2/preferences/tweaks.py @@ -27,11 +27,13 @@ from PyQt5.Qt import ( ROOT = QModelIndex() + class AdaptSQP(SearchQueryParser): def __init__(self, *args, **kwargs): pass + class Delegate(QStyledItemDelegate): # {{{ def __init__(self, view): @@ -47,6 +49,7 @@ class Delegate(QStyledItemDelegate): # {{{ # }}} + class Tweak(object): # {{{ def __init__(self, name, doc, var_names, defaults, custom): @@ -106,6 +109,7 @@ class Tweak(object): # {{{ # }}} + class Tweaks(QAbstractListModel, AdaptSQP): # {{{ def __init__(self, parent=None): @@ -298,6 +302,7 @@ class Tweaks(QAbstractListModel, AdaptSQP): # {{{ # }}} + class PluginTweaks(QDialog): # {{{ def __init__(self, raw, parent=None): @@ -324,6 +329,7 @@ class PluginTweaks(QDialog): # {{{ # }}} + class TweaksView(QListView): current_changed = pyqtSignal(object, object) @@ -340,6 +346,7 @@ class TweaksView(QListView): QListView.currentChanged(self, cur, prev) self.current_changed.emit(cur, prev) + class ConfigWidget(ConfigWidgetBase): def setupUi(self, x): diff --git a/src/calibre/gui2/proceed.py b/src/calibre/gui2/proceed.py index 6163fe0ae7..a8cd4647b7 100644 --- a/src/calibre/gui2/proceed.py +++ b/src/calibre/gui2/proceed.py @@ -23,6 +23,7 @@ Question = namedtuple('Question', 'payload callback cancel_callback ' 'action_label action_icon focus_action show_det show_ok icon ' 'log_viewer_unique_name') + class Icon(QWidget): @pyqtProperty(float) @@ -70,6 +71,7 @@ class Icon(QWidget): p.drawPixmap(self.rect(), self.icon) p.end() + class PlainTextEdit(QPlainTextEdit): def sizeHint(self): @@ -78,6 +80,7 @@ class PlainTextEdit(QPlainTextEdit): ans.setWidth(fm.averageCharWidth() * 50) return ans + class ProceedQuestion(QWidget): ask_question = pyqtSignal(object, object, object) @@ -392,6 +395,7 @@ class ProceedQuestion(QWidget): p.addRoundedRect(QRectF(self.rect()).adjusted(bw, bw, -bw, -bw), br, br) painter.fillPath(p, pal.color(pal.WindowText)) + def main(): from calibre.gui2 import Application from PyQt5.Qt import QMainWindow, QStatusBar, QTimer @@ -402,6 +406,7 @@ def main(): s.showMessage('Testing ProceedQuestion') w.show() p = ProceedQuestion(w) + def doit(): p.dummy_question() p.dummy_question(action_label='A very long button for testing relayout (indeed)') diff --git a/src/calibre/gui2/progress_indicator/__init__.py b/src/calibre/gui2/progress_indicator/__init__.py index 34717708a0..dc6649022a 100644 --- a/src/calibre/gui2/progress_indicator/__init__.py +++ b/src/calibre/gui2/progress_indicator/__init__.py @@ -11,6 +11,7 @@ from PyQt5.Qt import ( QPainter, QTimer, QVBoxLayout, QLabel, QStackedWidget, QDialog ) + def draw_snake_spinner(painter, rect, angle, light, dark): painter.setRenderHint(QPainter.Antialiasing) @@ -39,6 +40,7 @@ def draw_snake_spinner(painter, rect, angle, light, dark): painter.setPen(pen) painter.drawArc(drawing_rect, angle * 16, (360 - 2 * angle_for_width) * 16) + class ProgressSpinner(QWidget): def __init__(self, parent=None, size=64, interval=10): @@ -119,6 +121,7 @@ class ProgressSpinner(QWidget): ProgressIndicator = ProgressSpinner + class WaitPanel(QWidget): def __init__(self, msg, parent=None, size=256, interval=10): @@ -132,6 +135,7 @@ class WaitPanel(QWidget): self.la.setStyleSheet('QLabel { font-size: 40px; font-weight: bold }') l.addWidget(self.la, 0, Qt.AlignCenter), l.addStretch() + class WaitStack(QStackedWidget): def __init__(self, msg, after=None, parent=None, size=256, interval=10): diff --git a/src/calibre/gui2/save.py b/src/calibre/gui2/save.py index ce1cae14c0..b0d57aff68 100644 --- a/src/calibre/gui2/save.py +++ b/src/calibre/gui2/save.py @@ -28,6 +28,7 @@ from calibre.library.save_to_disk import sanitize_args, get_path_components, fin BookId = namedtuple('BookId', 'title authors') + def ensure_unique_components(data): # {{{ cmap = defaultdict(set) bid_map = {} @@ -43,6 +44,7 @@ def ensure_unique_components(data): # {{{ components[-1] = components[-1] + suffix # }}} + class SpooledFile(SpooledTemporaryFile): # {{{ def __init__(self, file_obj, max_size=50*1024*1024): @@ -67,6 +69,7 @@ class SpooledFile(SpooledTemporaryFile): # {{{ self._file.truncate(*args) # }}} + class Saver(QObject): do_one_signal = pyqtSignal() diff --git a/src/calibre/gui2/search_box.py b/src/calibre/gui2/search_box.py index 12a9bd371a..4897c58957 100644 --- a/src/calibre/gui2/search_box.py +++ b/src/calibre/gui2/search_box.py @@ -20,6 +20,7 @@ from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.dialogs.saved_search_editor import SavedSearchEditor from calibre.gui2.dialogs.search import SearchDialog + class AsYouType(unicode): def __new__(cls, text): @@ -27,6 +28,7 @@ class AsYouType(unicode): self.as_you_type = True return self + class SearchLineEdit(QLineEdit): # {{{ key_pressed = pyqtSignal(object) select_on_mouse_press = None @@ -74,6 +76,7 @@ class SearchLineEdit(QLineEdit): # {{{ self.select_on_mouse_press = None # }}} + class SearchBox2(QComboBox): # {{{ ''' @@ -285,6 +288,7 @@ class SearchBox2(QComboBox): # {{{ # }}} + class SavedSearchBox(QComboBox): # {{{ ''' @@ -425,6 +429,7 @@ class SavedSearchBox(QComboBox): # {{{ # }}} + class SearchBoxMixin(object): # {{{ def __init__(self, *args, **kwargs): @@ -511,6 +516,7 @@ class SearchBoxMixin(object): # {{{ # }}} + class SavedSearchBoxMixin(object): # {{{ def __init__(self, *args, **kwargs): diff --git a/src/calibre/gui2/search_restriction_mixin.py b/src/calibre/gui2/search_restriction_mixin.py index 50e13769df..8a53c4da81 100644 --- a/src/calibre/gui2/search_restriction_mixin.py +++ b/src/calibre/gui2/search_restriction_mixin.py @@ -20,6 +20,7 @@ from calibre.utils.icu import sort_key from calibre.utils.search_query_parser import ParseException from calibre.utils.localization import localize_user_manual_link + class SelectNames(QDialog): # {{{ def __init__(self, names, txt, parent=None): @@ -61,6 +62,7 @@ class SelectNames(QDialog): # {{{ MAX_VIRTUAL_LIBRARY_NAME_LENGTH = 40 + def _build_full_search_string(gui): search_templates = ( '', @@ -87,6 +89,7 @@ def _build_full_search_string(gui): template = search_templates[dex] return template.format(cl=cl, cr=cr, sb=sb).strip() + class CreateVirtualLibrary(QDialog): # {{{ def __init__(self, gui, existing_names, editing=None): @@ -305,6 +308,7 @@ class CreateVirtualLibrary(QDialog): # {{{ QDialog.accept(self) # }}} + class SearchRestrictionMixin(object): no_restriction = _('') diff --git a/src/calibre/gui2/shortcuts.py b/src/calibre/gui2/shortcuts.py index 5f784c464f..279d3dd124 100644 --- a/src/calibre/gui2/shortcuts.py +++ b/src/calibre/gui2/shortcuts.py @@ -23,6 +23,7 @@ DESCRIPTION = Qt.UserRole + 1 CUSTOM = Qt.UserRole + 2 KEY = Qt.UserRole + 3 + class Customize(QFrame): def __init__(self, index, dup_check, parent=None): @@ -198,6 +199,7 @@ class Delegate(QStyledItemDelegate): def updateEditorGeometry(self, editor, option, index): editor.setGeometry(option.rect) + class Shortcuts(QAbstractListModel): TEMPLATE = u''' @@ -286,6 +288,7 @@ class Shortcuts(QAbstractListModel): return Qt.ItemIsEnabled return QAbstractListModel.flags(self, index) | Qt.ItemIsEditable + class ShortcutConfig(QWidget): def __init__(self, model, parent=None): diff --git a/src/calibre/gui2/splash_screen.py b/src/calibre/gui2/splash_screen.py index 169c5c8ddd..5457dd50dc 100644 --- a/src/calibre/gui2/splash_screen.py +++ b/src/calibre/gui2/splash_screen.py @@ -10,6 +10,7 @@ from PyQt5.Qt import Qt, QSplashScreen, QIcon, QApplication, QTransform, QPainte from calibre.constants import __appname__, iswindows from calibre.utils.monotonic import monotonic + class SplashScreen(QSplashScreen): def __init__(self, develop=False): @@ -59,6 +60,7 @@ class SplashScreen(QSplashScreen): ev.accept() QApplication.instance().quit() + def main(): from calibre.gui2 import Application app = Application([]) diff --git a/src/calibre/gui2/store/__init__.py b/src/calibre/gui2/store/__init__.py index ec6f0276dd..3ece6c5dad 100644 --- a/src/calibre/gui2/store/__init__.py +++ b/src/calibre/gui2/store/__init__.py @@ -8,6 +8,7 @@ __docformat__ = 'restructuredtext en' from calibre.utils.filenames import ascii_filename + class StorePlugin(object): # {{{ ''' diff --git a/src/calibre/gui2/store/basic_config.py b/src/calibre/gui2/store/basic_config.py index df6000d78a..f7f1652d68 100644 --- a/src/calibre/gui2/store/basic_config.py +++ b/src/calibre/gui2/store/basic_config.py @@ -10,6 +10,7 @@ from PyQt5.Qt import QWidget from calibre.gui2.store.basic_config_widget_ui import Ui_Form + class BasicStoreConfigWidget(QWidget, Ui_Form): def __init__(self, store): @@ -26,6 +27,7 @@ class BasicStoreConfigWidget(QWidget, Ui_Form): self.open_external.setChecked(config.get('open_external', False)) self.tags.setText(config.get('tags', '')) + class BasicStoreConfig(object): def customization_help(self, gui=False): diff --git a/src/calibre/gui2/store/config/chooser/adv_search_builder.py b/src/calibre/gui2/store/config/chooser/adv_search_builder.py index 58d5d8a108..50964236d9 100644 --- a/src/calibre/gui2/store/config/chooser/adv_search_builder.py +++ b/src/calibre/gui2/store/config/chooser/adv_search_builder.py @@ -14,6 +14,7 @@ from calibre.gui2.store.config.chooser.adv_search_builder_ui import Ui_Dialog from calibre.library.caches import CONTAINS_MATCH, EQUALS_MATCH from calibre.utils.localization import localize_user_manual_link + class AdvSearchBuilderDialog(QDialog, Ui_Dialog): def __init__(self, parent): diff --git a/src/calibre/gui2/store/config/chooser/chooser_dialog.py b/src/calibre/gui2/store/config/chooser/chooser_dialog.py index 75e8e8fa2b..6a5b86984c 100644 --- a/src/calibre/gui2/store/config/chooser/chooser_dialog.py +++ b/src/calibre/gui2/store/config/chooser/chooser_dialog.py @@ -10,6 +10,7 @@ from PyQt5.Qt import (QDialog, QDialogButtonBox, QVBoxLayout) from calibre.gui2.store.config.chooser.chooser_widget import StoreChooserWidget + class StoreChooserDialog(QDialog): def __init__(self, parent): diff --git a/src/calibre/gui2/store/config/chooser/chooser_widget.py b/src/calibre/gui2/store/config/chooser/chooser_widget.py index 4cda0785b2..8946590aa1 100644 --- a/src/calibre/gui2/store/config/chooser/chooser_widget.py +++ b/src/calibre/gui2/store/config/chooser/chooser_widget.py @@ -11,6 +11,7 @@ from PyQt5.Qt import (QWidget, QIcon, QDialog, QComboBox) from calibre.gui2.store.config.chooser.adv_search_builder import AdvSearchBuilderDialog from calibre.gui2.store.config.chooser.chooser_widget_ui import Ui_Form + class StoreChooserWidget(QWidget, Ui_Form): def __init__(self): diff --git a/src/calibre/gui2/store/config/chooser/results_view.py b/src/calibre/gui2/store/config/chooser/results_view.py index 23df6bf309..1a3e32be6d 100644 --- a/src/calibre/gui2/store/config/chooser/results_view.py +++ b/src/calibre/gui2/store/config/chooser/results_view.py @@ -14,6 +14,7 @@ from calibre.customize.ui import store_plugins from calibre.gui2.metadata.single_download import RichTextDelegate from calibre.gui2.store.config.chooser.models import Matches + class ResultsView(QTreeView): def __init__(self, *args): diff --git a/src/calibre/gui2/store/config/search/search_widget.py b/src/calibre/gui2/store/config/search/search_widget.py index 860ae241f4..e0a37714ce 100644 --- a/src/calibre/gui2/store/config/search/search_widget.py +++ b/src/calibre/gui2/store/config/search/search_widget.py @@ -11,6 +11,7 @@ from PyQt5.Qt import QWidget from calibre.gui2 import JSONConfig from calibre.gui2.store.config.search.search_widget_ui import Ui_Form + class StoreConfigWidget(QWidget, Ui_Form): def __init__(self, config=None): diff --git a/src/calibre/gui2/store/config/store.py b/src/calibre/gui2/store/config/store.py index 852f602d08..7d786a0c1f 100644 --- a/src/calibre/gui2/store/config/store.py +++ b/src/calibre/gui2/store/config/store.py @@ -10,9 +10,11 @@ __docformat__ = 'restructuredtext en' Config widget access functions for configuring the store action. ''' + def config_widget(): from calibre.gui2.store.config.search.search_widget import StoreConfigWidget return StoreConfigWidget() + def save_settings(config_widget): config_widget.save_settings() diff --git a/src/calibre/gui2/store/loader.py b/src/calibre/gui2/store/loader.py index 6da3e4fa01..19b1e100d4 100644 --- a/src/calibre/gui2/store/loader.py +++ b/src/calibre/gui2/store/loader.py @@ -18,11 +18,14 @@ from calibre.constants import numeric_version, DEBUG from calibre.gui2.store import StorePlugin from calibre.utils.config import JSONConfig + class VersionMismatch(ValueError): + def __init__(self, ver): ValueError.__init__(self, 'calibre too old') self.ver = ver + def download_updates(ver_map={}, server='https://code.calibre-ebook.com'): from calibre.utils.https import get_https_resource_securely data = {k:type(u'')(v) for k, v in ver_map.iteritems()} @@ -45,6 +48,7 @@ def download_updates(ver_map={}, server='https://code.calibre-ebook.com'): yield name, src raw = d.unused_data + class Stores(OrderedDict): CHECK_INTERVAL = 24 * 60 * 60 diff --git a/src/calibre/gui2/store/opensearch_store.py b/src/calibre/gui2/store/opensearch_store.py index 3b7e8e47bf..4236add22a 100644 --- a/src/calibre/gui2/store/opensearch_store.py +++ b/src/calibre/gui2/store/opensearch_store.py @@ -20,6 +20,7 @@ from calibre.gui2.store.web_store_dialog import WebStoreDialog from calibre.utils.opensearch.description import Description from calibre.utils.opensearch.query import Query + class OpenSearchOPDSStore(StorePlugin): open_search_url = '' diff --git a/src/calibre/gui2/store/search/adv_search_builder.py b/src/calibre/gui2/store/search/adv_search_builder.py index 76544e6f62..b992712b1b 100644 --- a/src/calibre/gui2/store/search/adv_search_builder.py +++ b/src/calibre/gui2/store/search/adv_search_builder.py @@ -14,6 +14,7 @@ from calibre.gui2.store.search.adv_search_builder_ui import Ui_Dialog from calibre.library.caches import CONTAINS_MATCH, EQUALS_MATCH from calibre.utils.localization import localize_user_manual_link + class AdvSearchBuilderDialog(QDialog, Ui_Dialog): def __init__(self, parent): diff --git a/src/calibre/gui2/store/search/download_thread.py b/src/calibre/gui2/store/search/download_thread.py index a4dfba7fdc..fd6dc89638 100644 --- a/src/calibre/gui2/store/search/download_thread.py +++ b/src/calibre/gui2/store/search/download_thread.py @@ -15,6 +15,7 @@ from calibre import browser from calibre.constants import DEBUG from calibre.utils.img import scale_image + class GenericDownloadThreadPool(object): ''' add_task must be implemented in a subclass and must @@ -139,9 +140,11 @@ class CoverThreadPool(GenericDownloadThreadPool): self.tasks.put((search_result, update_callback, timeout)) GenericDownloadThreadPool.add_task(self) + def decode_data_url(url): return base64.standard_b64decode(url.partition(',')[2]) + class CoverThread(Thread): def __init__(self, tasks, results): diff --git a/src/calibre/gui2/store/search/models.py b/src/calibre/gui2/store/search/models.py index 83ebb976e2..339bd17362 100644 --- a/src/calibre/gui2/store/search/models.py +++ b/src/calibre/gui2/store/search/models.py @@ -20,6 +20,7 @@ from calibre.gui2.store.search.download_thread import DetailsThreadPool, \ from calibre.utils.icu import sort_key from calibre.utils.search_query_parser import SearchQueryParser + def comparable_price(text): # this keep thousand and fraction separators match = re.search(r'(?:\d|[,.](?=\d))(?:\d*(?:[,.\' ](?=\d))?)+', text) diff --git a/src/calibre/gui2/store/search/results_view.py b/src/calibre/gui2/store/search/results_view.py index 6a20d894f5..dca1914a4a 100644 --- a/src/calibre/gui2/store/search/results_view.py +++ b/src/calibre/gui2/store/search/results_view.py @@ -15,6 +15,7 @@ from calibre import fit_image from calibre.gui2.metadata.single_download import RichTextDelegate from calibre.gui2.store.search.models import Matches + class ImageDelegate(QStyledItemDelegate): def paint(self, painter, option, index): diff --git a/src/calibre/gui2/store/search/search.py b/src/calibre/gui2/store/search/search.py index a613f920b3..c2dee4c47b 100644 --- a/src/calibre/gui2/store/search/search.py +++ b/src/calibre/gui2/store/search/search.py @@ -24,6 +24,7 @@ from calibre.gui2.store.search.download_thread import SearchThreadPool, \ from calibre.gui2.store.search.search_ui import Ui_Dialog from calibre.utils.filenames import ascii_filename + class SearchDialog(QDialog, Ui_Dialog): SEARCH_TEXT = _('&Search') diff --git a/src/calibre/gui2/store/search_result.py b/src/calibre/gui2/store/search_result.py index d7b769ec61..0f870ee770 100644 --- a/src/calibre/gui2/store/search_result.py +++ b/src/calibre/gui2/store/search_result.py @@ -6,6 +6,7 @@ __license__ = 'GPL 3' __copyright__ = '2011, John Schember ' __docformat__ = 'restructuredtext en' + class SearchResult(object): DRM_LOCKED = 1 diff --git a/src/calibre/gui2/store/stores/amazon_au_plugin.py b/src/calibre/gui2/store/stores/amazon_au_plugin.py index 610358f6b8..ae114aea83 100644 --- a/src/calibre/gui2/store/stores/amazon_au_plugin.py +++ b/src/calibre/gui2/store/stores/amazon_au_plugin.py @@ -25,9 +25,11 @@ STORE_LINK = 'http://www.amazon.com.au' DRM_SEARCH_TEXT = 'Simultaneous Device Usage' DRM_FREE_TEXT = 'Unlimited' + def get_user_agent(): return 'Mozilla/5.0 (Windows NT 6.1; Trident/7.0; rv:11.0) like Gecko' + def search_amazon(query, max_results=10, timeout=60, write_html_to=None, base_url=SEARCH_BASE_URL, @@ -36,6 +38,7 @@ def search_amazon(query, max_results=10, timeout=60, ): uquery = base_query.copy() uquery[field_keywords] = query + def asbytes(x): if isinstance(x, type('')): x = x.encode('utf-8') @@ -110,6 +113,7 @@ def search_amazon(query, max_results=10, timeout=60, yield s + class AmazonKindleStore(StorePlugin): def open(self, parent=None, detail_item=None, external=False): diff --git a/src/calibre/gui2/store/stores/amazon_ca_plugin.py b/src/calibre/gui2/store/stores/amazon_ca_plugin.py index 923379f30d..9b90d7f128 100644 --- a/src/calibre/gui2/store/stores/amazon_ca_plugin.py +++ b/src/calibre/gui2/store/stores/amazon_ca_plugin.py @@ -25,9 +25,11 @@ STORE_LINK = 'http://www.amazon.ca' DRM_SEARCH_TEXT = 'Simultaneous Device Usage' DRM_FREE_TEXT = 'Unlimited' + def get_user_agent(): return 'Mozilla/5.0 (Windows NT 6.1; Trident/7.0; rv:11.0) like Gecko' + def search_amazon(query, max_results=10, timeout=60, write_html_to=None, base_url=SEARCH_BASE_URL, @@ -36,6 +38,7 @@ def search_amazon(query, max_results=10, timeout=60, ): uquery = base_query.copy() uquery[field_keywords] = query + def asbytes(x): if isinstance(x, type('')): x = x.encode('utf-8') @@ -110,6 +113,7 @@ def search_amazon(query, max_results=10, timeout=60, yield s + class AmazonKindleStore(StorePlugin): def open(self, parent=None, detail_item=None, external=False): diff --git a/src/calibre/gui2/store/stores/amazon_de_plugin.py b/src/calibre/gui2/store/stores/amazon_de_plugin.py index b357d5f44b..469ba52676 100644 --- a/src/calibre/gui2/store/stores/amazon_de_plugin.py +++ b/src/calibre/gui2/store/stores/amazon_de_plugin.py @@ -27,9 +27,11 @@ STORE_LINK = 'http://www.amazon.de' DRM_SEARCH_TEXT = 'Simultaneous Device Usage' DRM_FREE_TEXT = 'Unlimited' + def get_user_agent(): return 'Mozilla/5.0 (Windows NT 6.1; Trident/7.0; rv:11.0) like Gecko' + def search_amazon(query, max_results=10, timeout=60, write_html_to=None, base_url=SEARCH_BASE_URL, @@ -38,6 +40,7 @@ def search_amazon(query, max_results=10, timeout=60, ): uquery = base_query.copy() uquery[field_keywords] = query + def asbytes(x): if isinstance(x, type('')): x = x.encode('utf-8') @@ -112,6 +115,7 @@ def search_amazon(query, max_results=10, timeout=60, yield s + class AmazonKindleStore(StorePlugin): def open(self, parent=None, detail_item=None, external=False): diff --git a/src/calibre/gui2/store/stores/amazon_es_plugin.py b/src/calibre/gui2/store/stores/amazon_es_plugin.py index ebfb64edfb..0a5611dbde 100644 --- a/src/calibre/gui2/store/stores/amazon_es_plugin.py +++ b/src/calibre/gui2/store/stores/amazon_es_plugin.py @@ -27,9 +27,11 @@ STORE_LINK = 'http://www.amazon.es' DRM_SEARCH_TEXT = 'Simultaneous Device Usage' DRM_FREE_TEXT = 'Unlimited' + def get_user_agent(): return 'Mozilla/5.0 (Windows NT 6.1; Trident/7.0; rv:11.0) like Gecko' + def search_amazon(query, max_results=10, timeout=60, write_html_to=None, base_url=SEARCH_BASE_URL, @@ -38,6 +40,7 @@ def search_amazon(query, max_results=10, timeout=60, ): uquery = base_query.copy() uquery[field_keywords] = query + def asbytes(x): if isinstance(x, type('')): x = x.encode('utf-8') @@ -112,6 +115,7 @@ def search_amazon(query, max_results=10, timeout=60, yield s + class AmazonKindleStore(StorePlugin): def open(self, parent=None, detail_item=None, external=False): diff --git a/src/calibre/gui2/store/stores/amazon_fr_plugin.py b/src/calibre/gui2/store/stores/amazon_fr_plugin.py index 28f2a13571..e325d0eab9 100644 --- a/src/calibre/gui2/store/stores/amazon_fr_plugin.py +++ b/src/calibre/gui2/store/stores/amazon_fr_plugin.py @@ -27,9 +27,11 @@ STORE_LINK = 'http://www.amazon.fr' DRM_SEARCH_TEXT = 'Simultaneous Device Usage' DRM_FREE_TEXT = 'Unlimited' + def get_user_agent(): return 'Mozilla/5.0 (Windows NT 6.1; Trident/7.0; rv:11.0) like Gecko' + def search_amazon(query, max_results=10, timeout=60, write_html_to=None, base_url=SEARCH_BASE_URL, @@ -38,6 +40,7 @@ def search_amazon(query, max_results=10, timeout=60, ): uquery = base_query.copy() uquery[field_keywords] = query + def asbytes(x): if isinstance(x, type('')): x = x.encode('utf-8') @@ -112,6 +115,7 @@ def search_amazon(query, max_results=10, timeout=60, yield s + class AmazonKindleStore(StorePlugin): def open(self, parent=None, detail_item=None, external=False): diff --git a/src/calibre/gui2/store/stores/amazon_in_plugin.py b/src/calibre/gui2/store/stores/amazon_in_plugin.py index 3a5958d187..d9d3e1418f 100644 --- a/src/calibre/gui2/store/stores/amazon_in_plugin.py +++ b/src/calibre/gui2/store/stores/amazon_in_plugin.py @@ -25,9 +25,11 @@ STORE_LINK = 'http://www.amazon.in' DRM_SEARCH_TEXT = 'Simultaneous Device Usage' DRM_FREE_TEXT = 'Unlimited' + def get_user_agent(): return 'Mozilla/5.0 (Windows NT 6.1; Trident/7.0; rv:11.0) like Gecko' + def search_amazon(query, max_results=10, timeout=60, write_html_to=None, base_url=SEARCH_BASE_URL, @@ -36,6 +38,7 @@ def search_amazon(query, max_results=10, timeout=60, ): uquery = base_query.copy() uquery[field_keywords] = query + def asbytes(x): if isinstance(x, type('')): x = x.encode('utf-8') @@ -112,6 +115,7 @@ def search_amazon(query, max_results=10, timeout=60, yield s + class AmazonKindleStore(StorePlugin): def open(self, parent=None, detail_item=None, external=False): diff --git a/src/calibre/gui2/store/stores/amazon_it_plugin.py b/src/calibre/gui2/store/stores/amazon_it_plugin.py index a68747bc2e..ded3a8cae9 100644 --- a/src/calibre/gui2/store/stores/amazon_it_plugin.py +++ b/src/calibre/gui2/store/stores/amazon_it_plugin.py @@ -27,9 +27,11 @@ STORE_LINK = 'http://www.amazon.it' DRM_SEARCH_TEXT = 'Simultaneous Device Usage' DRM_FREE_TEXT = 'Unlimited' + def get_user_agent(): return 'Mozilla/5.0 (Windows NT 6.1; Trident/7.0; rv:11.0) like Gecko' + def search_amazon(query, max_results=10, timeout=60, write_html_to=None, base_url=SEARCH_BASE_URL, @@ -38,6 +40,7 @@ def search_amazon(query, max_results=10, timeout=60, ): uquery = base_query.copy() uquery[field_keywords] = query + def asbytes(x): if isinstance(x, type('')): x = x.encode('utf-8') @@ -112,6 +115,7 @@ def search_amazon(query, max_results=10, timeout=60, yield s + class AmazonKindleStore(StorePlugin): def open(self, parent=None, detail_item=None, external=False): diff --git a/src/calibre/gui2/store/stores/amazon_plugin.py b/src/calibre/gui2/store/stores/amazon_plugin.py index 398dbce40f..56ccc67225 100644 --- a/src/calibre/gui2/store/stores/amazon_plugin.py +++ b/src/calibre/gui2/store/stores/amazon_plugin.py @@ -25,9 +25,11 @@ STORE_LINK = 'http://www.amazon.com/Kindle-eBooks' DRM_SEARCH_TEXT = 'Simultaneous Device Usage' DRM_FREE_TEXT = 'Unlimited' + def get_user_agent(): return 'Mozilla/5.0 (Windows NT 6.1; Trident/7.0; rv:11.0) like Gecko' + def search_amazon(query, max_results=10, timeout=60, write_html_to=None, base_url=SEARCH_BASE_URL, @@ -36,6 +38,7 @@ def search_amazon(query, max_results=10, timeout=60, ): uquery = base_query.copy() uquery[field_keywords] = query + def asbytes(x): if isinstance(x, type('')): x = x.encode('utf-8') @@ -147,6 +150,7 @@ def search_amazon(query, max_results=10, timeout=60, yield s + class AmazonKindleStore(StorePlugin): def open(self, parent=None, detail_item=None, external=False): diff --git a/src/calibre/gui2/store/stores/amazon_uk_plugin.py b/src/calibre/gui2/store/stores/amazon_uk_plugin.py index 2f4a0e0e1e..9786c6f3b7 100644 --- a/src/calibre/gui2/store/stores/amazon_uk_plugin.py +++ b/src/calibre/gui2/store/stores/amazon_uk_plugin.py @@ -25,9 +25,11 @@ STORE_LINK = 'http://www.amazon.co.uk' DRM_SEARCH_TEXT = 'Simultaneous Device Usage' DRM_FREE_TEXT = 'Unlimited' + def get_user_agent(): return 'Mozilla/5.0 (Windows NT 6.1; Trident/7.0; rv:11.0) like Gecko' + def search_amazon(query, max_results=10, timeout=60, write_html_to=None, base_url=SEARCH_BASE_URL, @@ -36,6 +38,7 @@ def search_amazon(query, max_results=10, timeout=60, ): uquery = base_query.copy() uquery[field_keywords] = query + def asbytes(x): if isinstance(x, type('')): x = x.encode('utf-8') @@ -110,6 +113,7 @@ def search_amazon(query, max_results=10, timeout=60, yield s + class AmazonKindleStore(StorePlugin): def open(self, parent=None, detail_item=None, external=False): diff --git a/src/calibre/gui2/store/stores/archive_org_plugin.py b/src/calibre/gui2/store/stores/archive_org_plugin.py index af45d0ad84..bd425da928 100644 --- a/src/calibre/gui2/store/stores/archive_org_plugin.py +++ b/src/calibre/gui2/store/stores/archive_org_plugin.py @@ -11,6 +11,7 @@ from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.opensearch_store import OpenSearchOPDSStore from calibre.gui2.store.search_result import SearchResult + class ArchiveOrgStore(BasicStoreConfig, OpenSearchOPDSStore): open_search_url = 'http://bookserver.archive.org/catalog/opensearch.xml' diff --git a/src/calibre/gui2/store/stores/baen_webscription_plugin.py b/src/calibre/gui2/store/stores/baen_webscription_plugin.py index bc45245005..0a20467acc 100644 --- a/src/calibre/gui2/store/stores/baen_webscription_plugin.py +++ b/src/calibre/gui2/store/stores/baen_webscription_plugin.py @@ -22,6 +22,7 @@ from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.web_store_dialog import WebStoreDialog + class BaenWebScriptionStore(BasicStoreConfig, StorePlugin): def open(self, parent=None, detail_item=None, external=False): diff --git a/src/calibre/gui2/store/stores/beam_ebooks_de_plugin.py b/src/calibre/gui2/store/stores/beam_ebooks_de_plugin.py index 792a63fe1c..2e5a984557 100644 --- a/src/calibre/gui2/store/stores/beam_ebooks_de_plugin.py +++ b/src/calibre/gui2/store/stores/beam_ebooks_de_plugin.py @@ -21,6 +21,7 @@ from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.web_store_dialog import WebStoreDialog + class BeamEBooksDEStore(BasicStoreConfig, StorePlugin): def open(self, parent=None, detail_item=None, external=False): diff --git a/src/calibre/gui2/store/stores/biblio_plugin.py b/src/calibre/gui2/store/stores/biblio_plugin.py index 4d0f02ba79..03873283e2 100644 --- a/src/calibre/gui2/store/stores/biblio_plugin.py +++ b/src/calibre/gui2/store/stores/biblio_plugin.py @@ -13,6 +13,7 @@ from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.opensearch_store import OpenSearchOPDSStore from calibre.gui2.store.search_result import SearchResult + class BiblioStore(BasicStoreConfig, OpenSearchOPDSStore): open_search_url = 'http://biblio.bg/feed.opds.php' diff --git a/src/calibre/gui2/store/stores/bn_plugin.py b/src/calibre/gui2/store/stores/bn_plugin.py index e2b5dace4a..774c6e7fcb 100644 --- a/src/calibre/gui2/store/stores/bn_plugin.py +++ b/src/calibre/gui2/store/stores/bn_plugin.py @@ -22,6 +22,7 @@ from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.web_store_dialog import WebStoreDialog + class BNStore(BasicStoreConfig, StorePlugin): def open(self, parent=None, detail_item=None, external=False): diff --git a/src/calibre/gui2/store/stores/bubok_portugal_plugin.py b/src/calibre/gui2/store/stores/bubok_portugal_plugin.py index ffc85e6040..c61d5a9cab 100644 --- a/src/calibre/gui2/store/stores/bubok_portugal_plugin.py +++ b/src/calibre/gui2/store/stores/bubok_portugal_plugin.py @@ -21,6 +21,7 @@ from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.web_store_dialog import WebStoreDialog + class BubokPortugalStore(BasicStoreConfig, StorePlugin): def open(self, parent=None, detail_item=None, external=False): diff --git a/src/calibre/gui2/store/stores/bubok_publishing_plugin.py b/src/calibre/gui2/store/stores/bubok_publishing_plugin.py index c5d1912278..e8d672e51f 100644 --- a/src/calibre/gui2/store/stores/bubok_publishing_plugin.py +++ b/src/calibre/gui2/store/stores/bubok_publishing_plugin.py @@ -21,6 +21,7 @@ from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.web_store_dialog import WebStoreDialog + class BubokPublishingStore(BasicStoreConfig, StorePlugin): def open(self, parent=None, detail_item=None, external=False): diff --git a/src/calibre/gui2/store/stores/cdp_plugin.py b/src/calibre/gui2/store/stores/cdp_plugin.py index 50af730709..2d246abce4 100644 --- a/src/calibre/gui2/store/stores/cdp_plugin.py +++ b/src/calibre/gui2/store/stores/cdp_plugin.py @@ -22,6 +22,7 @@ from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.web_store_dialog import WebStoreDialog + class CdpStore(BasicStoreConfig, StorePlugin): def open(self, parent=None, detail_item=None, external=False): diff --git a/src/calibre/gui2/store/stores/chitanka_plugin.py b/src/calibre/gui2/store/stores/chitanka_plugin.py index 657d2fe4d6..4eecf87f9c 100644 --- a/src/calibre/gui2/store/stores/chitanka_plugin.py +++ b/src/calibre/gui2/store/stores/chitanka_plugin.py @@ -22,6 +22,7 @@ from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.web_store_dialog import WebStoreDialog + class ChitankaStore(BasicStoreConfig, StorePlugin): def open(self, parent=None, detail_item=None, external=False): diff --git a/src/calibre/gui2/store/stores/ebook_nl_plugin.py b/src/calibre/gui2/store/stores/ebook_nl_plugin.py index 9c79b38e9a..029fd5040e 100644 --- a/src/calibre/gui2/store/stores/ebook_nl_plugin.py +++ b/src/calibre/gui2/store/stores/ebook_nl_plugin.py @@ -21,6 +21,7 @@ from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.web_store_dialog import WebStoreDialog + class EBookNLStore(BasicStoreConfig, StorePlugin): def open(self, parent=None, detail_item=None, external=False): diff --git a/src/calibre/gui2/store/stores/ebookpoint_plugin.py b/src/calibre/gui2/store/stores/ebookpoint_plugin.py index f3c4da9a88..a72998168f 100644 --- a/src/calibre/gui2/store/stores/ebookpoint_plugin.py +++ b/src/calibre/gui2/store/stores/ebookpoint_plugin.py @@ -23,6 +23,7 @@ from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.web_store_dialog import WebStoreDialog + class EbookpointStore(BasicStoreConfig, StorePlugin): def open(self, parent=None, detail_item=None, external=False): diff --git a/src/calibre/gui2/store/stores/ebooks_com_plugin.py b/src/calibre/gui2/store/stores/ebooks_com_plugin.py index dcdc7bc782..455c705113 100644 --- a/src/calibre/gui2/store/stores/ebooks_com_plugin.py +++ b/src/calibre/gui2/store/stores/ebooks_com_plugin.py @@ -22,6 +22,7 @@ from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.web_store_dialog import WebStoreDialog + class EbookscomStore(BasicStoreConfig, StorePlugin): def open(self, parent=None, detail_item=None, external=False): diff --git a/src/calibre/gui2/store/stores/ebooksgratuits_plugin.py b/src/calibre/gui2/store/stores/ebooksgratuits_plugin.py index 3a58a16bdd..48b27badf2 100644 --- a/src/calibre/gui2/store/stores/ebooksgratuits_plugin.py +++ b/src/calibre/gui2/store/stores/ebooksgratuits_plugin.py @@ -12,6 +12,7 @@ from calibre.gui2.store.opensearch_store import OpenSearchOPDSStore from calibre.gui2.store.search_result import SearchResult from calibre.utils.filenames import ascii_text + class EbooksGratuitsStore(BasicStoreConfig, OpenSearchOPDSStore): open_search_url = 'http://www.ebooksgratuits.com/opds/opensearch.xml' diff --git a/src/calibre/gui2/store/stores/ebookshoppe_uk_plugin.py b/src/calibre/gui2/store/stores/ebookshoppe_uk_plugin.py index 1ad83b28b9..9f34ccd677 100644 --- a/src/calibre/gui2/store/stores/ebookshoppe_uk_plugin.py +++ b/src/calibre/gui2/store/stores/ebookshoppe_uk_plugin.py @@ -21,6 +21,7 @@ from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.web_store_dialog import WebStoreDialog + class EBookShoppeUKStore(BasicStoreConfig, StorePlugin): def open(self, parent=None, detail_item=None, external=False): diff --git a/src/calibre/gui2/store/stores/eknigi_plugin.py b/src/calibre/gui2/store/stores/eknigi_plugin.py index 82324c7bce..1eaf83e7b0 100644 --- a/src/calibre/gui2/store/stores/eknigi_plugin.py +++ b/src/calibre/gui2/store/stores/eknigi_plugin.py @@ -23,6 +23,7 @@ from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.web_store_dialog import WebStoreDialog + class eKnigiStore(BasicStoreConfig, StorePlugin): def open(self, parent=None, detail_item=None, external=False): diff --git a/src/calibre/gui2/store/stores/empik_plugin.py b/src/calibre/gui2/store/stores/empik_plugin.py index 2f1c0e96ce..e11bc0ba92 100644 --- a/src/calibre/gui2/store/stores/empik_plugin.py +++ b/src/calibre/gui2/store/stores/empik_plugin.py @@ -23,6 +23,7 @@ from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.web_store_dialog import WebStoreDialog + class EmpikStore(BasicStoreConfig, StorePlugin): def open(self, parent=None, detail_item=None, external=False): diff --git a/src/calibre/gui2/store/stores/feedbooks_plugin.py b/src/calibre/gui2/store/stores/feedbooks_plugin.py index d1c3094718..6bfe47a19f 100644 --- a/src/calibre/gui2/store/stores/feedbooks_plugin.py +++ b/src/calibre/gui2/store/stores/feedbooks_plugin.py @@ -11,6 +11,7 @@ from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.opensearch_store import OpenSearchOPDSStore from calibre.gui2.store.search_result import SearchResult + class FeedbooksStore(BasicStoreConfig, OpenSearchOPDSStore): open_search_url = 'http://assets0.feedbooks.net/opensearch.xml?t=1253087147' diff --git a/src/calibre/gui2/store/stores/google_books_plugin.py b/src/calibre/gui2/store/stores/google_books_plugin.py index 3561effc3c..23a1099458 100644 --- a/src/calibre/gui2/store/stores/google_books_plugin.py +++ b/src/calibre/gui2/store/stores/google_books_plugin.py @@ -21,6 +21,7 @@ from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.web_store_dialog import WebStoreDialog + class GoogleBooksStore(BasicStoreConfig, StorePlugin): def open(self, parent=None, detail_item=None, external=False): diff --git a/src/calibre/gui2/store/stores/gutenberg_plugin.py b/src/calibre/gui2/store/stores/gutenberg_plugin.py index a5fd6527e5..dab8c7a61e 100644 --- a/src/calibre/gui2/store/stores/gutenberg_plugin.py +++ b/src/calibre/gui2/store/stores/gutenberg_plugin.py @@ -23,11 +23,13 @@ from calibre.gui2.store.search_result import SearchResult web_url = 'http://m.gutenberg.org/' + def fix_url(url): if url and url.startswith('//'): url = 'http:' + url return url + def search(query, max_results=10, timeout=60, write_raw_to=None): url = 'http://m.gutenberg.org/ebooks/search.opds/?query=' + urllib.quote_plus(query) @@ -85,6 +87,7 @@ def search(query, max_results=10, timeout=60, write_raw_to=None): yield s + class GutenbergStore(BasicStoreConfig, OpenSearchOPDSStore): open_search_url = 'http://www.gutenberg.org/catalog/osd-books.xml' diff --git a/src/calibre/gui2/store/stores/kobo_plugin.py b/src/calibre/gui2/store/stores/kobo_plugin.py index 14d189a69a..4bbed4f9f7 100644 --- a/src/calibre/gui2/store/stores/kobo_plugin.py +++ b/src/calibre/gui2/store/stores/kobo_plugin.py @@ -22,6 +22,7 @@ from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.web_store_dialog import WebStoreDialog + def search_kobo(query, max_results=10, timeout=60, write_html_to=None): from css_selectors import Select url = 'http://www.kobobooks.com/search/search.html?q=' + urllib.quote_plus(query) @@ -80,6 +81,7 @@ def search_kobo(query, max_results=10, timeout=60, write_html_to=None): yield s + class KoboStore(BasicStoreConfig, StorePlugin): minimum_calibre_version = (2, 21, 0) diff --git a/src/calibre/gui2/store/stores/koobe_plugin.py b/src/calibre/gui2/store/stores/koobe_plugin.py index f6bceda323..62381a1d4a 100644 --- a/src/calibre/gui2/store/stores/koobe_plugin.py +++ b/src/calibre/gui2/store/stores/koobe_plugin.py @@ -22,6 +22,7 @@ from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.web_store_dialog import WebStoreDialog + class KoobeStore(BasicStoreConfig, StorePlugin): def open(self, parent=None, detail_item=None, external=False): diff --git a/src/calibre/gui2/store/stores/legimi_plugin.py b/src/calibre/gui2/store/stores/legimi_plugin.py index d3f3613a18..94d3b3ef53 100644 --- a/src/calibre/gui2/store/stores/legimi_plugin.py +++ b/src/calibre/gui2/store/stores/legimi_plugin.py @@ -23,6 +23,7 @@ from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.web_store_dialog import WebStoreDialog + class LegimiStore(BasicStoreConfig, StorePlugin): def open(self, parent=None, detail_item=None, external=False): diff --git a/src/calibre/gui2/store/stores/libri_de_plugin.py b/src/calibre/gui2/store/stores/libri_de_plugin.py index 4d1f0895c9..bc64db07e7 100644 --- a/src/calibre/gui2/store/stores/libri_de_plugin.py +++ b/src/calibre/gui2/store/stores/libri_de_plugin.py @@ -21,6 +21,7 @@ from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.web_store_dialog import WebStoreDialog + class LibreDEStore(BasicStoreConfig, StorePlugin): def open(self, parent=None, detail_item=None, external=False): diff --git a/src/calibre/gui2/store/stores/litres_plugin.py b/src/calibre/gui2/store/stores/litres_plugin.py index 5ef223cabc..5e67eff854 100644 --- a/src/calibre/gui2/store/stores/litres_plugin.py +++ b/src/calibre/gui2/store/stores/litres_plugin.py @@ -23,6 +23,7 @@ from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.web_store_dialog import WebStoreDialog + class LitResStore(BasicStoreConfig, StorePlugin): shop_url = u'http://www.litres.ru' # http://robot.litres.ru/pages/biblio_book/?art=174405 @@ -98,6 +99,7 @@ class LitResStore(BasicStoreConfig, StorePlugin): sRes.formats = ', '.join(fmt_set) return sRes + def format_price_in_RUR(price): ''' Try to format price according ru locale: '12 212,34 руб.' @@ -113,6 +115,7 @@ def format_price_in_RUR(price): pass return price + def ungzipResponse(r,b): headers = r.info() if headers['Content-Encoding']=='gzip': @@ -124,6 +127,7 @@ def ungzipResponse(r,b): r.set_data(data) b.set_response(r) + def _get_affiliate_id(): aff_id = u'3623565' # Use Kovid's affiliate id 30% of the time. @@ -131,6 +135,7 @@ def _get_affiliate_id(): aff_id = u'4084465' return u'lfrom=' + aff_id + def _parse_ebook_formats(formatsStr): ''' Creates a set with displayable names of the formats diff --git a/src/calibre/gui2/store/stores/manybooks_plugin.py b/src/calibre/gui2/store/stores/manybooks_plugin.py index af91b86f0e..6520089510 100644 --- a/src/calibre/gui2/store/stores/manybooks_plugin.py +++ b/src/calibre/gui2/store/stores/manybooks_plugin.py @@ -19,6 +19,7 @@ from calibre.gui2.store.search_result import SearchResult from calibre.utils.opensearch.description import Description from calibre.utils.opensearch.query import Query + class ManyBooksStore(BasicStoreConfig, OpenSearchOPDSStore): open_search_url = 'http://www.manybooks.net/opds/' diff --git a/src/calibre/gui2/store/stores/mills_boon_uk_plugin.py b/src/calibre/gui2/store/stores/mills_boon_uk_plugin.py index ce29d8f770..21b1a8fbac 100644 --- a/src/calibre/gui2/store/stores/mills_boon_uk_plugin.py +++ b/src/calibre/gui2/store/stores/mills_boon_uk_plugin.py @@ -21,6 +21,7 @@ from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.web_store_dialog import WebStoreDialog + class MillsBoonUKStore(BasicStoreConfig, StorePlugin): def open(self, parent=None, detail_item=None, external=False): diff --git a/src/calibre/gui2/store/stores/mobileread/adv_search_builder.py b/src/calibre/gui2/store/stores/mobileread/adv_search_builder.py index 3fd65944d4..df90041ddc 100644 --- a/src/calibre/gui2/store/stores/mobileread/adv_search_builder.py +++ b/src/calibre/gui2/store/stores/mobileread/adv_search_builder.py @@ -13,6 +13,7 @@ from PyQt5.Qt import (QDialog, QDialogButtonBox) from calibre.gui2.store.stores.mobileread.adv_search_builder_ui import Ui_Dialog from calibre.library.caches import CONTAINS_MATCH, EQUALS_MATCH + class AdvSearchBuilderDialog(QDialog, Ui_Dialog): def __init__(self, parent): diff --git a/src/calibre/gui2/store/stores/mobileread/cache_progress_dialog.py b/src/calibre/gui2/store/stores/mobileread/cache_progress_dialog.py index d659190769..6032bb4789 100644 --- a/src/calibre/gui2/store/stores/mobileread/cache_progress_dialog.py +++ b/src/calibre/gui2/store/stores/mobileread/cache_progress_dialog.py @@ -10,6 +10,7 @@ from PyQt5.Qt import QDialog from calibre.gui2.store.stores.mobileread.cache_progress_dialog_ui import Ui_Dialog + class CacheProgressDialog(QDialog, Ui_Dialog): def __init__(self, parent=None, total=None): diff --git a/src/calibre/gui2/store/stores/mobileread/cache_update_thread.py b/src/calibre/gui2/store/stores/mobileread/cache_update_thread.py index ed62a175b3..e9df195878 100644 --- a/src/calibre/gui2/store/stores/mobileread/cache_update_thread.py +++ b/src/calibre/gui2/store/stores/mobileread/cache_update_thread.py @@ -17,6 +17,7 @@ from PyQt5.Qt import (pyqtSignal, QObject) from calibre import browser from calibre.gui2.store.search_result import SearchResult + class CacheUpdateThread(Thread, QObject): total_changed = pyqtSignal(int) diff --git a/src/calibre/gui2/store/stores/mobileread/mobileread_plugin.py b/src/calibre/gui2/store/stores/mobileread/mobileread_plugin.py index 53a1b11f21..b60290b636 100644 --- a/src/calibre/gui2/store/stores/mobileread/mobileread_plugin.py +++ b/src/calibre/gui2/store/stores/mobileread/mobileread_plugin.py @@ -22,6 +22,7 @@ from calibre.gui2.store.stores.mobileread.cache_progress_dialog import CacheProg from calibre.gui2.store.stores.mobileread.cache_update_thread import CacheUpdateThread from calibre.gui2.store.stores.mobileread.store_dialog import MobileReadStoreDialog + class MobileReadStore(BasicStoreConfig, StorePlugin): def __init__(self, *args, **kwargs): diff --git a/src/calibre/gui2/store/stores/mobileread/models.py b/src/calibre/gui2/store/stores/mobileread/models.py index 75d050900e..7cdff25439 100644 --- a/src/calibre/gui2/store/stores/mobileread/models.py +++ b/src/calibre/gui2/store/stores/mobileread/models.py @@ -15,6 +15,7 @@ from calibre.utils.config_base import prefs from calibre.utils.icu import sort_key from calibre.utils.search_query_parser import SearchQueryParser + class BooksModel(QAbstractItemModel): total_changed = pyqtSignal(int) diff --git a/src/calibre/gui2/store/stores/mobileread/store_dialog.py b/src/calibre/gui2/store/stores/mobileread/store_dialog.py index 256f3a0ecd..ce8e806a58 100644 --- a/src/calibre/gui2/store/stores/mobileread/store_dialog.py +++ b/src/calibre/gui2/store/stores/mobileread/store_dialog.py @@ -13,6 +13,7 @@ from calibre.gui2.store.stores.mobileread.adv_search_builder import AdvSearchBui from calibre.gui2.store.stores.mobileread.models import BooksModel from calibre.gui2.store.stores.mobileread.store_dialog_ui import Ui_Dialog + class MobileReadStoreDialog(QDialog, Ui_Dialog): def __init__(self, plugin, *args): diff --git a/src/calibre/gui2/store/stores/nexto_plugin.py b/src/calibre/gui2/store/stores/nexto_plugin.py index ec2dc6cd3b..52a89bfc91 100644 --- a/src/calibre/gui2/store/stores/nexto_plugin.py +++ b/src/calibre/gui2/store/stores/nexto_plugin.py @@ -22,6 +22,7 @@ from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.web_store_dialog import WebStoreDialog + class NextoStore(BasicStoreConfig, StorePlugin): def open(self, parent=None, detail_item=None, external=False): diff --git a/src/calibre/gui2/store/stores/nook_uk_plugin.py b/src/calibre/gui2/store/stores/nook_uk_plugin.py index 35d5bacea5..8eda06ede6 100644 --- a/src/calibre/gui2/store/stores/nook_uk_plugin.py +++ b/src/calibre/gui2/store/stores/nook_uk_plugin.py @@ -22,6 +22,7 @@ from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.web_store_dialog import WebStoreDialog + class NookUKStore(BasicStoreConfig, StorePlugin): def open(self, parent=None, detail_item=None, external=False): diff --git a/src/calibre/gui2/store/stores/open_books_plugin.py b/src/calibre/gui2/store/stores/open_books_plugin.py index b62bf2541f..a5683d07ca 100644 --- a/src/calibre/gui2/store/stores/open_books_plugin.py +++ b/src/calibre/gui2/store/stores/open_books_plugin.py @@ -21,6 +21,7 @@ from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.web_store_dialog import WebStoreDialog + class OpenBooksStore(BasicStoreConfig, StorePlugin): def open(self, parent=None, detail_item=None, external=False): diff --git a/src/calibre/gui2/store/stores/ozon_ru_plugin.py b/src/calibre/gui2/store/stores/ozon_ru_plugin.py index 80e67d4f0b..c8da9f2bfb 100644 --- a/src/calibre/gui2/store/stores/ozon_ru_plugin.py +++ b/src/calibre/gui2/store/stores/ozon_ru_plugin.py @@ -22,6 +22,7 @@ from calibre.gui2.store.web_store_dialog import WebStoreDialog shop_url = 'http://www.ozon.ru' + def search(query, max_results=15, timeout=60): url = 'http://www.ozon.ru/?context=search&text=%s&store=1,0&group=div_book' % urllib.quote_plus(query) @@ -45,6 +46,7 @@ def search(query, max_results=15, timeout=60): s.price = format_price_in_RUR(s.price) yield s + class OzonRUStore(StorePlugin): def open(self, parent=None, detail_item=None, external=False): @@ -61,6 +63,7 @@ class OzonRUStore(StorePlugin): for s in search(query, max_results=max_results, timeout=timeout): yield s + def format_price_in_RUR(price): ''' Try to format price according ru locale: '12 212,34 руб.' diff --git a/src/calibre/gui2/store/stores/pragmatic_bookshelf_plugin.py b/src/calibre/gui2/store/stores/pragmatic_bookshelf_plugin.py index 3199445a3a..0415dbb074 100644 --- a/src/calibre/gui2/store/stores/pragmatic_bookshelf_plugin.py +++ b/src/calibre/gui2/store/stores/pragmatic_bookshelf_plugin.py @@ -11,6 +11,7 @@ from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.opensearch_store import OpenSearchOPDSStore from calibre.gui2.store.search_result import SearchResult + class PragmaticBookshelfStore(BasicStoreConfig, OpenSearchOPDSStore): open_search_url = 'http://pragprog.com/catalog/search-description' diff --git a/src/calibre/gui2/store/stores/publio_plugin.py b/src/calibre/gui2/store/stores/publio_plugin.py index a62365b39f..6765e3fcd9 100644 --- a/src/calibre/gui2/store/stores/publio_plugin.py +++ b/src/calibre/gui2/store/stores/publio_plugin.py @@ -21,6 +21,7 @@ from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.web_store_dialog import WebStoreDialog + class PublioStore(BasicStoreConfig, StorePlugin): def open(self, parent=None, detail_item=None, external=False): diff --git a/src/calibre/gui2/store/stores/rw2010_plugin.py b/src/calibre/gui2/store/stores/rw2010_plugin.py index 78fff95fd1..1bc56884ea 100644 --- a/src/calibre/gui2/store/stores/rw2010_plugin.py +++ b/src/calibre/gui2/store/stores/rw2010_plugin.py @@ -22,6 +22,7 @@ from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.web_store_dialog import WebStoreDialog + class RW2010Store(BasicStoreConfig, StorePlugin): def open(self, parent=None, detail_item=None, external=False): diff --git a/src/calibre/gui2/store/stores/smashwords_plugin.py b/src/calibre/gui2/store/stores/smashwords_plugin.py index 3b28142e7f..7e640248d8 100644 --- a/src/calibre/gui2/store/stores/smashwords_plugin.py +++ b/src/calibre/gui2/store/stores/smashwords_plugin.py @@ -23,6 +23,7 @@ from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.web_store_dialog import WebStoreDialog + def search(query, max_results=10, timeout=60): url = 'http://www.smashwords.com/books/search?query=' + urllib2.quote(query) @@ -71,6 +72,7 @@ def search(query, max_results=10, timeout=60): yield s + class SmashwordsStore(BasicStoreConfig, StorePlugin): def open(self, parent=None, detail_item=None, external=False): diff --git a/src/calibre/gui2/store/stores/sony_au_plugin.py b/src/calibre/gui2/store/stores/sony_au_plugin.py index 301687c997..860da67c20 100644 --- a/src/calibre/gui2/store/stores/sony_au_plugin.py +++ b/src/calibre/gui2/store/stores/sony_au_plugin.py @@ -22,6 +22,7 @@ from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.web_store_dialog import WebStoreDialog + class SonyStore(BasicStoreConfig, StorePlugin): SEARCH_URL = 'https://au.readerstore.sony.com/catalog/search/?query=%s' diff --git a/src/calibre/gui2/store/stores/sony_plugin.py b/src/calibre/gui2/store/stores/sony_plugin.py index 8d00904539..cabad78a52 100644 --- a/src/calibre/gui2/store/stores/sony_plugin.py +++ b/src/calibre/gui2/store/stores/sony_plugin.py @@ -22,6 +22,7 @@ from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.web_store_dialog import WebStoreDialog + class SonyStore(BasicStoreConfig, StorePlugin): def open(self, parent=None, detail_item=None, external=False): diff --git a/src/calibre/gui2/store/stores/virtualo_plugin.py b/src/calibre/gui2/store/stores/virtualo_plugin.py index ab5bbd8ed2..6dee57aee2 100644 --- a/src/calibre/gui2/store/stores/virtualo_plugin.py +++ b/src/calibre/gui2/store/stores/virtualo_plugin.py @@ -23,6 +23,7 @@ from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.web_store_dialog import WebStoreDialog + class VirtualoStore(BasicStoreConfig, StorePlugin): def open(self, parent=None, detail_item=None, external=False): diff --git a/src/calibre/gui2/store/stores/waterstones_uk_plugin.py b/src/calibre/gui2/store/stores/waterstones_uk_plugin.py index 406f544d56..2e5608fc28 100644 --- a/src/calibre/gui2/store/stores/waterstones_uk_plugin.py +++ b/src/calibre/gui2/store/stores/waterstones_uk_plugin.py @@ -21,6 +21,7 @@ from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.web_store_dialog import WebStoreDialog + class WaterstonesUKStore(BasicStoreConfig, StorePlugin): def open(self, parent=None, detail_item=None, external=False): diff --git a/src/calibre/gui2/store/stores/weightless_books_plugin.py b/src/calibre/gui2/store/stores/weightless_books_plugin.py index bcf81e9d45..258a854f78 100644 --- a/src/calibre/gui2/store/stores/weightless_books_plugin.py +++ b/src/calibre/gui2/store/stores/weightless_books_plugin.py @@ -21,6 +21,7 @@ from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.web_store_dialog import WebStoreDialog + class WeightlessBooksStore(BasicStoreConfig, StorePlugin): def open(self, parent=None, detail_item=None, external=False): diff --git a/src/calibre/gui2/store/stores/whsmith_uk_plugin.py b/src/calibre/gui2/store/stores/whsmith_uk_plugin.py index c525b39b95..1fba7fdb62 100644 --- a/src/calibre/gui2/store/stores/whsmith_uk_plugin.py +++ b/src/calibre/gui2/store/stores/whsmith_uk_plugin.py @@ -21,6 +21,7 @@ from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.web_store_dialog import WebStoreDialog + class WHSmithUKStore(BasicStoreConfig, StorePlugin): def open(self, parent=None, detail_item=None, external=False): diff --git a/src/calibre/gui2/store/stores/woblink_plugin.py b/src/calibre/gui2/store/stores/woblink_plugin.py index 4e440e8673..5ad413214e 100644 --- a/src/calibre/gui2/store/stores/woblink_plugin.py +++ b/src/calibre/gui2/store/stores/woblink_plugin.py @@ -22,6 +22,7 @@ from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.web_store_dialog import WebStoreDialog + def search(query, max_results=10, timeout=60): url = 'http://woblink.com/publication/ajax?mode=none&query=' + urllib.quote_plus(query.encode('utf-8')) if max_results > 10: diff --git a/src/calibre/gui2/store/stores/wolnelektury_plugin.py b/src/calibre/gui2/store/stores/wolnelektury_plugin.py index aceef1b508..702162e066 100644 --- a/src/calibre/gui2/store/stores/wolnelektury_plugin.py +++ b/src/calibre/gui2/store/stores/wolnelektury_plugin.py @@ -21,6 +21,7 @@ from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.web_store_dialog import WebStoreDialog + class WolneLekturyStore(BasicStoreConfig, StorePlugin): def open(self, parent=None, detail_item=None, external=False): diff --git a/src/calibre/gui2/store/stores/xinxii_plugin.py b/src/calibre/gui2/store/stores/xinxii_plugin.py index 78e357e409..b38e0841b3 100644 --- a/src/calibre/gui2/store/stores/xinxii_plugin.py +++ b/src/calibre/gui2/store/stores/xinxii_plugin.py @@ -17,6 +17,7 @@ from calibre.gui2.store.basic_config import BasicStoreConfig from calibre.gui2.store.opensearch_store import OpenSearchOPDSStore from calibre.gui2.store.search_result import SearchResult + class XinXiiStore(BasicStoreConfig, OpenSearchOPDSStore): open_search_url = 'http://www.xinxii.com/catalog-search/' diff --git a/src/calibre/gui2/store/web_control.py b/src/calibre/gui2/store/web_control.py index 47fde46b1f..e7ee1d2d6f 100644 --- a/src/calibre/gui2/store/web_control.py +++ b/src/calibre/gui2/store/web_control.py @@ -20,6 +20,7 @@ from calibre.ptempfile import PersistentTemporaryFile from calibre.utils.filenames import ascii_filename from calibre.web import get_download_filename + class NPWebView(QWebView): def __init__(self, *args): diff --git a/src/calibre/gui2/store/web_store_dialog.py b/src/calibre/gui2/store/web_store_dialog.py index 77ccd7de6b..90fec50ccb 100644 --- a/src/calibre/gui2/store/web_store_dialog.py +++ b/src/calibre/gui2/store/web_store_dialog.py @@ -11,6 +11,7 @@ from PyQt5.Qt import QDialog, QUrl from calibre import url_slash_cleaner from calibre.gui2.store.web_store_dialog_ui import Ui_Dialog + class WebStoreDialog(QDialog, Ui_Dialog): def __init__(self, gui, base_url, parent=None, detail_url=None, create_browser=None): diff --git a/src/calibre/gui2/tag_browser/model.py b/src/calibre/gui2/tag_browser/model.py index 478d8177c4..e7a825b628 100644 --- a/src/calibre/gui2/tag_browser/model.py +++ b/src/calibre/gui2/tag_browser/model.py @@ -30,6 +30,8 @@ DRAG_IMAGE_ROLE = Qt.UserRole + 1000 COUNT_ROLE = DRAG_IMAGE_ROLE + 1 _bf = None + + def bf(): global _bf if _bf is None: @@ -38,6 +40,7 @@ def bf(): _bf = (_bf) return _bf + class TagTreeItem(object): # {{{ CATEGORY = 0 @@ -252,6 +255,7 @@ class TagTreeItem(object): # {{{ def all_children(self): res = [] + def recurse(nodes, res): for t in nodes: res.append(t) @@ -261,6 +265,7 @@ class TagTreeItem(object): # {{{ def child_tags(self): res = [] + def recurse(nodes, res, depth): if depth > 100: return @@ -272,6 +277,7 @@ class TagTreeItem(object): # {{{ return res # }}} + class TagsModel(QAbstractItemModel): # {{{ search_item_renamed = pyqtSignal() @@ -1254,6 +1260,7 @@ class TagsModel(QAbstractItemModel): # {{{ def reset_all_states(self, except_=None): update_list = [] + def process_tag(tag_item): tag = tag_item.tag if tag is except_: diff --git a/src/calibre/gui2/tag_browser/ui.py b/src/calibre/gui2/tag_browser/ui.py index c8ba6160e1..4939ee6223 100644 --- a/src/calibre/gui2/tag_browser/ui.py +++ b/src/calibre/gui2/tag_browser/ui.py @@ -23,6 +23,7 @@ from calibre.gui2.dialogs.tag_categories import TagCategories from calibre.gui2.dialogs.tag_list_editor import TagListEditor from calibre.gui2.dialogs.edit_authors_dialog import EditAuthorsDialog + class TagBrowserMixin(object): # {{{ def __init__(self, *args, **kwargs): @@ -306,6 +307,7 @@ class TagBrowserMixin(object): # {{{ # }}} + class TagBrowserWidget(QWidget): # {{{ def __init__(self, parent): diff --git a/src/calibre/gui2/tag_browser/view.py b/src/calibre/gui2/tag_browser/view.py index 67fbccad70..e91b55f627 100644 --- a/src/calibre/gui2/tag_browser/view.py +++ b/src/calibre/gui2/tag_browser/view.py @@ -25,6 +25,7 @@ from calibre.gui2.tag_browser.model import (TagTreeItem, TAG_SEARCH_STATES, from calibre.gui2 import config, gprefs, choose_files, pixmap_to_data, rating_font from calibre.utils.icu import sort_key + class TagDelegate(QStyledItemDelegate): # {{{ def __init__(self, *args, **kwargs): @@ -108,6 +109,7 @@ class TagDelegate(QStyledItemDelegate): # {{{ # }}} + class TagsView(QTreeView): # {{{ refresh_required = pyqtSignal() @@ -533,6 +535,7 @@ class TagsView(QTreeView): # {{{ m = self.context_menu.addMenu(self.user_category_icon, _('Add %s to user category')%display_name(tag)) nt = self.model().user_category_node_tree + def add_node_tree(tree_dict, m, path): p = path[:] for k in sorted(tree_dict.keys(), key=sort_key): diff --git a/src/calibre/gui2/tag_mapper.py b/src/calibre/gui2/tag_mapper.py index b66f1e18ba..612657af68 100644 --- a/src/calibre/gui2/tag_mapper.py +++ b/src/calibre/gui2/tag_mapper.py @@ -24,12 +24,14 @@ from calibre.utils.localization import localize_user_manual_link tag_maps = JSONConfig('tag-map-rules') + def intelligent_strip(action, val): ans = val.strip() if not ans and action == 'split': ans = ' ' return ans + class QueryEdit(QLineEdit): def contextMenuEvent(self, ev): @@ -37,6 +39,7 @@ class QueryEdit(QLineEdit): self.parent().specialise_context_menu(menu) menu.exec_(ev.globalPos()) + class RuleEdit(QWidget): ACTION_MAP = OrderedDict(( @@ -192,6 +195,7 @@ class RuleEdit(QWidget): return False return True + class RuleEditDialog(Dialog): PREFS_NAME = 'edit-tag-mapper-rule' @@ -214,6 +218,7 @@ class RuleEditDialog(Dialog): DATA_ROLE = Qt.UserRole RENDER_ROLE = DATA_ROLE + 1 + class RuleItem(QListWidgetItem): @staticmethod @@ -234,6 +239,7 @@ class RuleItem(QListWidgetItem): self.setData(RENDER_ROLE, st) self.setData(DATA_ROLE, rule) + class Delegate(QStyledItemDelegate): MARGIN = 16 @@ -372,6 +378,7 @@ class Rules(QWidget): if 'action' in rule and 'match_type' in rule and 'query' in rule: self.RuleItemClass(rule, self.rule_list) + class Tester(Dialog): DIALOG_TITLE = _('Test tag mapper rules') @@ -418,6 +425,7 @@ class Tester(Dialog): ans.setWidth(ans.width() + 150) return ans + class SaveLoadMixin(object): def save_ruleset(self): @@ -460,6 +468,7 @@ class SaveLoadMixin(object): del self.PREFS_OBJECT[name] self.build_load_menu() + class RulesDialog(Dialog, SaveLoadMixin): DIALOG_TITLE = _('Edit tag mapper rules') diff --git a/src/calibre/gui2/threaded_jobs.py b/src/calibre/gui2/threaded_jobs.py index 455bc51e43..77f66dec4f 100644 --- a/src/calibre/gui2/threaded_jobs.py +++ b/src/calibre/gui2/threaded_jobs.py @@ -15,6 +15,7 @@ from calibre.utils.ipc.job import BaseJob from calibre.utils.logging import GUILog from calibre.ptempfile import base_dir + class ThreadedJob(BaseJob): def __init__(self, @@ -150,6 +151,7 @@ class ThreadedJob(BaseJob): return self.log.html return self.read_consolidated_log()[0] + class ThreadedJobWorker(Thread): def __init__(self, job): @@ -166,6 +168,7 @@ class ThreadedJobWorker(Thread): prints('Job had unhandled exception:', self.job.description) traceback.print_exc() + class ThreadedJobServer(Thread): def __init__(self): diff --git a/src/calibre/gui2/throbber.py b/src/calibre/gui2/throbber.py index b555ec7287..b9c37435e6 100644 --- a/src/calibre/gui2/throbber.py +++ b/src/calibre/gui2/throbber.py @@ -14,6 +14,7 @@ from PyQt5.Qt import ( from calibre.gui2 import config + class ThrobbingButton(QToolButton): @pyqtProperty(int) diff --git a/src/calibre/gui2/toc/location.py b/src/calibre/gui2/toc/location.py index 7623218bbd..763f6f3558 100644 --- a/src/calibre/gui2/toc/location.py +++ b/src/calibre/gui2/toc/location.py @@ -20,6 +20,7 @@ from calibre.ebooks.oeb.display.webview import load_html from calibre.gui2 import error_dialog, question_dialog, gprefs from calibre.utils.logging import default_log + class Page(QWebPage): # {{{ elem_clicked = pyqtSignal(object, object, object, object, object) @@ -59,6 +60,7 @@ class Page(QWebPage): # {{{ self.evaljs(self.js) # }}} + class WebView(QWebView): # {{{ elem_clicked = pyqtSignal(object, object, object, object, object) @@ -95,6 +97,7 @@ class WebView(QWebView): # {{{ return val # }}} + class ItemEdit(QWidget): def __init__(self, parent, prefs=None): diff --git a/src/calibre/gui2/toc/main.py b/src/calibre/gui2/toc/main.py index 40ccb96588..f4f5d356d1 100644 --- a/src/calibre/gui2/toc/main.py +++ b/src/calibre/gui2/toc/main.py @@ -27,6 +27,7 @@ from calibre.utils.logging import GUILog ICON_SIZE = 24 + class XPathDialog(QDialog): # {{{ def __init__(self, parent, prefs): @@ -120,6 +121,7 @@ class XPathDialog(QDialog): # {{{ return [w.xpath for w in self.widgets if w.xpath.strip()] # }}} + class ItemView(QFrame): # {{{ add_new_item = pyqtSignal(object, object) @@ -351,6 +353,7 @@ class ItemView(QFrame): # {{{ # }}} + class TreeWidget(QTreeWidget): # {{{ edit_item = pyqtSignal() @@ -544,6 +547,7 @@ class TreeWidget(QTreeWidget): # {{{ def show_context_menu(self, point): item = self.currentItem() + def key(k): sc = unicode(QKeySequence(k | Qt.CTRL).toString(QKeySequence.NativeText)) return ' [%s]'%sc @@ -569,6 +573,7 @@ class TreeWidget(QTreeWidget): # {{{ m.exec_(QCursor.pos()) # }}} + class TOCView(QWidget): # {{{ add_new_item = pyqtSignal(object, object) diff --git a/src/calibre/gui2/tools.py b/src/calibre/gui2/tools.py index 7d6f602f5d..a39412b025 100644 --- a/src/calibre/gui2/tools.py +++ b/src/calibre/gui2/tools.py @@ -24,6 +24,7 @@ from calibre.ebooks.conversion.config import GuiRecommendations, \ load_defaults, load_specifics, save_specifics from calibre.gui2.convert import bulk_defaults_for_input_format + def convert_single_ebook(parent, db, book_ids, auto_conversion=False, # {{{ out_format=None, show_no_format_warning=True): changed = False @@ -126,6 +127,8 @@ def convert_single_ebook(parent, db, book_ids, auto_conversion=False, # {{{ # }}} # Bulk convert {{{ + + def convert_bulk_ebook(parent, queue, db, book_ids, out_format=None, args=[]): total = len(book_ids) if total == 0: @@ -146,6 +149,7 @@ def convert_bulk_ebook(parent, queue, db, book_ids, out_format=None, args=[]): return QueueBulk(parent, book_ids, output_format, queue, db, user_recs, args, use_saved_single_settings=use_saved_single_settings) + class QueueBulk(QProgressDialog): def __init__(self, parent, book_ids, output_format, queue, db, user_recs, @@ -260,6 +264,7 @@ class QueueBulk(QProgressDialog): # }}} + def fetch_scheduled_recipe(arg): # {{{ fmt = prefs['output_format'].lower() # Never use AZW3 for periodicals... @@ -310,6 +315,7 @@ def fetch_scheduled_recipe(arg): # {{{ # }}} + def generate_catalog(parent, dbspec, ids, device_manager, db): # {{{ from calibre.gui2.dialogs.catalog import Catalog @@ -370,6 +376,7 @@ def generate_catalog(parent, dbspec, ids, device_manager, db): # {{{ d.catalog_title # }}} + def convert_existing(parent, db, book_ids, output_format): # {{{ already_converted_ids = [] already_converted_titles = [] diff --git a/src/calibre/gui2/tweak_book/__init__.py b/src/calibre/gui2/tweak_book/__init__.py index aa6fc6f3ab..f13b5b9342 100644 --- a/src/calibre/gui2/tweak_book/__init__.py +++ b/src/calibre/gui2/tweak_book/__init__.py @@ -78,18 +78,23 @@ d['file_list_shows_full_pathname'] = False del d ucase_map = {l:string.ascii_uppercase[i] for i, l in enumerate(string.ascii_lowercase)} + + def capitalize(x): return ucase_map[x[0]] + x[1:] _current_container = None + def current_container(): return _current_container + def set_current_container(container): global _current_container _current_container = container + class NonReplaceDict(dict): def __setitem__(self, k, v): @@ -106,11 +111,13 @@ editor_toolbar_actions = { TOP = object() dictionaries = Dictionaries() + def editor_name(editor): for n, ed in editors.iteritems(): if ed is editor: return n + def set_book_locale(lang): dictionaries.initialize() try: @@ -122,6 +129,7 @@ def set_book_locale(lang): from calibre.gui2.tweak_book.editor.syntax.html import refresh_spell_check_status refresh_spell_check_status() + def verify_link(url, name=None): if _current_container is None or name is None: return None @@ -137,6 +145,7 @@ def verify_link(url, name=None): return True return False + def update_mark_text_action(ed=None): has_mark = False if ed is not None and ed.has_line_numbers: diff --git a/src/calibre/gui2/tweak_book/boss.py b/src/calibre/gui2/tweak_book/boss.py index 58366e6793..18af33edc3 100644 --- a/src/calibre/gui2/tweak_book/boss.py +++ b/src/calibre/gui2/tweak_book/boss.py @@ -52,14 +52,17 @@ from calibre.utils.icu import numeric_sort_key _diff_dialogs = [] last_used_transform_rules = [] + def get_container(*args, **kwargs): kwargs['tweak_mode'] = True container = _gc(*args, **kwargs) return container + def setup_cssutils_serialization(): scs(tprefs['editor_tab_stop_width']) + def in_thread_job(func): @wraps(func) def ans(*args, **kwargs): @@ -68,6 +71,8 @@ def in_thread_job(func): return ans _boss = None + + def get_boss(): return _boss @@ -241,6 +246,7 @@ class Boss(QObject): from calibre.ebooks.oeb.polish.import_book import import_book_as_epub src, dest = d.data self._clear_notify_data = True + def func(src, dest, tdir): import_book_as_epub(src, dest) return get_container(dest, tdir=tdir) @@ -695,6 +701,7 @@ class Boss(QObject): def create_diff_dialog(self, revert_msg=_('&Revert changes'), show_open_in_editor=True): global _diff_dialogs from calibre.gui2.tweak_book.diff.main import Diff + def line_activated(name, lnum, right): if right: self.edit_file_requested(name, None, guess_type(name)) @@ -1531,6 +1538,7 @@ class Boss(QObject): d.bb.accepted.connect(d.accept) d.l.addWidget(d.bb, 1, 0, 1, 2) d.do_save = None + def endit(x): d.do_save = x d.accept() diff --git a/src/calibre/gui2/tweak_book/char_select.py b/src/calibre/gui2/tweak_book/char_select.py index 0fc2d1ab7b..6e2dab8e76 100644 --- a/src/calibre/gui2/tweak_book/char_select.py +++ b/src/calibre/gui2/tweak_book/char_select.py @@ -35,6 +35,7 @@ non_printing = { # Searching {{{ + def load_search_index(): topchar = 0x10ffff ver = (1, topchar, icu_unicode_version or unicodedata.unidata_version) # Increment this when you make any changes to the index @@ -64,6 +65,7 @@ def load_search_index(): _index = None + def search_for_chars(query, and_tokens=False): global _index if _index is None: @@ -84,6 +86,7 @@ def search_for_chars(query, and_tokens=False): return sorted(ans) # }}} + class CategoryModel(QAbstractItemModel): def __init__(self, parent=None): @@ -467,6 +470,7 @@ class CategoryModel(QAbstractItemModel): category = subcategory = _('Unknown') return category, subcategory, (character_name_from_code(char_code) or _('Unknown')) + class CategoryDelegate(QStyledItemDelegate): def __init__(self, parent=None): @@ -478,6 +482,7 @@ class CategoryDelegate(QStyledItemDelegate): ans += QSize(0, 6) return ans + class CategoryView(QTreeView): category_selected = pyqtSignal(object, object) @@ -519,6 +524,7 @@ class CategoryView(QTreeView): self.setItemDelegate(self._delegate) self.initialized = True + class CharModel(QAbstractListModel): def __init__(self, parent=None): @@ -568,6 +574,7 @@ class CharModel(QAbstractListModel): tprefs['charmap_favorites'] = list(self.chars) return True + class CharDelegate(QStyledItemDelegate): def __init__(self, parent=None): @@ -605,6 +612,7 @@ class CharDelegate(QStyledItemDelegate): painter.setPen(QPen(Qt.DashLine)) painter.drawRect(option.rect.adjusted(1, 1, -1, -1)) + class CharView(QListView): show_name = pyqtSignal(object) @@ -718,6 +726,7 @@ class CharView(QListView): self.model().chars.remove(char_code) self.model().endResetModel() + class CharSelect(Dialog): def __init__(self, parent=None): diff --git a/src/calibre/gui2/tweak_book/check.py b/src/calibre/gui2/tweak_book/check.py index 2454dc49f9..be3009d3ac 100644 --- a/src/calibre/gui2/tweak_book/check.py +++ b/src/calibre/gui2/tweak_book/check.py @@ -18,6 +18,7 @@ from calibre.gui2 import NO_URL_FORMATTING from calibre.gui2.tweak_book import tprefs from calibre.gui2.tweak_book.widgets import BusyCursor + def icon_for_level(level): if level > WARN: icon = 'dialog_error.png' @@ -29,6 +30,7 @@ def icon_for_level(level): icon = None return QIcon(I(icon)) if icon else QIcon() + def prefix_for_level(level): if level > WARN: text = _('ERROR') @@ -42,6 +44,7 @@ def prefix_for_level(level): text += ': ' return text + class Delegate(QStyledItemDelegate): def initStyleOption(self, option, index): @@ -50,6 +53,7 @@ class Delegate(QStyledItemDelegate): option.font.setBold(True) option.backgroundBrush = self.parent().palette().brush(QPalette.AlternateBase) + class Check(QSplitter): item_activated = pyqtSignal(object) @@ -155,6 +159,7 @@ class Check(QSplitter): def current_item_changed(self, *args): i = self.items.currentItem() self.help.setText('') + def loc_to_string(line, col): loc = '' if line is not None: @@ -241,6 +246,7 @@ class Check(QSplitter): self.items.clear() self.clear_help() + def main(): from calibre.gui2 import Application from calibre.gui2.tweak_book.boss import get_container diff --git a/src/calibre/gui2/tweak_book/check_links.py b/src/calibre/gui2/tweak_book/check_links.py index 7fa639cb31..02a6f871cc 100644 --- a/src/calibre/gui2/tweak_book/check_links.py +++ b/src/calibre/gui2/tweak_book/check_links.py @@ -18,12 +18,14 @@ from calibre.gui2.tweak_book import current_container, set_current_container, ed from calibre.gui2.tweak_book.boss import get_boss from calibre.gui2.tweak_book.widgets import Dialog + def get_data(name): 'Get the data for name. Returns a unicode string if name is a text document/stylesheet' if name in editors: return editors[name].get_raw_data() return current_container().raw_data(name) + def set_data(name, val): if name in editors: editors[name].replace_data(val, only_if_different=False) @@ -32,6 +34,7 @@ def set_data(name, val): f.write(val) get_boss().set_modified() + class CheckExternalLinks(Dialog): progress_made = pyqtSignal(object, object) diff --git a/src/calibre/gui2/tweak_book/completion/basic.py b/src/calibre/gui2/tweak_book/completion/basic.py index d7d52eb428..56ead0af4c 100644 --- a/src/calibre/gui2/tweak_book/completion/basic.py +++ b/src/calibre/gui2/tweak_book/completion/basic.py @@ -26,6 +26,7 @@ Request = namedtuple('Request', 'id type data query') names_cache = {} file_cache = {} + @control def clear_caches(cache_type, data_conn): global names_cache, file_cache @@ -41,11 +42,13 @@ def clear_caches(cache_type, data_conn): if name.lower().endswith('.opf'): names_cache.clear() + @data def names_data(request_data): c = current_container() return c.mime_map, {n for n, is_linear in c.spine_names} + @data def file_data(name): 'Get the data for name. Returns a unicode string if name is a text document/stylesheet' @@ -53,6 +56,7 @@ def file_data(name): return editors[name].get_raw_data() return current_container().raw_data(name) + def get_data(data_conn, data_type, data=None): eintr_retry_call(data_conn.send, Request(None, data_type, data, None)) result, tb = eintr_retry_call(data_conn.recv) @@ -60,6 +64,7 @@ def get_data(data_conn, data_type, data=None): raise DataError(tb) return result + class Name(unicode): def __new__(self, name, mime_type, spine_names): @@ -68,6 +73,7 @@ class Name(unicode): ans.in_spine = name in spine_names return ans + @control def complete_names(names_data, data_conn): if not names_cache: @@ -91,6 +97,7 @@ def complete_names(names_data, data_conn): descriptions = {href:d(name) for name, href in nmap.iteritems()} return items, descriptions, {} + def create_anchor_map(root): ans = {} for elem in root.xpath('//*[@id or @name]'): @@ -99,6 +106,7 @@ def create_anchor_map(root): ans[anchor] = description_for_anchor(elem) return ans + @control def complete_anchor(name, data_conn): if name not in file_cache: @@ -117,6 +125,7 @@ def complete_anchor(name, data_conn): _current_matcher = (None, None, None) + def handle_control_request(request, data_conn): global _current_matcher ans = control_funcs[request.type](request.data, data_conn) @@ -132,6 +141,7 @@ def handle_control_request(request, data_conn): ans = items, descriptions return ans + class HandleDataRequest(QObject): # Ensure data is obtained in the GUI thread diff --git a/src/calibre/gui2/tweak_book/completion/popup.py b/src/calibre/gui2/tweak_book/completion/popup.py index eb6b65c07c..e58a192ec4 100644 --- a/src/calibre/gui2/tweak_book/completion/popup.py +++ b/src/calibre/gui2/tweak_book/completion/popup.py @@ -17,6 +17,7 @@ from calibre.gui2 import error_dialog from calibre.gui2.tweak_book.widgets import make_highlighted_text from calibre.utils.icu import string_length + class ChoosePopupWidget(QWidget): TOP_MARGIN = BOTTOM_MARGIN = 2 @@ -234,6 +235,7 @@ class ChoosePopupWidget(QWidget): self.ensure_index_visible(self.current_index) self.update() + class CompletionPopup(ChoosePopupWidget): def __init__(self, parent, max_height=1000): @@ -292,6 +294,7 @@ class CompletionPopup(ChoosePopupWidget): if __name__ == '__main__': from calibre.utils.matcher import Matcher + def test(editor): c = editor.__c = CompletionPopup(editor.editor, max_height=100) items = 'a ab abc abcd abcde abcdef abcdefg abcdefgh'.split() diff --git a/src/calibre/gui2/tweak_book/completion/utils.py b/src/calibre/gui2/tweak_book/completion/utils.py index 57fd374f45..67f7a588e5 100644 --- a/src/calibre/gui2/tweak_book/completion/utils.py +++ b/src/calibre/gui2/tweak_book/completion/utils.py @@ -11,6 +11,7 @@ def control(func): func.function_type = 'control' return func + def data(func): func.function_type = 'data' return func diff --git a/src/calibre/gui2/tweak_book/completion/worker.py b/src/calibre/gui2/tweak_book/completion/worker.py index 18e2a14ff6..0a4be9cf9a 100644 --- a/src/calibre/gui2/tweak_book/completion/worker.py +++ b/src/calibre/gui2/tweak_book/completion/worker.py @@ -20,6 +20,7 @@ from calibre.utils.ipc import eintr_retry_call COMPLETION_REQUEST = 'completion request' CLEAR_REQUEST = 'clear request' + class CompletionWorker(Thread): daemon = True @@ -159,12 +160,15 @@ class CompletionWorker(Thread): return self.worker_process.returncode _completion_worker = None + + def completion_worker(): global _completion_worker if _completion_worker is None: _completion_worker = CompletionWorker() return _completion_worker + def run_main(func): from multiprocessing.connection import Client address, key = cPickle.loads(eintr_retry_call(sys.stdin.read)) @@ -173,6 +177,7 @@ def run_main(func): Result = namedtuple('Result', 'request_id ans traceback query') + def main(control_conn, data_conn): from calibre.gui2.tweak_book.completion.basic import handle_control_request while True: @@ -196,10 +201,12 @@ def main(control_conn, data_conn): except EOFError: break + def test_main(control_conn, data_conn): obj = control_conn.recv() control_conn.send(obj) + def test(): w = CompletionWorker(worker_entry_point='test_main') w.wait_for_connection() diff --git a/src/calibre/gui2/tweak_book/diff/__init__.py b/src/calibre/gui2/tweak_book/diff/__init__.py index 2388e25ee3..ef1bd4b37c 100644 --- a/src/calibre/gui2/tweak_book/diff/__init__.py +++ b/src/calibre/gui2/tweak_book/diff/__init__.py @@ -8,12 +8,14 @@ __copyright__ = '2014, Kovid Goyal ' from calibre.constants import plugins + def load_patience_module(): p, err = plugins['_patiencediff_c'] if err: raise ImportError('Failed to import the PatienceDiff C module with error: %r' % err) return p + def get_sequence_matcher(): return load_patience_module().PatienceSequenceMatcher_c diff --git a/src/calibre/gui2/tweak_book/diff/highlight.py b/src/calibre/gui2/tweak_book/diff/highlight.py index b18ae8a0c7..da00a5b8b1 100644 --- a/src/calibre/gui2/tweak_book/diff/highlight.py +++ b/src/calibre/gui2/tweak_book/diff/highlight.py @@ -15,6 +15,7 @@ from calibre.gui2.tweak_book.editor.text import get_highlighter as calibre_highl from calibre.gui2.tweak_book.editor.themes import get_theme, highlight_to_char_format from calibre.gui2.tweak_book.editor.syntax.utils import format_for_pygments_token, NULL_FMT + class QtHighlighter(QTextDocument): def __init__(self, parent, text, hlclass): @@ -51,6 +52,7 @@ class QtHighlighter(QTextDocument): cursor.setCharFormat(NULL_FMT) block = block.next() + class NullHighlighter(object): def __init__(self, text): @@ -61,6 +63,7 @@ class NullHighlighter(object): cursor.insertText(self.lines[i]) cursor.insertBlock() + def pygments_lexer(filename): try: from pygments.lexers import get_lexer_for_filename @@ -75,12 +78,14 @@ def pygments_lexer(filename): return glff('a.py') return None + class PygmentsHighlighter(object): def __init__(self, text, lexer): theme, cache = get_theme(tprefs['editor_theme']), {} theme = {k:highlight_to_char_format(v) for k, v in theme.iteritems()} theme[None] = NULL_FMT + def fmt(token): return format_for_pygments_token(theme, cache, token) @@ -101,6 +106,7 @@ class PygmentsHighlighter(object): cursor.insertText(text, fmt) cursor.setCharFormat(NULL_FMT) + def get_highlighter(parent, text, syntax): hlclass = calibre_highlighter(syntax) if hlclass is SyntaxHighlighter: diff --git a/src/calibre/gui2/tweak_book/diff/main.py b/src/calibre/gui2/tweak_book/diff/main.py index fb29b066fa..fc11dd1b13 100644 --- a/src/calibre/gui2/tweak_book/diff/main.py +++ b/src/calibre/gui2/tweak_book/diff/main.py @@ -25,6 +25,7 @@ from calibre.gui2.widgets2 import HistoryLineEdit2 from calibre.utils.filenames import samefile from calibre.utils.icu import numeric_sort_key + class BusyWidget(QWidget): # {{{ def __init__(self, parent): @@ -55,6 +56,7 @@ class BusyWidget(QWidget): # {{{ p.end() # }}} + class Cache(object): def __init__(self): @@ -62,6 +64,7 @@ class Cache(object): self.left, self.right = self._left.get, self._right.get self.set_left, self.set_right = self._left.__setitem__, self._right.__setitem__ + def changed_files(list_of_names1, list_of_names2, get_data1, get_data2): list_of_names1, list_of_names2 = frozenset(list_of_names1), frozenset(list_of_names2) changed_names = set() @@ -124,6 +127,7 @@ def get_decoded_raw(name): pass return raw, syntax + def string_diff(left, right, left_syntax=None, right_syntax=None, left_name='left', right_name='right'): left, right = unicode(left), unicode(right) cache = Cache() @@ -131,6 +135,7 @@ def string_diff(left, right, left_syntax=None, right_syntax=None, left_name='lef changed_names = {} if left == right else {left_name:right_name} return cache, {left_name:left_syntax, right_name:right_syntax}, changed_names, {}, set(), set() + def file_diff(left, right): (raw1, syntax1), (raw2, syntax2) = map(get_decoded_raw, (left, right)) if type(raw1) is not type(raw2): @@ -140,6 +145,7 @@ def file_diff(left, right): changed_names = {} if raw1 == raw2 else {left:right} return cache, {left:syntax1, right:syntax2}, changed_names, {}, set(), set() + def dir_diff(left, right): ldata, rdata, lsmap, rsmap = {}, {}, {}, {} for base, data, smap in ((left, ldata, lsmap), (right, rdata, rsmap)): @@ -157,6 +163,7 @@ def dir_diff(left, right): syntax_map.update({name:lsmap[name] for name in removed_names}) return cache, syntax_map, changed_names, renamed_names, removed_names, added_names + def container_diff(left, right): left_names, right_names = set(left.name_path_map), set(right.name_path_map) if left.cloned or right.cloned: @@ -186,12 +193,14 @@ def container_diff(left, right): syntax_map.update({name:syntax(left, name) for name in removed_names}) return cache, syntax_map, changed_names, renamed_names, removed_names, added_names + def ebook_diff(path1, path2): from calibre.ebooks.oeb.polish.container import get_container left = get_container(path1, tweak_mode=True) right = get_container(path2, tweak_mode=True) return container_diff(left, right) + class Diff(Dialog): revert_requested = pyqtSignal() @@ -396,6 +405,7 @@ class Diff(Dialog): def apply_diff(self, identical_msg, cache, syntax_map, changed_names, renamed_names, removed_names, added_names): self.view.clear() self.apply_diff_calls = calls = [] + def add(args, kwargs): self.view.add_diff(*args, **kwargs) calls.append((args, kwargs)) @@ -448,6 +458,7 @@ class Diff(Dialog): return return Dialog.keyPressEvent(self, ev) + def compare_books(path1, path2, revert_msg=None, revert_callback=None, parent=None, names=None): d = Diff(parent=parent, revert_button_msg=revert_msg) if revert_msg is not None: @@ -460,6 +471,7 @@ def compare_books(path1, path2, revert_msg=None, revert_callback=None, parent=No pass d.break_cycles() + def main(args=sys.argv): from calibre.gui2 import Application left, right = args[-2:] diff --git a/src/calibre/gui2/tweak_book/diff/view.py b/src/calibre/gui2/tweak_book/diff/view.py index b51ed07258..661ff816b6 100644 --- a/src/calibre/gui2/tweak_book/diff/view.py +++ b/src/calibre/gui2/tweak_book/diff/view.py @@ -32,6 +32,7 @@ from calibre.gui2.tweak_book.diff.highlight import get_highlighter Change = namedtuple('Change', 'ltop lbot rtop rbot kind') + class BusyCursor(object): def __enter__(self): @@ -40,6 +41,7 @@ class BusyCursor(object): def __exit__(self, *args): QApplication.restoreOverrideCursor() + def beautify_text(raw, syntax): from lxml import etree from calibre.ebooks.oeb.polish.parsing import parse @@ -86,6 +88,7 @@ class LineNumberMap(dict): # {{{ self.max_width = 1 # }}} + class TextBrowser(PlainTextEdit): # {{{ resized = pyqtSignal() @@ -391,6 +394,7 @@ class TextBrowser(PlainTextEdit): # {{{ # }}} + class DiffSplitHandle(QSplitterHandle): # {{{ WIDTH = 30 # px @@ -497,6 +501,7 @@ class DiffSplitHandle(QSplitterHandle): # {{{ return QSplitterHandle.wheelEvent(self, ev) # }}} + class DiffSplit(QSplitter): # {{{ def __init__(self, parent=None, show_open_in_editor=False): @@ -642,6 +647,7 @@ class DiffSplit(QSplitter): # {{{ c.removeSelectedText() c.endEditBlock() v.images[top] = (img, w, lines) + def mapnum(x): return x if x <= top else x + delta lnm = LineNumberMap() @@ -895,6 +901,7 @@ class DiffSplit(QSplitter): # {{{ # }}} + class DiffView(QWidget): # {{{ SYNC_POSITION = 0.4 diff --git a/src/calibre/gui2/tweak_book/editor/__init__.py b/src/calibre/gui2/tweak_book/editor/__init__.py index 8e41139ab1..3296939240 100644 --- a/src/calibre/gui2/tweak_book/editor/__init__.py +++ b/src/calibre/gui2/tweak_book/editor/__init__.py @@ -15,6 +15,7 @@ _xml_types = {'application/oebps-page-map+xml', 'application/vnd.adobe-page-temp guess_type('a.'+x) for x in ('ncx', 'opf', 'svg', 'xpgt', 'xml')} _js_types = {'application/javascript', 'application/x-javascript'} + def syntax_from_mime(name, mime): for syntax, types in (('html', OEB_DOCS), ('css', OEB_STYLES), ('xml', _xml_types)): if mime in types: @@ -31,6 +32,7 @@ def syntax_from_mime(name, mime): all_text_syntaxes = frozenset({'text', 'html', 'xml', 'css', 'javascript'}) + def editor_from_syntax(syntax, parent=None): if syntax in all_text_syntaxes: from calibre.gui2.tweak_book.editor.widget import Editor @@ -48,11 +50,13 @@ TAG_NAME_PROPERTY = LINK_PROPERTY + 1 CSS_PROPERTY = TAG_NAME_PROPERTY + 1 CLASS_ATTRIBUTE_PROPERTY = CSS_PROPERTY + 1 + def syntax_text_char_format(*args): ans = QTextCharFormat(*args) ans.setProperty(SYNTAX_PROPERTY, True) return ans + class StoreLocale(object): __slots__ = ('enabled',) diff --git a/src/calibre/gui2/tweak_book/editor/canvas.py b/src/calibre/gui2/tweak_book/editor/canvas.py index 99355491e8..352197b56c 100644 --- a/src/calibre/gui2/tweak_book/editor/canvas.py +++ b/src/calibre/gui2/tweak_book/editor/canvas.py @@ -26,6 +26,7 @@ from calibre.utils.img import ( normalize_image, oil_paint_image ) + def painter(func): @wraps(func) def ans(self, painter): @@ -36,6 +37,7 @@ def painter(func): painter.restore() return ans + class SelectionState(object): __slots__ = ('last_press_point', 'current_mode', 'rect', 'in_selection', 'drag_corner', 'dragging', 'last_drag_pos') @@ -53,6 +55,7 @@ class SelectionState(object): self.dragging = None self.last_drag_pos = None + class Command(QUndoCommand): TEXT = '' @@ -75,6 +78,7 @@ class Command(QUndoCommand): canvas = self.canvas_ref() canvas.set_image(self.after_image) + def get_selection_rect(img, sr, target): ' Given selection rect return the corresponding rectangle in the underlying image as left, top, width, height ' left_border = (abs(sr.left() - target.left())/target.width()) * img.width() @@ -83,6 +87,7 @@ def get_selection_rect(img, sr, target): bottom_border = (abs(target.bottom() - sr.bottom())/target.height()) * img.height() return left_border, top_border, img.width() - left_border - right_border, img.height() - top_border - bottom_border + class Trim(Command): ''' Remove the areas of the image outside the current selection. ''' @@ -95,6 +100,7 @@ class Trim(Command): sr = canvas.selection_state.rect return img.copy(*get_selection_rect(img, sr, target)) + class AutoTrim(Trim): ''' Auto trim borders from the image ''' @@ -103,6 +109,7 @@ class AutoTrim(Trim): def __call__(self, canvas): return remove_borders_from_image(canvas.current_image) + class Rotate(Command): TEXT = _('Rotate image') @@ -113,6 +120,7 @@ class Rotate(Command): m.rotate(90) return img.transformed(m, Qt.SmoothTransformation) + class Scale(Command): TEXT = _('Resize image') @@ -125,6 +133,7 @@ class Scale(Command): img = canvas.current_image return img.scaled(self.width, self.height, transformMode=Qt.SmoothTransformation) + class Sharpen(Command): TEXT = _('Sharpen image') @@ -137,6 +146,7 @@ class Sharpen(Command): def __call__(self, canvas): return gaussian_sharpen_image(canvas.current_image, sigma=self.sigma) + class Blur(Sharpen): TEXT = _('Blur image') @@ -145,6 +155,7 @@ class Blur(Sharpen): def __call__(self, canvas): return gaussian_blur_image(canvas.current_image, sigma=self.sigma) + class Oilify(Command): TEXT = _('Make image look like an oil painting') @@ -156,6 +167,7 @@ class Oilify(Command): def __call__(self, canvas): return oil_paint_image(canvas.current_image, radius=self.radius) + class Despeckle(Command): TEXT = _('De-speckle image') @@ -163,6 +175,7 @@ class Despeckle(Command): def __call__(self, canvas): return despeckle_image(canvas.current_image) + class Normalize(Command): TEXT = _('Normalize image') @@ -170,6 +183,7 @@ class Normalize(Command): def __call__(self, canvas): return normalize_image(canvas.current_image) + class Replace(Command): ''' Replace the current image with another image. If there is a selection, @@ -191,6 +205,7 @@ class Replace(Command): p.end() return self.after_image + def imageop(func): @wraps(func) def ans(self, *args, **kwargs): @@ -205,6 +220,7 @@ def imageop(func): QApplication.restoreOverrideCursor() return ans + class Canvas(QWidget): BACKGROUND = QColor(60, 60, 60) diff --git a/src/calibre/gui2/tweak_book/editor/comments.py b/src/calibre/gui2/tweak_book/editor/comments.py index f2592ecd65..78c8de89e4 100644 --- a/src/calibre/gui2/tweak_book/editor/comments.py +++ b/src/calibre/gui2/tweak_book/editor/comments.py @@ -21,6 +21,7 @@ closing_map = { 'javascript':'*/', } + def apply_smart_comment(editor, opening='/*', closing='*/', line_comment=None): doc = editor.document() c = QTextCursor(editor.textCursor()) @@ -41,5 +42,6 @@ def apply_smart_comment(editor, opening='/*', closing='*/', line_comment=None): c.setPosition(left), c.insertText(opening) c.endEditBlock() + def smart_comment(editor, syntax): apply_smart_comment(editor, opening=opening_map.get(syntax, '/*'), closing=closing_map.get(syntax, '*/')) diff --git a/src/calibre/gui2/tweak_book/editor/help.py b/src/calibre/gui2/tweak_book/editor/help.py index eca1b4d997..11f3182b6d 100644 --- a/src/calibre/gui2/tweak_book/editor/help.py +++ b/src/calibre/gui2/tweak_book/editor/help.py @@ -15,6 +15,7 @@ from calibre import browser from calibre.ebooks.oeb.polish.container import OEB_DOCS from calibre.ebooks.oeb.polish.utils import guess_type + class URLMap(object): def __init__(self): @@ -32,6 +33,7 @@ class URLMap(object): _url_map = URLMap() + def help_url(item, item_type, doc_name, extra_data=None): url = None url_maps = () @@ -64,6 +66,7 @@ def help_url(item, item_type, doc_name, extra_data=None): return url + def get_mdn_tag_index(category): url = 'https://developer.mozilla.org/docs/Web/%s/Element' % category if category == 'CSS': @@ -81,6 +84,7 @@ def get_mdn_tag_index(category): ans[href.rpartition('/')[-1].lower()] = 'https://developer.mozilla.org' + href return ans + def get_opf2_tag_index(): base = 'http://www.idpf.org/epub/20/spec/OPF_2.0.1_draft.htm#' ans = {} @@ -98,6 +102,7 @@ def get_opf2_tag_index(): ans[tag.lower()] = base + 'Section2.4.1.2' return ans + def get_opf3_tag_index(): base = 'http://www.idpf.org/epub/301/spec/epub-publications.html#' ans = {} @@ -110,6 +115,7 @@ def get_opf3_tag_index(): ans[tag.lower()] = base + 'sec-opf-dc' + tag return ans + def write_tag_help(): base = 'editor-help/%s.json' dump = partial(json.dumps, indent=2, sort_keys=True) diff --git a/src/calibre/gui2/tweak_book/editor/image.py b/src/calibre/gui2/tweak_book/editor/image.py index c74ba8871a..616ca3153c 100644 --- a/src/calibre/gui2/tweak_book/editor/image.py +++ b/src/calibre/gui2/tweak_book/editor/image.py @@ -17,6 +17,7 @@ from calibre.gui2 import error_dialog from calibre.gui2.tweak_book import actions, tprefs, editors from calibre.gui2.tweak_book.editor.canvas import Canvas + class ResizeDialog(QDialog): # {{{ def __init__(self, width, height, parent=None): @@ -65,6 +66,7 @@ class ResizeDialog(QDialog): # {{{ def width(self): def fget(self): return self._width.value() + def fset(self, val): self._width.setValue(val) return property(fget=fget, fset=fset) @@ -73,11 +75,13 @@ class ResizeDialog(QDialog): # {{{ def height(self): def fget(self): return self._height.value() + def fset(self, val): self._height.setValue(val) return property(fget=fget, fset=fset) # }}} + class Editor(QMainWindow): has_line_numbers = False @@ -111,6 +115,7 @@ class Editor(QMainWindow): def is_modified(self): def fget(self): return self._is_modified + def fset(self, val): self._is_modified = val self.modification_state_changed.emit(val) @@ -120,6 +125,7 @@ class Editor(QMainWindow): def current_editing_state(self): def fget(self): return {} + def fset(self, val): pass return property(fget=fget, fset=fset) @@ -136,6 +142,7 @@ class Editor(QMainWindow): def current_line(self): def fget(self): return 0 + def fset(self, val): pass return property(fget=fget, fset=fset) @@ -157,6 +164,7 @@ class Editor(QMainWindow): def data(self): def fget(self): return self.get_raw_data() + def fset(self, val): self.canvas.load_image(val) self._is_modified = False # The image_changed signal will have been triggered causing this editor to be incorrectly marked as modified @@ -329,6 +337,7 @@ class Editor(QMainWindow): if ok: self.canvas.oilify_image(radius=val) + def launch_editor(path_to_edit, path_is_raw=False): app = QApplication([]) if path_is_raw: diff --git a/src/calibre/gui2/tweak_book/editor/insert_resource.py b/src/calibre/gui2/tweak_book/editor/insert_resource.py index 75a4846e02..5746896e95 100644 --- a/src/calibre/gui2/tweak_book/editor/insert_resource.py +++ b/src/calibre/gui2/tweak_book/editor/insert_resource.py @@ -29,6 +29,7 @@ from calibre.ptempfile import PersistentTemporaryFile from calibre.utils.localization import get_lang, canonicalize_lang from calibre.utils.icu import sort_key + class ChooseName(Dialog): # {{{ ''' Chooses the filename for a newly imported file, with error checking ''' @@ -71,6 +72,8 @@ class ChooseName(Dialog): # {{{ # }}} # Images {{{ + + class ImageDelegate(QStyledItemDelegate): MARGIN = 4 @@ -139,6 +142,7 @@ class ImageDelegate(QStyledItemDelegate): finally: painter.restore() + class Images(QAbstractListModel): def __init__(self, parent): @@ -175,6 +179,7 @@ class Images(QAbstractListModel): return name return None + class InsertImage(Dialog): image_activated = pyqtSignal(object) @@ -318,12 +323,14 @@ class InsertImage(Dialog): self.fm.setFilterFixedString(f) # }}} + def get_resource_data(rtype, parent): if rtype == 'image': d = InsertImage(parent) if d.exec_() == d.Accepted: return d.chosen_image, d.chosen_image_is_external, d.fullpage.isChecked(), d.preserve_aspect_ratio.isChecked() + def create_folder_tree(container): root = {} @@ -336,6 +343,7 @@ def create_folder_tree(container): current[x] = current = current.get(x, {}) return root + class ChooseFolder(Dialog): # {{{ def __init__(self, msg=None, parent=None): diff --git a/src/calibre/gui2/tweak_book/editor/smarts/__init__.py b/src/calibre/gui2/tweak_book/editor/smarts/__init__.py index ca22a0f782..c748c479f1 100644 --- a/src/calibre/gui2/tweak_book/editor/smarts/__init__.py +++ b/src/calibre/gui2/tweak_book/editor/smarts/__init__.py @@ -6,6 +6,7 @@ from __future__ import (unicode_literals, division, absolute_import, __license__ = 'GPL v3' __copyright__ = '2014, Kovid Goyal ' + class NullSmarts(object): override_tab_stop_width = None diff --git a/src/calibre/gui2/tweak_book/editor/smarts/css.py b/src/calibre/gui2/tweak_book/editor/smarts/css.py index b0374caa79..af41b8a49f 100644 --- a/src/calibre/gui2/tweak_book/editor/smarts/css.py +++ b/src/calibre/gui2/tweak_book/editor/smarts/css.py @@ -16,6 +16,7 @@ from calibre.gui2.tweak_book.editor.smarts.utils import ( no_modifiers, get_leading_whitespace_on_block, get_text_before_cursor, smart_home, smart_backspace, smart_tab, expand_tabs) + def find_rule(raw, rule_address): import tinycss parser = tinycss.make_full_parser() @@ -34,6 +35,7 @@ def find_rule(raw, rule_address): rules = getattr(r, 'rules', ()) return ans + class Smarts(NullSmarts): def __init__(self, *args, **kwargs): diff --git a/src/calibre/gui2/tweak_book/editor/smarts/html.py b/src/calibre/gui2/tweak_book/editor/smarts/html.py index 3bc3f4ab18..819ebfcd55 100644 --- a/src/calibre/gui2/tweak_book/editor/smarts/html.py +++ b/src/calibre/gui2/tweak_book/editor/smarts/html.py @@ -26,6 +26,7 @@ from calibre.utils.icu import utf16_length get_offset = itemgetter(0) PARAGRAPH_SEPARATOR = '\u2029' + class Tag(object): def __init__(self, start_block, tag_start, end_block, tag_end, self_closing=False): @@ -42,6 +43,7 @@ class Tag(object): self.name, self.start_block.blockNumber(), self.start_offset, self.end_block.blockNumber(), self.end_offset, self.self_closing) __str__ = __repr__ + def next_tag_boundary(block, offset, forward=True, max_lines=10000): while block.isValid() and max_lines > 0: ud = block.userData() @@ -57,6 +59,7 @@ def next_tag_boundary(block, offset, forward=True, max_lines=10000): max_lines -= 1 return None, None + def next_attr_boundary(block, offset, forward=True): while block.isValid(): ud = block.userData() @@ -71,6 +74,7 @@ def next_attr_boundary(block, offset, forward=True): offset = -1 if forward else sys.maxint return None, None + def find_closest_containing_tag(block, offset, max_tags=sys.maxint): ''' Find the closest containing tag. To find it, we search for the first opening tag that does not have a matching closing tag before the specified @@ -113,6 +117,7 @@ def find_closest_containing_tag(block, offset, max_tags=sys.maxint): max_tags -= 1 return None # Could not find a containing tag + def find_tag_definition(block, offset): ''' Return the definition, if any that (block, offset) is inside. ''' block, boundary = next_tag_boundary(block, offset, forward=False) @@ -125,6 +130,7 @@ def find_tag_definition(block, offset): tag = tag_start.prefix + ':' + tag return tag, closing + def find_containing_attribute(block, offset): block, boundary = next_attr_boundary(block, offset, forward=False) if block is None: @@ -136,6 +142,7 @@ def find_containing_attribute(block, offset): return boundary.data return None + def find_attribute_in_tag(block, offset, attr_name): ' Return the start of the attribute value as block, offset or None, None if attribute not found ' end_block, boundary = next_tag_boundary(block, offset) @@ -159,6 +166,7 @@ def find_attribute_in_tag(block, offset, attr_name): found_attr = True current_offset += 1 + def find_end_of_attribute(block, offset): ' Find the end of an attribute that occurs somewhere after the position specified by (block, offset) ' block, boundary = next_attr_boundary(block, offset) @@ -168,6 +176,7 @@ def find_end_of_attribute(block, offset): return None, None return block, boundary.offset + def find_closing_tag(tag, max_tags=sys.maxint): ''' Find the closing tag corresponding to the specified tag. To find it we search for the first closing tag after the specified tag that does not @@ -198,11 +207,13 @@ def find_closing_tag(tag, max_tags=sys.maxint): max_tags -= 1 return None + def select_tag(cursor, tag): cursor.setPosition(tag.start_block.position() + tag.start_offset) cursor.setPosition(tag.end_block.position() + tag.end_offset + 1, cursor.KeepAnchor) return unicode(cursor.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n').rstrip('\0') + def rename_tag(cursor, opening_tag, closing_tag, new_name, insert=False): cursor.beginEditBlock() text = select_tag(cursor, closing_tag) @@ -219,6 +230,7 @@ def rename_tag(cursor, opening_tag, closing_tag, new_name, insert=False): cursor.insertText(text) cursor.endEditBlock() + def ensure_not_within_tag_definition(cursor, forward=True): ''' Ensure the cursor is not inside a tag definition <>. Returns True iff the cursor was moved. ''' block, offset = cursor.block(), cursor.positionInBlock() @@ -244,6 +256,7 @@ BLOCK_TAG_NAMES = frozenset(( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'p', 'div', 'dd', 'dl', 'ul', 'ol', 'li', 'body', 'td', 'th')) + def find_closest_containing_block_tag(block, offset, block_tag_names=BLOCK_TAG_NAMES): while True: tag = find_closest_containing_tag(block, offset) @@ -253,6 +266,7 @@ def find_closest_containing_block_tag(block, offset, block_tag_names=BLOCK_TAG_N return tag block, offset = tag.start_block, tag.start_offset + def set_style_property(tag, property_name, value, editor): ''' Set a style property, i.e. a CSS property inside the style attribute of the tag. @@ -260,6 +274,7 @@ def set_style_property(tag, property_name, value, editor): ''' block, offset = find_attribute_in_tag(tag.start_block, tag.start_offset + 1, 'style') c = editor.textCursor() + def css(d): return d.cssText.replace('\n', ' ') if block is None or offset is None: @@ -281,6 +296,7 @@ def set_style_property(tag, property_name, value, editor): entity_pat = re.compile(r'&(#{0,1}[a-zA-Z0-9]{1,8});$') + class Smarts(NullSmarts): def __init__(self, *args, **kwargs): @@ -757,6 +773,7 @@ if __name__ == '__main__': # {{{ ''' + def callback(ed): import regex ed.find_text(regex.compile('A bold word')) diff --git a/src/calibre/gui2/tweak_book/editor/smarts/python.py b/src/calibre/gui2/tweak_book/editor/smarts/python.py index c1a9332691..f3f8ba4ae3 100644 --- a/src/calibre/gui2/tweak_book/editor/smarts/python.py +++ b/src/calibre/gui2/tweak_book/editor/smarts/python.py @@ -19,9 +19,11 @@ get_leading_whitespace_on_block = lambda editor, previous=False: expand_tabs(lw( tw = 4 # The tab width (hardcoded to the pep8 value) + def expand_tabs(text): return text.replace('\t', ' ' * tw) + class Smarts(NullSmarts): override_tab_stop_width = tw diff --git a/src/calibre/gui2/tweak_book/editor/smarts/utils.py b/src/calibre/gui2/tweak_book/editor/smarts/utils.py index eff06f73ff..8e1a6fb301 100644 --- a/src/calibre/gui2/tweak_book/editor/smarts/utils.py +++ b/src/calibre/gui2/tweak_book/editor/smarts/utils.py @@ -8,6 +8,7 @@ __copyright__ = '2014, Kovid Goyal ' from PyQt5.Qt import Qt + def get_text_around_cursor(editor, before=True): cursor = editor.textCursor() cursor.clearSelection() @@ -17,6 +18,7 @@ def get_text_around_cursor(editor, before=True): get_text_before_cursor = get_text_around_cursor get_text_after_cursor = lambda editor: get_text_around_cursor(editor, before=False) + def is_cursor_on_wrapped_line(editor): cursor = editor.textCursor() cursor.movePosition(cursor.StartOfLine) @@ -24,6 +26,7 @@ def is_cursor_on_wrapped_line(editor): cursor.movePosition(cursor.StartOfBlock) return sol != cursor.position() + def get_leading_whitespace_on_block(editor, previous=False): cursor = editor.textCursor() block = cursor.block() @@ -35,6 +38,7 @@ def get_leading_whitespace_on_block(editor, previous=False): return text[:len(text)-len(ntext)] return '' + def no_modifiers(ev, *args): mods = ev.modifiers() for mod_mask in args: @@ -42,6 +46,7 @@ def no_modifiers(ev, *args): return False return True + def test_modifiers(ev, *args): mods = ev.modifiers() for mod_mask in args: @@ -49,6 +54,7 @@ def test_modifiers(ev, *args): return False return True + def smart_home(editor, ev): if no_modifiers(ev, Qt.ControlModifier) and not is_cursor_on_wrapped_line(editor): cursor, text = get_text_before_cursor(editor) @@ -62,9 +68,11 @@ def smart_home(editor, ev): return True return False + def expand_tabs(text, tw): return text.replace('\t', ' ' * tw) + def smart_tab(editor, ev): cursor, text = get_text_before_cursor(editor) if not text.lstrip(): @@ -77,6 +85,7 @@ def smart_tab(editor, ev): return True return False + def smart_backspace(editor, ev): cursor, text = get_text_before_cursor(editor) if text and not text.lstrip(): diff --git a/src/calibre/gui2/tweak_book/editor/snippets.py b/src/calibre/gui2/tweak_book/editor/snippets.py index f99f1436b4..1056a048e6 100644 --- a/src/calibre/gui2/tweak_book/editor/snippets.py +++ b/src/calibre/gui2/tweak_book/editor/snippets.py @@ -30,11 +30,14 @@ KEY = Qt.Key_J MODIFIER = Qt.META if isosx else Qt.CTRL SnipKey = namedtuple('SnipKey', 'trigger syntaxes') + + def snip_key(trigger, *syntaxes): if '*' in syntaxes: syntaxes = all_text_syntaxes return SnipKey(trigger, frozenset(syntaxes)) + def contains(l1, r1, l2, r2): # True iff (l2, r2) if contained in (l1, r1) return l2 > l1 and r2 < r1 @@ -85,6 +88,8 @@ obtain some advantage from it? But.

# Parsing of snippets {{{ escape = unescape = None + + def escape_funcs(): global escape, unescape if escape is None: @@ -96,6 +101,7 @@ def escape_funcs(): unescape = lambda x:unescape_pat.sub(lambda m:unescapem[m.group()], x) return escape, unescape + class TabStop(unicode): def __new__(self, raw, start_offset, tab_stops, is_toplevel=True): @@ -129,6 +135,7 @@ class TabStop(unicode): return 'TabStop(text=%s num=%d start=%d is_mirror=%s takes_selection=%s is_toplevel=%s)' % ( unicode.__repr__(self), self.num, self.start, self.is_mirror, self.takes_selection, self.is_toplevel) + def parse_template(template, start_offset=0, is_toplevel=True, grouped=True): escape, unescape = escape_funcs() template = escape(template) @@ -158,6 +165,7 @@ def parse_template(template, start_offset=0, is_toplevel=True, grouped=True): _snippets = None user_snippets = JSONConfig('editor_snippets') + def snippets(refresh=False): global _snippets if _snippets is None or refresh: @@ -171,6 +179,7 @@ def snippets(refresh=False): # Editor integration {{{ + class EditorTabStop(object): def __init__(self, left, tab_stops, editor): @@ -218,6 +227,7 @@ class EditorTabStop(object): c = editor.textCursor() c.setPosition(self.left), c.setPosition(self.right, c.KeepAnchor) return editor.selected_text_from_cursor(c) + def fset(self, text): editor = self.editor() if editor is None or self.is_deleted: @@ -262,6 +272,7 @@ class EditorTabStop(object): if position <= self.right: self.right += chars_added + class Template(list): def __new__(self, tab_stops): @@ -329,6 +340,7 @@ class Template(list): dist, ans = x, c return ans + def expand_template(editor, trigger, template): c = editor.textCursor() c.beginEditBlock() @@ -348,6 +360,7 @@ def expand_template(editor, trigger, template): c.endEditBlock() return tl + def find_matching_snip(text, syntax=None, snip_func=None): ans_snip = ans_trigger = None for key, snip in (snip_func or snippets)(): @@ -356,6 +369,7 @@ def find_matching_snip(text, syntax=None, snip_func=None): break return ans_snip, ans_trigger + class SnippetManager(QObject): def __init__(self, editor): @@ -420,6 +434,7 @@ class SnippetManager(QObject): # Config {{{ + class SnippetTextEdit(PlainTextEdit): def __init__(self, text, parent=None): @@ -433,6 +448,7 @@ class SnippetTextEdit(PlainTextEdit): return PlainTextEdit.keyPressEvent(self, ev) + class EditSnippet(QWidget): def __init__(self, parent=None): @@ -521,6 +537,7 @@ class EditSnippet(QWidget): def snip(self): def fset(self, snip): self.apply_snip(snip) + def fget(self): ftypes = [] for i in xrange(self.types.count()): @@ -544,6 +561,7 @@ class EditSnippet(QWidget): err = _('You must specify at least one file type') return err + class UserSnippets(Dialog): def __init__(self, parent=None): diff --git a/src/calibre/gui2/tweak_book/editor/syntax/base.py b/src/calibre/gui2/tweak_book/editor/syntax/base.py index 6e425d1869..70eca80c85 100644 --- a/src/calibre/gui2/tweak_book/editor/syntax/base.py +++ b/src/calibre/gui2/tweak_book/editor/syntax/base.py @@ -17,6 +17,7 @@ from calibre.utils.icu import utf16_length is_wide_build = sys.maxunicode >= 0x10ffff + def run_loop(user_data, state_map, formats, text): state = user_data.state i = 0 @@ -40,6 +41,7 @@ def run_loop(user_data, state_map, formats, text): print ('Syntax highlighter returned a zero length format, parse state:', state.parse) break + class SimpleState(object): __slots__ = ('parse',) @@ -52,6 +54,7 @@ class SimpleState(object): s.parse = self.parse return s + class SimpleUserData(QTextBlockUserData): def __init__(self): @@ -63,6 +66,7 @@ class SimpleUserData(QTextBlockUserData): self.state = SimpleState() if state is None else state self.doc_name = doc_name + class SyntaxHighlighter(object): create_formats_func = lambda highlighter: {} diff --git a/src/calibre/gui2/tweak_book/editor/syntax/css.py b/src/calibre/gui2/tweak_book/editor/syntax/css.py index 848e443b0f..be68dae79a 100644 --- a/src/calibre/gui2/tweak_book/editor/syntax/css.py +++ b/src/calibre/gui2/tweak_book/editor/syntax/css.py @@ -141,6 +141,7 @@ IN_DQS = 3 IN_CONTENT = 4 IN_COMMENT_CONTENT = 5 + class CSSState(object): __slots__ = ('parse', 'blocks') @@ -165,6 +166,7 @@ class CSSState(object): return "CSSState(parse=%s, blocks=%s)" % (self.parse, self.blocks) __str__ = __repr__ + class CSSUserData(QTextBlockUserData): def __init__(self): @@ -176,6 +178,7 @@ class CSSUserData(QTextBlockUserData): self.state = CSSState() if state is None else state self.doc_name = doc_name + def normal(state, text, i, formats, user_data): ' The normal state (outside content blocks {})' m = space_pat.match(text, i) @@ -202,6 +205,7 @@ def normal(state, text, i, formats, user_data): return [(len(text) - i, formats['unknown-normal'])] + def content(state, text, i, formats, user_data): ' Inside content blocks ' m = space_pat.match(text, i) @@ -241,6 +245,7 @@ def content(state, text, i, formats, user_data): return [(len(text) - i, formats['unknown-normal'])] + def comment(state, text, i, formats, user_data): ' Inside a comment ' pos = text.find('*/', i) @@ -249,6 +254,7 @@ def comment(state, text, i, formats, user_data): state.parse = NORMAL if state.parse == IN_COMMENT_NORMAL else IN_CONTENT return [(pos - i + 2, formats['comment'])] + def in_string(state, text, i, formats, user_data): 'Inside a string' q = '"' if state.parse == IN_DQS else "'" @@ -271,6 +277,7 @@ state_map = { IN_CONTENT: content, } + def create_formats(highlighter): theme = highlighter.theme formats = { diff --git a/src/calibre/gui2/tweak_book/editor/syntax/html.py b/src/calibre/gui2/tweak_book/editor/syntax/html.py index e0cc5e6742..46a00943d4 100644 --- a/src/calibre/gui2/tweak_book/editor/syntax/html.py +++ b/src/calibre/gui2/tweak_book/editor/syntax/html.py @@ -57,6 +57,7 @@ LINK_ATTRS = frozenset(('href', 'src', 'poster', 'xlink:href')) do_spell_check = False + def refresh_spell_check_status(): global do_spell_check do_spell_check = tprefs['inline_spell_check'] and hasattr(dictionaries, 'active_user_dictionaries') @@ -68,6 +69,7 @@ if _speedup is not None: Tag = _speedup.Tag bold_tags, italic_tags = _speedup.bold_tags, _speedup.italic_tags State = _speedup.State + def spell_property(sfmt, locale): s = QTextCharFormat(sfmt) s.setProperty(SPELL_LOCALE_PROPERTY, locale) @@ -168,6 +170,7 @@ else: del _speedup + def finish_opening_tag(state, cdata_tags): state.parse = NORMAL if state.tag_being_defined is None: @@ -181,6 +184,7 @@ def finish_opening_tag(state, cdata_tags): state.parse = CSS if t.name == 'style' else CDATA state.sub_parser_state = None + def close_tag(state, name): removed_tags = [] for tag in reversed(state.tags): @@ -212,6 +216,7 @@ def close_tag(state, name): state.current_lang = tag.lang break + class HTMLUserData(QTextBlockUserData): def __init__(self): @@ -231,20 +236,24 @@ class HTMLUserData(QTextBlockUserData): def tag_ok_for_spell(cls, name): return name not in html_spell_tags + class XMLUserData(HTMLUserData): @classmethod def tag_ok_for_spell(cls, name): return name in xml_spell_tags + def add_tag_data(user_data, tag): user_data.tags.append(tag) ATTR_NAME, ATTR_VALUE, ATTR_START, ATTR_END = object(), object(), object(), object() + def add_attr_data(user_data, data_type, data, offset): user_data.attributes.append(Attr(offset, data_type, data)) + def css(state, text, i, formats, user_data): ' Inside a ''') # }}} + class ThemeEditor(Dialog): def __init__(self, parent=None): diff --git a/src/calibre/gui2/tweak_book/editor/widget.py b/src/calibre/gui2/tweak_book/editor/widget.py index 5829057084..32a1281316 100644 --- a/src/calibre/gui2/tweak_book/editor/widget.py +++ b/src/calibre/gui2/tweak_book/editor/widget.py @@ -25,6 +25,7 @@ from calibre.gui2.tweak_book.editor.help import help_url from calibre.gui2.tweak_book.editor.text import TextEdit from calibre.utils.icu import utf16_length + def create_icon(text, palette=None, sz=None, divider=2, fill='white'): if isinstance(fill, basestring): fill = QColor(fill) @@ -44,6 +45,7 @@ def create_icon(text, palette=None, sz=None, divider=2, fill='white'): p.end() return QIcon(QPixmap.fromImage(img)) + def register_text_editor_actions(_reg, palette): def reg(*args, **kw): ac = _reg(*args) @@ -157,6 +159,7 @@ class Editor(QMainWindow): def current_line(self): def fget(self): return self.editor.textCursor().blockNumber() + def fset(self, val): self.editor.go_to_line(val) return property(fget=fget, fset=fset) @@ -166,6 +169,7 @@ class Editor(QMainWindow): def fget(self): c = self.editor.textCursor() return {'cursor':(c.anchor(), c.position())} + def fset(self, val): anchor, position = val.get('cursor', (None, None)) if anchor is not None and position is not None: @@ -189,6 +193,7 @@ class Editor(QMainWindow): if changed: self.data = ans return ans.encode('utf-8') + def fset(self, val): self.editor.load_text(val, syntax=self.syntax, doc_name=editor_name(self)) return property(fget=fget, fset=fset) @@ -312,6 +317,7 @@ class Editor(QMainWindow): def is_modified(self): def fget(self): return self.editor.is_modified + def fset(self, val): self.editor.is_modified = val return property(fget=fget, fset=fset) @@ -353,6 +359,7 @@ class Editor(QMainWindow): def populate_toolbars(self): self.action_bar.clear(), self.tools_bar.clear() + def add_action(name, bar): if name is None: bar.addSeparator() @@ -584,6 +591,7 @@ class Editor(QMainWindow): dictionaries.add_to_user_dictionary(dic, word, locale) self.word_ignored.emit(word, locale) + def launch_editor(path_to_edit, path_is_raw=False, syntax='html', callback=None): from calibre.gui2.tweak_book import dictionaries from calibre.gui2.tweak_book.main import option_parser diff --git a/src/calibre/gui2/tweak_book/file_list.py b/src/calibre/gui2/tweak_book/file_list.py index a06fdd02d7..ac26921e80 100644 --- a/src/calibre/gui2/tweak_book/file_list.py +++ b/src/calibre/gui2/tweak_book/file_list.py @@ -45,6 +45,7 @@ CATEGORIES = ( ('misc', _('Miscellaneous'), _('Misc-')), ) + def name_is_ok(name, show_error): if not name or not name.strip(): return show_error('') and False @@ -61,6 +62,7 @@ def name_is_ok(name, show_error): show_error('') return True + def get_bulk_rename_settings(parent, number, msg=None, sanitize=sanitize_file_name_unicode, leading_zeros=True, prefix=None, category='text'): # {{{ d = QDialog(parent) d.setWindowTitle(_('Bulk rename items')) @@ -92,6 +94,7 @@ def get_bulk_rename_settings(parent, number, msg=None, sanitize=sanitize_file_na return None, None # }}} + class ItemDelegate(QStyledItemDelegate): # {{{ rename_requested = pyqtSignal(object, object) @@ -151,6 +154,7 @@ class ItemDelegate(QStyledItemDelegate): # {{{ painter.drawText(option.rect, Qt.AlignRight|Qt.AlignVCenter, suffix) # }}} + class FileList(QTreeWidget): delete_requested = pyqtSignal(object, object) @@ -738,6 +742,7 @@ class FileList(QTreeWidget): if sheets: self.link_stylesheets_requested.emit(names, sheets, r.isChecked()) + class NewFileDialog(QDialog): # {{{ def __init__(self, parent=None): @@ -819,6 +824,7 @@ class NewFileDialog(QDialog): # {{{ QDialog.accept(self) # }}} + class MergeDialog(QDialog): # {{{ def __init__(self, names, parent=None): @@ -854,6 +860,7 @@ class MergeDialog(QDialog): # {{{ # }}} + class FileListWidget(QWidget): def __init__(self, parent=None): diff --git a/src/calibre/gui2/tweak_book/function_replace.py b/src/calibre/gui2/tweak_book/function_replace.py index 09da71b635..5656ca5dac 100644 --- a/src/calibre/gui2/tweak_book/function_replace.py +++ b/src/calibre/gui2/tweak_book/function_replace.py @@ -26,6 +26,7 @@ from calibre.utils.localization import localize_user_manual_link user_functions = JSONConfig('editor-search-replace-functions') + def compile_code(src, name=''): if not isinstance(src, unicode): match = re.search(r'coding[:=]\s*([-\w.]+)', src[:200]) @@ -43,6 +44,7 @@ def compile_code(src, name=''): exec code in namespace return namespace + class Function(object): def __init__(self, name, source=None, func=None): @@ -101,6 +103,7 @@ class Function(object): sys.stdout, sys.stderr = oo, oe self.data, self.boss, self.functions = {}, None, {} + class DebugOutput(Dialog): def __init__(self, parent=None): @@ -132,12 +135,15 @@ class DebugOutput(Dialog): def copy_to_clipboard(self): QApplication.instance().clipboard().setText(self.log_text) + def builtin_functions(): for name, obj in globals().iteritems(): if name.startswith('replace_') and callable(obj) and hasattr(obj, 'imports'): yield obj _functions = None + + def functions(refresh=False): global _functions if _functions is None or refresh: @@ -152,6 +158,7 @@ def functions(refresh=False): ans[f.name] = f return _functions + def remove_function(name, gui_parent=None): funcs = functions() if not name: @@ -170,12 +177,14 @@ def remove_function(name, gui_parent=None): boxes = [] + def refresh_boxes(): for ref in boxes: box = ref() if box is not None: box.refresh() + class FunctionBox(EditWithComplete): save_search = pyqtSignal() @@ -200,6 +209,7 @@ class FunctionBox(EditWithComplete): menu.addAction(_('Show saved searches'), self.show_saved_searches.emit) menu.exec_(event.globalPos()) + class FunctionEditor(Dialog): def __init__(self, func_name='', parent=None): @@ -272,6 +282,7 @@ class FunctionEditor(Dialog): # Builtin functions ########################################################## + def builtin(name, *args): def f(func): func.name = name @@ -284,6 +295,7 @@ def replace(match, number, file_name, metadata, dictionaries, data, functions, * return '' ''' + @builtin('Upper-case text', upper, apply_func_to_match_groups) def replace_uppercase(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs): '''Make matched text upper case. If the regular expression contains groups, @@ -291,6 +303,7 @@ def replace_uppercase(match, number, file_name, metadata, dictionaries, data, fu changed.''' return apply_func_to_match_groups(match, upper) + @builtin('Lower-case text', lower, apply_func_to_match_groups) def replace_lowercase(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs): '''Make matched text lower case. If the regular expression contains groups, @@ -298,6 +311,7 @@ def replace_lowercase(match, number, file_name, metadata, dictionaries, data, fu changed.''' return apply_func_to_match_groups(match, lower) + @builtin('Capitalize text', capitalize, apply_func_to_match_groups) def replace_capitalize(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs): '''Capitalize matched text. If the regular expression contains groups, @@ -305,6 +319,7 @@ def replace_capitalize(match, number, file_name, metadata, dictionaries, data, f changed.''' return apply_func_to_match_groups(match, capitalize) + @builtin('Title-case text', titlecase, apply_func_to_match_groups) def replace_titlecase(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs): '''Title-case matched text. If the regular expression contains groups, @@ -312,6 +327,7 @@ def replace_titlecase(match, number, file_name, metadata, dictionaries, data, fu changed.''' return apply_func_to_match_groups(match, titlecase) + @builtin('Swap the case of text', swapcase, apply_func_to_match_groups) def replace_swapcase(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs): '''Swap the case of the matched text. If the regular expression contains groups, @@ -319,26 +335,31 @@ def replace_swapcase(match, number, file_name, metadata, dictionaries, data, fun changed.''' return apply_func_to_match_groups(match, swapcase) + @builtin('Upper-case text (ignore tags)', upper, apply_func_to_html_text) def replace_uppercase_ignore_tags(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs): '''Make matched text upper case, ignoring the text inside tag definitions.''' return apply_func_to_html_text(match, upper) + @builtin('Lower-case text (ignore tags)', lower, apply_func_to_html_text) def replace_lowercase_ignore_tags(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs): '''Make matched text lower case, ignoring the text inside tag definitions.''' return apply_func_to_html_text(match, lower) + @builtin('Capitalize text (ignore tags)', capitalize, apply_func_to_html_text) def replace_capitalize_ignore_tags(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs): '''Capitalize matched text, ignoring the text inside tag definitions.''' return apply_func_to_html_text(match, capitalize) + @builtin('Title-case text (ignore tags)', titlecase, apply_func_to_html_text) def replace_titlecase_ignore_tags(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs): '''Title-case matched text, ignoring the text inside tag definitions.''' return apply_func_to_html_text(match, titlecase) + @builtin('Swap the case of text (ignore tags)', swapcase, apply_func_to_html_text) def replace_swapcase_ignore_tags(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs): '''Swap the case of the matched text, ignoring the text inside tag definitions.''' diff --git a/src/calibre/gui2/tweak_book/job.py b/src/calibre/gui2/tweak_book/job.py index bc0edb3493..8cbdcc75ea 100644 --- a/src/calibre/gui2/tweak_book/job.py +++ b/src/calibre/gui2/tweak_book/job.py @@ -15,6 +15,7 @@ from PyQt5.Qt import (QWidget, QVBoxLayout, QLabel, Qt, QPainter, QBrush, QRect, from calibre.gui2 import Dispatcher from calibre.gui2.progress_indicator import ProgressIndicator + class LongJob(Thread): daemon = True @@ -41,6 +42,7 @@ class LongJob(Thread): finally: pass + class BlockingJob(QWidget): def __init__(self, parent): diff --git a/src/calibre/gui2/tweak_book/live_css.py b/src/calibre/gui2/tweak_book/live_css.py index c1112d1cbd..9ca671999e 100644 --- a/src/calibre/gui2/tweak_book/live_css.py +++ b/src/calibre/gui2/tweak_book/live_css.py @@ -19,6 +19,7 @@ from calibre.gui2.tweak_book.editor.themes import get_theme, theme_color from calibre.gui2.tweak_book.editor.text import default_font_family from css_selectors import parse, SelectorError + class Heading(QWidget): # {{{ toggled = pyqtSignal(object) @@ -89,6 +90,7 @@ class Heading(QWidget): # {{{ self.context_menu_requested.emit(self, ev) # }}} + class Cell(object): # {{{ __slots__ = ('rect', 'text', 'right_align', 'color_role', 'override_color', 'swatch', 'is_overriden') @@ -122,6 +124,7 @@ class Cell(object): # {{{ painter.drawLine(br.left(), br.top() + br.height() // 2, br.right(), br.top() + br.height() // 2) # }}} + class Declaration(QWidget): hyperlink_activated = pyqtSignal(object) @@ -254,6 +257,7 @@ class Declaration(QWidget): def contextMenuEvent(self, ev): self.context_menu_requested.emit(self, ev) + class Box(QWidget): hyperlink_activated = pyqtSignal(object) @@ -370,6 +374,7 @@ class Property(object): return '' % ( self.name, self.value, self.important, self.color, self.specificity, self.is_overriden) + class LiveCSS(QWidget): goto_declaration = pyqtSignal(object) diff --git a/src/calibre/gui2/tweak_book/main.py b/src/calibre/gui2/tweak_book/main.py index 8ddd9aaee7..a715edb7cc 100644 --- a/src/calibre/gui2/tweak_book/main.py +++ b/src/calibre/gui2/tweak_book/main.py @@ -15,6 +15,7 @@ from calibre.gui2 import Application, setup_gui_option_parser, decouple, set_gui from calibre.ptempfile import reset_base_dir from calibre.utils.config import OptionParser + def option_parser(): parser = OptionParser(_('''\ %prog [opts] [path_to_ebook] [name_of_file_inside_book ...] @@ -25,6 +26,7 @@ files inside the book which will be opened for editing automatically. setup_gui_option_parser(parser) return parser + class EventAccumulator(object): def __init__(self): @@ -33,9 +35,11 @@ class EventAccumulator(object): def __call__(self, ev): self.events.append(ev) + def gui_main(path=None, notify=None): _run(['ebook-edit', path], notify=notify) + def _run(args, notify=None): # Ensure we can continue to function if GUI is closed os.environ.pop('CALIBRE_WORKER_TEMP_DIR', None) @@ -85,6 +89,7 @@ def _run(args, notify=None): while parse_worker.is_alive() and time.time() - st < 120: time.sleep(0.1) + def main(args=sys.argv): _run(args) diff --git a/src/calibre/gui2/tweak_book/manage_fonts.py b/src/calibre/gui2/tweak_book/manage_fonts.py index 1930f1992b..0f65284251 100644 --- a/src/calibre/gui2/tweak_book/manage_fonts.py +++ b/src/calibre/gui2/tweak_book/manage_fonts.py @@ -21,6 +21,7 @@ from calibre.gui2.tweak_book.widgets import Dialog, BusyCursor from calibre.utils.icu import primary_sort_key as sort_key from calibre.utils.fonts.scanner import font_scanner, NoFonts + class EmbeddingData(Dialog): def __init__(self, family, faces, parent=None): @@ -51,6 +52,7 @@ class EmbeddingData(Dialog): text.append('
' + 'font-style:\xa0' + type('')(face['font-style'])) self.text.setHtml('\n'.join(text)) + class AllFonts(QAbstractTableModel): def __init__(self, parent=None): @@ -128,6 +130,7 @@ class AllFonts(QAbstractTableModel): pass return ans + class ChangeFontFamily(Dialog): def __init__(self, old_family, embedded_families, parent=None): diff --git a/src/calibre/gui2/tweak_book/plugin.py b/src/calibre/gui2/tweak_book/plugin.py index f7f6b59be9..723421233e 100644 --- a/src/calibre/gui2/tweak_book/plugin.py +++ b/src/calibre/gui2/tweak_book/plugin.py @@ -15,6 +15,7 @@ from calibre.customize.ui import all_edit_book_tool_plugins from calibre.gui2.tweak_book import tprefs, current_container from calibre.gui2.tweak_book.boss import get_boss + class Tool(object): ''' @@ -110,6 +111,7 @@ class Tool(object): ''' raise NotImplementedError() + def load_plugin_tools(plugin): try: main = importlib.import_module(plugin.__class__.__module__+'.main') @@ -123,11 +125,13 @@ def load_plugin_tools(plugin): ans.plugin = plugin yield ans + def plugin_action_sid(plugin, tool, for_toolbar=True): return plugin.name + tool.name + ('toolbar' if for_toolbar else 'menu') plugin_toolbar_actions = [] + def create_plugin_action(plugin, tool, for_toolbar, actions=None, toolbar_actions=None, plugin_menu_actions=None): try: ac = tool.create_action(for_toolbar=for_toolbar) @@ -158,6 +162,7 @@ def create_plugin_action(plugin, tool, for_toolbar, actions=None, toolbar_action _tool_memory = [] # Needed to prevent the tool object from being garbage collected + def create_plugin_actions(actions, toolbar_actions, plugin_menu_actions): del _tool_memory[:] del plugin_toolbar_actions[:] @@ -170,6 +175,7 @@ def create_plugin_actions(actions, toolbar_actions, plugin_menu_actions): if tool.allowed_in_menu: create_plugin_action(plugin, tool, False, actions, toolbar_actions, plugin_menu_actions) + def install_plugin(plugin): for tool in load_plugin_tools(plugin): if tool.allowed_in_toolbar: diff --git a/src/calibre/gui2/tweak_book/polish.py b/src/calibre/gui2/tweak_book/polish.py index a7beb7fd81..fb7059be3e 100644 --- a/src/calibre/gui2/tweak_book/polish.py +++ b/src/calibre/gui2/tweak_book/polish.py @@ -21,14 +21,17 @@ from calibre.gui2.tweak_book import tprefs, current_container, set_current_conta from calibre.gui2.tweak_book.widgets import Dialog from calibre.utils.icu import numeric_sort_key + class Abort(Exception): pass + def customize_remove_unused_css(name, parent, ans): d = QDialog(parent) d.l = l = QVBoxLayout() d.setLayout(d.l) d.setWindowTitle(_('Remove unused CSS')) + def label(text): la = QLabel(text) la.setWordWrap(True), l.addWidget(la), la.setMinimumWidth(450) @@ -59,6 +62,7 @@ def customize_remove_unused_css(name, parent, ans): ans['remove_unused_classes'] = tprefs['remove_unused_classes'] = c.isChecked() ans['merge_identical_selectors'] = tprefs['merge_identical_selectors'] = m.isChecked() + def get_customization(action, name, parent): ans = CUSTOMIZATION.copy() try: @@ -68,11 +72,13 @@ def get_customization(action, name, parent): return None return ans + def format_report(title, report): from calibre.ebooks.markdown import markdown report = [force_unicode(line) for line in report] return markdown('# %s\n\n'%force_unicode(title) + '\n\n'.join(report), output_format='html4') + def show_report(changed, title, report, parent, show_current_diff): report = format_report(title, report) d = QDialog(parent) @@ -96,6 +102,7 @@ def show_report(changed, title, report, parent, show_current_diff): # CompressImages {{{ + class ImageItemDelegate(QStyledItemDelegate): def sizeHint(self, option, index): @@ -125,6 +132,7 @@ class ImageItemDelegate(QStyledItemDelegate): painter.drawText(trect, Qt.AlignVCenter | Qt.AlignLeft, name + '\n' + sz) painter.restore() + class CompressImages(Dialog): def __init__(self, parent=None): @@ -181,6 +189,7 @@ class CompressImages(Dialog): return None return self.jq.value() + class CompressImagesProgress(Dialog): gui_loop = pyqtSignal(object, object, object) diff --git a/src/calibre/gui2/tweak_book/preferences.py b/src/calibre/gui2/tweak_book/preferences.py index 701ac24119..3125dc2cf8 100644 --- a/src/calibre/gui2/tweak_book/preferences.py +++ b/src/calibre/gui2/tweak_book/preferences.py @@ -29,6 +29,7 @@ from calibre.gui2.tweak_book.spell import ManageDictionaries from calibre.gui2.font_family_chooser import FontFamilyChooser from calibre.gui2.tweak_book.widgets import Dialog + class BasicSettings(QWidget): # {{{ changed_signal = pyqtSignal() @@ -150,6 +151,7 @@ class BasicSettings(QWidget): # {{{ return self.current_value(name) != self.initial_value(name) # }}} + class EditorSettings(BasicSettings): def __init__(self, parent=None): @@ -266,6 +268,7 @@ class EditorSettings(BasicSettings): if d.theme_name: s.setter(s.widget, d.theme_name) + class IntegrationSettings(BasicSettings): def __init__(self, parent=None): @@ -289,6 +292,7 @@ class IntegrationSettings(BasicSettings): ' multiple formats, this is the preference order.')) l.addRow(_('Preferred format order (drag and drop to change)'), order) + class MainWindowSettings(BasicSettings): def __init__(self, parent=None): @@ -329,6 +333,7 @@ class MainWindowSettings(BasicSettings): )) l.addRow(nd) + class PreviewSettings(BasicSettings): def __init__(self, parent=None): @@ -363,12 +368,14 @@ class PreviewSettings(BasicSettings): # ToolbarSettings {{{ + class ToolbarList(QListWidget): def __init__(self, parent=None): QListWidget.__init__(self, parent) self.setSelectionMode(self.ExtendedSelection) + class ToolbarSettings(QWidget): changed_signal = pyqtSignal() @@ -570,6 +577,7 @@ class ToolbarSettings(QWidget): # }}} + class TemplatesDialog(Dialog): # {{{ def __init__(self, parent=None): @@ -643,6 +651,7 @@ class TemplatesDialog(Dialog): # {{{ self._save_syntax() # }}} + class Preferences(QDialog): def __init__(self, gui, initial_panel=None): diff --git a/src/calibre/gui2/tweak_book/preview.py b/src/calibre/gui2/tweak_book/preview.py index b94209303b..c7dff003e8 100644 --- a/src/calibre/gui2/tweak_book/preview.py +++ b/src/calibre/gui2/tweak_book/preview.py @@ -35,6 +35,7 @@ from calibre.utils.ipc.simple_worker import offload_worker shutdown = object() + def get_data(name): 'Get the data for name. Returns a unicode string if name is a text document/stylesheet' if name in editors: @@ -42,10 +43,13 @@ def get_data(name): return current_container().raw_data(name) # Parsing of html to add linenumbers {{{ + + def parse_html(raw): root = parse(raw, decoder=lambda x:x.decode('utf-8'), line_numbers=True, linenumber_attribute='data-lnum') return serialize(root, 'text/html').encode('utf-8') + class ParseItem(object): __slots__ = ('name', 'length', 'fingerprint', 'parsing_done', 'parsed_data') @@ -60,6 +64,7 @@ class ParseItem(object): return 'ParsedItem(name=%r, length=%r, fingerprint=%r, parsing_done=%r, parsed_data_is_None=%r)' % ( self.name, self.length, self.fingerprint, self.parsing_done, self.parsed_data is None) + class ParseWorker(Thread): daemon = True @@ -144,6 +149,8 @@ parse_worker = ParseWorker() # }}} # Override network access to load data "live" from the editors {{{ + + class NetworkReply(QNetworkReply): def __init__(self, parent, request, mime_type, name): @@ -241,6 +248,7 @@ class NetworkAccessManager(QNetworkAccessManager): # }}} + def uniq(vals): ''' Remove all duplicates from vals, while preserving order. ''' vals = vals or () @@ -248,6 +256,7 @@ def uniq(vals): seen_add = seen.add return tuple(x for x in vals if x not in seen and not seen_add(x)) + def find_le(a, x): 'Find rightmost value in a less than or equal to x' try: @@ -255,6 +264,7 @@ def find_le(a, x): except IndexError: return a[-1] + class WebPage(QWebPage): sync_requested = pyqtSignal(object, object, object) @@ -287,6 +297,7 @@ class WebPage(QWebPage): def current_root(self): def fget(self): return self.networkAccessManager().current_root + def fset(self, val): self.networkAccessManager().current_root = val return property(fget=fget, fset=fset) @@ -384,6 +395,7 @@ class WebView(QWebView): def fget(self): mf = self.page().mainFrame() return (mf.scrollBarValue(Qt.Horizontal), mf.scrollBarValue(Qt.Vertical)) + def fset(self, val): mf = self.page().mainFrame() mf.setScrollBarValue(Qt.Horizontal, val[0]) @@ -428,6 +440,7 @@ class WebView(QWebView): menu.addAction(_('Open link'), partial(open_url, r.linkUrl())) menu.exec_(ev.globalPos()) + class Preview(QWidget): sync_requested = pyqtSignal(object, object) diff --git a/src/calibre/gui2/tweak_book/reports.py b/src/calibre/gui2/tweak_book/reports.py index e4643d4525..e79c679854 100644 --- a/src/calibre/gui2/tweak_book/reports.py +++ b/src/calibre/gui2/tweak_book/reports.py @@ -40,12 +40,14 @@ from calibre.utils.localization import calibre_langcode_to_name, canonicalize_la ROOT = QModelIndex() + def read_state(name, default=None): data = tprefs.get('reports-ui-state') if data is None: tprefs['reports-ui-state'] = data = {} return data.get(name, default) + def save_state(name, val): data = tprefs.get('reports-ui-state') if isinstance(val, QByteArray): @@ -56,6 +58,7 @@ def save_state(name, val): SORT_ROLE = Qt.UserRole + 1 + class ProxyModel(QSortFilterProxyModel): def __init__(self, parent=None): @@ -81,6 +84,7 @@ class ProxyModel(QSortFilterProxyModel): return section + 1 return QSortFilterProxyModel.headerData(self, section, orientation, role) + class FileCollection(QAbstractTableModel): COLUMN_HEADERS = () @@ -110,6 +114,7 @@ class FileCollection(QAbstractTableModel): except IndexError: pass + class FilesView(QTableView): double_clicked = pyqtSignal(object) @@ -213,6 +218,7 @@ class FilesView(QTableView): # Files {{{ + class FilesModel(FileCollection): COLUMN_HEADERS = (_('Folder'), _('Name'), _('Size (KB)'), _('Type')) @@ -262,6 +268,7 @@ class FilesModel(FileCollection): if col == 3: return self.CATEGORY_NAMES.get(entry.category) + class FilesWidget(QWidget): edit_requested = pyqtSignal(object) @@ -307,6 +314,7 @@ class FilesWidget(QWidget): # Jump {{{ + def jump_to_location(loc): from calibre.gui2.tweak_book.boss import get_boss boss = get_boss() @@ -327,6 +335,7 @@ def jump_to_location(loc): if loc.text_on_line is not None: editor.find(regex.compile(regex.escape(loc.text_on_line))) + class Jump(object): def __init__(self): @@ -345,6 +354,7 @@ jump = Jump() # }}} # Images {{{ + class ImagesDelegate(QStyledItemDelegate): MARGIN = 5 @@ -496,6 +506,7 @@ class ImagesWidget(QWidget): # Links {{{ + class LinksModel(FileCollection): COLUMN_HEADERS = ['✓ ', _('Source'), _('Source text'), _('Target'), _('Anchor'), _('Target text')] @@ -560,11 +571,13 @@ class LinksModel(FileCollection): except IndexError: pass + class WebView(QWebView): def sizeHint(self): return QSize(600, 200) + class LinksWidget(QWidget): def __init__(self, parent=None): @@ -649,6 +662,7 @@ class LinksWidget(QWidget): # Words {{{ + class WordsModel(FileCollection): COLUMN_HEADERS = (_('Word'), _('Language'), _('Times used')) @@ -660,6 +674,7 @@ class WordsModel(FileCollection): self.total_size = len({entry.locale for entry in self.files}) psk = numeric_sort_key lsk_cache = {} + def locale_sort_key(loc): try: return lsk_cache[loc] @@ -700,6 +715,7 @@ class WordsModel(FileCollection): def location(self, index): return None + class WordsWidget(QWidget): def __init__(self, parent=None): @@ -742,6 +758,7 @@ class WordsWidget(QWidget): # Characters {{{ + class CharsModel(FileCollection): COLUMN_HEADERS = (_('Character'), _('Name'), _('Codepoint'), _('Times used')) @@ -786,6 +803,7 @@ class CharsModel(FileCollection): def location(self, index): return None + class CharsWidget(QWidget): def __init__(self, parent=None): @@ -855,6 +873,7 @@ class CharsWidget(QWidget): # CSS {{{ + class CSSRulesModel(QAbstractItemModel): def __init__(self, parent): @@ -957,6 +976,7 @@ class CSSRulesModel(QAbstractItemModel): self.build_maps() self.endResetModel() + class CSSProxyModel(QSortFilterProxyModel): def __init__(self, parent=None): @@ -977,6 +997,7 @@ class CSSProxyModel(QSortFilterProxyModel): return True return primary_contains(self._filter_text, entry.rule.selector) + class CSSWidget(QWidget): SETTING_PREFIX = 'css-' @@ -1032,6 +1053,7 @@ class CSSWidget(QWidget): def sort_order(self): def fget(self): return [Qt.AscendingOrder, Qt.DescendingOrder][self._sort_order.currentIndex()] + def fset(self, val): self._sort_order.setCurrentIndex({Qt.AscendingOrder:0}.get(val, 1)) return property(fget=fget, fset=fset) @@ -1100,6 +1122,7 @@ class CSSWidget(QWidget): # Classes {{{ + class ClassesModel(CSSRulesModel): def __init__(self, parent): @@ -1177,6 +1200,7 @@ class ClassesModel(CSSRulesModel): self.build_maps() self.endResetModel() + class ClassProxyModel(CSSProxyModel): def filterAcceptsRow(self, row, parent): @@ -1188,6 +1212,7 @@ class ClassProxyModel(CSSProxyModel): return True return primary_contains(self._filter_text, entry.cls) + class ClassesWidget(CSSWidget): SETTING_PREFIX = 'classes-' @@ -1231,6 +1256,8 @@ class ClassesWidget(CSSWidget): # }}} # Wrapper UI {{{ + + class ReportsWidget(QWidget): edit_requested = pyqtSignal(object) @@ -1320,6 +1347,7 @@ class ReportsWidget(QWidget): with open(fname, 'wb') as f: f.write(data) + class Reports(Dialog): data_gathered = pyqtSignal(object, object) diff --git a/src/calibre/gui2/tweak_book/save.py b/src/calibre/gui2/tweak_book/save.py index 6dedb8ab92..52ce1ac6e0 100644 --- a/src/calibre/gui2/tweak_book/save.py +++ b/src/calibre/gui2/tweak_book/save.py @@ -19,6 +19,7 @@ from calibre.utils import join_with_timeout from calibre.utils.filenames import atomic_rename, format_permissions from calibre.utils.ipc import RC + def save_dir_container(container, path): if not os.path.exists(path): os.makedirs(path) @@ -26,6 +27,7 @@ def save_dir_container(container, path): raise ValueError('%s is not a folder, cannot save a directory based container to it' % path) container.commit(path) + def save_container(container, path): if container.is_dir: return save_dir_container(container, path) @@ -74,6 +76,7 @@ def save_container(container, path): if os.path.exists(temp): os.remove(temp) + def send_message(msg=''): if msg: t = RC(print_error=False) @@ -83,6 +86,7 @@ def send_message(msg=''): t.conn.send('bookedited:'+msg) t.conn.close() + def find_first_existing_ancestor(path): while path and not os.path.exists(path): npath = os.path.dirname(path) @@ -91,6 +95,7 @@ def find_first_existing_ancestor(path): path = npath return path + class SaveWidget(QWidget): def __init__(self, parent=None): @@ -116,6 +121,7 @@ class SaveWidget(QWidget): self.pi.stopAnimation() self.label.setText('') + class SaveManager(QObject): start_save = pyqtSignal() diff --git a/src/calibre/gui2/tweak_book/search.py b/src/calibre/gui2/tweak_book/search.py index 09440a343f..b67641adea 100644 --- a/src/calibre/gui2/tweak_book/search.py +++ b/src/calibre/gui2/tweak_book/search.py @@ -35,6 +35,7 @@ REGEX_FLAGS = regex.VERSION1 | regex.WORD | regex.FULLCASE | regex.MULTILINE | r # The search panel {{{ + class AnimatablePushButton(QPushButton): 'A push button that can be animated without actually emitting a clicked signal' @@ -53,12 +54,14 @@ class AnimatablePushButton(QPushButton): self.setDown(False) self.update() + class PushButton(AnimatablePushButton): def __init__(self, text, action, parent): AnimatablePushButton.__init__(self, text, parent) self.clicked.connect(lambda : parent.search_triggered.emit(action)) + def expand_template(line_edit): pos = line_edit.cursorPosition() text = line_edit.text()[:pos] @@ -76,6 +79,7 @@ def expand_template(line_edit): return True return False + class HistoryBox(HistoryComboBox): max_history_items = 100 @@ -113,6 +117,7 @@ class HistoryBox(HistoryComboBox): self.disable_popup = not bool(self.disable_popup) tprefs['disable_completion_popup_for_search'] = self.disable_popup + class WhereBox(QComboBox): def __init__(self, parent, emphasize=False): @@ -145,8 +150,10 @@ class WhereBox(QComboBox): @dynamic_property def where(self): wm = {0:'current', 1:'text', 2:'styles', 3:'selected', 4:'open', 5:'selected-text'} + def fget(self): return wm[self.currentIndex()] + def fset(self, val): self.setCurrentIndex({v:k for k, v in wm.iteritems()}[val]) return property(fget=fget, fset=fset) @@ -162,6 +169,7 @@ class WhereBox(QComboBox): self.setFont(self.emph_font) QComboBox.hidePopup(self) + class DirectionBox(QComboBox): def __init__(self, parent): @@ -181,10 +189,12 @@ class DirectionBox(QComboBox): def direction(self): def fget(self): return 'down' if self.currentIndex() == 0 else 'up' + def fset(self, val): self.setCurrentIndex(1 if val == 'up' else 0) return property(fget=fget, fset=fset) + class ModeBox(QComboBox): def __init__(self, parent): @@ -205,6 +215,7 @@ class ModeBox(QComboBox): def mode(self): def fget(self): return ('normal', 'regex', 'function')[self.currentIndex()] + def fset(self, val): self.setCurrentIndex({'regex':1, 'function':2}.get(val, 0)) return property(fget=fget, fset=fset) @@ -348,6 +359,7 @@ class SearchWidget(QWidget): def mode(self): def fget(self): return self.mode_box.mode + def fset(self, val): self.mode_box.mode = val self.da.setVisible(self.mode in ('regex', 'function')) @@ -357,6 +369,7 @@ class SearchWidget(QWidget): def find(self): def fget(self): return unicode(self.find_text.text()) + def fset(self, val): self.find_text.setText(val) return property(fget=fget, fset=fset) @@ -367,6 +380,7 @@ class SearchWidget(QWidget): if self.mode == 'function': return self.functions.text() return unicode(self.replace_text.text()) + def fset(self, val): self.replace_text.setText(val) return property(fget=fget, fset=fset) @@ -375,6 +389,7 @@ class SearchWidget(QWidget): def where(self): def fget(self): return self.where_box.where + def fset(self, val): self.where_box.where = val return property(fget=fget, fset=fset) @@ -383,6 +398,7 @@ class SearchWidget(QWidget): def case_sensitive(self): def fget(self): return self.cs.isChecked() + def fset(self, val): self.cs.setChecked(bool(val)) return property(fget=fget, fset=fset) @@ -391,6 +407,7 @@ class SearchWidget(QWidget): def direction(self): def fget(self): return self.direction_box.direction + def fset(self, val): self.direction_box.direction = val return property(fget=fget, fset=fset) @@ -399,6 +416,7 @@ class SearchWidget(QWidget): def wrap(self): def fget(self): return self.wr.isChecked() + def fset(self, val): self.wr.setChecked(bool(val)) return property(fget=fget, fset=fset) @@ -407,6 +425,7 @@ class SearchWidget(QWidget): def dot_all(self): def fget(self): return self.da.isChecked() + def fset(self, val): self.da.setChecked(bool(val)) return property(fget=fget, fset=fset) @@ -415,6 +434,7 @@ class SearchWidget(QWidget): def state(self): def fget(self): return {x:getattr(self, x) for x in self.DEFAULT_STATE} + def fset(self, val): for x in self.DEFAULT_STATE: if x in val: @@ -439,6 +459,7 @@ class SearchWidget(QWidget): regex_cache = {} + class SearchPanel(QWidget): # {{{ search_triggered = pyqtSignal(object) @@ -497,6 +518,7 @@ class SearchPanel(QWidget): # {{{ return QWidget.keyPressEvent(self, ev) # }}} + class SearchDescription(QScrollArea): def __init__(self, parent): @@ -508,6 +530,7 @@ class SearchDescription(QScrollArea): self.label.setWordWrap(True) self.set_text = self.label.setText + class SearchesModel(QAbstractListModel): def __init__(self, parent): @@ -565,6 +588,7 @@ class SearchesModel(QAbstractListModel): tprefs['saved_searches'] = self.searches self.do_filter('') + class EditSearch(QFrame): # {{{ done = pyqtSignal(object) @@ -762,6 +786,7 @@ class EditSearch(QFrame): # {{{ # }}} + class SearchDelegate(QStyledItemDelegate): def sizeHint(self, *args): @@ -769,6 +794,7 @@ class SearchDelegate(QStyledItemDelegate): ans.setHeight(ans.height() + 4) return ans + class SavedSearches(QWidget): run_saved_searches = pyqtSignal(object, object) @@ -905,6 +931,7 @@ class SavedSearches(QWidget): def state(self): def fget(self): return {'wrap':self.wrap, 'direction':self.direction, 'where':self.where} + def fset(self, val): self.wrap, self.where, self.direction = val['wrap'], val['where'], val['direction'] return property(fget=fget, fset=fset) @@ -938,6 +965,7 @@ class SavedSearches(QWidget): def where(self): def fget(self): return self.where_box.where + def fset(self, val): self.where_box.where = val return property(fget=fget, fset=fset) @@ -946,6 +974,7 @@ class SavedSearches(QWidget): def direction(self): def fget(self): return self.direction_box.direction + def fset(self, val): self.direction_box.direction = val return property(fget=fget, fset=fset) @@ -954,6 +983,7 @@ class SavedSearches(QWidget): def wrap(self): def fget(self): return self.wr.isChecked() + def fset(self, val): self.wr.setChecked(bool(val)) return property(fget=fget, fset=fset) @@ -1095,6 +1125,7 @@ class SavedSearches(QWidget): with open(path[0], 'rb') as f: obj = json.loads(f.read()) needed_keys = {'name', 'find', 'replace', 'case_sensitive', 'dot_all', 'mode'} + def err(): error_dialog(self, _('Invalid data'), _( 'The file %s does not contain valid saved searches') % path, show=True) @@ -1139,6 +1170,7 @@ class SavedSearches(QWidget): with open(path, 'wb') as f: f.write(raw.encode('utf-8')) + def validate_search_request(name, searchable_names, has_marked_text, state, gui_parent): err = None where = state['where'] @@ -1156,12 +1188,14 @@ def validate_search_request(name, searchable_names, has_marked_text, state, gui_ return False return True + class InvalidRegex(regex.error): def __init__(self, raw, e): regex.error.__init__(self, e.message) self.regex = raw + def get_search_regex(state): raw = state['find'] is_regex = state['mode'] != 'normal' @@ -1183,6 +1217,7 @@ def get_search_regex(state): return ans + def initialize_search_request(state, action, current_editor, current_editor_name, searchable_names): editor = None where = state['where'] @@ -1218,9 +1253,11 @@ def initialize_search_request(state, action, current_editor, current_editor_name return editor, where, files, do_all, marked + class NoSuchFunction(ValueError): pass + def get_search_function(search): ans = search['replace'] if search['mode'] == 'function': @@ -1232,6 +1269,7 @@ def get_search_function(search): raise NoSuchFunction(ans) return ans + def show_function_debug_output(func): if isinstance(func, Function): val = func.debug_buf.getvalue().strip() @@ -1240,11 +1278,13 @@ def show_function_debug_output(func): from calibre.gui2.tweak_book.boss import get_boss get_boss().gui.sr_debug_output.show_log(func.name, val) + def reorder_files(names, order): reverse = order in {'spine-reverse', 'reverse-spine'} spine_order = {name:i for i, (name, is_linear) in enumerate(current_container().spine_names)} return sorted(frozenset(names), key=spine_order.get, reverse=reverse) + def run_search( searches, action, current_editor, current_editor_name, searchable_names, gui_parent, show_editor, edit_file, show_current_diff, add_savepoint, rewind_savepoint, set_modified): diff --git a/src/calibre/gui2/tweak_book/spell.py b/src/calibre/gui2/tweak_book/spell.py index ba20e8fc1e..66f86e0fd9 100644 --- a/src/calibre/gui2/tweak_book/spell.py +++ b/src/calibre/gui2/tweak_book/spell.py @@ -42,12 +42,14 @@ DICTIONARY = 2 _country_map = None + def country_map(): global _country_map if _country_map is None: _country_map = cPickle.loads(P('localization/iso3166.pickle', data=True, allow_user_override=False)) return _country_map + class AddDictionary(QDialog): # {{{ def __init__(self, parent=None): @@ -127,6 +129,7 @@ class AddDictionary(QDialog): # {{{ # User Dictionaries {{{ + class UserWordList(QListWidget): def __init__(self, parent=None): @@ -587,6 +590,8 @@ class ManageDictionaries(Dialog): # {{{ # }}} # Spell Check Dialog {{{ + + class WordsModel(QAbstractTableModel): word_ignored = pyqtSignal(object, object) @@ -669,6 +674,7 @@ class WordsModel(QAbstractTableModel): def sort_key(self, col): if col == 0: f = (lambda x: x) if tprefs['spell_check_case_sensitive_sort'] else primary_sort_key + def key(w): return f(w[0]) elif col == 1: @@ -801,6 +807,7 @@ class WordsModel(QAbstractTableModel): except ValueError: return -1 + class WordsView(QTableView): ignore_all = pyqtSignal() @@ -868,6 +875,7 @@ class WordsView(QTableView): def currentChanged(self, cur, prev): self.current_changed.emit(cur, prev) + class SpellCheck(Dialog): work_finished = pyqtSignal(object, object, object) @@ -1278,6 +1286,8 @@ class SpellCheck(Dialog): # }}} # Find next occurrence {{{ + + def find_next(word, locations, current_editor, current_editor_name, gui_parent, show_editor, edit_file): files = OrderedDict() @@ -1311,6 +1321,7 @@ def find_next(word, locations, current_editor, current_editor_name, return True return False + def find_next_error(current_editor, current_editor_name, gui_parent, show_editor, edit_file): files = get_checkable_file_names(current_container())[0] if current_editor_name not in files: diff --git a/src/calibre/gui2/tweak_book/templates.py b/src/calibre/gui2/tweak_book/templates.py index 04272d42ef..3764880bf9 100644 --- a/src/calibre/gui2/tweak_book/templates.py +++ b/src/calibre/gui2/tweak_book/templates.py @@ -33,9 +33,11 @@ DEFAULT_TEMPLATES = { } + def raw_template_for(syntax): return tprefs['templates'].get(syntax, DEFAULT_TEMPLATES.get(syntax, '')) + def template_for(syntax): mi = current_container().mi data = { diff --git a/src/calibre/gui2/tweak_book/text_search.py b/src/calibre/gui2/tweak_book/text_search.py index 4af6c11dfc..67740ad5e3 100644 --- a/src/calibre/gui2/tweak_book/text_search.py +++ b/src/calibre/gui2/tweak_book/text_search.py @@ -21,6 +21,7 @@ from calibre.gui2.widgets2 import HistoryComboBox # UI {{{ + class ModeBox(QComboBox): def __init__(self, parent): @@ -39,10 +40,12 @@ class ModeBox(QComboBox): def mode(self): def fget(self): return ('normal', 'regex')[self.currentIndex()] + def fset(self, val): self.setCurrentIndex({'regex':1}.get(val, 0)) return property(fget=fget, fset=fset) + class WhereBox(QComboBox): def __init__(self, parent, emphasize=False): @@ -71,8 +74,10 @@ class WhereBox(QComboBox): @dynamic_property def where(self): wm = {0:'current', 1:'text', 2:'selected', 3:'open'} + def fget(self): return wm[self.currentIndex()] + def fset(self, val): self.setCurrentIndex({v:k for k, v in wm.iteritems()}[val]) return property(fget=fget, fset=fset) @@ -134,6 +139,7 @@ class TextSearch(QWidget): def state(self): def fget(self): return {'mode': self.mode.mode, 'where':self.where_box.where, 'case_sensitive':self.cs.isChecked(), 'dot_all':self.da.isChecked()} + def fset(self, val): self.mode.mode = val.get('mode', 'normal') self.where_box.where = val.get('where', 'current') @@ -151,6 +157,7 @@ class TextSearch(QWidget): self.find_text.emit(state) # }}} + def run_text_search(search, current_editor, current_editor_name, searchable_names, gui_parent, show_editor, edit_file): try: pat = get_search_regex(search) @@ -185,6 +192,7 @@ def run_text_search(search, current_editor, current_editor_name, searchable_name msg = '

' + _('No matches were found for %s') % ('

' + prepare_string_for_xml(search['find']) + '
') return error_dialog(gui_parent, _('Not found'), msg, show=True) + def find_text_in_chunks(pat, chunks): text = ''.join(x[0] for x in chunks) m = pat.search(text) diff --git a/src/calibre/gui2/tweak_book/toc.py b/src/calibre/gui2/tweak_book/toc.py index 5b8cbfae91..108f5fd3be 100644 --- a/src/calibre/gui2/tweak_book/toc.py +++ b/src/calibre/gui2/tweak_book/toc.py @@ -17,6 +17,7 @@ from calibre.gui2 import error_dialog from calibre.gui2.toc.main import TOCView, ItemEdit from calibre.gui2.tweak_book import current_container, TOP, actions, tprefs + class TOCEditor(QDialog): explode_done = pyqtSignal(object) @@ -102,12 +103,14 @@ class TOCEditor(QDialog): DEST_ROLE = Qt.UserRole FRAG_ROLE = DEST_ROLE + 1 + class Delegate(QStyledItemDelegate): def sizeHint(self, *args): ans = QStyledItemDelegate.sizeHint(self, *args) return ans + QSize(0, 10) + class TOCViewer(QWidget): navigate_requested = pyqtSignal(object, object) diff --git a/src/calibre/gui2/tweak_book/ui.py b/src/calibre/gui2/tweak_book/ui.py index 88743b987e..74f03c251c 100644 --- a/src/calibre/gui2/tweak_book/ui.py +++ b/src/calibre/gui2/tweak_book/ui.py @@ -48,9 +48,11 @@ from calibre.gui2.tweak_book.editor.insert_resource import InsertImage from calibre.utils.icu import character_name, sort_key from calibre.utils.localization import localize_user_manual_link + def open_donate(): open_url(QUrl('https://calibre-ebook.com/donate')) + class Central(QStackedWidget): # {{{ ' The central widget, hosts the editors ' @@ -198,6 +200,7 @@ class Central(QStackedWidget): # {{{ return True # }}} + class CursorPositionWidget(QWidget): # {{{ def __init__(self, parent): @@ -227,6 +230,7 @@ class CursorPositionWidget(QWidget): # {{{ self.la.setText(text) # }}} + class Main(MainWindow): APP_NAME = _('Edit Book') @@ -322,6 +326,7 @@ class Main(MainWindow): sid, unicode(ac.text()).replace('&', ''), default_keys=keys, description=description, action=ac, group=group) self.addAction(ac) return ac + def treg(icon, text, target, sid, keys, description): return reg(icon, text, target, sid, keys, description, toolbar_allowed=icon is not None) @@ -430,6 +435,7 @@ class Main(MainWindow): # Search actions group = _('Search') self.action_find = treg('search.png', _('&Find/Replace'), self.boss.show_find, 'find-replace', ('Ctrl+F',), _('Show the Find/Replace panel')) + def sreg(name, text, action, overrides={}, keys=(), description=None, icon=None): return reg(icon, text, partial(self.boss.search_action_triggered, action, overrides), name, keys, description or text.replace('&', '')) self.action_find_next = sreg('find-next', _('Find &Next'), @@ -635,6 +641,7 @@ class Main(MainWindow): def populate_toolbars(self, animate=False): self.global_bar.clear(), self.tools_bar.clear(), self.plugins_bar.clear() + def add(bar, ac): if ac is None: bar.addSeparator() diff --git a/src/calibre/gui2/tweak_book/undo.py b/src/calibre/gui2/tweak_book/undo.py index 579c276993..48e8770707 100644 --- a/src/calibre/gui2/tweak_book/undo.py +++ b/src/calibre/gui2/tweak_book/undo.py @@ -18,6 +18,7 @@ ROOT = QModelIndex() MAX_SAVEPOINTS = 100 + def cleanup(containers): for container in containers: try: @@ -25,6 +26,7 @@ def cleanup(containers): except: pass + class State(object): def __init__(self, container): @@ -32,6 +34,7 @@ class State(object): self.message = None self.rewind_message = None + class GlobalUndoHistory(QAbstractListModel): def __init__(self, parent=None): @@ -173,6 +176,7 @@ class GlobalUndoHistory(QAbstractListModel): for state in self.states: state.container.path_to_ebook = path + class SpacedDelegate(QStyledItemDelegate): def sizeHint(self, *args): @@ -180,6 +184,7 @@ class SpacedDelegate(QStyledItemDelegate): ans.setHeight(ans.height() + 4) return ans + class CheckpointView(QWidget): revert_requested = pyqtSignal(object) diff --git a/src/calibre/gui2/tweak_book/widgets.py b/src/calibre/gui2/tweak_book/widgets.py index 6df74312c7..6725e7ae40 100644 --- a/src/calibre/gui2/tweak_book/widgets.py +++ b/src/calibre/gui2/tweak_book/widgets.py @@ -31,6 +31,7 @@ from calibre.gui2.complete2 import EditWithComplete ROOT = QModelIndex() PARAGRAPH_SEPARATOR = '\u2029' + class BusyCursor(object): def __enter__(self): @@ -39,11 +40,13 @@ class BusyCursor(object): def __exit__(self, *args): QApplication.restoreOverrideCursor() + class Dialog(BaseDialog): def __init__(self, title, name, parent=None): BaseDialog.__init__(self, title, name, parent=parent, prefs=tprefs) + class InsertTag(Dialog): # {{{ def __init__(self, parent=None): @@ -77,6 +80,7 @@ class InsertTag(Dialog): # {{{ # }}} + class RationalizeFolders(Dialog): # {{{ TYPE_MAP = ( @@ -136,6 +140,7 @@ class RationalizeFolders(Dialog): # {{{ return Dialog.accept(self) # }}} + class MultiSplit(Dialog): # {{{ def __init__(self, parent=None): @@ -170,6 +175,7 @@ class MultiSplit(Dialog): # {{{ # }}} + class ImportForeign(Dialog): # {{{ def __init__(self, parent=None): @@ -252,6 +258,7 @@ class ImportForeign(Dialog): # {{{ # Quick Open {{{ + def make_highlighted_text(emph, text, positions): positions = sorted(set(positions) - {-1}) if positions: @@ -266,6 +273,7 @@ def make_highlighted_text(emph, text, positions): return ''.join(parts) return text + class Results(QWidget): EMPH = "color:magenta; font-weight:bold" @@ -396,6 +404,7 @@ class Results(QWidget): except IndexError: pass + class QuickOpen(Dialog): def __init__(self, items, parent=None): @@ -470,6 +479,7 @@ class QuickOpen(Dialog): # Filterable names list {{{ + class NamesDelegate(QStyledItemDelegate): def sizeHint(self, option, index): @@ -509,6 +519,7 @@ class NamesDelegate(QStyledItemDelegate): doc.drawContents(painter) painter.restore() + class NamesModel(QAbstractListModel): filtered = pyqtSignal(object) @@ -553,6 +564,7 @@ class NamesModel(QAbstractListModel): except IndexError: pass + def create_filterable_names_list(names, filter_text=None, parent=None, model=NamesModel): nl = QListView(parent) nl.m = m = model(names, parent=nl) @@ -570,6 +582,7 @@ def create_filterable_names_list(names, filter_text=None, parent=None, model=Nam # Insert Link {{{ + class AnchorsModel(QAbstractListModel): filtered = pyqtSignal(object) @@ -602,6 +615,7 @@ class AnchorsModel(QAbstractListModel): self.endResetModel() self.filtered.emit(not bool(query)) + class InsertLink(Dialog): def __init__(self, container, source_name, initial_text=None, parent=None): @@ -718,6 +732,7 @@ class InsertLink(Dialog): # Insert Semantics {{{ + class InsertSemantics(Dialog): def __init__(self, container, parent=None): @@ -910,6 +925,7 @@ class InsertSemantics(Dialog): # }}} + class FilterCSS(Dialog): # {{{ def __init__(self, current_name=None, parent=None): @@ -981,6 +997,7 @@ class FilterCSS(Dialog): # {{{ # Add Cover {{{ + class CoverView(QWidget): def __init__(self, parent=None): @@ -1018,6 +1035,7 @@ class CoverView(QWidget): def sizeHint(self): return QSize(300, 400) + class AddCover(Dialog): import_requested = pyqtSignal(object, object) @@ -1116,6 +1134,7 @@ class AddCover(Dialog): # }}} + class PlainTextEdit(QPlainTextEdit): # {{{ ''' A class that overrides some methods from QPlainTextEdit to fix handling diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 94af9991c5..c9dd98deea 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -52,6 +52,7 @@ from calibre.gui2.dbus_export.widgets import factory from calibre.gui2.open_with import register_keyboard_shortcuts from calibre.library import current_library_name + class Listener(Thread): # {{{ def __init__(self, listener): @@ -85,9 +86,11 @@ class Listener(Thread): # {{{ _gui = None + def get_gui(): return _gui + def add_quick_start_guide(library_view, refresh_cover_browser=None): from calibre.ebooks.metadata.meta import get_metadata from calibre.ebooks.covers import calibre_cover2 @@ -922,6 +925,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ # We cannot restore the original excepthook as that causes PyQt to # call abort() on unhandled exceptions import traceback + def eh(t, v, tb): try: traceback.print_exception(t, v, tb, file=sys.stderr) diff --git a/src/calibre/gui2/update.py b/src/calibre/gui2/update.py index 04d2319b32..a8f221473f 100644 --- a/src/calibre/gui2/update.py +++ b/src/calibre/gui2/update.py @@ -20,6 +20,7 @@ URL = 'https://code.calibre-ebook.com/latest' # URL = 'http://localhost:8000/latest' NO_CALIBRE_UPDATE = (0, 0, 0) + def get_download_url(): which = ('portable' if isportable else 'windows' if iswindows else 'osx' if isosx else 'linux') @@ -27,6 +28,7 @@ def get_download_url(): which += '64' return 'https://calibre-ebook.com/download_' + which + def get_newest_version(): try: icon_theme_name = json.loads(I('icon-theme.json', data=True))['name'] @@ -58,10 +60,12 @@ def get_newest_version(): ans = tuple(map(int, (m.group(1), m.group(2), m.group(3)))) return ans + class Signal(QObject): update_found = pyqtSignal(object, object) + class CheckForUpdates(Thread): INTERVAL = 24*60*60 # seconds @@ -95,6 +99,7 @@ class CheckForUpdates(Thread): def shutdown(self): self.shutdown_event.set() + class UpdateNotification(QDialog): def __init__(self, calibre_version, plugin_updates, parent=None): @@ -154,6 +159,7 @@ class UpdateNotification(QDialog): QDialog.accept(self) + class UpdateMixin(object): def __init__(self, *args, **kw): diff --git a/src/calibre/gui2/viewer/bookmarkmanager.py b/src/calibre/gui2/viewer/bookmarkmanager.py index 24ed6a0ec1..b2ea37f347 100644 --- a/src/calibre/gui2/viewer/bookmarkmanager.py +++ b/src/calibre/gui2/viewer/bookmarkmanager.py @@ -15,6 +15,7 @@ from PyQt5.Qt import ( from calibre.gui2 import choose_save_file, choose_files from calibre.utils.icu import sort_key + class BookmarksList(QListWidget): changed = pyqtSignal() @@ -174,6 +175,7 @@ class BookmarkManager(QWidget): def sort_by_pos(self): from calibre.ebooks.epub.cfi.parse import cfi_sort_key + def pos_key(b): if b.get('type', None) == 'cfi': return b['spine'], cfi_sort_key(b['pos']) diff --git a/src/calibre/gui2/viewer/config.py b/src/calibre/gui2/viewer/config.py index b9ce3079c0..f917aac3b9 100644 --- a/src/calibre/gui2/viewer/config.py +++ b/src/calibre/gui2/viewer/config.py @@ -23,6 +23,7 @@ from calibre.gui2.languages import LanguagesEdit from calibre.gui2.shortcuts import ShortcutConfig from calibre.gui2.viewer.config_ui import Ui_Dialog + def config(defaults=None): desc = _('Options to customize the ebook viewer') if defaults is None: @@ -116,9 +117,11 @@ def config(defaults=None): return c + def load_themes(): return JSONConfig('viewer_themes') + class ConfigDialog(QDialog, Ui_Dialog): def __init__(self, shortcuts, parent=None): @@ -145,6 +148,7 @@ class ConfigDialog(QDialog, Ui_Dialog): 'el_monoton': get_language('el').partition(';')[0] + _(' monotone'), 'el_polyton':get_language('el').partition(';')[0] + _(' polytone'), 'sr_cyrl': get_language('sr') + _(' cyrillic'), 'sr_latn': get_language('sr') + _(' latin'), } + def gl(pat): return lang_pats.get(pat, get_language(pat)) names = list(map(gl, pats)) @@ -235,6 +239,7 @@ class ConfigDialog(QDialog, Ui_Dialog): def word_lookups(self): def fget(self): return dict(self.dictionary_list.item(i).data(Qt.UserRole) for i in range(self.dictionary_list.count())) + def fset(self, wl): self.dictionary_list.clear() for langcode, url in sorted(wl.iteritems(), key=lambda (lc, url):sort_key(calibre_langcode_to_name(lc))): diff --git a/src/calibre/gui2/viewer/documentview.py b/src/calibre/gui2/viewer/documentview.py index 7dcce73acb..d96c1166ca 100644 --- a/src/calibre/gui2/viewer/documentview.py +++ b/src/calibre/gui2/viewer/documentview.py @@ -34,6 +34,7 @@ from calibre.ebooks.oeb.display.webview import load_html from calibre.constants import isxp, iswindows, DEBUG, __version__ # }}} + def apply_settings(settings, opts): settings.setFontSize(QWebSettings.DefaultFontSize, opts.default_font_size) settings.setFontSize(QWebSettings.DefaultFixedFontSize, opts.mono_font_size) @@ -45,6 +46,7 @@ def apply_settings(settings, opts): settings.setFontFamily(QWebSettings.FixedFont, opts.mono_family) settings.setAttribute(QWebSettings.ZoomTextOnly, True) + def apply_basic_settings(settings): # Security settings.setAttribute(QWebSettings.JavaEnabled, False) @@ -475,6 +477,7 @@ class Document(QWebPage): # {{{ return abs(float(self.ypos)/(self.height-self.window_height)) except ZeroDivisionError: return 0. + def fset(self, val): if self.in_paged_mode and self.loaded_javascript: self.javascript('paged_display.scroll_to_pos(%f)'%val) @@ -488,10 +491,12 @@ class Document(QWebPage): # {{{ @dynamic_property def page_number(self): ' The page number is the number of the page at the left most edge of the screen (starting from 0) ' + def fget(self): if self.in_paged_mode: return self.javascript( 'ans = 0; if (window.paged_display) ans = window.paged_display.column_boundaries()[0]; ans;', typ='int') + def fset(self, val): if self.in_paged_mode and self.loaded_javascript: self.javascript('if (window.paged_display) window.paged_display.scroll_to_column(%d)' % int(val)) @@ -544,6 +549,7 @@ class Document(QWebPage): # {{{ # }}} + class DocumentView(QWebView): # {{{ magnification_changed = pyqtSignal(object) @@ -885,6 +891,7 @@ class DocumentView(QWebView): # {{{ def scroll_fraction(self): def fget(self): return self.document.scroll_fraction + def fset(self, val): self.document.scroll_fraction = float(val) return property(fget=fget, fset=fset) @@ -901,6 +908,7 @@ class DocumentView(QWebView): # {{{ def current_language(self): def fget(self): return self.document.current_language + def fset(self, val): self.document.current_language = val return property(fget=fget, fset=fset) @@ -1200,6 +1208,7 @@ class DocumentView(QWebView): # {{{ def multiplier(self): def fget(self): return self.zoomFactor() + def fset(self, val): oval = self.zoomFactor() self.setZoomFactor(val) diff --git a/src/calibre/gui2/viewer/flip.py b/src/calibre/gui2/viewer/flip.py index d434e75f2e..185c4d6d4a 100644 --- a/src/calibre/gui2/viewer/flip.py +++ b/src/calibre/gui2/viewer/flip.py @@ -8,6 +8,7 @@ __docformat__ = 'restructuredtext en' from PyQt5.Qt import QWidget, QPainter, QPropertyAnimation, QEasingCurve, \ QRect, QPixmap, Qt, pyqtProperty + class SlideFlip(QWidget): # API {{{ diff --git a/src/calibre/gui2/viewer/footnote.py b/src/calibre/gui2/viewer/footnote.py index 0f484200e2..26d8f788cc 100644 --- a/src/calibre/gui2/viewer/footnote.py +++ b/src/calibre/gui2/viewer/footnote.py @@ -19,6 +19,7 @@ from calibre import prints from calibre.constants import DEBUG from calibre.ebooks.oeb.display.webview import load_html + class FootnotesPage(QWebPage): def __init__(self, parent): diff --git a/src/calibre/gui2/viewer/gestures.py b/src/calibre/gui2/viewer/gestures.py index e2dc963a06..a2aa9eb66c 100644 --- a/src/calibre/gui2/viewer/gestures.py +++ b/src/calibre/gui2/viewer/gestures.py @@ -29,6 +29,7 @@ Tap, TapAndHold, Pinch, Swipe, SwipeAndHold = 'Tap', 'TapAndHold', 'Pinch', 'Swi Left, Right, Up, Down = 'Left', 'Right', 'Up', 'Down' In, Out = 'In', 'Out' + class Help(QDialog): # {{{ def __init__(self, parent=None): @@ -80,6 +81,7 @@ class Help(QDialog): # {{{ self.resize(600, 500) # }}} + class TouchPoint(object): def __init__(self, tp): @@ -115,6 +117,7 @@ class TouchPoint(object): y_movement = self.current_screen_position.y() - self.previous_screen_position.y() return (x_movement, y_movement) + def get_pinch(p1, p2): starts = [p1.start_screen_position, p2.start_screen_position] ends = [p1.current_screen_position, p2.current_screen_position] @@ -128,6 +131,7 @@ def get_pinch(p1, p2): return None return In if start_length > end_length else Out + class State(QObject): tapped = pyqtSignal(object) diff --git a/src/calibre/gui2/viewer/image_popup.py b/src/calibre/gui2/viewer/image_popup.py index 713519e6ac..a50cd68b1d 100644 --- a/src/calibre/gui2/viewer/image_popup.py +++ b/src/calibre/gui2/viewer/image_popup.py @@ -13,6 +13,7 @@ from PyQt5.Qt import (QDialog, QPixmap, QUrl, QScrollArea, QLabel, QSizePolicy, from calibre.gui2 import choose_save_file, gprefs, NO_URL_FORMATTING + class ImageView(QDialog): def __init__(self, parent, current_img, current_url, geom_name='viewer_image_popup_geometry'): @@ -122,6 +123,7 @@ class ImageView(QDialog): event.accept() (self.zoom_out if d < 0 else self.zoom_in)() + class ImagePopup(object): def __init__(self, parent): diff --git a/src/calibre/gui2/viewer/inspector.py b/src/calibre/gui2/viewer/inspector.py index c0c293d49f..1f28cd603e 100644 --- a/src/calibre/gui2/viewer/inspector.py +++ b/src/calibre/gui2/viewer/inspector.py @@ -11,6 +11,7 @@ from PyQt5.QtWebKitWidgets import QWebInspector from calibre.gui2 import gprefs + class WebInspector(QDialog): def __init__(self, parent, page): diff --git a/src/calibre/gui2/viewer/javascript.py b/src/calibre/gui2/viewer/javascript.py index 491ad53fa9..29126bdd97 100644 --- a/src/calibre/gui2/viewer/javascript.py +++ b/src/calibre/gui2/viewer/javascript.py @@ -13,6 +13,7 @@ import calibre from calibre.utils.localization import lang_as_iso639_1 from calibre.utils.resources import compiled_coffeescript + class JavaScriptLoader(object): JS = {x:('viewer/%s.js'%x if y is None else y) for x, y in { diff --git a/src/calibre/gui2/viewer/main.py b/src/calibre/gui2/viewer/main.py index 01f12249ba..bedd840b4d 100644 --- a/src/calibre/gui2/viewer/main.py +++ b/src/calibre/gui2/viewer/main.py @@ -35,6 +35,7 @@ dprefs = JSONConfig('viewer_dictionaries') dprefs.defaults['word_lookups'] = {} singleinstance_name = 'calibre_viewer' + class ResizeEvent(object): INTERVAL = 20 # mins @@ -67,6 +68,7 @@ class ResizeEvent(object): return False return True + class Worker(Thread): def run(self): @@ -86,12 +88,14 @@ class Worker(Thread): self.exception = err self.traceback = traceback.format_exc() + class RecentAction(QAction): def __init__(self, path, parent): self.path = path QAction.__init__(self, os.path.basename(path), parent) + def default_lookup_website(lang): if lang == 'und': lang = get_lang() @@ -102,12 +106,14 @@ def default_lookup_website(lang): prefix = 'http://%s.wiktionary.org/wiki/' % lang return prefix + '{word}' + def lookup_website(lang): if lang == 'und': lang = get_lang() wm = dprefs['word_lookups'] return wm.get(lang, default_lookup_website(lang)) + def listen(self): while True: try: @@ -121,6 +127,7 @@ def listen(self): prints('Failed to read message from other instance with error: %s' % as_unicode(e)) self.listener = None + class EbookViewer(MainWindow): STATE_VERSION = 2 @@ -201,6 +208,7 @@ class EbookViewer(MainWindow): self.search.focus_to_library.connect(lambda: self.view.setFocus(Qt.OtherFocusReason)) self.toc.pressed[QModelIndex].connect(self.toc_clicked) self.toc.searched.connect(partial(self.toc_clicked, force=True)) + def toggle_toc(ev): try: key = self.view.shortcuts.get_match(ev) @@ -1115,6 +1123,7 @@ class EbookViewer(MainWindow): def show_footnote_view(self): self.footnotes_dock.show() + def config(defaults=None): desc = _('Options to control the ebook viewer') if defaults is None: @@ -1140,6 +1149,7 @@ def config(defaults=None): return c + def option_parser(): c = config() parser = c.option_parser(usage=_('''\ @@ -1150,6 +1160,7 @@ View an ebook. setup_gui_option_parser(parser) return parser + def create_listener(): if islinux: from calibre.utils.ipc.server import LinuxListener as Listener @@ -1184,6 +1195,7 @@ def ensure_single_instance(args, open_at): listener = create_listener() return listener + class EventAccumulator(QObject): got_file = pyqtSignal(object) @@ -1203,6 +1215,7 @@ class EventAccumulator(QObject): self.got_file.emit(self.events[-1]) self.events = [] + def main(args=sys.argv): # Ensure viewer can continue to function if GUI is closed os.environ.pop('CALIBRE_WORKER_TEMP_DIR', None) diff --git a/src/calibre/gui2/viewer/position.py b/src/calibre/gui2/viewer/position.py index d97348fc28..8e02683805 100644 --- a/src/calibre/gui2/viewer/position.py +++ b/src/calibre/gui2/viewer/position.py @@ -13,6 +13,7 @@ from PyQt5.Qt import QApplication, QEventLoop from calibre.constants import DEBUG + class PagePosition(object): def __init__(self, document): diff --git a/src/calibre/gui2/viewer/printing.py b/src/calibre/gui2/viewer/printing.py index 0675e8cec3..3b09885783 100644 --- a/src/calibre/gui2/viewer/printing.py +++ b/src/calibre/gui2/viewer/printing.py @@ -23,6 +23,7 @@ from calibre.utils.icu import numeric_sort_key from calibre.utils.ipc.simple_worker import start_pipe_worker from calibre.utils.filenames import expanduser + class PrintDialog(Dialog): OUTPUT_NAME = 'print-to-pdf-choose-file' @@ -128,6 +129,7 @@ class PrintDialog(Dialog): self.save_used_values() return Dialog.accept(self) + class DoPrint(Thread): daemon = True @@ -154,6 +156,7 @@ class DoPrint(Thread): import traceback self.tb = traceback.format_exc() + def do_print(): data = cPickle.loads(sys.stdin.read()) args = ['ebook-convert', data['input'], data['output'], '--override-profile-size', '--paper-size', data['paper_size'], '--pdf-add-toc', @@ -165,6 +168,7 @@ def do_print(): from calibre.ebooks.conversion.cli import main main(args) + class Printing(QProgressDialog): def __init__(self, thread, show_file, parent=None): @@ -200,6 +204,7 @@ class Printing(QProgressDialog): self.timer.stop() self.reject() + def print_book(path_to_book, parent=None, book_title=None): book_title = book_title or os.path.splitext(os.path.basename(path_to_book))[0] d = PrintDialog(book_title, parent) diff --git a/src/calibre/gui2/viewer/table_popup.py b/src/calibre/gui2/viewer/table_popup.py index 25d9692d3c..566bbc1608 100644 --- a/src/calibre/gui2/viewer/table_popup.py +++ b/src/calibre/gui2/viewer/table_popup.py @@ -13,6 +13,7 @@ from PyQt5.QtWebKitWidgets import QWebView from calibre.gui2 import gprefs, error_dialog + class TableView(QDialog): def __init__(self, parent, font_magnification_step): @@ -60,6 +61,7 @@ class TableView(QDialog): gprefs['viewer_table_popup_geometry'] = bytearray(self.saveGeometry()) return QDialog.done(self, e) + class TablePopup(object): def __init__(self, parent): diff --git a/src/calibre/gui2/viewer/toc.py b/src/calibre/gui2/viewer/toc.py index e994aefd45..5202a27a7f 100644 --- a/src/calibre/gui2/viewer/toc.py +++ b/src/calibre/gui2/viewer/toc.py @@ -20,6 +20,7 @@ from calibre.gui2 import error_dialog from calibre.gui2.search_box import SearchBox2 from calibre.utils.icu import primary_contains + class Delegate(QStyledItemDelegate): def helpEvent(self, ev, view, option, index): @@ -35,6 +36,7 @@ class Delegate(QStyledItemDelegate): return True return QStyledItemDelegate.helpEvent(self, ev, view, option, index) + class TOCView(QTreeView): searched = pyqtSignal(object) @@ -109,6 +111,7 @@ class TOCView(QTreeView): m = self.model() QApplication.clipboard().setText(getattr(m, 'as_plain_text', '')) + class TOCSearch(QWidget): def __init__(self, toc_view, parent=None): @@ -307,6 +310,7 @@ class TOCItem(QStandardItem): def __str__(self): return repr(self) + class TOC(QStandardItemModel): def __init__(self, spine, toc=None): diff --git a/src/calibre/gui2/viewer/ui.py b/src/calibre/gui2/viewer/ui.py index ef0c99d8b2..5a2f0db0f1 100644 --- a/src/calibre/gui2/viewer/ui.py +++ b/src/calibre/gui2/viewer/ui.py @@ -23,6 +23,7 @@ from calibre.gui2.viewer.toc import TOCView, TOCSearch from calibre.gui2.viewer.footnote import FootnotesView from calibre.utils.localization import is_rtl + class DoubleSpinBox(QDoubleSpinBox): # {{{ value_changed = pyqtSignal(object, object) @@ -44,6 +45,7 @@ class DoubleSpinBox(QDoubleSpinBox): # {{{ self.value_changed.emit(self.value(), self.maximum()) # }}} + class Reference(QLineEdit): # {{{ goto = pyqtSignal(object) @@ -64,6 +66,7 @@ class Reference(QLineEdit): # {{{ self.goto.emit(text) # }}} + class Metadata(QWebView): # {{{ def __init__(self, parent): @@ -102,6 +105,7 @@ class Metadata(QWebView): # {{{ QWebView.paintEvent(self, ev) # }}} + class History(list): # {{{ def __init__(self, action_back=None, action_forward=None): @@ -170,6 +174,7 @@ class History(list): # {{{ def __str__(self): return 'History: Items=%s back_pos=%s insert_pos=%s forward_pos=%s' % (tuple(self), self.back_pos, self.insert_pos, self.forward_pos) + def test_history(): h = History() for i in xrange(4): @@ -181,6 +186,7 @@ def test_history(): assert h == [0, 9] # }}} + class ToolBar(QToolBar): # {{{ def contextMenuEvent(self, ev): @@ -194,6 +200,7 @@ class ToolBar(QToolBar): # {{{ sm() # }}} + class Main(MainWindow): def __init__(self, debug_javascript): diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py index 428a31fb29..7947b7b114 100644 --- a/src/calibre/gui2/widgets.py +++ b/src/calibre/gui2/widgets.py @@ -25,6 +25,7 @@ from calibre.utils.localization import localize_user_manual_link history = XMLConfig('history') + class ProgressIndicator(QWidget): # {{{ def __init__(self, *args): @@ -58,6 +59,7 @@ class ProgressIndicator(QWidget): # {{{ self.setVisible(False) # }}} + class FilenamePattern(QWidget, Ui_Form): # {{{ changed_signal = pyqtSignal() @@ -170,6 +172,7 @@ class FilenamePattern(QWidget, Ui_Form): # {{{ # }}} + class FormatList(QListWidget): # {{{ DROPABBLE_EXTENSIONS = BOOK_EXTENSIONS formats_dropped = pyqtSignal(object, object) @@ -210,6 +213,7 @@ class FormatList(QListWidget): # {{{ # }}} + class ImageDropMixin(object): # {{{ ''' Adds support for dropping images onto widgets and a context menu for @@ -290,6 +294,7 @@ class ImageDropMixin(object): # {{{ pixmap_to_data(pmap, format='PNG')) # }}} + class ImageView(QWidget, ImageDropMixin): # {{{ BORDER_WIDTH = 1 @@ -372,6 +377,7 @@ class ImageView(QWidget, ImageDropMixin): # {{{ p.end() # }}} + class CoverView(QGraphicsView, ImageDropMixin): # {{{ cover_changed = pyqtSignal(object) @@ -393,6 +399,8 @@ class CoverView(QGraphicsView, ImageDropMixin): # {{{ # }}} # BasicList {{{ + + class BasicListItem(QListWidgetItem): def __init__(self, text, user_data=None): @@ -404,6 +412,7 @@ class BasicListItem(QListWidgetItem): return self.text() == other.text() return False + class BasicList(QListWidget): def add_item(self, text, user_data=None, replace=False): @@ -427,6 +436,7 @@ class BasicList(QListWidget): yield self.item(i) # }}} + class LineEditECM(object): # {{{ ''' @@ -475,6 +485,7 @@ class LineEditECM(object): # {{{ # }}} + class EnLineEdit(LineEditECM, QLineEdit): # {{{ ''' @@ -492,6 +503,7 @@ class EnLineEdit(LineEditECM, QLineEdit): # {{{ # }}} + class ItemsCompleter(QCompleter): # {{{ ''' @@ -519,6 +531,7 @@ class ItemsCompleter(QCompleter): # {{{ # }}} + class CompleteLineEdit(EnLineEdit): # {{{ ''' @@ -578,6 +591,7 @@ class CompleteLineEdit(EnLineEdit): # {{{ # }}} + class EnComboBox(QComboBox): # {{{ ''' @@ -604,6 +618,7 @@ class EnComboBox(QComboBox): # {{{ # }}} + class CompleteComboBox(EnComboBox): # {{{ def __init__(self, *args): @@ -621,6 +636,7 @@ class CompleteComboBox(EnComboBox): # {{{ # }}} + class HistoryLineEdit(QComboBox): # {{{ lost_focus = pyqtSignal() @@ -674,12 +690,14 @@ class HistoryLineEdit(QComboBox): # {{{ # }}} + class ComboBoxWithHelp(QComboBox): # {{{ ''' A combobox where item 0 is help text. CurrentText will return '' for item 0. Be sure to always fetch the text with currentText. Don't use the signals that pass a string, because they will not correct the text. ''' + def __init__(self, parent=None): QComboBox.__init__(self, parent) self.currentIndexChanged[int].connect(self.index_changed) @@ -723,6 +741,7 @@ class ComboBoxWithHelp(QComboBox): # {{{ # }}} + class EncodingComboBox(QComboBox): # {{{ ''' A combobox that holds text encodings support @@ -748,6 +767,7 @@ class EncodingComboBox(QComboBox): # {{{ # }}} + class PythonHighlighter(QSyntaxHighlighter): # {{{ Rules = [] @@ -907,6 +927,8 @@ class PythonHighlighter(QSyntaxHighlighter): # {{{ # }}} # Splitter {{{ + + class SplitterHandle(QSplitterHandle): double_clicked = pyqtSignal(object) @@ -929,6 +951,7 @@ class SplitterHandle(QSplitterHandle): def mouseDoubleClickEvent(self, ev): self.double_clicked.emit(self) + class LayoutButton(QToolButton): def __init__(self, icon, text, splitter=None, parent=None, shortcut=None): @@ -964,6 +987,7 @@ class LayoutButton(QToolButton): else: self.set_state_to_hide() + class Splitter(QSplitter): state_changed = pyqtSignal(object) diff --git a/src/calibre/gui2/widgets2.py b/src/calibre/gui2/widgets2.py index e168388199..b08b37b68d 100644 --- a/src/calibre/gui2/widgets2.py +++ b/src/calibre/gui2/widgets2.py @@ -19,6 +19,7 @@ from calibre.gui2 import gprefs, rating_font from calibre.gui2.complete2 import LineEdit, EditWithComplete from calibre.gui2.widgets import history + class HistoryMixin(object): max_history_items = None @@ -59,16 +60,19 @@ class HistoryMixin(object): history.set(self.store_name, self.history) self.update_items_cache(self.history) + class HistoryLineEdit2(LineEdit, HistoryMixin): def __init__(self, parent=None, completer_widget=None, sort_func=lambda x:None): LineEdit.__init__(self, parent=parent, completer_widget=completer_widget, sort_func=sort_func) + class HistoryComboBox(EditWithComplete, HistoryMixin): def __init__(self, parent=None): EditWithComplete.__init__(self, parent, sort_func=lambda x:None) + class ColorButton(QPushButton): color_changed = pyqtSignal(object) @@ -84,6 +88,7 @@ class ColorButton(QPushButton): def color(self): def fget(self): return self._color + def fset(self, val): val = unicode(val or '') col = QColor(val) @@ -114,6 +119,7 @@ def access_key(k): return '\t' + QKeySequence(k).toString(QKeySequence.NativeText) return '' + def populate_standard_spinbox_context_menu(spinbox, menu, add_clear=False): m = menu le = spinbox.lineEdit() @@ -128,6 +134,7 @@ def populate_standard_spinbox_context_menu(spinbox, menu, add_clear=False): m.addAction(_('Step &down'), spinbox.stepDown) m.setAttribute(Qt.WA_DeleteOnClose) + class RightClickButton(QToolButton): def mousePressEvent(self, ev): @@ -137,6 +144,7 @@ class RightClickButton(QToolButton): return return QToolButton.mousePressEvent(self, ev) + class Dialog(QDialog): ''' @@ -204,6 +212,7 @@ class RatingModel(QAbstractListModel): if role == Qt.FontRole: return QApplication.instance().font() if index.row() == 0 else self.rating_font + class UndoCommand(QUndoCommand): def __init__(self, widget, val): @@ -220,6 +229,7 @@ class UndoCommand(QUndoCommand): w = self.widget() w.setCurrentIndex(self.redo_val) + class RatingEditor(QComboBox): def __init__(self, parent=None, is_half_star=False): diff --git a/src/calibre/gui2/win_file_dialogs.py b/src/calibre/gui2/win_file_dialogs.py index 11001c0552..bbbeea9d07 100644 --- a/src/calibre/gui2/win_file_dialogs.py +++ b/src/calibre/gui2/win_file_dialogs.py @@ -14,6 +14,7 @@ is64bit = sys.maxsize > (1 << 32) base = sys.extensions_location if hasattr(sys, 'new_app_layout') else os.path.dirname(sys.executable) HELPER = os.path.join(base, 'calibre-file-dialog.exe') + def is_ok(): return os.path.exists(HELPER) @@ -26,6 +27,7 @@ except ImportError: expanduser = os.path.expanduser dynamic = {} + def get_hwnd(widget=None): ewid = None if widget is not None: @@ -34,18 +36,22 @@ def get_hwnd(widget=None): return None return int(ewid) + def serialize_hwnd(hwnd): if hwnd is None: return b'' return struct.pack(b'=B4s' + (b'Q' if is64bit else b'I'), 4, b'HWND', int(hwnd)) + def serialize_secret(secret): return struct.pack(b'=B6s32s', 6, b'SECRET', secret) + def serialize_binary(key, val): key = key.encode('ascii') if not isinstance(key, bytes) else key return struct.pack(b'=B%ssB' % len(key), len(key), key, int(val)) + def serialize_string(key, val): key = key.encode('ascii') if not isinstance(key, bytes) else key val = type('')(val).encode('utf-8') @@ -53,9 +59,11 @@ def serialize_string(key, val): raise ValueError('%s is too long' % key) return struct.pack(b'=B%dsH%ds' % (len(key), len(val)), len(key), key, len(val), val) + def serialize_file_types(file_types): key = b"FILE_TYPES" buf = [struct.pack(b'=B%dsH' % len(key), len(key), key, len(file_types))] + def add(x): x = x.encode('utf-8').replace(b'\0', b'') buf.append(struct.pack(b'=H%ds' % len(x), len(x), x)) @@ -66,6 +74,7 @@ def serialize_file_types(file_types): add('; '.join('*.' + ext.lower() for ext in extensions)) return b''.join(buf) + class Helper(Thread): def __init__(self, process, data, callback): @@ -82,6 +91,7 @@ class Helper(Thread): self.rc = self.process.wait() self.callback() + class Loop(QEventLoop): dialog_closed = pyqtSignal() @@ -90,11 +100,13 @@ class Loop(QEventLoop): QEventLoop.__init__(self) self.dialog_closed.connect(self.exit, type=Qt.QueuedConnection) + def process_path(x): if isinstance(x, bytes): x = x.decode(filesystem_encoding) return os.path.abspath(expanduser(x)) + def select_initial_dir(q): while q: c = os.path.dirname(q) @@ -105,6 +117,7 @@ def select_initial_dir(q): q = c return expanduser('~') + def run_file_dialog( parent=None, title=None, initial_folder=None, filename=None, save_path=None, allow_multiple=False, only_dirs=False, confirm_overwrite=True, save_as=False, no_symlinks=False, @@ -161,6 +174,7 @@ def run_file_dialog( data, loop.dialog_closed.emit) h.start() loop.exec_(QEventLoop.ExcludeUserInputEvents) + def decode(x): x = x or b'' try: @@ -168,6 +182,7 @@ def run_file_dialog( except Exception: x = repr(x) return x + def get_errors(): return decode(h.stdoutdata) + ' ' + decode(h.stderrdata) from calibre import prints @@ -194,6 +209,7 @@ def run_file_dialog( ans = tuple((os.path.abspath(x.decode('utf-8')) for x in parts[1:])) return ans + def get_initial_folder(name, title, default_dir='~', no_save_dir=False): name = name or 'dialog_' + title if no_save_dir: @@ -204,6 +220,7 @@ def get_initial_folder(name, title, default_dir='~', no_save_dir=False): initial_folder = select_initial_dir(initial_folder) return name, initial_folder + def choose_dir(window, name, title, default_dir='~', no_save_dir=False): name, initial_folder = get_initial_folder(name, title, default_dir, no_save_dir) ans = run_file_dialog(window, title, only_dirs=True, initial_folder=initial_folder) @@ -213,6 +230,7 @@ def choose_dir(window, name, title, default_dir='~', no_save_dir=False): dynamic.set(name, ans) return ans + def choose_files(window, name, title, filters=(), all_files=True, select_only_single_file=False, default_dir=u'~'): name, initial_folder = get_initial_folder(name, title, default_dir) @@ -225,6 +243,7 @@ def choose_files(window, name, title, return ans return None + def choose_images(window, name, title, select_only_single_file=True, formats=None): if formats is None: from calibre.gui2.dnd import image_extensions @@ -232,6 +251,7 @@ def choose_images(window, name, title, select_only_single_file=True, formats=Non file_types = [(_('Images'), list(formats))] return choose_files(window, name, title, select_only_single_file=select_only_single_file, filters=file_types) + def choose_save_file(window, name, title, filters=[], all_files=True, initial_path=None, initial_filename=None): no_save_dir = False default_dir = '~' @@ -251,6 +271,7 @@ def choose_save_file(window, name, title, filters=[], all_files=True, initial_pa dynamic.set(name, ans) return ans + class PipeServer(Thread): def __init__(self, pipename): @@ -270,6 +291,7 @@ class PipeServer(Thread): def run(self): import win32pipe, win32file, winerror, win32api + def as_unicode(err): try: self.err_msg = type('')(err) @@ -304,6 +326,7 @@ class PipeServer(Thread): win32api.CloseHandle(self.pipe_handle) self.pipe_handle = None + def test(helper=HELPER): pipename = '\\\\.\\pipe\\%s' % uuid4() echo = '\U0001f431 Hello world!' diff --git a/src/calibre/gui2/wizard/__init__.py b/src/calibre/gui2/wizard/__init__.py index a75f8278d3..718e5e700c 100644 --- a/src/calibre/gui2/wizard/__init__.py +++ b/src/calibre/gui2/wizard/__init__.py @@ -31,6 +31,7 @@ if iswindows: # Devices {{{ + class Device(object): output_profile = 'generic_eink' @@ -63,12 +64,14 @@ class Device(object): recs['dont_grayscale'] = True save_defaults('comic_input', recs) + class Smartphone(Device): id = 'smartphone' name = 'Smartphone' supports_color = True + class Tablet(Device): id = 'tablet' @@ -76,6 +79,7 @@ class Tablet(Device): output_profile = 'tablet' supports_color = True + class Kindle(Device): output_profile = 'kindle' @@ -84,6 +88,7 @@ class Kindle(Device): manufacturer = 'Amazon' id = 'kindle' + class JetBook(Device): output_profile = 'jetbook5' @@ -92,6 +97,7 @@ class JetBook(Device): manufacturer = 'Ectaco' id = 'jetbook' + class JetBookMini(Device): output_profile = 'jetbook5' @@ -100,6 +106,7 @@ class JetBookMini(Device): manufacturer = 'Ectaco' id = 'jetbookmini' + class KindleDX(Kindle): output_profile = 'kindle_dx' @@ -107,22 +114,26 @@ class KindleDX(Kindle): name = 'Kindle DX' id = 'kindledx' + class KindleFire(KindleDX): name = 'Kindle Fire and Fire HD' id = 'kindle_fire' output_profile = 'kindle_fire' supports_color = True + class KindlePW(Kindle): name = 'Kindle PaperWhite' id = 'kindle_pw' output_profile = 'kindle_pw' + class KindleVoyage(Kindle): name = 'Kindle Voyage/Oasis' id = 'kindle_voyage' output_profile = 'kindle_voyage' + class Sony505(Device): output_profile = 'sony' @@ -131,6 +142,7 @@ class Sony505(Device): manufacturer = 'SONY' id = 'prs505' + class Kobo(Device): name = 'Kobo and Kobo Touch Readers' manufacturer = 'Kobo' @@ -138,11 +150,13 @@ class Kobo(Device): output_format = 'EPUB' id = 'kobo' + class KoboVox(Kobo): name = 'Kobo Vox, Aura and Glo families' output_profile = 'tablet' id = 'kobo_vox' + class Booq(Device): name = 'bq Classic' manufacturer = 'Booq' @@ -150,6 +164,7 @@ class Booq(Device): output_format = 'EPUB' id = 'booq' + class TheBook(Device): name = 'The Book' manufacturer = 'Augen' @@ -157,55 +172,66 @@ class TheBook(Device): output_format = 'EPUB' id = 'thebook' + class Avant(Booq): name = 'bq Avant' + class AvantXL(Booq): name = 'bq Avant XL' output_profile = 'ipad' + class BooqPocketPlus(Booq): name = 'bq Pocket Plus' output_profile = 'sony300' + class BooqCervantes(Booq): name = 'bq Cervantes' + class Sony300(Sony505): name = 'SONY Reader Pocket Edition' id = 'prs300' output_profile = 'sony300' + class Sony900(Sony505): name = 'SONY Reader Daily Edition' id = 'prs900' output_profile = 'sony900' + class SonyT3(Sony505): name = 'SONY Reader T3' id = 'prst3' output_profile = 'sonyt3' + class Nook(Sony505): id = 'nook' name = 'Nook and Nook Simple Reader' manufacturer = 'Barnes & Noble' output_profile = 'nook' + class NookColor(Nook): id = 'nook_color' name = 'Nook Color' output_profile = 'nook_color' supports_color = True + class NookTablet(NookColor): id = 'nook_tablet' name = 'Nook Tablet/HD' output_profile = 'nook_hd_plus' + class CybookG3(Device): name = 'Cybook Gen 3' @@ -214,6 +240,7 @@ class CybookG3(Device): manufacturer = 'Bookeen' id = 'cybookg3' + class CybookOpus(CybookG3): name = 'Cybook Opus' @@ -221,22 +248,26 @@ class CybookOpus(CybookG3): output_profile = 'cybook_opus' id = 'cybook_opus' + class CybookOrizon(CybookOpus): name = 'Cybook Orizon' id = 'cybook_orizon' + class CybookOdyssey(CybookOpus): name = 'Cybook Odyssey' id = 'cybook_odyssey' + class CybookMuse(CybookOpus): name = 'Cybook Muse' id = 'cybook_muse' output_profile = 'tablet' + class PocketBook360(CybookOpus): manufacturer = 'PocketBook' @@ -244,6 +275,7 @@ class PocketBook360(CybookOpus): id = 'pocketbook360' output_profile = 'cybook_opus' + class PocketBook(CybookG3): manufacturer = 'PocketBook' @@ -251,12 +283,14 @@ class PocketBook(CybookG3): id = 'pocketbook' output_profile = 'cybookg3' + class PocketBook900(PocketBook): name = 'PocketBook 900' id = 'pocketbook900' output_profile = 'pocketbook_900' + class PocketBookPro912(PocketBook): name = 'PocketBook Pro 912' @@ -273,6 +307,7 @@ class iPhone(Device): supports_color = True output_profile = 'ipad3' + class Android(Device): name = 'Android phone' @@ -289,12 +324,14 @@ class Android(Device): if hasattr(plugin, 'configure_for_generic_epub_app'): plugin.configure_for_generic_epub_app() + class AndroidTablet(Android): name = 'Android tablet' id = 'android_tablet' output_profile = 'tablet' + class AndroidPhoneWithKindle(Android): name = 'Android phone with Kindle reader' @@ -310,12 +347,14 @@ class AndroidPhoneWithKindle(Android): if hasattr(plugin, 'configure_for_kindle_app'): plugin.configure_for_kindle_app() + class AndroidTabletWithKindle(AndroidPhoneWithKindle): name = 'Android tablet with Kindle reader' id = 'android_tablet_with_kindle' output_profile = 'kindle_fire' + class HanlinV3(Device): name = 'Hanlin V3' @@ -324,30 +363,35 @@ class HanlinV3(Device): manufacturer = 'Jinke' id = 'hanlinv3' + class HanlinV5(HanlinV3): name = 'Hanlin V5' output_profile = 'hanlinv5' id = 'hanlinv5' + class BeBook(HanlinV3): name = 'BeBook' manufacturer = 'BeBook' id = 'bebook' + class BeBookMini(HanlinV5): name = 'BeBook Mini' manufacturer = 'BeBook' id = 'bebook_mini' + class EZReader(HanlinV3): name = 'EZReader' manufacturer = 'Astak' id = 'ezreader' + class EZReaderPP(HanlinV5): name = 'EZReader Pocket Pro' @@ -356,11 +400,13 @@ class EZReaderPP(HanlinV5): # }}} + def get_devices(): for x in globals().values(): if isinstance(x, type) and issubclass(x, Device): yield x + def get_manufacturers(): mans = set([]) for x in get_devices(): @@ -369,10 +415,12 @@ def get_manufacturers(): mans.remove(Device.manufacturer) return [Device.manufacturer] + sorted(mans) + def get_devices_of(manufacturer): ans = [d for d in get_devices() if d.manufacturer == manufacturer] return sorted(ans, cmp=lambda x,y:cmp(x.name, y.name)) + class ManufacturerModel(QAbstractListModel): def __init__(self): @@ -397,6 +445,7 @@ class ManufacturerModel(QAbstractListModel): if x == man: return self.index(i) + class DeviceModel(QAbstractListModel): def __init__(self, manufacturer): @@ -421,6 +470,7 @@ class DeviceModel(QAbstractListModel): if device is dev: return self.index(i) + class KindlePage(QWizardPage, KindleUI): ID = 3 @@ -443,6 +493,7 @@ class KindlePage(QWizardPage, KindleUI): accs = [x for x in accs if x[1]] if accs: self.to_address.setText(accs[0][0]) + def x(): t = unicode(self.to_address.text()) if t.strip(): @@ -472,6 +523,7 @@ class KindlePage(QWizardPage, KindleUI): if hasattr(self, 'send_email_widget'): self.send_email_widget.retranslateUi(self.send_email_widget) + class StanzaPage(QWizardPage, StanzaUI): ID = 5 @@ -579,6 +631,7 @@ class DevicePage(QWizardPage, DeviceUI): return StanzaPage.ID return FinishPage.ID + class LibraryPage(QWizardPage, LibraryUI): ID = 1 @@ -605,6 +658,7 @@ class LibraryPage(QWizardPage, LibraryUI): lang = get_lc_messages_path(lang) if lang else lang if lang is None or lang not in available_translations(): lang = 'en' + def get_esc_lang(l): if l == 'en': return 'English' @@ -746,6 +800,7 @@ class LibraryPage(QWizardPage, LibraryUI): def nextId(self): return DevicePage.ID + class FinishPage(QWizardPage, FinishUI): ID = 4 diff --git a/src/calibre/gui2/wizard/send_email.py b/src/calibre/gui2/wizard/send_email.py index 7511c7c6da..28ecca3bf2 100644 --- a/src/calibre/gui2/wizard/send_email.py +++ b/src/calibre/gui2/wizard/send_email.py @@ -21,6 +21,7 @@ from calibre.gui2.wizard.send_email_ui import Ui_Form from calibre.utils.smtp import config as smtp_prefs from calibre.gui2 import error_dialog, question_dialog + class TestEmail(QDialog): test_done = pyqtSignal(object) @@ -74,6 +75,7 @@ class TestEmail(QDialog): self.test_button.setEnabled(True) self.log.setPlainText(txt) + class RelaySetup(QDialog): def __init__(self, service, parent): diff --git a/src/calibre/gui_launch.py b/src/calibre/gui_launch.py index afe0f33910..ba6d2a3eed 100644 --- a/src/calibre/gui_launch.py +++ b/src/calibre/gui_launch.py @@ -16,6 +16,7 @@ import os, sys is_detached = False + def do_detach(fork=True, setsid=True, redirect=True): global is_detached if fork: @@ -29,11 +30,13 @@ def do_detach(fork=True, setsid=True, redirect=True): plugins['speedup'][0].detach(os.devnull) is_detached = True + def detach_gui(): from calibre.constants import islinux, isbsd, DEBUG if (islinux or isbsd) and not DEBUG and '--detach' in sys.argv: do_detach() + def init_dbus(): from calibre.constants import islinux, isbsd if islinux or isbsd: @@ -41,6 +44,7 @@ def init_dbus(): threads_init() DBusGMainLoop(set_as_default=True) + def register_with_default_programs(): from calibre.constants import iswindows if iswindows: @@ -49,12 +53,15 @@ def register_with_default_programs(): return Register(gprefs) else: class Dummy(object): + def __enter__(self): return self + def __exit__(self, *args): pass return Dummy() + def calibre(args=sys.argv): detach_gui() init_dbus() @@ -62,6 +69,7 @@ def calibre(args=sys.argv): from calibre.gui2.main import main main(args) + def ebook_viewer(args=sys.argv): detach_gui() init_dbus() @@ -69,12 +77,14 @@ def ebook_viewer(args=sys.argv): from calibre.gui2.viewer.main import main main(args) + def gui_ebook_edit(path=None, notify=None): ' For launching the editor from inside calibre ' init_dbus() from calibre.gui2.tweak_book.main import gui_main gui_main(path, notify) + def ebook_edit(args=sys.argv): detach_gui() init_dbus() @@ -82,6 +92,7 @@ def ebook_edit(args=sys.argv): from calibre.gui2.tweak_book.main import main main(args) + def option_parser(basename): if basename == 'calibre': from calibre.gui2.main import option_parser diff --git a/src/calibre/library/__init__.py b/src/calibre/library/__init__.py index cec9c3a4e2..2cef575283 100644 --- a/src/calibre/library/__init__.py +++ b/src/calibre/library/__init__.py @@ -2,6 +2,7 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' ''' Code to manage ebook library''' + def db(path=None, read_only=False): from calibre.db.legacy import LibraryDatabase from calibre.utils.config import prefs @@ -62,6 +63,7 @@ def generate_test_db(library_path, # {{{ print 'Time per record:', t/float(num_of_records) # }}} + def current_library_path(): from calibre.utils.config import prefs path = prefs['library_path'] @@ -71,6 +73,7 @@ def current_library_path(): path = path[:-1] return path + def current_library_name(): import posixpath path = current_library_path() diff --git a/src/calibre/library/add_to_library.py b/src/calibre/library/add_to_library.py index 61bd192576..3ec82673f1 100644 --- a/src/calibre/library/add_to_library.py +++ b/src/calibre/library/add_to_library.py @@ -10,6 +10,7 @@ from hashlib import sha1 from calibre.ebooks import BOOK_EXTENSIONS + def find_folders_under(root, db, add_root=True, # {{{ follow_links=False, cancel_callback=lambda : False): ''' @@ -47,6 +48,7 @@ def find_folders_under(root, db, add_root=True, # {{{ # }}} + class FormatCollection(object): # {{{ def __init__(self, parent_folder, formats): @@ -100,6 +102,7 @@ class FormatCollection(object): # {{{ # }}} + def books_in_folder(folder, one_per_folder, # {{{ cancel_callback=lambda : False): assert not isinstance(folder, unicode) @@ -145,6 +148,7 @@ def books_in_folder(folder, one_per_folder, # {{{ # }}} + def hash_merge_format_collections(collections, cancel_callback=lambda:False): ans = [] diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index 21c798699e..67b0c2bbdf 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -21,6 +21,7 @@ from calibre.ebooks.metadata import title_sort, author_to_author_sort from calibre.ebooks.metadata.opf2 import metadata_to_opf from calibre import prints + class MetadataBackup(Thread): # {{{ ''' Continuously backup changed metadata into OPF files @@ -122,6 +123,7 @@ class MetadataBackup(Thread): # {{{ # This is a global for performance pref_use_primary_find_in_search = False + def set_use_primary_find_in_search(toWhat): global pref_use_primary_find_in_search pref_use_primary_find_in_search = toWhat @@ -131,6 +133,7 @@ yes_vals = {y, c, 'true'} no_vals = {n, u, 'false'} del y, c, n, u + def force_to_bool(val): if isinstance(val, (str, unicode)): try: @@ -147,6 +150,7 @@ def force_to_bool(val): val = None return val + class CacheRow(list): # {{{ def __init__(self, db, composites, val, series_col, series_sort_col): @@ -196,11 +200,13 @@ class CacheRow(list): # {{{ # }}} + class ResultCache(SearchQueryParser): # {{{ ''' Stores sorted and filtered metadata in memory. ''' + def __init__(self, FIELD_MAP, field_metadata, db_prefs=None): self.FIELD_MAP = FIELD_MAP self.db_prefs = db_prefs @@ -1096,6 +1102,7 @@ class ResultCache(SearchQueryParser): # {{{ else: only_ids.sort(key=keyg) + class SortKey(object): def __init__(self, orders, values): @@ -1108,6 +1115,7 @@ class SortKey(object): return ans * ascending return 0 + class SortKeyGenerator(object): def __init__(self, fields, field_metadata, data, db_prefs): diff --git a/src/calibre/library/catalogs/__init__.py b/src/calibre/library/catalogs/__init__.py index 47688b9ee0..2e4623aaca 100644 --- a/src/calibre/library/catalogs/__init__.py +++ b/src/calibre/library/catalogs/__init__.py @@ -17,10 +17,15 @@ FIELDS = ['all', 'title', 'title_sort', 'author_sort', 'authors', 'comments', TEMPLATE_ALLOWED_FIELDS = ['author_sort', 'authors', 'id', 'isbn', 'pubdate', 'title_sort', 'publisher', 'series_index', 'series', 'tags', 'timestamp', 'title', 'uuid'] + class AuthorSortMismatchException(Exception): pass + + class EmptyCatalogException(Exception): pass + + class InvalidGenresSourceFieldException(Exception): pass diff --git a/src/calibre/library/catalogs/epub_mobi.py b/src/calibre/library/catalogs/epub_mobi.py index 5ea7157e1a..ac5c449520 100644 --- a/src/calibre/library/catalogs/epub_mobi.py +++ b/src/calibre/library/catalogs/epub_mobi.py @@ -20,6 +20,7 @@ from calibre.utils.localization import calibre_langcode_to_name, canonicalize_la Option = namedtuple('Option', 'option, default, dest, action, help') + class EPUB_MOBI(CatalogPlugin): 'ePub catalog generator' diff --git a/src/calibre/library/catalogs/epub_mobi_builder.py b/src/calibre/library/catalogs/epub_mobi_builder.py index c76edbda13..1e2ccb4b7d 100644 --- a/src/calibre/library/catalogs/epub_mobi_builder.py +++ b/src/calibre/library/catalogs/epub_mobi_builder.py @@ -25,6 +25,7 @@ from calibre.utils.icu import capitalize, collation_order, sort_key from calibre.utils.img import scale_image from calibre.utils.zipfile import ZipFile + class Formatter(TemplateFormatter): def get_value(self, key, args, kwargs): diff --git a/src/calibre/library/catalogs/utils.py b/src/calibre/library/catalogs/utils.py index 26905f5199..acb4fd0027 100644 --- a/src/calibre/library/catalogs/utils.py +++ b/src/calibre/library/catalogs/utils.py @@ -10,6 +10,7 @@ import re from calibre import prints from calibre.utils.logging import default_log as log + class NumberToText(object): # {{{ ''' Converts numbers to text diff --git a/src/calibre/library/cli.py b/src/calibre/library/cli.py index c70491fd02..8d7adbbf00 100644 --- a/src/calibre/library/cli.py +++ b/src/calibre/library/cli.py @@ -30,6 +30,8 @@ FIELDS = { } do_notify = True + + def send_message(msg=''): global do_notify if not do_notify: @@ -43,10 +45,12 @@ def send_message(msg=''): t.conn.send('refreshdb:'+msg) t.conn.close() + def write_dirtied(db): prints('Backing up metadata') db.dump_metadata() + def get_parser(usage): parser = OptionParser(usage) go = parser.add_option_group(_('GLOBAL OPTIONS')) @@ -60,6 +64,7 @@ def get_parser(usage): return parser + def get_db(dbpath, options): global do_notify if options.library_path is not None: @@ -72,6 +77,7 @@ def get_db(dbpath, options): do_notify = False return LibraryDatabase(dbpath) + def do_list(db, fields, afields, sort_by, ascending, search_text, line_width, separator, prefix, limit, for_machine=False): from calibre.utils.terminal import ColoredStream, geometry @@ -89,6 +95,7 @@ def do_list(db, fields, afields, sort_by, ascending, search_text, line_width, se pass fields = ['id'] + fields title_fields = fields + def field_name(f): ans = f if f[0] == '*': @@ -129,8 +136,10 @@ def do_list(db, fields, afields, sort_by, ascending, search_text, line_width, se else: record[f] = unicode(record[f]) record[f] = record[f].replace('\n', ' ') + def chr_width(x): return 1 + unicodedata.east_asian_width(x).startswith('W') + def str_width(x): return sum(map(chr_width, x)) @@ -176,6 +185,7 @@ def do_list(db, fields, afields, sort_by, ascending, search_text, line_width, se print >>o return o.getvalue() + def list_option_parser(db=None): fields = set(FIELDS) | {'id'} if db is not None: @@ -259,6 +269,7 @@ class DevNull(object): pass NULL = DevNull() + def do_add(db, paths, one_book_per_directory, recurse, add_duplicates, otitle, oauthors, oisbn, otags, oseries, oseries_index, ocover, oidentifiers, olanguages, compiled_rules): orig = sys.stdout @@ -360,6 +371,7 @@ def do_add(db, paths, one_book_per_directory, recurse, add_duplicates, otitle, finally: sys.stdout = orig + def add_option_parser(): parser = get_parser(_( '''\ @@ -394,6 +406,7 @@ the directory related options below. g = OptionGroup(parser, _('ADDING FROM DIRECTORIES'), _( 'Options to control the adding of books from directories. By default only files that have extensions of known e-book file types are added.')) + def filter_pat(option, opt, value, parser, action): try: getattr(parser.values, option.dest).append(compile_rule({'match_type':'glob', 'query':value, 'action':action})) @@ -404,6 +417,7 @@ the directory related options below. help=_('Assume that each directory has only a single logical book and that all files in it are different e-book formats of that book')) g.add_option('-r', '--recurse', action='store_true', default=False, help=_('Process directories recursively')) + def fadd(opt, action, help): g.add_option( opt, action='callback', type='string', nargs=1, default=[], @@ -421,6 +435,7 @@ the directory related options below. return parser + def do_add_empty(db, title, authors, isbn, tags, series, series_index, cover, identifiers, languages): from calibre.ebooks.metadata import MetaInformation mi = MetaInformation(None) @@ -445,6 +460,7 @@ def do_add_empty(db, title, authors, isbn, tags, series, series_index, cover, id prints(_('Added book ids: %s')%book_id) send_message() + def command_add(args, dbpath): from calibre.ebooks.metadata import string_to_authors parser = add_option_parser() @@ -469,6 +485,7 @@ def command_add(args, dbpath): tags, opts.series, opts.series_index, opts.cover, identifiers, lcodes, opts.filters) return 0 + def do_remove(db, ids): book_ids = set() for x in ids: @@ -483,6 +500,7 @@ def do_remove(db, ids): from calibre.db.delete_service import delete_service delete_service().wait() + def remove_option_parser(): return get_parser(_( '''\ @@ -494,6 +512,7 @@ list of id numbers (you can get id numbers by using the search command). For exa included). ''')) + def command_remove(args, dbpath): parser = remove_option_parser() opts, args = parser.parse_args(sys.argv[:1] + args) @@ -515,6 +534,7 @@ def command_remove(args, dbpath): return 0 + def do_add_format(db, id, fmt, path, opts): done = db.add_format_with_hooks(id, fmt.upper(), path, index_is_id=True, replace=opts.replace) @@ -523,6 +543,7 @@ def do_add_format(db, id, fmt, path, opts): else: send_message() + def add_format_option_parser(): parser = get_parser(_( '''\ @@ -552,12 +573,14 @@ def command_add_format(args, dbpath): do_add_format(get_db(dbpath, opts), id, fmt[1:], path, opts) return 0 + def do_remove_format(db, id, fmt): db.remove_format(id, fmt, index_is_id=True) send_message() from calibre.db.delete_service import delete_service delete_service().wait() + def remove_format_option_parser(): return get_parser(_( ''' @@ -583,6 +606,7 @@ def command_remove_format(args, dbpath): do_remove_format(get_db(dbpath, opts), id, fmt) return 0 + def do_show_metadata(db, id, as_opf): if not db.has_id(id): raise ValueError('Id #%d is not present in database.'%id) @@ -593,6 +617,7 @@ def do_show_metadata(db, id, as_opf): else: prints(unicode(mi)) + def show_metadata_option_parser(): parser = get_parser(_( ''' @@ -605,6 +630,7 @@ id is an id number from the search command. help=_('Print metadata in OPF form (XML)')) return parser + def command_show_metadata(args, dbpath): parser = show_metadata_option_parser() opts, args = parser.parse_args(sys.argv[1:]+args) @@ -617,10 +643,12 @@ def command_show_metadata(args, dbpath): do_show_metadata(get_db(dbpath, opts), id, opts.as_opf) return 0 + def do_set_metadata(db, id, stream): mi = OPF(stream).to_book_metadata() db.set_metadata(id, mi) + def set_metadata_option_parser(): parser = get_parser(_( ''' @@ -648,6 +676,7 @@ an OPF file. ' with the --field option')) return parser + def embed_metadata_option_parser(): parser = get_parser(_( ''' @@ -665,6 +694,7 @@ separated by hyphens. For example: %prog embed_metadata 1 2 10-15 23''')) ' times for multiple formats. By default, all formats are updated.')) return parser + def command_embed_metadata(args, dbpath): parser = embed_metadata_option_parser() opts, args = parser.parse_args(sys.argv[0:1]+args) @@ -680,11 +710,13 @@ def command_embed_metadata(args, dbpath): else: ids |= {x for x in xrange(int(parts[0], int(parts[1])))} only_fmts = opts.only_formats or None + def progress(i, total, mi): prints(_('Processed {0} ({1} of {2})').format(mi.title, i, total)) db.new_api.embed_metadata(ids, only_fmts=only_fmts, report_progress=progress) send_message() + def command_set_metadata(args, dbpath): parser = set_metadata_option_parser() opts, args = parser.parse_args(sys.argv[0:1]+args) @@ -781,6 +813,7 @@ def command_set_metadata(args, dbpath): return 0 + def do_export(db, ids, dir, opts): if ids is None: ids = list(db.all_ids()) @@ -794,6 +827,7 @@ def do_export(db, ids, dir, opts): prints('\t'+'\n\t'.join(tb.splitlines())) prints(' ') + def export_option_parser(): parser = get_parser(_('''\ %prog export [options] ids @@ -831,6 +865,7 @@ an opf file). You can get id numbers from the search command. return parser + def command_export(args, dbpath): parser = export_option_parser() opts, args = parser.parse_args(sys.argv[1:]+args) @@ -844,10 +879,12 @@ def command_export(args, dbpath): do_export(get_db(dbpath, opts), ids, dir, opts) return 0 + def do_add_custom_column(db, label, name, datatype, is_multiple, display): num = db.create_custom_column(label, name, datatype, is_multiple, display=display) prints('Custom column created with id: %s'%num) + def add_custom_column_option_parser(): from calibre.library.custom_columns import CustomColumns parser = get_parser(_('''\ @@ -901,6 +938,7 @@ def command_add_custom_column(args, dbpath): db.prefs['field_metadata'] = db.field_metadata.all_metadata() return 0 + def catalog_option_parser(args): from calibre.customize.ui import available_catalog_formats, plugin_for_catalog_format from calibre.utils.logging import Log @@ -981,6 +1019,7 @@ def catalog_option_parser(args): return parser, plugin, log + def command_catalog(args, dbpath): parser, plugin, log = catalog_option_parser(args) opts, args = parser.parse_args(sys.argv[1:]) @@ -1007,6 +1046,7 @@ def command_catalog(args, dbpath): with plugin: return int(bool(plugin.run(args[1], opts, get_db(dbpath, opts)))) + def parse_series_string(db, label, value): val = unicode(value).strip() s_index = None @@ -1024,6 +1064,7 @@ def parse_series_string(db, label, value): s_index = 1.0 return val, s_index + def do_set_custom(db, col, id_, val, append): if id_ not in db.all_ids(): prints(_('No book with id: %s in the database')%id_, file=sys.stderr) @@ -1040,6 +1081,7 @@ def do_set_custom(db, col, id_, val, append): write_dirtied(db) send_message() + def set_custom_option_parser(): parser = get_parser(_( ''' @@ -1069,6 +1111,7 @@ def command_set_custom(args, dbpath): opts.append) return 0 + def do_custom_columns(db, details): from pprint import pformat cols = db.custom_column_label_map @@ -1081,6 +1124,7 @@ def do_custom_columns(db, details): else: prints(col, '(%d)'%data['num']) + def custom_columns_option_parser(): parser = get_parser(_( ''' @@ -1099,6 +1143,7 @@ def command_custom_columns(args, dbpath): do_custom_columns(get_db(dbpath, opts), opts.details) return 0 + def do_remove_custom_column(db, label, force): if not force: q = raw_input(_('You will lose all data in the column: %s.' @@ -1113,6 +1158,7 @@ def do_remove_custom_column(db, label, force): raise SystemExit(1) prints('Column %r removed.'%label) + def remove_custom_column_option_parser(): parser = get_parser(_( ''' @@ -1141,6 +1187,7 @@ def command_remove_custom_column(args, dbpath): db.prefs['field_metadata'] = db.field_metadata.all_metadata() return 0 + def saved_searches_option_parser(): parser = get_parser(_( ''' @@ -1160,6 +1207,7 @@ def saved_searches_option_parser(): ''')) return parser + def command_saved_searches(args, dbpath): parser = saved_searches_option_parser() opts, args = parser.parse_args(args) @@ -1199,6 +1247,7 @@ def command_saved_searches(args, dbpath): return 0 + def backup_metadata_option_parser(): parser = get_parser(_('''\ %prog backup_metadata [options] @@ -1216,6 +1265,7 @@ automatically, every time metadata is changed. ' books.')) return parser + class BackupProgress(object): def __init__(self): @@ -1230,6 +1280,7 @@ class BackupProgress(object): prints(u'%.1f%% %s - %s'%((self.count*100)/float(self.total), book_id, getattr(mi, 'title', 'Unknown'))) + def command_backup_metadata(args, dbpath): parser = backup_metadata_option_parser() opts, args = parser.parse_args(args) @@ -1272,6 +1323,7 @@ Perform some checks on the filesystem representing a library. Reports are {0} "Default: all")) return parser + def command_check_library(args, dbpath): from calibre.library.check_library import CheckLibrary, CHECKS parser = check_library_option_parser() @@ -1354,6 +1406,7 @@ what is found in the OPF files. 'unless this option is specified.')) return parser + def command_restore_database(args, dbpath): parser = restore_database_option_parser() opts, args = parser.parse_args(args) @@ -1401,6 +1454,7 @@ def command_restore_database(args, dbpath): prints('Some errors occurred. A detailed report was ' 'saved to', name) + def list_categories_option_parser(): parser = get_parser(_('''\ %prog list_categories [options] @@ -1428,6 +1482,7 @@ information is the equivalent of what is shown in the tags pane. 'Default is a comma.')) return parser + def command_list_categories(args, dbpath): parser = list_categories_option_parser() opts, args = parser.parse_args(args) @@ -1527,6 +1582,7 @@ def command_list_categories(args, dbpath): return parser + def clone_option_parser(): return get_parser(_( '''\ @@ -1539,6 +1595,7 @@ The cloned library will contain no books. If you want to create a full duplicate all books, then simply use your filesystem tools to copy the library folder. ''')) + def command_clone(args, dbpath): parser = clone_option_parser() opts, args = parser.parse_args(args) @@ -1568,6 +1625,7 @@ def command_clone(args, dbpath): db.close() LibraryDatabase(loc, default_prefs=dbprefs) + def search_option_parser(): parser = get_parser(_( '''\ @@ -1591,6 +1649,7 @@ COMMANDS = ('list', 'add', 'remove', 'add_format', 'remove_format', 'check_library', 'list_categories', 'backup_metadata', 'clone', 'embed_metadata', 'search') + def command_search(args, dbpath): parser = search_option_parser() opts, args = parser.parse_args(args) diff --git a/src/calibre/library/coloring.py b/src/calibre/library/coloring.py index ef48508233..b4ffad297e 100644 --- a/src/calibre/library/coloring.py +++ b/src/calibre/library/coloring.py @@ -13,6 +13,7 @@ from textwrap import dedent color_row_key = '*row' + class Rule(object): # {{{ SIGNATURE = '# BasicColorRule():' @@ -201,6 +202,7 @@ class Rule(object): # {{{ # }}} + def rule_from_template(fm, template): ok_lines = [] for line in template.splitlines(): @@ -223,6 +225,7 @@ def rule_from_template(fm, template): ok_lines.append(line) return '\n'.join(ok_lines) + def conditionable_columns(fm): for key in fm: m = fm[key] @@ -234,6 +237,7 @@ def conditionable_columns(fm): else: yield key + def displayable_columns(fm): yield color_row_key for key in fm.displayable_field_keys(): @@ -241,6 +245,7 @@ def displayable_columns(fm): 'identifiers', 'path'): yield key + def migrate_old_rule(fm, template): if template.startswith('program:\n#tag wizard'): rules = [] diff --git a/src/calibre/library/comments.py b/src/calibre/library/comments.py index b3e26472a4..edb1aff0e7 100644 --- a/src/calibre/library/comments.py +++ b/src/calibre/library/comments.py @@ -20,6 +20,7 @@ lost_cr_exception_pat = re.compile(r'(Ph\.D)|(D\.Phil)|((Dr|Mr|Mrs|Ms)\.[A-Z])') sanitize_pat = re.compile(r's @@ -130,6 +131,7 @@ def comments_to_html(comments): return result.renderContents(encoding=None) + def markdown(val): try: md = markdown.Markdown @@ -138,9 +140,11 @@ def markdown(val): md = markdown.Markdown = Markdown() return md.convert(val) + def merge_comments(one, two): return comments_to_html(one) + '\n\n' + comments_to_html(two) + def sanitize_html(html): if not html: return u'' @@ -157,6 +161,7 @@ def sanitize_html(html): stream = TreeWalker(tree) return serializer.render(stream) + def sanitize_comments_html(html): from calibre.ebooks.markdown import Markdown text = html2text(html) @@ -164,6 +169,7 @@ def sanitize_comments_html(html): html = md.convert(text) return sanitize_html(html) + def test(): for pat, val in [ ('lineone\n\nlinetwo', diff --git a/src/calibre/library/custom_columns.py b/src/calibre/library/custom_columns.py index 4a2ea64035..5fb50b701d 100644 --- a/src/calibre/library/custom_columns.py +++ b/src/calibre/library/custom_columns.py @@ -15,6 +15,7 @@ from calibre.library.field_metadata import FieldMetadata from calibre.utils.date import parse_date from calibre.utils.config import tweaks + class CustomColumns(object): CUSTOM_DATA_TYPES = frozenset(['rating', 'text', 'comments', 'datetime', diff --git a/src/calibre/library/database.py b/src/calibre/library/database.py index 492b48d598..787269940c 100644 --- a/src/calibre/library/database.py +++ b/src/calibre/library/database.py @@ -11,8 +11,10 @@ from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata import string_to_authors from calibre import isbytestring + class Concatenate(object): '''String concatenation aggregator for sqlite''' + def __init__(self, sep=','): self.sep = sep self.ans = '' @@ -27,6 +29,8 @@ class Concatenate(object): if self.sep: return self.ans[:-len(self.sep)] return self.ans + + class Connection(sqlite.Connection): def get(self, *args, **kw): @@ -38,6 +42,7 @@ class Connection(sqlite.Connection): return ans[0] return ans.fetchall() + def _connect(path): if isinstance(path, unicode): path = path.encode('utf-8') @@ -45,6 +50,7 @@ def _connect(path): conn.row_factory = lambda cursor, row : list(row) conn.create_aggregate('concat', 1, Concatenate) title_pat = re.compile('^(A|The|An)\s+', re.IGNORECASE) + def title_sort(title): match = title_pat.search(title) if match: @@ -54,6 +60,7 @@ def _connect(path): conn.create_function('title_sort', 1, title_sort) return conn + class LibraryDatabase(object): @staticmethod @@ -815,6 +822,7 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE; @dynamic_property def user_version(self): doc = 'The user version of this database' + def fget(self): return self.conn.get('pragma user_version;', all=False) return property(doc=doc, fget=fget) @@ -1483,6 +1491,7 @@ class SearchToken(object): text = ' '.join([item[i] if item[i] else '' for i in self.FIELD_MAP.values()]) return bool(self.pattern.search(text)) ^ self.negate + def text_to_tokens(text): OR = False match = re.match(r'\[(.*)\]', text) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 9cc5d148de..66a08440a3 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -54,6 +54,7 @@ SPOOL_SIZE = 30*1024*1024 ProxyMetadata = namedtuple('ProxyMetadata', 'book_size ondevice_col db_approx_formats') + class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): ''' An ebook metadata database that stores references to ebook files on disk. diff --git a/src/calibre/library/field_metadata.py b/src/calibre/library/field_metadata.py index 3bc25619f7..729fcb03ab 100644 --- a/src/calibre/library/field_metadata.py +++ b/src/calibre/library/field_metadata.py @@ -26,6 +26,7 @@ category_icon_map = { # Builtin metadata {{{ + def _builtin_field_metadata(): # This is a function so that changing the UI language allows newly created # field metadata objects to have correctly translated labels for builtin @@ -320,6 +321,7 @@ def _builtin_field_metadata(): ] # }}} + class FieldMetadata(dict): ''' key: the key to the dictionary is: diff --git a/src/calibre/library/prefs.py b/src/calibre/library/prefs.py index f44c470f7b..dd10702854 100644 --- a/src/calibre/library/prefs.py +++ b/src/calibre/library/prefs.py @@ -11,6 +11,7 @@ from calibre.constants import preferred_encoding from calibre.utils.config import to_json, from_json from calibre import prints + class DBPrefs(dict): def __init__(self, db): diff --git a/src/calibre/library/restore.py b/src/calibre/library/restore.py index e266046419..b22ec16253 100644 --- a/src/calibre/library/restore.py +++ b/src/calibre/library/restore.py @@ -22,6 +22,7 @@ NON_EBOOK_EXTENSIONS = frozenset([ 'opf', 'swp', 'swo' ]) + class RestoreDatabase(LibraryDatabase2): PATH_LIMIT = 10 @@ -33,6 +34,7 @@ class RestoreDatabase(LibraryDatabase2): def dirtied(self, *args, **kwargs): pass + class Restore(Thread): def __init__(self, library_path, progress_callback=None): diff --git a/src/calibre/library/save_to_disk.py b/src/calibre/library/save_to_disk.py index d86083979c..1ce87da058 100644 --- a/src/calibre/library/save_to_disk.py +++ b/src/calibre/library/save_to_disk.py @@ -72,6 +72,7 @@ def find_plugboard(device_name, format, plugboards): prints('Device using plugboard', format, device_name, cpb) return cpb + def config(defaults=None): if defaults is None: c = Config('save_to_disk', _('Options to control saving to disk')) @@ -127,6 +128,7 @@ def config(defaults=None): ' directory structure')) return c + def preprocess_template(template): template = template.replace('//', '/') template = template.replace('{author}', '{authors}') @@ -135,6 +137,7 @@ def preprocess_template(template): template = template.decode(preferred_encoding, 'replace') return template + class Formatter(TemplateFormatter): ''' Provides a format function that substitutes '' for any missing value @@ -167,6 +170,7 @@ class Formatter(TemplateFormatter): traceback.print_exc() return key + def get_components(template, mi, id, timefmt='%b %Y', length=250, sanitize_func=ascii_filename, replace_whitespace=False, to_lowercase=False, safe_format=True, last_has_extension=True): @@ -281,6 +285,7 @@ def save_book_to_disk(id_, db, root, opts, length): except: pass + def get_path_components(opts, mi, book_id, path_length): try: components = get_components(opts.template, mi, book_id, opts.timefmt, path_length, @@ -326,6 +331,7 @@ def update_metadata(mi, fmt, stream, plugboards, cdata, error_report=None, plugb else: error_report(fmt, traceback.format_exc()) + def do_save_book_to_disk(id_, mi, cover, plugboards, format_map, root, opts, length): available_formats = [x.lower().strip() for x in format_map.keys()] @@ -391,6 +397,7 @@ def do_save_book_to_disk(id_, mi, cover, plugboards, return not written, id_, mi.title + def sanitize_args(root, opts): if opts is None: opts = config().parse() @@ -403,6 +410,7 @@ def sanitize_args(root, opts): raise ValueError('%r is too long.'%root) return root, opts, length + def save_to_disk(db, ids, root, opts=None, callback=None): ''' Save books from the database ``db`` to the path specified by ``root``. @@ -432,6 +440,7 @@ def save_to_disk(db, ids, root, opts=None, callback=None): break return failures + def read_serialized_metadata(data): from calibre.ebooks.metadata.opf2 import OPF from calibre.utils.date import parse_date @@ -447,6 +456,7 @@ def read_serialized_metadata(data): cdata = f.read() return mi, cdata + def update_serialized_metadata(book, common_data=None): result = [] plugboard_cache = common_data @@ -455,6 +465,7 @@ def update_serialized_metadata(book, common_data=None): fmts = [fp.rpartition(os.extsep)[-1] for fp in book['fmts']] mi, cdata = read_serialized_metadata(book) + def report_error(fmt, tb): result.append((fmt, tb)) diff --git a/src/calibre/library/schema_upgrades.py b/src/calibre/library/schema_upgrades.py index f1bef505cd..644ed9b55b 100644 --- a/src/calibre/library/schema_upgrades.py +++ b/src/calibre/library/schema_upgrades.py @@ -10,6 +10,7 @@ import os from calibre.utils.date import isoformat, DEFAULT_DATE + class SchemaUpgrade(object): def __init__(self): @@ -415,6 +416,7 @@ class SchemaUpgrade(object): 'Cache has_cover' self.conn.execute('ALTER TABLE books ADD COLUMN has_cover BOOL DEFAULT 0') data = self.conn.get('SELECT id,path FROM books', all=True) + def has_cover(path): if path: path = os.path.join(self.library_path, path.replace('/', os.sep), diff --git a/src/calibre/library/server/__init__.py b/src/calibre/library/server/__init__.py index 2ed6f42448..e535145e78 100644 --- a/src/calibre/library/server/__init__.py +++ b/src/calibre/library/server/__init__.py @@ -50,6 +50,7 @@ def server_config(defaults=None): return c + def custom_fields_to_display(db): ckeys = set(db.field_metadata.ignorable_field_keys()) yes_fields = set(tweaks['content_server_will_display']) @@ -60,6 +61,7 @@ def custom_fields_to_display(db): no_fields = ckeys return frozenset(ckeys & (yes_fields - no_fields)) + def main(): from calibre.library.server.main import main return main() diff --git a/src/calibre/library/server/ajax.py b/src/calibre/library/server/ajax.py index a46f3149d4..a865da49e6 100644 --- a/src/calibre/library/server/ajax.py +++ b/src/calibre/library/server/ajax.py @@ -23,6 +23,7 @@ from calibre.library.server import custom_fields_to_display from calibre import force_unicode, isbytestring from calibre.library.field_metadata import category_icon_map + class Endpoint(object): # {{{ 'Manage mime-type json serialization, etc.' @@ -56,6 +57,7 @@ class Endpoint(object): # {{{ return wrapper # }}} + def category_icon(category, meta): # {{{ if category in category_icon_map: icon = category_icon_map[category] @@ -69,28 +71,36 @@ def category_icon(category, meta): # {{{ # }}} # URL Encoding {{{ + + def encode_name(name): if isinstance(name, unicode): name = name.encode('utf-8') return hexlify(name) + def decode_name(name): return unhexlify(name).decode('utf-8') + def absurl(prefix, url): return prefix + url + def category_url(prefix, cid): return absurl(prefix, '/ajax/category/'+encode_name(cid)) + def icon_url(prefix, name): return absurl(prefix, '/browse/icon/'+name) + def books_in_url(prefix, category, cid): return absurl(prefix, '/ajax/books_in/%s/%s'%( encode_name(category), encode_name(cid))) # }}} + class AjaxServer(object): def __init__(self): diff --git a/src/calibre/library/server/base.py b/src/calibre/library/server/base.py index 873f7c3fa1..e80a274f89 100644 --- a/src/calibre/library/server/base.py +++ b/src/calibre/library/server/base.py @@ -75,6 +75,7 @@ class DispatchController(object): # {{{ # }}} + class BonJour(SimplePlugin): # {{{ def __init__(self, engine, port=8080, prefix=''): @@ -119,6 +120,7 @@ cherrypy.engine.bonjour = BonJour(cherrypy.engine) # }}} + class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache, BrowseServer, AjaxServer): diff --git a/src/calibre/library/server/browse.py b/src/calibre/library/server/browse.py index 451890bb9e..9dde4f5382 100644 --- a/src/calibre/library/server/browse.py +++ b/src/calibre/library/server/browse.py @@ -24,10 +24,12 @@ from calibre.library.server.utils import quote, unquote from calibre.db.categories import Tag from calibre.ebooks.metadata.sources.identify import urls_from_identifiers + def xml(*args, **kwargs): ans = prepare_string_for_xml(*args, **kwargs) return ans.replace(''', ''') + def render_book_list(ids, prefix, suffix=''): # {{{ pages = [] num = len(ids) @@ -113,12 +115,14 @@ def render_book_list(ids, prefix, suffix=''): # {{{ # }}} + def utf8(x): # {{{ if isinstance(x, unicode): x = x.encode('utf-8') return x # }}} + def render_rating(rating, url_prefix, container='span', prefix=None): # {{{ if rating < 0.1: return '', '' @@ -145,6 +149,7 @@ def render_rating(rating, url_prefix, container='span', prefix=None): # {{{ # }}} + def get_category_items(category, items, datatype, prefix): # {{{ def item(i): @@ -177,6 +182,7 @@ def get_category_items(category, items, datatype, prefix): # {{{ # }}} + class Endpoint(object): # {{{ 'Manage encoding, mime-type, last modified, cookies, etc.' @@ -212,6 +218,7 @@ class Endpoint(object): # {{{ return do # }}} + class BrowseServer(object): def add_routes(self, connect): diff --git a/src/calibre/library/server/cache.py b/src/calibre/library/server/cache.py index e40d05625a..0ef13122b4 100644 --- a/src/calibre/library/server/cache.py +++ b/src/calibre/library/server/cache.py @@ -9,6 +9,7 @@ from collections import OrderedDict from calibre.utils.date import utcnow + class Cache(object): def __init__(self): diff --git a/src/calibre/library/server/content.py b/src/calibre/library/server/content.py index 24c3c4f5f8..9509abfed9 100644 --- a/src/calibre/library/server/content.py +++ b/src/calibre/library/server/content.py @@ -22,6 +22,7 @@ from calibre.utils.config import tweaks plugboard_content_server_value = 'content_server' plugboard_content_server_formats = ['epub', 'mobi', 'azw3'] + class CSSortKeyGenerator(SortKeyGenerator): def __init__(self, fields, fm, db_prefs): @@ -30,6 +31,7 @@ class CSSortKeyGenerator(SortKeyGenerator): def __call__(self, record): return self.itervals(record).next() + class ContentServer(object): ''' diff --git a/src/calibre/library/server/main.py b/src/calibre/library/server/main.py index 208370cfd0..a0a9382381 100644 --- a/src/calibre/library/server/main.py +++ b/src/calibre/library/server/main.py @@ -13,6 +13,7 @@ from calibre.library.server.base import LibraryServer from calibre.constants import iswindows, plugins import cherrypy + def start_threaded_server(db, opts): server = LibraryServer(db, opts, embedded=True, show_tracebacks=False) server.thread = Thread(target=server.start) @@ -20,10 +21,12 @@ def start_threaded_server(db, opts): server.thread.start() return server + def stop_threaded_server(server): server.exit() server.thread = None + def create_wsgi_app(path_to_library=None, prefix='', virtual_library=None): 'WSGI entry point' from calibre.library import db @@ -36,6 +39,7 @@ def create_wsgi_app(path_to_library=None, prefix='', virtual_library=None): server = LibraryServer(db, opts, wsgi=True, show_tracebacks=True) return cherrypy.Application(server, script_name=None, config=server.config) + def option_parser(): parser = config().option_parser('%prog '+ _( '''[options] @@ -67,6 +71,7 @@ The OPDS interface is advertised via BonJour automatically. ' work in all environments.')) return parser + def daemonize(): try: pid = os.fork() diff --git a/src/calibre/library/server/mobile.py b/src/calibre/library/server/mobile.py index 2effd855b7..013f0a95e9 100644 --- a/src/calibre/library/server/mobile.py +++ b/src/calibre/library/server/mobile.py @@ -24,6 +24,7 @@ from calibre.utils.date import utcfromtimestamp, as_local_time, is_date_undefine from calibre.utils.filenames import ascii_filename from calibre.utils.icu import sort_key + def CLASS(*args, **kwargs): # class is a reserved word in Python kwargs['class'] = ' '.join(args) return kwargs @@ -70,6 +71,7 @@ def build_search_box(num, search, sort, order, prefix): # {{{ return div # }}} + def build_navigation(start, num, total, url_base): # {{{ end = min((start+num-1), total) tagline = SPAN('Books %d to %d of %d'%(start, end, total), @@ -92,6 +94,7 @@ def build_navigation(start, num, total, url_base): # {{{ # }}} + def build_index(books, num, search, sort, order, start, total, url_base, CKEYS, prefix, have_kobo_browser=False): logo = DIV(IMG(src=prefix+'/static/calibre.png', alt=__appname__), id='logo') diff --git a/src/calibre/library/server/opds.py b/src/calibre/library/server/opds.py index f88b48e3bc..34157a0c5b 100644 --- a/src/calibre/library/server/opds.py +++ b/src/calibre/library/server/opds.py @@ -30,16 +30,19 @@ BASE_HREFS = { STANZA_FORMATS = frozenset(['epub', 'pdb', 'pdf', 'cbr', 'cbz', 'djvu']) + def url_for(name, version, **kwargs): if not name.endswith('_'): name += '_' return routes.url_for(name+str(version), **kwargs) + def hexlify(x): if isinstance(x, unicode): x = x.encode('utf-8') return binascii.hexlify(x) + def unhexlify(x): return binascii.unhexlify(x).decode('utf-8') @@ -58,6 +61,7 @@ TITLE = E.title ID = E.id ICON = E.icon + def UPDATED(dt, *args, **kwargs): return E.updated(as_utc(dt).strftime('%Y-%m-%dT%H:%M:%S+00:00'), *args, **kwargs) @@ -65,12 +69,14 @@ LINK = partial(E.link, type='application/atom+xml') NAVLINK = partial(E.link, type='application/atom+xml;type=feed;profile=opds-catalog') + def SEARCH_LINK(base_href, *args, **kwargs): kwargs['rel'] = 'search' kwargs['title'] = 'Search' kwargs['href'] = base_href+'/search/{searchTerms}' return LINK(*args, **kwargs) + def AUTHOR(name, uri=None): args = [E.name(name)] if uri is not None: @@ -79,6 +85,7 @@ def AUTHOR(name, uri=None): SUBTITLE = E.subtitle + def NAVCATALOG_ENTRY(base_href, updated, title, description, query, version=0): href = base_href+'/navcatalog/'+hexlify(query) id_ = 'calibre-navcatalog:'+str(hashlib.sha1(href).hexdigest()) @@ -97,6 +104,7 @@ LAST_LINK = partial(NAVLINK, rel='last') NEXT_LINK = partial(NAVLINK, rel='next', title='Next') PREVIOUS_LINK = partial(NAVLINK, rel='previous') + def html_to_lxml(raw): raw = u'
%s
'%raw root = html.fragment_fromstring(raw) @@ -119,6 +127,7 @@ def html_to_lxml(raw): from calibre.ebooks.oeb.parse_utils import _html4_parse return _html4_parse(raw) + def CATALOG_ENTRY(item, item_kind, base_href, version, updated, ignore_count=False, add_kind=False): id_ = 'calibre:category:'+item.name @@ -142,6 +151,7 @@ def CATALOG_ENTRY(item, item_kind, base_href, version, updated, link ) + def CATALOG_GROUP_ENTRY(item, category, base_href, version, updated): id_ = 'calibre:category-group:'+category+':'+item.text iid = item.text @@ -154,6 +164,7 @@ def CATALOG_GROUP_ENTRY(item, category, base_href, version, updated): link ) + def ACQUISITION_ENTRY(item, version, db, updated, CFM, CKEYS, prefix): FM = db.FIELD_MAP title = item[FM['title']] @@ -241,6 +252,7 @@ def ACQUISITION_ENTRY(item, version, db, updated, CFM, CKEYS, prefix): default_feed_title = __appname__ + ' ' + _('Library') + class Feed(object): # {{{ def __init__(self, id_, updated, version, subtitle=None, @@ -277,6 +289,7 @@ class Feed(object): # {{{ xml_declaration=True) # }}} + class TopLevel(Feed): # {{{ def __init__(self, @@ -297,6 +310,7 @@ class TopLevel(Feed): # {{{ self.root.append(x) # }}} + class NavFeed(Feed): def __init__(self, id_, updated, version, offsets, page_url, up_url, title=None): @@ -313,6 +327,7 @@ class NavFeed(Feed): kwargs['title'] = title Feed.__init__(self, id_, updated, version, **kwargs) + class AcquisitionFeed(NavFeed): def __init__(self, updated, id_, items, offsets, page_url, up_url, version, @@ -325,6 +340,7 @@ class AcquisitionFeed(NavFeed): self.root.append(ACQUISITION_ENTRY(item, version, db, updated, CFM, CKEYS, prefix)) + class CategoryFeed(NavFeed): def __init__(self, items, which, id_, updated, version, offsets, page_url, up_url, db, title=None): @@ -338,6 +354,7 @@ class CategoryFeed(NavFeed): updated, ignore_count=ignore_count, add_kind=which != item.category)) + class CategoryGroupFeed(NavFeed): def __init__(self, items, which, id_, updated, version, offsets, page_url, up_url, title=None): @@ -449,6 +466,7 @@ class OPDSServer(object): owhich = hexlify('N'+which) up_url = url_for('opdsnavcatalog', version, which=owhich) items = categories[category] + def belongs(x, which): return getattr(x, 'sort', x.name).lower().startswith(which.lower()) items = [x for x in items if belongs(x, which)] @@ -516,6 +534,7 @@ class OPDSServer(object): page_url, up_url, self.db, title=feed_title) else: class Group: + def __init__(self, text, count): self.text, self.count = text, count @@ -610,6 +629,7 @@ class OPDSServer(object): (_('Newest'), _('Date'), 'Onewest'), (_('Title'), _('Title'), 'Otitle'), ] + def getter(x): try: return category_meta[x]['name'].lower() diff --git a/src/calibre/library/server/utils.py b/src/calibre/library/server/utils.py index b7a4ef2f79..b2c0a33152 100644 --- a/src/calibre/library/server/utils.py +++ b/src/calibre/library/server/utils.py @@ -17,6 +17,7 @@ from calibre.utils.date import now as nowf from calibre.utils.config import tweaks from calibre.utils.icu import sort_key + class Offsets(object): 'Calculate offsets for a paginated view' @@ -59,6 +60,7 @@ def expose(func): return do + class AuthController(object): ''' @@ -142,6 +144,7 @@ class AuthController(object): is_valid = s_hashpart == hashpart return (is_valid and (time.time() - timestamp) < self.MAX_AGE) + def strftime(fmt='%Y/%m/%d %H:%M:%S', dt=None): if not hasattr(dt, 'timetuple'): dt = nowf() @@ -151,6 +154,7 @@ def strftime(fmt='%Y/%m/%d %H:%M:%S', dt=None): except: return _strftime(fmt, nowf().timetuple()) + def format_tag_string(tags, sep, ignore_max=False, no_tag_count=False, joinval=', '): MAX = sys.maxint if ignore_max else tweaks['max_content_server_tags_shown'] if tags: @@ -166,20 +170,24 @@ def format_tag_string(tags, sep, ignore_max=False, no_tag_count=False, joinval=' return u'%s:&:%s'%(tweaks['max_content_server_tags_shown'], joinval.join(tlist)) if tlist else '' + def quote(s): if isinstance(s, unicode): s = s.encode('utf-8') return quote_(s) + def unquote(s): ans = unquote_(s) if isbytestring(ans): ans = ans.decode('utf-8') return ans + def cookie_time_fmt(time_t): return time.strftime('%a, %d-%b-%Y %H:%M:%S GMT', time_t) + def cookie_max_age_to_expires(max_age): gmt_expiration_time = time.gmtime(time.time() + max_age) return cookie_time_fmt(gmt_expiration_time) diff --git a/src/calibre/library/server/xml.py b/src/calibre/library/server/xml.py index 86130890b7..c3b1e61adc 100644 --- a/src/calibre/library/server/xml.py +++ b/src/calibre/library/server/xml.py @@ -21,6 +21,7 @@ from calibre.utils.icu import sort_key E = ElementMaker() + class XMLServer(object): 'Serves XML and the Ajax based HTML frontend' diff --git a/src/calibre/library/sqlite.py b/src/calibre/library/sqlite.py index 6b2172d8cd..b53f1c9a8d 100644 --- a/src/calibre/library/sqlite.py +++ b/src/calibre/library/sqlite.py @@ -29,6 +29,7 @@ global_lock = RLock() _c_speedup = plugins['speedup'][0] + def _c_convert_timestamp(val): if not val: return None @@ -45,6 +46,7 @@ def _c_convert_timestamp(val): except OverflowError: return UNDEFINED_DATE.astimezone(local_tz) + def _py_convert_timestamp(val): if val: tzsecs = 0 @@ -68,12 +70,14 @@ def _py_convert_timestamp(val): convert_timestamp = _py_convert_timestamp if _c_speedup is None else \ _c_convert_timestamp + def adapt_datetime(dt): return isoformat(dt, sep=' ') sqlite.register_adapter(datetime, adapt_datetime) sqlite.register_converter('timestamp', convert_timestamp) + def convert_bool(val): return val != '0' @@ -81,6 +85,7 @@ sqlite.register_adapter(bool, lambda x : 1 if x else 0) sqlite.register_converter('bool', convert_bool) sqlite.register_converter('BOOL', convert_bool) + class DynamicFilter(object): def __init__(self, name): @@ -96,6 +101,7 @@ class DynamicFilter(object): class Concatenate(object): '''String concatenation aggregator for sqlite''' + def __init__(self, sep=','): self.sep = sep self.ans = [] @@ -109,9 +115,11 @@ class Concatenate(object): return None return self.sep.join(self.ans) + class SortedConcatenate(object): '''String concatenation aggregator for sqlite, sorted by supplied index''' sep = ',' + def __init__(self): self.ans = {} @@ -124,14 +132,18 @@ class SortedConcatenate(object): return None return self.sep.join(map(self.ans.get, sorted(self.ans.keys()))) + class SortedConcatenateBar(SortedConcatenate): sep = '|' + class SortedConcatenateAmper(SortedConcatenate): sep = '&' + class IdentifiersConcat(object): '''String concatenation aggregator for the identifiers map''' + def __init__(self): self.ans = [] @@ -144,6 +156,7 @@ class IdentifiersConcat(object): class AumSortedConcatenate(object): '''String concatenation aggregator for the author sort map''' + def __init__(self): self.ans = {} @@ -160,6 +173,7 @@ class AumSortedConcatenate(object): return self.ans[keys[0]] return ':#:'.join([self.ans[v] for v in sorted(keys)]) + class Connection(sqlite.Connection): def get(self, *args, **kw): @@ -171,11 +185,13 @@ class Connection(sqlite.Connection): return ans[0] return ans.fetchall() + def _author_to_author_sort(x): if not x: return '' return author_to_author_sort(x.replace('|', ',')) + def pynocase(one, two, encoding='utf-8'): if isbytestring(one): try: @@ -189,10 +205,12 @@ def pynocase(one, two, encoding='utf-8'): pass return cmp(one.lower(), two.lower()) + def icu_collator(s1, s2): return cmp(sort_key(force_unicode(s1, 'utf-8')), sort_key(force_unicode(s2, 'utf-8'))) + def load_c_extensions(conn, debug=DEBUG): try: conn.enable_load_extension(True) @@ -207,6 +225,7 @@ def load_c_extensions(conn, debug=DEBUG): print e return False + def do_connect(path, row_factory=None): conn = sqlite.connect(path, factory=Connection, detect_types=sqlite.PARSE_DECLTYPES|sqlite.PARSE_COLNAMES) @@ -231,6 +250,7 @@ def do_connect(path, row_factory=None): conn.create_collation('icucollate', icu_collator) return conn + class DBThread(Thread): CLOSE = '-------close---------' @@ -291,6 +311,7 @@ class DBThread(Thread): except Exception as err: self.unhandled_error = (err, traceback.format_exc()) + class DatabaseException(Exception): def __init__(self, err, tb): @@ -303,8 +324,10 @@ class DatabaseException(Exception): self.orig_err = err self.orig_tb = tb + def proxy(fn): ''' Decorator to call methods on the database connection in the proxy thread ''' + def run(self, *args, **kwargs): if self.closed: raise DatabaseException('Connection closed', '') @@ -372,6 +395,7 @@ class ConnectionProxy(object): def create_dynamic_filter(self): pass + def connect(dbpath, row_factory=None): conn = ConnectionProxy(DBThread(dbpath, row_factory)) conn.proxy.start() @@ -381,6 +405,7 @@ def connect(dbpath, row_factory=None): raise DatabaseException(*conn.proxy.unhandled_error) return conn + def test(): c = sqlite.connect(':memory:') if load_c_extensions(c, True): diff --git a/src/calibre/library/test.py b/src/calibre/library/test.py index df1bf87c3b..f1563fb996 100644 --- a/src/calibre/library/test.py +++ b/src/calibre/library/test.py @@ -14,6 +14,7 @@ from calibre.ptempfile import PersistentTemporaryDirectory from calibre.library.database2 import LibraryDatabase2 from calibre.ebooks.metadata import MetaInformation + class DBTest(unittest.TestCase): img = '\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x01\x00d\x00d\x00\x00\xff\xdb\x00C\x00\x05\x03\x04\x04\x04\x03\x05\x04\x04\x04\x05\x05\x05\x06\x07\x0c\x08\x07\x07\x07\x07\x0f\x0b\x0b\t\x0c\x11\x0f\x12\x12\x11\x0f\x11\x11\x13\x16\x1c\x17\x13\x14\x1a\x15\x11\x11\x18!\x18\x1a\x1d\x1d\x1f\x1f\x1f\x13\x17"$"\x1e$\x1c\x1e\x1f\x1e\xff\xdb\x00C\x01\x05\x05\x05\x07\x06\x07\x0e\x08\x08\x0e\x1e\x14\x11\x14\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\xff\xc0\x00\x11\x08\x00\x01\x00\x01\x03\x01\x11\x00\x02\x11\x01\x03\x11\x01\xff\xc4\x00\x14\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xc4\x00\x14\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\xff\xc4\x00\x14\x11\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xda\x00\x0c\x03\x01\x00\x02\x11\x03\x11\x00?\x00p\xf9+\xff\xd9' # noqa @@ -91,6 +92,7 @@ class DBTest(unittest.TestCase): def suite(): return unittest.TestLoader().loadTestsFromTestCase(DBTest) + def test(): unittest.TextTestRunner(verbosity=2).run(suite()) diff --git a/src/calibre/libunzip.py b/src/calibre/libunzip.py index b320b49656..ab58079c4b 100644 --- a/src/calibre/libunzip.py +++ b/src/calibre/libunzip.py @@ -7,6 +7,7 @@ import re from calibre.utils import zipfile from calibre.utils.icu import numeric_sort_key + def update(pathtozip, patterns, filepaths, names, compression=zipfile.ZIP_DEFLATED, verbose=True): ''' Update files in the zip file at `pathtozip` matching the given @@ -37,6 +38,7 @@ def update(pathtozip, patterns, filepaths, names, compression=zipfile.ZIP_DEFLAT break z.close() + def extract(filename, dir): """ Extract archive C{filename} into directory C{dir} @@ -44,12 +46,14 @@ def extract(filename, dir): zf = zipfile.ZipFile(filename) zf.extractall(dir) + def sort_key(filename): bn, ext = filename.rpartition('.')[::2] if not bn and ext: bn, ext = ext, bn return (numeric_sort_key(bn), numeric_sort_key(ext)) + def extract_member(filename, match=re.compile(r'\.(jpg|jpeg|gif|png)\s*$', re.I), sort_alphabetically=False): zf = zipfile.ZipFile(filename) names = list(zf.namelist()) @@ -61,9 +65,11 @@ def extract_member(filename, match=re.compile(r'\.(jpg|jpeg|gif|png)\s*$', re.I) comic_exts = {'png', 'jpg', 'jpeg', 'gif', 'webp'} + def name_ok(name): return bool(name and not name.startswith('__MACOSX/') and name.rpartition('.')[-1].lower() in comic_exts) + def extract_cover_image(filename): with zipfile.ZipFile(filename) as zf: for name in sorted(zf.namelist(), key=sort_key): diff --git a/src/calibre/linux.py b/src/calibre/linux.py index 5a4eb29a44..31a115546f 100644 --- a/src/calibre/linux.py +++ b/src/calibre/linux.py @@ -41,6 +41,7 @@ entry_points = { ], } + class PreserveMIMEDefaults(object): def __init__(self): @@ -164,6 +165,7 @@ if mimetype_icons and raw_input('Remove the ebook format icons? [y/n]:').lower() # Completion {{{ + class ZshCompleter(object): # {{{ def __init__(self, opts): @@ -286,6 +288,7 @@ class ZshCompleter(object): # {{{ w('\n}\n') log = DevNull() + def get_parser(input_fmt='epub', output_fmt=None): of = ('dummy2.'+output_fmt) if output_fmt else 'dummy' return create_option_parser(('ec', 'dummy1.'+input_fmt, of, '-h'), log)[0] @@ -484,6 +487,7 @@ _ebook_edit() { f.write('esac\n') # }}} + def get_bash_completion_path(root, share, info): if root == '/usr': # Try to get the system bash completion dir since we are installing to @@ -499,6 +503,7 @@ def get_bash_completion_path(root, share, info): # Use the default bash-completion dir under staging_share return os.path.join(share, 'bash-completion', 'completions', 'calibre') + def write_completion(bash_comp_dest, zsh): from calibre.ebooks.metadata.cli import option_parser as metaop, filetypes as meta_filetypes from calibre.ebooks.lrf.lrfparser import option_parser as lrf2lrsop @@ -528,6 +533,7 @@ def write_completion(bash_comp_dest, zsh): def o_and_e(*args, **kwargs): f.write(opts_and_exts(*args, **kwargs)) zsh.opts_and_exts(*args, **kwargs) + def o_and_w(*args, **kwargs): f.write(opts_and_words(*args, **kwargs)) zsh.opts_and_words(*args, **kwargs) @@ -632,6 +638,7 @@ def write_completion(bash_comp_dest, zsh): zsh.write() # }}} + class PostInstall: def task_failed(self, msg): @@ -867,6 +874,7 @@ class PostInstall: # }}} + def option_parser(): from calibre.utils.config import OptionParser parser = OptionParser() @@ -893,6 +901,7 @@ def options(option_parser): opts.extend(opt._long_opts) return opts + def opts_and_words(name, op, words, takes_files=False): opts = '|'.join(options(op)) words = '|'.join([w.replace("'", "\\'") for w in words]) @@ -925,6 +934,7 @@ complete -F _'''%(opts, words) + fname + ' ' + name +"\n\n").encode('utf-8') pics = {'jpg', 'jpeg', 'gif', 'png', 'bmp'} + def opts_and_exts(name, op, exts, cover_opts=('--cover',), opf_opts=(), file_map={}): opts = ' '.join(options(op)) @@ -1028,6 +1038,7 @@ Icon=calibre-gui Categories=Office; ''' + def get_appdata(): _ = lambda x: x # Make sure the text below is not translated, but is marked for translation return { @@ -1073,6 +1084,7 @@ def get_appdata(): }, } + def write_appdata(key, entry, base, translators): from lxml.etree import tostring from lxml.builder import E @@ -1116,12 +1128,14 @@ def render_img(image, dest, width=128, height=128): img = QImage(I(image)).scaled(width, height, Qt.IgnoreAspectRatio, Qt.SmoothTransformation) img.save(dest) + def main(): p = option_parser() opts, args = p.parse_args() PostInstall(opts) return 0 + def cli_index_strings(): return _('Command Line Interface'), _( 'On OS X, the command line tools are inside the calibre bundle, for example,' diff --git a/src/calibre/ptempfile.py b/src/calibre/ptempfile.py index e7c51337fd..5a8babecf6 100644 --- a/src/calibre/ptempfile.py +++ b/src/calibre/ptempfile.py @@ -11,6 +11,7 @@ from future_builtins import map from calibre.constants import (__version__, __appname__, filesystem_encoding, get_unicode_windows_env_var, iswindows, get_windows_temp_path, isosx) + def cleanup(path): try: import os as oss @@ -22,6 +23,7 @@ def cleanup(path): _base_dir = None + def remove_dir(x): try: import shutil @@ -29,6 +31,7 @@ def remove_dir(x): except: pass + def determined_remove_dir(x): for i in range(10): try: @@ -55,6 +58,7 @@ def app_prefix(prefix): return '%s_'%__appname__ return '%s_%s_%s'%(__appname__, __version__, prefix) + def reset_temp_folder_permissions(): # There are some broken windows installs where the permissions for the temp # folder are set to not be executable, which means chdir() into temp @@ -71,6 +75,7 @@ def reset_temp_folder_permissions(): _osx_cache_dir = None + def osx_cache_dir(): global _osx_cache_dir if _osx_cache_dir: @@ -90,6 +95,7 @@ def osx_cache_dir(): _osx_cache_dir = q return q + def base_dir(): global _base_dir if _base_dir is not None and not os.path.exists(_base_dir): @@ -139,11 +145,13 @@ def base_dir(): return _base_dir + def reset_base_dir(): global _base_dir _base_dir = None base_dir() + def force_unicode(x): # Cannot use the implementation in calibre.__init__ as it causes a circular # dependency @@ -151,14 +159,17 @@ def force_unicode(x): x = x.decode(filesystem_encoding) return x + def _make_file(suffix, prefix, base): suffix, prefix = map(force_unicode, (suffix, prefix)) return tempfile.mkstemp(suffix, prefix, dir=base) + def _make_dir(suffix, prefix, base): suffix, prefix = map(force_unicode, (suffix, prefix)) return tempfile.mkdtemp(suffix, prefix, base) + class PersistentTemporaryFile(object): """ @@ -196,6 +207,7 @@ class PersistentTemporaryFile(object): except: pass + def PersistentTemporaryDirectory(suffix='', prefix='', dir=None): ''' Return the path to a newly created temporary directory that will @@ -208,11 +220,13 @@ def PersistentTemporaryDirectory(suffix='', prefix='', dir=None): atexit.register(remove_dir, tdir) return tdir + class TemporaryDirectory(object): ''' A temporary directory to be used in a with statement. ''' + def __init__(self, suffix='', prefix='', dir=None, keep=False): self.suffix = suffix self.prefix = prefix @@ -230,6 +244,7 @@ class TemporaryDirectory(object): if not self.keep and os.path.exists(self.tdir): remove_dir(self.tdir) + class TemporaryFile(object): def __init__(self, suffix="", prefix="", dir=None, mode='w+b'): @@ -272,6 +287,7 @@ class SpooledTemporaryFile(tempfile.SpooledTemporaryFile): # allow specifying a size. self._file.truncate(*args) + def better_mktemp(*args, **kwargs): fd, path = tempfile.mkstemp(*args, **kwargs) os.close(fd) diff --git a/src/calibre/rpdb.py b/src/calibre/rpdb.py index d9a14b52a3..4835e8e1f2 100644 --- a/src/calibre/rpdb.py +++ b/src/calibre/rpdb.py @@ -15,6 +15,7 @@ from calibre.constants import cache_dir PROMPT = b'(debug) ' QUESTION = b'\x00\x01\x02' + class RemotePdb(pdb.Pdb): def __init__(self, addr="127.0.0.1", port=4444, skip=None): @@ -77,6 +78,7 @@ class RemotePdb(pdb.Pdb): do_EOF = do_quit = do_exit = do_q = end_session + def set_trace(port=4444, skip=None): frame = inspect.currentframe().f_back @@ -90,6 +92,7 @@ def set_trace(port=4444, skip=None): import traceback traceback.print_exc() + def cli(port=4444): prints('Connecting to remote debugger on port %d...' % port) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) diff --git a/src/calibre/spell/__init__.py b/src/calibre/spell/__init__.py index 5dfda22f25..3e82c3fa69 100644 --- a/src/calibre/spell/__init__.py +++ b/src/calibre/spell/__init__.py @@ -15,6 +15,7 @@ DictionaryLocale = namedtuple('DictionaryLocale', 'langcode countrycode') ccodes, ccodemap, country_names = None, None, None + def get_codes(): global ccodes, ccodemap, country_names if ccodes is None: @@ -22,6 +23,7 @@ def get_codes(): ccodes, ccodemap, country_names = data['codes'], data['three_map'], data['names'] return ccodes, ccodemap + def parse_lang_code(raw): raw = raw or '' parts = raw.replace('_', '-').split('-') diff --git a/src/calibre/spell/break_iterator.py b/src/calibre/spell/break_iterator.py index fa3aeba1c3..b40a996b85 100644 --- a/src/calibre/spell/break_iterator.py +++ b/src/calibre/spell/break_iterator.py @@ -14,6 +14,7 @@ from calibre.utils.localization import lang_as_iso639_1 _iterators = {} _lock = Lock() + def split_into_words(text, lang='en'): with _lock: it = _iterators.get(lang, None) @@ -22,6 +23,7 @@ def split_into_words(text, lang='en'): it.set_text(text) return [text[p:p+s] for p, s in it.split2()] + def split_into_words_and_positions(text, lang='en'): with _lock: it = _iterators.get(lang, None) @@ -39,6 +41,7 @@ def index_of(needle, haystack, lang='en'): it.set_text(haystack) return it.index(needle) + def count_words(text, lang='en'): with _lock: it = _iterators.get(lang, None) diff --git a/src/calibre/spell/dictionary.py b/src/calibre/spell/dictionary.py index 138dce08be..268550b8f2 100644 --- a/src/calibre/spell/dictionary.py +++ b/src/calibre/spell/dictionary.py @@ -30,6 +30,7 @@ dprefs.defaults['preferred_locales'] = {} dprefs.defaults['user_dictionaries'] = [{'name':_('Default'), 'is_active':True, 'words':[]}] not_present = object() + class UserDictionary(object): __slots__ = ('name', 'is_active', 'words') @@ -45,6 +46,7 @@ class UserDictionary(object): _builtins = _custom = None + def builtin_dictionaries(): global _builtins if _builtins is None: @@ -59,6 +61,7 @@ def builtin_dictionaries(): _builtins = frozenset(dics) return _builtins + def custom_dictionaries(reread=False): global _custom if _custom is None or reread: @@ -88,14 +91,17 @@ if ul is not None and ul.langcode == 'eng' and ul.countrycode in 'GB BS BZ GH IE default_en_locale = 'en-' + ul.countrycode default_preferred_locales = {'eng':default_en_locale, 'deu':'de-DE', 'spa':'es-ES', 'fra':'fr-FR'} + def best_locale_for_language(langcode): best_locale = dprefs['preferred_locales'].get(langcode, default_preferred_locales.get(langcode, None)) if best_locale is not None: return parse_lang_code(best_locale) + def preferred_dictionary(locale): return {parse_lang_code(k):v for k, v in dprefs['preferred_dictionaries'].iteritems()}.get(locale, None) + def remove_dictionary(dictionary): if dictionary.builtin: raise ValueError('Cannot remove builtin dictionaries') @@ -103,6 +109,7 @@ def remove_dictionary(dictionary): shutil.rmtree(base) dprefs['preferred_dictionaries'] = {k:v for k, v in dprefs['preferred_dictionaries'].iteritems() if v != dictionary.id} + def rename_dictionary(dictionary, name): lf = os.path.join(os.path.dirname(dictionary.dicpath), 'locales') with open(lf, 'r+b') as f: @@ -111,6 +118,7 @@ def rename_dictionary(dictionary, name): f.seek(0), f.truncate(), f.write(b'\n'.join(lines)) custom_dictionaries(reread=True) + def get_dictionary(locale, exact_match=False): preferred = preferred_dictionary(locale) # First find all dictionaries that match locale exactly @@ -152,6 +160,7 @@ def get_dictionary(locale, exact_match=False): if d.primary_locale.langcode == locale.langcode: return d + def load_dictionary(dictionary): from calibre.spell.import_from import convert_to_utf8 with open(dictionary.dicpath, 'rb') as dic, open(dictionary.affpath, 'rb') as aff: @@ -160,6 +169,7 @@ def load_dictionary(dictionary): obj = hunspell.Dictionary(dic_data, aff_data) return LoadedDictionary(dictionary.primary_locale, dictionary.locales, obj, dictionary.builtin, dictionary.name, dictionary.id) + class Dictionaries(object): def __init__(self): @@ -399,6 +409,7 @@ class Dictionaries(object): return ans + def test_dictionaries(): dictionaries = Dictionaries() dictionaries.initialize() diff --git a/src/calibre/spell/import_from.py b/src/calibre/spell/import_from.py index 2cacf2597c..e2763eb04e 100644 --- a/src/calibre/spell/import_from.py +++ b/src/calibre/spell/import_from.py @@ -22,6 +22,7 @@ NS_MAP = { XPath = lambda x: etree.XPath(x, namespaces=NS_MAP) BUILTIN_LOCALES = {'en-US', 'en-GB', 'es-ES'} + def parse_xcu(raw, origin='%origin%'): ' Get the dictionary and affix file names as well as supported locales for each dictionary ' ans = {} @@ -40,6 +41,7 @@ def parse_xcu(raw, origin='%origin%'): ans[(dic, aff)] = locales return ans + def convert_to_utf8(dic_data, aff_data, errors='strict'): m = re.search(br'^SET\s+(\S+)$', aff_data[:2048], flags=re.MULTILINE) if m is not None: @@ -55,6 +57,7 @@ def convert_to_utf8(dic_data, aff_data, errors='strict'): dic_data = dic_data.decode(enc, errors).encode('utf-8') return dic_data, aff_data + def import_from_libreoffice_source_tree(source_path): dictionaries = {} for x in glob.glob(os.path.join(source_path, '*', 'dictionaries.xcu')): @@ -85,9 +88,11 @@ def import_from_libreoffice_source_tree(source_path): if want_locales: raise Exception('Failed to find dictionaries for some wanted locales: %s' % want_locales) + def fill_country_code(x): return {'lt':'lt_LT'}.get(x, x) + def uniq(vals, kmap=lambda x:x): ''' Remove all duplicates from vals, while preserving order. kmap must be a callable that returns a hashable value for every item in vals ''' @@ -97,6 +102,7 @@ def uniq(vals, kmap=lambda x:x): seen_add = seen.add return tuple(x for x, k in zip(vals, lvals) if k not in seen and not seen_add(k)) + def import_from_oxt(source_path, name, dest_dir=None, prefix='dic-'): from calibre.spell.dictionary import parse_lang_code dest_dir = dest_dir or os.path.join(config_dir, 'dictionaries') diff --git a/src/calibre/srv/ajax.py b/src/calibre/srv/ajax.py index 68c7877aa5..799c7b10ce 100644 --- a/src/calibre/srv/ajax.py +++ b/src/calibre/srv/ajax.py @@ -23,11 +23,13 @@ from calibre.utils.config import prefs, tweaks from calibre.utils.date import isoformat, timestampfromdt from calibre.utils.icu import numeric_sort_key as sort_key + def ensure_val(x, *allowed): if x not in allowed: x = allowed[0] return x + def get_pagination(query, num=100, offset=0): try: num = int(query.get('num', num)) @@ -39,6 +41,7 @@ def get_pagination(query, num=100, offset=0): raise HTTPNotFound("Invalid offset") return num, offset + def category_icon(category, meta): # {{{ if category in category_icon_map: icon = category_icon_map[category] @@ -53,6 +56,7 @@ def category_icon(category, meta): # {{{ # Book metadata {{{ + def book_to_json(ctx, rd, db, book_id, get_category_urls=True, device_compatible=False, device_for_template=None): mi = db.get_metadata(book_id, get_cover=False) @@ -137,6 +141,7 @@ def book_to_json(ctx, rd, db, book_id, return data, mi.last_modified + @endpoint('/ajax/book/{book_id}/{library_id=None}', postprocess=json) def book(ctx, rd, book_id, library_id): ''' @@ -176,6 +181,7 @@ def book(ctx, rd, book_id, library_id): rd.outheaders['Last-Modified'] = http_date(timestampfromdt(last_modified)) return data + @endpoint('/ajax/books/{library_id=None}', postprocess=json) def books(ctx, rd, library_id): ''' @@ -227,6 +233,8 @@ def books(ctx, rd, library_id): # }}} # Categories (Tag Browser) {{{ + + @endpoint('/ajax/categories/{library_id=None}', postprocess=json) def categories(ctx, rd, library_id): ''' @@ -246,6 +254,7 @@ def categories(ctx, rd, library_id): categories = ctx.get_categories(rd, db) category_meta = db.field_metadata library_id = db.server_library_id + def getter(x): return category_meta[x]['name'] @@ -517,6 +526,8 @@ def books_in(ctx, rd, encoded_category, encoded_item, library_id): # }}} # Search {{{ + + def search_result(ctx, rd, db, query, num, offset, sort, sort_order): multisort = [(sanitize_sort_field_name(db.field_metadata, s), ensure_val(o, 'asc', 'desc') == 'asc') for s, o in zip(sort.split(','), cycle(sort_order.split(',')))] @@ -540,6 +551,7 @@ def search_result(ctx, rd, db, query, num, offset, sort, sort_order): 'book_ids':ids } + @endpoint('/ajax/search/{library_id=None}', postprocess=json) def search(ctx, rd, library_id): ''' @@ -555,6 +567,7 @@ def search(ctx, rd, library_id): # }}} + @endpoint('/ajax/library-info', postprocess=json) def library_info(ctx, rd): ' Return info about available libraries ' diff --git a/src/calibre/srv/auth.py b/src/calibre/srv/auth.py index c43348782c..aaf27b6a1f 100644 --- a/src/calibre/srv/auth.py +++ b/src/calibre/srv/auth.py @@ -19,20 +19,25 @@ from calibre.utils.monotonic import monotonic MAX_AGE_SECONDS = 3600 nonce_counter, nonce_counter_lock = 0, Lock() + def as_bytestring(x): if not isinstance(x, bytes): x = x.encode('utf-8') return x + def md5_hex(s): return md5(as_bytestring(s)).hexdigest().decode('ascii') + def sha256_hex(s): return sha256(as_bytestring(s)).hexdigest().decode('ascii') + def base64_decode(s): return base64.standard_b64decode(as_bytestring(s)).decode('utf-8') + def synthesize_nonce(key_order, realm, secret, timestamp=None): ''' Create a nonce. Can be used for either digest or cookie based auth. @@ -52,11 +57,13 @@ def synthesize_nonce(key_order, realm, secret, timestamp=None): nonce = ':'.join((timestamp, h)) return nonce + def validate_nonce(key_order, nonce, realm, secret): timestamp, hashpart = nonce.partition(':')[::2] s_nonce = synthesize_nonce(key_order, realm, secret, timestamp) return s_nonce == nonce + def is_nonce_stale(nonce, max_age_seconds=MAX_AGE_SECONDS): try: timestamp = struct.unpack(b'!dH', binascii.unhexlify(as_bytestring(nonce.partition(':')[0])))[0] @@ -158,6 +165,7 @@ class DigestAuth(object): # {{{ return self.response is not None and path == data.path and self.request_digest(pw, data) == self.response # }}} + class AuthController(object): ''' diff --git a/src/calibre/srv/auto_reload.py b/src/calibre/srv/auto_reload.py index 34d3c4d8bf..62a1b1a1c6 100644 --- a/src/calibre/srv/auto_reload.py +++ b/src/calibre/srv/auto_reload.py @@ -21,11 +21,13 @@ from calibre.utils.monotonic import monotonic MAX_RETRIES = 10 + class NoAutoReload(EnvironmentError): pass # Filesystem watcher {{{ + class WatcherBase(object): EXTENSIONS_TO_WATCH = frozenset('py pyj svg'.split()) @@ -202,6 +204,7 @@ else: def find_dirs_to_watch(fpath, dirs, add_default_dirs): dirs = {os.path.abspath(x) for x in dirs} + def add(x): if os.path.isdir(x): dirs.add(x) @@ -217,6 +220,7 @@ def find_dirs_to_watch(fpath, dirs, add_default_dirs): return dirs # }}} + def join_process(p, timeout=5): t = Thread(target=p.wait, name='JoinProcess') t.daemon = True @@ -224,6 +228,7 @@ def join_process(p, timeout=5): t.join(timeout) return p.poll() + class Worker(object): def __init__(self, cmd, log, server, timeout=5): @@ -314,6 +319,7 @@ class Worker(object): # WebSocket reload notifier {{{ + class ReloadHandler(DummyHandler): def __init__(self, *args, **kw): @@ -377,6 +383,7 @@ class ReloadServer(Thread): self.join(self.loop.opts.shutdown_timeout) # }}} + def auto_reload(log, dirs=frozenset(), cmd=None, add_default_dirs=True, listen_on=None): if Watcher is None: raise NoAutoReload('Auto-reload is not supported on this operating system') diff --git a/src/calibre/srv/bonjour.py b/src/calibre/srv/bonjour.py index 92b448736f..e652ae84b7 100644 --- a/src/calibre/srv/bonjour.py +++ b/src/calibre/srv/bonjour.py @@ -8,6 +8,7 @@ __copyright__ = '2015, Kovid Goyal ' from threading import Event + class BonJour(object): # {{{ def __init__(self, name='Books in calibre', service_type='_calibre._tcp', path='/opds', add_hostname=True): @@ -27,6 +28,7 @@ class BonJour(object): # {{{ self.zeroconf_ip_address = zipa = verify_ipV4_address(ip_address) or get_external_ip() prefix = loop.opts.url_prefix or '' # The Zeroconf module requires everything to be bytestrings + def enc(x): if not isinstance(x, bytes): x = x.encode('ascii') diff --git a/src/calibre/srv/books.py b/src/calibre/srv/books.py index 10a2c672bd..45c9c99554 100644 --- a/src/calibre/srv/books.py +++ b/src/calibre/srv/books.py @@ -24,6 +24,7 @@ cache_lock = RLock() queued_jobs = {} failed_jobs = {} + def abspath(x): x = os.path.abspath(x) if iswindows and not x.startswith('\\\\?\\'): @@ -31,6 +32,8 @@ def abspath(x): return x _books_cache_dir = None + + def books_cache_dir(): global _books_cache_dir if _books_cache_dir: @@ -52,6 +55,7 @@ def book_hash(library_uuid, book_id, fmt, size, mtime): staging_cleaned = False + def safe_remove(x, is_file=None): if is_file is None: is_file = os.path.isfile(x) @@ -80,6 +84,7 @@ def queue_job(ctx, copy_format_to, bhash, fmt, book_id, size, mtime): last_final_clean_time = 0 + def clean_final(interval=24 * 60 * 60): global last_final_clean_time now = time.time() @@ -96,6 +101,7 @@ def clean_final(interval=24 * 60 * 60): # This book has not been accessed for a long time, delete it safe_remove(x) + def job_done(job): with cache_lock: bhash, pathtoebook, tdir = job.data @@ -114,6 +120,7 @@ def job_done(job): import traceback failed_jobs[bhash] = (False, traceback.format_exc()) + @endpoint('/book-manifest/{book_id}/{fmt}', postprocess=json, types={'book_id':int}) def book_manifest(ctx, rd, book_id, fmt): db, library_id = get_library_data(ctx, rd)[:2] @@ -150,6 +157,7 @@ def book_manifest(ctx, rd, book_id, fmt): status, result, tb, aborted = ctx.job_status(job_id) return {'aborted': aborted, 'traceback':tb, 'job_status':status, 'job_id':job_id} + @endpoint('/book-file/{book_id}/{fmt}/{size}/{mtime}/{+name}', types={'book_id':int, 'size':int, 'mtime':int}) def book_file(ctx, rd, book_id, fmt, size, mtime, name): db, library_id = get_library_data(ctx, rd)[:2] @@ -170,6 +178,7 @@ def book_file(ctx, rd, book_id, fmt, size, mtime, name): mathjax_lock = Lock() mathjax_manifest = None + def get_mathjax_manifest(tdir=None): global mathjax_manifest with mathjax_lock: @@ -186,12 +195,14 @@ def get_mathjax_manifest(tdir=None): zf.close(), f.close() return mathjax_manifest + def manifest_as_json(): ans = jsonlib.dumps(get_mathjax_manifest(), ensure_ascii=False) if not isinstance(ans, bytes): ans = ans.encode('utf-8') return ans + @endpoint('/mathjax/{+which=""}', auth_required=False) def mathjax(ctx, rd, which): manifest = get_mathjax_manifest(rd.tdir) diff --git a/src/calibre/srv/code.py b/src/calibre/srv/code.py index 5c00b02e6b..0cef6bde2f 100644 --- a/src/calibre/srv/code.py +++ b/src/calibre/srv/code.py @@ -22,10 +22,12 @@ from calibre.utils.search_query_parser import ParseException POSTABLE = frozenset({'GET', 'POST', 'HEAD'}) + @endpoint('', auth_required=False) def index(ctx, rd): return lopen(P('content-server/index-generated.html'), 'rb') + @endpoint('/auto-reload', auth_required=False) def auto_reload(ctx, rd): auto_reload_port = getattr(rd.opts, 'auto_reload_port', 0) @@ -41,6 +43,7 @@ def console_print(ctx, rd): shutil.copyfileobj(rd.request_body_file, sys.stdout) return '' + def get_basic_query_data(ctx, rd): db, library_id, library_map, default_library = get_library_data(ctx, rd) skeys = db.field_metadata.sortable_field_keys() @@ -63,6 +66,7 @@ def get_basic_query_data(ctx, rd): _cached_translations = None + def get_translations(): global _cached_translations if _cached_translations is None: @@ -80,6 +84,7 @@ def get_translations(): DEFAULT_NUMBER_OF_BOOKS = 50 + @endpoint('/interface-data/init', postprocess=json) def interface_data(ctx, rd): ''' @@ -144,6 +149,7 @@ def interface_data(ctx, rd): return ans + @endpoint('/interface-data/more-books', postprocess=json, methods=POSTABLE) def more_books(ctx, rd): ''' @@ -176,6 +182,7 @@ def more_books(ctx, rd): return ans + @endpoint('/interface-data/set-session-data', postprocess=json, methods=POSTABLE) def set_session_data(ctx, rd): ''' @@ -193,6 +200,7 @@ def set_session_data(ctx, rd): ud.update(new_data) ctx.user_manager.set_session_data(rd.username, ud) + @endpoint('/interface-data/get-books', postprocess=json) def get_books(ctx, rd): ''' @@ -222,6 +230,7 @@ def get_books(ctx, rd): mdata[book_id] = data return ans + @endpoint('/interface-data/book-metadata/{book_id=0}', postprocess=json) def book_metadata(ctx, rd, book_id): ''' @@ -231,6 +240,7 @@ def book_metadata(ctx, rd, book_id): ''' library_id, db = get_basic_query_data(ctx, rd)[:2] book_ids = ctx.allowed_book_ids(rd, db) + def notfound(): raise HTTPNotFound(_('No book with id: %d in library') % book_id) if not book_ids: @@ -245,6 +255,7 @@ def book_metadata(ctx, rd, book_id): data['id'] = book_id # needed for random book view (when book_id=0) return data + @endpoint('/interface-data/tag-browser') def tag_browser(ctx, rd): ''' @@ -255,6 +266,7 @@ def tag_browser(ctx, rd): db, library_id = get_library_data(ctx, rd)[:2] etag = '%s||%s||%s' % (db.last_modified(), rd.username, library_id) etag = hashlib.sha1(etag.encode('utf-8')).hexdigest() + def generate(): db, library_id = get_library_data(ctx, rd)[:2] return json(ctx, rd, tag_browser, categories_as_json(ctx, rd, db)) diff --git a/src/calibre/srv/content.py b/src/calibre/srv/content.py index d638203a44..6e46233e59 100644 --- a/src/calibre/srv/content.py +++ b/src/calibre/srv/content.py @@ -42,6 +42,7 @@ lock = Lock() mtimes = {} rename_counter = 0 + def create_file_copy(ctx, rd, prefix, library_id, book_id, ext, mtime, copy_func, extra_etag_data=''): ''' We cannot copy files directly from the library folder to the output socket, as this can potentially lock the library for an extended period. So @@ -96,6 +97,7 @@ def create_file_copy(ctx, rd, prefix, library_id, book_id, ext, mtime, copy_func rd.outheaders['Tempfile'] = hexlify(fname.encode('utf-8')) return rd.filesystem_file_with_custom_etag(ans, prefix, library_id, book_id, mtime, extra_etag_data) + def write_generated_cover(db, book_id, width, height, destf): mi = db.get_metadata(book_id) set_use_roman(get_use_roman()) @@ -108,6 +110,7 @@ def write_generated_cover(db, book_id, width, height, destf): cdata = generate_cover(mi, prefs=prefs) destf.write(cdata) + def generated_cover(ctx, rd, library_id, db, book_id, width=None, height=None): prefix = 'generated-cover' if height is not None: @@ -116,6 +119,7 @@ def generated_cover(ctx, rd, library_id, db, book_id, width=None, height=None): mtime = timestampfromdt(db.field_for('last_modified', book_id)) return create_file_copy(ctx, rd, prefix, library_id, book_id, 'jpg', mtime, partial(write_generated_cover, db, book_id, width, height)) + def cover(ctx, rd, library_id, db, book_id, width=None, height=None): mtime = db.cover_last_modified(book_id) if mtime is None: @@ -126,6 +130,7 @@ def cover(ctx, rd, library_id, db, book_id, width=None, height=None): db.copy_cover_to(book_id, dest) else: prefix += '-%sx%s' % (width, height) + def copy_func(dest): buf = BytesIO() db.copy_cover_to(book_id, buf) @@ -134,6 +139,7 @@ def cover(ctx, rd, library_id, db, book_id, width=None, height=None): dest.write(data) return create_file_copy(ctx, rd, prefix, library_id, book_id, 'jpg', mtime, copy_func) + def book_fmt(ctx, rd, library_id, db, book_id, fmt): mdata = db.format_metadata(book_id, fmt) if not mdata: @@ -176,6 +182,7 @@ def book_fmt(ctx, rd, library_id, db, book_id, fmt): return create_file_copy(ctx, rd, 'fmt', library_id, book_id, fmt, mtime, copy_func, extra_etag_data=extra_etag_data) # }}} + @endpoint('/static/{+what}', auth_required=False, cache_control=24) def static(ctx, rd, what): if not what: @@ -191,10 +198,12 @@ def static(ctx, rd, what): except EnvironmentError: raise HTTPNotFound() + @endpoint('/favicon.png', auth_required=False, cache_control=24) def favicon(ctx, rd): return share_open(I('lt.png'), 'rb') + @endpoint('/icon/{+which}', auth_required=False, cache_control=24) def icon(ctx, rd, which): sz = rd.query.get('sz') diff --git a/src/calibre/srv/errors.py b/src/calibre/srv/errors.py index c53480fab2..131d21b07a 100644 --- a/src/calibre/srv/errors.py +++ b/src/calibre/srv/errors.py @@ -8,12 +8,15 @@ __copyright__ = '2015, Kovid Goyal ' import httplib + class JobQueueFull(Exception): pass + class RouteError(ValueError): pass + class HTTPSimpleResponse(Exception): def __init__(self, http_code, http_message='', close_connection=False, location=None, authenticate=None, log=None): @@ -24,21 +27,25 @@ class HTTPSimpleResponse(Exception): self.authenticate = authenticate self.log = log + class HTTPRedirect(HTTPSimpleResponse): def __init__(self, location, http_code=httplib.MOVED_PERMANENTLY, http_message='', close_connection=False): HTTPSimpleResponse.__init__(self, http_code, http_message, close_connection, location) + class HTTPNotFound(HTTPSimpleResponse): def __init__(self, http_message='', close_connection=False): HTTPSimpleResponse.__init__(self, httplib.NOT_FOUND, http_message, close_connection) + class HTTPAuthRequired(HTTPSimpleResponse): def __init__(self, payload, log=None): HTTPSimpleResponse.__init__(self, httplib.UNAUTHORIZED, authenticate=payload, log=log) + class HTTPBadRequest(HTTPSimpleResponse): def __init__(self, message, close_connection=False): diff --git a/src/calibre/srv/handler.py b/src/calibre/srv/handler.py index 8a9ecba584..f816be8074 100644 --- a/src/calibre/srv/handler.py +++ b/src/calibre/srv/handler.py @@ -19,11 +19,13 @@ from calibre.srv.routes import Router from calibre.srv.users import UserManager from calibre.utils.date import utcnow + def init_library(library_path): db = Cache(create_backend(library_path)) db.init() return db + class LibraryBroker(object): def __init__(self, libraries): @@ -76,6 +78,7 @@ class LibraryBroker(object): return x.backend.library_path return {k:os.path.basename(lpath(v)) for k, v in self.lmap.iteritems()} + class Context(object): log = None @@ -178,6 +181,7 @@ class Context(object): cache[key] = old return old[1] + class Handler(object): def __init__(self, libraries, opts, testing=False): diff --git a/src/calibre/srv/http_request.py b/src/calibre/srv/http_request.py index 12e9af6c26..91d81627e4 100644 --- a/src/calibre/srv/http_request.py +++ b/src/calibre/srv/http_request.py @@ -21,6 +21,8 @@ quoted_slash = re.compile(br'%2[fF]') HTTP_METHODS = {'HEAD', 'GET', 'PUT', 'POST', 'TRACE', 'DELETE', 'OPTIONS'} # Parse URI {{{ + + def parse_request_uri(uri): """Parse a Request-URI into (scheme, authority, path). @@ -62,6 +64,7 @@ def parse_request_uri(uri): # An authority. return None, uri, None + def parse_uri(uri, parse_query=True): scheme, authority, path = parse_request_uri(uri) if path is None: @@ -110,6 +113,7 @@ decoded_headers = { uppercase_headers = {'WWW', 'TE'} + def normalize_header_name(name): parts = [x.capitalize() for x in name.split('-')] q = parts[0].upper() @@ -119,6 +123,7 @@ def normalize_header_name(name): parts[1] = 'WebSocket' return '-'.join(parts) + class HTTPHeaderParser(object): ''' @@ -186,6 +191,7 @@ class HTTPHeaderParser(object): commit() self.lines.append(line) + def read_headers(readline): p = HTTPHeaderParser() while not p.finished: @@ -193,6 +199,7 @@ def read_headers(readline): return p.hdict # }}} + class HTTPRequest(Connection): request_handler = None diff --git a/src/calibre/srv/http_response.py b/src/calibre/srv/http_response.py index a041b1f8c1..af9f8f6c92 100644 --- a/src/calibre/srv/http_response.py +++ b/src/calibre/srv/http_response.py @@ -34,11 +34,13 @@ if zlib2_err: raise RuntimeError('Failed to laod the zlib2 module with error: ' + zlib2_err) del zlib2_err + def header_list_to_file(buf): # {{{ buf.append('') return ReadOnlyFileBuffer(b''.join((x + '\r\n').encode('ascii') for x in buf)) # }}} + def parse_multipart_byterange(buf, content_type): # {{{ sep = (content_type.rsplit('=', 1)[-1]).encode('utf-8') ans = [] @@ -75,10 +77,12 @@ def parse_multipart_byterange(buf, content_type): # {{{ return ans # }}} + def parse_if_none_match(val): # {{{ return {x.strip() for x in val.split(',')} # }}} + def acceptable_encoding(val, allowed=frozenset({'gzip'})): # {{{ for x in sort_q_values(val): x = x.lower() @@ -86,6 +90,7 @@ def acceptable_encoding(val, allowed=frozenset({'gzip'})): # {{{ return x # }}} + def preferred_lang(val, get_translator_for_lang): # {{{ for x in sort_q_values(val): x = x.lower() @@ -95,6 +100,7 @@ def preferred_lang(val, get_translator_for_lang): # {{{ return 'en' # }}} + def get_ranges(headervalue, content_length): # {{{ ''' Return a list of ranges from the Range header. If this function returns an empty list, it indicates no valid range was found. ''' @@ -139,6 +145,8 @@ def get_ranges(headervalue, content_length): # {{{ # }}} # gzip transfer encoding {{{ + + def gzip_prefix(): # See http://www.gzip.org/zlib/rfc-gzip.html return b''.join(( @@ -151,6 +159,7 @@ def gzip_prefix(): b'\xff', # OS: unknown )) + def compress_readable_output(src_file, compress_level=6): crc = zlib.crc32(b"") size = 0 @@ -172,6 +181,7 @@ def compress_readable_output(src_file, compress_level=6): yield zobj.flush() + struct.pack(b" 0), root['children']) return {'root':root, 'item_map': items} + def categories_as_json(ctx, rd, db): opts = categories_settings(rd.query, db) return ctx.get_tag_browser(rd, db, opts, partial(render_categories, opts)) # Test tag browser {{{ + def dump_categories_tree(data): root, items = data['root'], data['item_map'] ans, indent = [], ' ' + def dump_node(node, level=0): item = items[node['id']] rating = item.get('avg_rating', None) or 0 @@ -536,9 +556,11 @@ def dump_categories_tree(data): [dump_node(c) for c in root['children']] return '\n'.join(ans) + def dump_tags_model(m): from PyQt5.Qt import QModelIndex, Qt ans, indent = [], ' ' + def dump_node(index, level=-1): if level > -1: ans.append(indent*level + index.data(Qt.UserRole).dump_data()) @@ -549,6 +571,7 @@ def dump_tags_model(m): dump_node(QModelIndex()) return '\n'.join(ans) + def test_tag_browser(library_path=None): ' Compare output of server and GUI tag browsers ' from calibre.library import db diff --git a/src/calibre/srv/opds.py b/src/calibre/srv/opds.py index d50dbf96c2..2e8d98b280 100644 --- a/src/calibre/srv/opds.py +++ b/src/calibre/srv/opds.py @@ -25,14 +25,17 @@ from calibre.srv.errors import HTTPNotFound from calibre.srv.routes import endpoint from calibre.srv.utils import get_library_data, http_date, Offsets + def hexlify(x): if isinstance(x, unicode): x = x.encode('utf-8') return binascii.hexlify(x) + def unhexlify(x): return binascii.unhexlify(x).decode('utf-8') + def atom(ctx, rd, endpoint, output): rd.outheaders.set('Content-Type', 'application/atom+xml; charset=UTF-8', replace_all=True) if isinstance(output, bytes): @@ -44,6 +47,7 @@ def atom(ctx, rd, endpoint, output): ans = etree.tostring(output, encoding='utf-8', xml_declaration=True, pretty_print=True) return ans + def format_tag_string(tags, sep, joinval=', '): if tags: tlist = tags if sep is None else [t.strip() for t in tags.split(sep)] @@ -67,6 +71,7 @@ TITLE = E.title ID = E.id ICON = E.icon + def UPDATED(dt, *args, **kwargs): return E.updated(as_utc(dt).strftime('%Y-%m-%dT%H:%M:%S+00:00'), *args, **kwargs) @@ -74,12 +79,14 @@ LINK = partial(E.link, type='application/atom+xml') NAVLINK = partial(E.link, type='application/atom+xml;type=feed;profile=opds-catalog') + def SEARCH_LINK(url_for, *args, **kwargs): kwargs['rel'] = 'search' kwargs['title'] = 'Search' kwargs['href'] = url_for('/opds/search', query='XXX').replace('XXX', '{searchTerms}') return LINK(*args, **kwargs) + def AUTHOR(name, uri=None): args = [E.name(name)] if uri is not None: @@ -88,6 +95,7 @@ def AUTHOR(name, uri=None): SUBTITLE = E.subtitle + def NAVCATALOG_ENTRY(url_for, updated, title, description, query): href = url_for('/opds/navcatalog', which=hexlify(query)) id_ = 'calibre-navcatalog:'+str(hashlib.sha1(href).hexdigest()) @@ -106,6 +114,7 @@ LAST_LINK = partial(NAVLINK, rel='last') NEXT_LINK = partial(NAVLINK, rel='next', title='Next') PREVIOUS_LINK = partial(NAVLINK, rel='previous') + def html_to_lxml(raw): raw = u'
%s
'%raw root = html.fragment_fromstring(raw) @@ -128,6 +137,7 @@ def html_to_lxml(raw): from calibre.ebooks.oeb.parse_utils import _html4_parse return _html4_parse(raw) + def CATALOG_ENTRY(item, item_kind, request_context, updated, catalog_name, ignore_count=False, add_kind=False): id_ = 'calibre:category:'+item.name @@ -152,6 +162,7 @@ def CATALOG_ENTRY(item, item_kind, request_context, updated, catalog_name, link ) + def CATALOG_GROUP_ENTRY(item, category, request_context, updated): id_ = 'calibre:category-group:'+category+':'+item.text iid = item.text @@ -164,6 +175,7 @@ def CATALOG_GROUP_ENTRY(item, category, request_context, updated): link ) + def ACQUISITION_ENTRY(book_id, updated, request_context): field_metadata = request_context.db.field_metadata mi = request_context.db.get_metadata(book_id) @@ -228,6 +240,7 @@ def ACQUISITION_ENTRY(book_id, updated, request_context): default_feed_title = __appname__ + ' ' + _('Library') + class Feed(object): # {{{ def __init__(self, id_, updated, request_context, subtitle=None, @@ -261,6 +274,7 @@ class Feed(object): # {{{ # }}} + class TopLevel(Feed): # {{{ def __init__(self, @@ -289,6 +303,7 @@ class TopLevel(Feed): # {{{ )) # }}} + class NavFeed(Feed): def __init__(self, id_, updated, request_context, offsets, page_url, up_url, title=None): @@ -305,6 +320,7 @@ class NavFeed(Feed): kwargs['title'] = title Feed.__init__(self, id_, updated, request_context, **kwargs) + class AcquisitionFeed(NavFeed): def __init__(self, id_, updated, request_context, items, offsets, page_url, up_url, title=None): @@ -312,6 +328,7 @@ class AcquisitionFeed(NavFeed): for book_id in items: self.root.append(ACQUISITION_ENTRY(book_id, updated, request_context)) + class CategoryFeed(NavFeed): def __init__(self, items, which, id_, updated, request_context, offsets, page_url, up_url, title=None): @@ -323,6 +340,7 @@ class CategoryFeed(NavFeed): self.root.append(CATALOG_ENTRY( item, item.category, request_context, updated, which, ignore_count=ignore_count, add_kind=which != item.category)) + class CategoryGroupFeed(NavFeed): def __init__(self, items, which, id_, updated, request_context, offsets, page_url, up_url, title=None): @@ -330,6 +348,7 @@ class CategoryGroupFeed(NavFeed): for item in items: self.root.append(CATALOG_GROUP_ENTRY(item, which, request_context, updated)) + class RequestContext(object): def __init__(self, ctx, rd): @@ -363,6 +382,7 @@ class RequestContext(object): def search(self, query): return self.ctx.search(self.rd, self.db, query) + def get_acquisition_feed(rc, ids, offset, page_url, up_url, id_, sort_by='title', ascending=True, feed_title=None): if not ids: @@ -377,6 +397,7 @@ def get_acquisition_feed(rc, ids, offset, page_url, up_url, id_, rc.outheaders['Last-Modified'] = http_date(timestampfromdt(lm)) return AcquisitionFeed(id_, lm, rc, items, offsets, page_url, up_url, title=feed_title).root + def get_all_books(rc, which, page_url, up_url, offset=0): try: offset = int(offset) @@ -439,6 +460,7 @@ def get_navcatalog(request_context, which, page_url, up_url, offset=0): return ans.root + @endpoint('/opds', postprocess=atom) def opds(ctx, rd): rc = RequestContext(ctx, rd) @@ -469,6 +491,7 @@ def opds(ctx, rd): rd.outheaders['Last-Modified'] = http_date(timestampfromdt(last_modified)) return TopLevel(last_modified, cats, rc).root + @endpoint('/opds/navcatalog/{which}', postprocess=atom) def opds_navcatalog(ctx, rd, which): try: @@ -488,6 +511,7 @@ def opds_navcatalog(ctx, rd, which): return get_navcatalog(rc, which, page_url, up_url, offset=offset) raise HTTPNotFound('Not found') + @endpoint('/opds/category/{category}/{which}', postprocess=atom) def opds_category(ctx, rd, category, which): try: @@ -565,6 +589,7 @@ def opds_categorygroup(ctx, rd, category, which): owhich = hexlify('N'+which) up_url = rc.url_for('/opds/navcatalog', which=owhich) items = categories[category] + def belongs(x, which): return getattr(x, 'sort', x.name).lower().startswith(which.lower()) items = [x for x in items if belongs(x, which)] @@ -582,6 +607,7 @@ def opds_categorygroup(ctx, rd, category, which): return CategoryFeed(items, category, id_, updated, rc, offsets, page_url, up_url, title=feed_title).root + @endpoint('/opds/search/{query=""}', postprocess=atom) def opds_search(ctx, rd, query): try: diff --git a/src/calibre/srv/opts.py b/src/calibre/srv/opts.py index 0439c13c69..cdb003ea1c 100644 --- a/src/calibre/srv/opts.py +++ b/src/calibre/srv/opts.py @@ -13,7 +13,9 @@ from functools import partial Option = namedtuple('Option', 'name default longdoc shortdoc choices') + class Choices(frozenset): + def __new__(cls, *args): self = super(Choices, cls).__new__(cls, args) self.default = args[0] @@ -154,6 +156,7 @@ assert len(raw_options) % 4 == 0 options = [] + def grouper(n, iterable, fillvalue=None): "grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx" args = [iter(iterable)] * n @@ -168,6 +171,7 @@ for shortdoc, name, default, doc in grouper(4, raw_options): options = OrderedDict([(o.name, o) for o in sorted(options, key=attrgetter('name'))]) del raw_options + class Options(object): __slots__ = tuple(name for name in options) @@ -176,6 +180,7 @@ class Options(object): for opt in options.itervalues(): setattr(self, opt.name, kwargs.get(opt.name, opt.default)) + def opt_to_cli_help(opt): ans = opt.shortdoc if not ans.endswith('.'): @@ -184,12 +189,14 @@ def opt_to_cli_help(opt): ans += '\n\t' + opt.longdoc return ans + def boolean_option(add_option, opt): name = opt.name.replace('_', '-') help = opt_to_cli_help(opt) add_option('--enable-' + name, action='store_true', help=help) add_option('--disable-' + name, action='store_false', help=help) + def opts_to_parser(usage): from calibre.utils.config import OptionParser parser = OptionParser(usage) diff --git a/src/calibre/srv/pool.py b/src/calibre/srv/pool.py index bb2e2e626a..f8bcaca019 100644 --- a/src/calibre/srv/pool.py +++ b/src/calibre/srv/pool.py @@ -12,6 +12,7 @@ from threading import Thread from calibre.utils.monotonic import monotonic + class Worker(Thread): daemon = True @@ -46,6 +47,7 @@ class Worker(Thread): def handle_error(self, job_id): self.result_queue.put((job_id, False, sys.exc_info())) + class ThreadPool(object): def __init__(self, log, notify_server, count=10, queue_size=1000): @@ -83,6 +85,7 @@ class ThreadPool(object): def idle(self): return sum(int(not w.working) for w in self.workers) + class PluginPool(object): def __init__(self, loop, plugins): diff --git a/src/calibre/srv/pre_activated.py b/src/calibre/srv/pre_activated.py index ec0274885c..66b1b4161d 100644 --- a/src/calibre/srv/pre_activated.py +++ b/src/calibre/srv/pre_activated.py @@ -11,6 +11,7 @@ __copyright__ = '2015, Kovid Goyal ' import socket, errno from calibre.constants import islinux + def pre_activated_socket(): return None has_preactivated_support = False @@ -39,6 +40,7 @@ if islinux: else: del pre_activated_socket has_preactivated_support = True + def pre_activated_socket(): # noqa num = systemd.sd_listen_fds(1) # Remove systemd env vars so that child processes do not inherit them if num > 1: diff --git a/src/calibre/srv/render_book.py b/src/calibre/srv/render_book.py index 272212ebbd..bf1be52618 100644 --- a/src/calibre/srv/render_book.py +++ b/src/calibre/srv/render_book.py @@ -35,18 +35,22 @@ RENDER_VERSION = 1 BLANK_JPEG = b'\xff\xd8\xff\xdb\x00C\x00\x03\x02\x02\x02\x02\x02\x03\x02\x02\x02\x03\x03\x03\x03\x04\x06\x04\x04\x04\x04\x04\x08\x06\x06\x05\x06\t\x08\n\n\t\x08\t\t\n\x0c\x0f\x0c\n\x0b\x0e\x0b\t\t\r\x11\r\x0e\x0f\x10\x10\x11\x10\n\x0c\x12\x13\x12\x10\x13\x0f\x10\x10\x10\xff\xc9\x00\x0b\x08\x00\x01\x00\x01\x01\x01\x11\x00\xff\xcc\x00\x06\x00\x10\x10\x05\xff\xda\x00\x08\x01\x01\x00\x00?\x00\xd2\xcf \xff\xd9' # noqa + def encode_component(x): return standard_b64encode(x.encode('utf-8')).decode('ascii') + def decode_component(x): return standard_b64decode(x).decode('utf-8') + def encode_url(name, frag=''): name = encode_component(name) if frag: name += '#' + frag return name + def decode_url(x): parts = x.split('#', 1) return decode_component(parts[0]), (parts[1] if len(parts) > 1 else '') @@ -54,6 +58,7 @@ def decode_url(x): absolute_units = frozenset('px mm cm pt in pc q'.split()) length_factors = {'mm':2.8346456693, 'cm':28.346456693, 'in': 72, 'pc': 12, 'q':0.708661417325} + def convert_fontsize(length, unit, base_font_size=16.0, dpi=96.0): ' Convert font size to rem so that font size scaling works. Assumes the document has the specified base font size in px ' if unit == 'px': @@ -62,6 +67,7 @@ def convert_fontsize(length, unit, base_font_size=16.0, dpi=96.0): pt_to_rem = pt_to_px / base_font_size return length * length_factors.get(unit, 1) * pt_to_rem + def transform_declaration(decl): decl = StyleDeclaration(decl) changed = False @@ -82,6 +88,7 @@ def transform_declaration(decl): decl.change_property(prop, parent_prop, str(l) + 'rem') return changed + def transform_sheet(sheet): changed = False for rule in sheet.cssRules.rulesOfType(CSSRule.STYLE_RULE): @@ -89,6 +96,7 @@ def transform_sheet(sheet): changed = True return changed + def check_for_maths(root): for x in root.iterdescendants('{*}math'): return True @@ -97,6 +105,7 @@ def check_for_maths(root): return True return False + def has_ancestor(elem, q): while elem is not None: elem = elem.getparent() @@ -104,9 +113,11 @@ def has_ancestor(elem, q): return True return False + def get_length(root): strip_space = re.compile(r'\s+') ans = 0 + def count(elem): num = 0 tname = elem.tag.rpartition('}')[-1].lower() @@ -124,6 +135,7 @@ def get_length(root): ans += count(elem) return ans + class Container(ContainerBase): tweak_mode = True @@ -158,6 +170,7 @@ class Container(ContainerBase): self.transform_css() self.virtualized_names = set() self.virtualize_resources() + def manifest_data(name): mt = (self.mime_map.get(name) or 'application/octet-stream').lower() ans = { @@ -313,6 +326,7 @@ class Container(ContainerBase): root = self.parsed(name) return json.dumps(html_as_dict(root), ensure_ascii=False, separators=(',', ':')).encode('utf-8') + def split_name(name): l, r = name.partition('}')[::2] if r: @@ -330,6 +344,7 @@ for k in 'figure term definition directory list list-item table row cell'.split( EPUB_TYPE_MAP['help'] = 'doc-tip' + def map_epub_type(epub_type, attribs, elem): val = EPUB_TYPE_MAP.get(epub_type.lower()) if val: @@ -351,6 +366,7 @@ def map_epub_type(epub_type, attribs, elem): else: attribs[i] = ['role', role] + def serialize_elem(elem, nsmap): ns, name = split_name(elem.tag) nl = name.lower() @@ -390,6 +406,7 @@ def serialize_elem(elem, nsmap): ans['a'] = attribs return ans + def ensure_head(root): # Make sure we have only a single heads = list(root.iterchildren(XHTML('head'))) @@ -405,6 +422,7 @@ def ensure_head(root): return head return heads[0] + def ensure_body(root): # Make sure we have only a single bodies = list(root.iterchildren(XHTML('body'))) @@ -421,6 +439,7 @@ def ensure_body(root): div.append(child) body.append(div) + def html_as_dict(root): ensure_body(root) for child in tuple(root.iterchildren('*')): @@ -444,6 +463,7 @@ def html_as_dict(root): ns_map = [ns for ns, nsnum in sorted(nsmap.iteritems(), key=lambda x: x[1])] return {'ns_map':ns_map, 'tag_map':tags, 'tree':tree} + def render(pathtoebook, output_dir, book_hash=None): Container(pathtoebook, output_dir, book_hash=book_hash) diff --git a/src/calibre/srv/routes.py b/src/calibre/srv/routes.py index 1da306a659..a024898a73 100644 --- a/src/calibre/srv/routes.py +++ b/src/calibre/srv/routes.py @@ -16,6 +16,7 @@ from calibre.srv.utils import http_date default_methods = frozenset(('HEAD', 'GET')) + def json(ctx, rd, endpoint, output): rd.outheaders.set('Content-Type', 'application/json; charset=UTF-8', replace_all=True) if isinstance(output, bytes) or hasattr(output, 'fileno'): @@ -26,9 +27,11 @@ def json(ctx, rd, endpoint, output): ans = ans.encode('utf-8') return ans + def route_key(route): return route.partition('{')[0].rstrip('/') + def endpoint(route, methods=default_methods, types=None, @@ -50,6 +53,7 @@ def endpoint(route, ): from calibre.srv.handler import Context from calibre.srv.http_response import RequestData + def annotate(f): f.route = route.rstrip('/') or '/' f.route_key = route_key(f.route) @@ -75,6 +79,7 @@ def endpoint(route, return f return annotate + class Route(object): var_pat = None @@ -92,6 +97,7 @@ class Route(object): found_optional_part = False self.soak_up_extra = False self.type_checkers = self.endpoint.types.copy() + def route_error(msg): return RouteError('%s is not valid: %s' % (self.endpoint.route, msg)) @@ -153,6 +159,7 @@ class Route(object): num = len(path) if num < len(path): return False + def check(tc, val): try: return tc(val) @@ -170,6 +177,7 @@ class Route(object): unknown = names - self.all_names if unknown: raise RouteError('The variable(s) %s are not part of the route: %s' % (','.join(unknown), self.endpoint.route)) + def quoted(x): if not isinstance(x, unicode) and not isinstance(x, bytes): x = unicode(x) diff --git a/src/calibre/srv/sendfile.py b/src/calibre/srv/sendfile.py index ade243e50b..54a51776ce 100644 --- a/src/calibre/srv/sendfile.py +++ b/src/calibre/srv/sendfile.py @@ -13,6 +13,7 @@ from select import select from calibre.constants import islinux, isosx from calibre.srv.utils import eintr_retry_call + def file_metadata(fileobj): try: fd = fileobj.fileno() @@ -20,6 +21,7 @@ def file_metadata(fileobj): except Exception: pass + def copy_range(src_file, start, size, dest): total_sent = 0 src_file.seek(start) @@ -33,9 +35,11 @@ def copy_range(src_file, start, size, dest): del data return total_sent + class CannotSendfile(Exception): pass + class SendfileInterrupted(Exception): pass diff --git a/src/calibre/srv/standalone.py b/src/calibre/srv/standalone.py index ebce0ed671..6dfb86568d 100644 --- a/src/calibre/srv/standalone.py +++ b/src/calibre/srv/standalone.py @@ -19,6 +19,7 @@ from calibre.srv.utils import RotatingLog from calibre.utils.config import prefs from calibre.db.legacy import LibraryDatabase + def daemonize(): # {{{ try: pid = os.fork() @@ -54,6 +55,7 @@ def daemonize(): # {{{ os.dup2(se, sys.stderr.fileno()) # }}} + class Server(object): def __init__(self, libraries, opts): @@ -78,6 +80,8 @@ class Server(object): compile_srv() # Manage users CLI {{{ + + def manage_users(path=None): from calibre.srv.users import UserManager m = UserManager(path) @@ -207,6 +211,7 @@ program will be used. return parser + def main(args=sys.argv): opts, args=create_option_parser().parse_args(args) if opts.manage_users: diff --git a/src/calibre/srv/tests/ajax.py b/src/calibre/srv/tests/ajax.py index 90e3843f44..4d659056d3 100644 --- a/src/calibre/srv/tests/ajax.py +++ b/src/calibre/srv/tests/ajax.py @@ -12,6 +12,7 @@ from urllib import urlencode from calibre.srv.tests.base import LibraryBaseTest + def make_request(conn, url, headers={}, prefix='/ajax'): conn.request('GET', prefix + url, headers=headers) r = conn.getresponse() @@ -20,6 +21,7 @@ def make_request(conn, url, headers={}, prefix='/ajax'): data = json.loads(data) return r, data + class ContentTest(LibraryBaseTest): def test_ajax_book(self): # {{{ diff --git a/src/calibre/srv/tests/auth.py b/src/calibre/srv/tests/auth.py index e56b06a5d0..0d1963df14 100644 --- a/src/calibre/srv/tests/auth.py +++ b/src/calibre/srv/tests/auth.py @@ -17,34 +17,41 @@ from calibre.srv.routes import endpoint, Router REALM = 'calibre-test' + @endpoint('/open', auth_required=False) def noauth(ctx, data): return 'open' + @endpoint('/closed', auth_required=True) def auth(ctx, data): return 'closed' + @endpoint('/android', auth_required=True, android_workaround=True) def android(ctx, data): return 'android' + @endpoint('/android2', auth_required=True, android_workaround=True) def android2(ctx, data): return 'android2' + def router(prefer_basic_auth=False): from calibre.srv.auth import AuthController return Router(globals().itervalues(), auth_controller=AuthController( {'testuser':'testpw', '!@#$%^&*()-=_+':'!@#$%^&*()-=_+'}, prefer_basic_auth=prefer_basic_auth, realm=REALM, max_age_seconds=1)) + def urlopen(server, path='/closed', un='testuser', pw='testpw', method='digest'): auth_handler = urllib2.HTTPBasicAuthHandler() if method == 'basic' else urllib2.HTTPDigestAuthHandler() url = 'http://localhost:%d%s' % (server.address[1], path) auth_handler.add_password(realm=REALM, uri=url, user=un, passwd=pw) return urllib2.build_opener(auth_handler).open(url) + def digest(un, pw, nonce=None, uri=None, method='GET', nc=1, qop='auth', realm=REALM, cnonce=None, algorithm='MD5', body=b'', modify=lambda x:None): 'Create the payload for a digest based Authorization header' from calibre.srv.auth import DigestAuth @@ -54,15 +61,19 @@ def digest(un, pw, nonce=None, uri=None, method='GET', nc=1, qop='auth', realm=R da = DigestAuth(h) modify(da) pw = getattr(da, 'pw', pw) + class Data(object): + def __init__(self): self.method = method + def peek(): return body response = da.request_digest(pw, Data()) return ('Digest ' + templ.format( un=un, realm=realm, qop=qop, uri=uri, method=method, nonce=nonce, nc=nc, cnonce=cnonce, algorithm=algorithm, response=response)).encode('ascii') + class TestAuth(BaseTest): def test_basic_auth(self): # {{{ @@ -109,6 +120,7 @@ class TestAuth(BaseTest): r = router() with TestServer(r.dispatch) as server: r.auth_controller.log = server.log + def test(conn, path, headers={}, status=httplib.OK, body=b'', request_body=b''): conn.request('GET', path, request_body, headers) r = conn.getresponse() diff --git a/src/calibre/srv/tests/base.py b/src/calibre/srv/tests/base.py index 622016118b..85795aa2cd 100644 --- a/src/calibre/srv/tests/base.py +++ b/src/calibre/srv/tests/base.py @@ -16,6 +16,7 @@ from calibre.srv.utils import ServerLog rmtree = partial(shutil.rmtree, ignore_errors=True) + class BaseTest(unittest.TestCase): longMessage = True @@ -23,6 +24,7 @@ class BaseTest(unittest.TestCase): ae = unittest.TestCase.assertEqual + class LibraryBaseTest(BaseTest): def setUp(self): @@ -69,6 +71,7 @@ class LibraryBaseTest(BaseTest): args = (self.library_path ,) + args return LibraryServer(*args, **kwargs) + class TestServer(Thread): daemon = True @@ -120,6 +123,7 @@ class TestServer(Thread): from calibre.srv.http_response import create_http_handler self.loop.handler = create_http_handler(handler) + class LibraryServer(TestServer): def __init__(self, library_path, libraries=(), plugins=(), specialize=lambda x:None, **kwargs): diff --git a/src/calibre/srv/tests/content.py b/src/calibre/srv/tests/content.py index cd71287863..e7b1950eec 100644 --- a/src/calibre/srv/tests/content.py +++ b/src/calibre/srv/tests/content.py @@ -15,11 +15,13 @@ from calibre.srv.tests.base import LibraryBaseTest from calibre.utils.imghdr import identify from calibre.utils.shared_file import share_open + def setUpModule(): # Needed for cover generation from calibre.gui2 import ensure_app, load_builtin_fonts ensure_app(), load_builtin_fonts() + class ContentTest(LibraryBaseTest): def test_static(self): # {{{ diff --git a/src/calibre/srv/tests/http.py b/src/calibre/srv/tests/http.py index 48e1271ce6..6510ae6904 100644 --- a/src/calibre/srv/tests/http.py +++ b/src/calibre/srv/tests/http.py @@ -16,6 +16,7 @@ from calibre.utils.monotonic import monotonic is_ci = os.environ.get('CI', '').lower() == 'true' + class TestHTTP(BaseTest): def test_header_parsing(self): # {{{ @@ -59,6 +60,7 @@ class TestHTTP(BaseTest): def test_accept_encoding(self): # {{{ 'Test parsing of Accept-Encoding' from calibre.srv.http_response import acceptable_encoding + def test(name, val, ans, allowed={'gzip'}): self.ae(acceptable_encoding(val, allowed), ans, name + ' failed') test('Empty field', '', None) @@ -72,6 +74,7 @@ class TestHTTP(BaseTest): 'Test parsing of Accept-Language' from calibre.srv.http_response import preferred_lang from calibre.utils.localization import get_translator + def test(name, val, ans): self.ae(preferred_lang(val, lambda x:(True, x, None)), ans, name + ' failed') test('Empty field', '', 'en') @@ -102,6 +105,7 @@ class TestHTTP(BaseTest): def test_range_parsing(self): # {{{ 'Test parsing of Range header' from calibre.srv.http_response import get_ranges + def test(val, *args): pval = get_ranges(val, 100) if len(args) == 1 and args[0] is None: @@ -124,8 +128,10 @@ class TestHTTP(BaseTest): 'Test basic HTTP protocol conformance' from calibre.srv.errors import HTTPNotFound, HTTPRedirect body = 'Requested resource not found' + def handler(data): raise HTTPNotFound(body) + def raw_send(conn, raw): conn.send(raw) conn._HTTPConnection__state = httplib._CS_REQ_SENT @@ -292,6 +298,7 @@ class TestHTTP(BaseTest): def test_http_response(self): # {{{ 'Test HTTP protocol responses' from calibre.srv.http_response import parse_multipart_byterange + def handler(conn): return conn.generate_static_output('test', lambda : ''.join(conn.path)) with NamedTemporaryFile(suffix='test.epub') as f, open(P('localization/locales.zip'), 'rb') as lf, \ @@ -322,6 +329,7 @@ class TestHTTP(BaseTest): # Test dynamic etagged content num_calls = [0] + def edfunc(): num_calls[0] += 1 return b'data' @@ -411,6 +419,7 @@ class TestHTTP(BaseTest): def test_static_generation(self): # {{{ 'Test static generation' nums = list(map(str, xrange(10))) + def handler(conn): return conn.generate_static_output('test', nums.pop) with TestServer(handler) as server: diff --git a/src/calibre/srv/tests/loop.py b/src/calibre/srv/tests/loop.py index 555d5253fd..cba95a4a3e 100644 --- a/src/calibre/srv/tests/loop.py +++ b/src/calibre/srv/tests/loop.py @@ -23,6 +23,7 @@ from calibre.ptempfile import TemporaryDirectory from calibre.utils.monotonic import monotonic is_ci = os.environ.get('CI', '').lower() == 'true' + class LoopTest(BaseTest): def test_log_rotation(self): @@ -56,15 +57,18 @@ class LoopTest(BaseTest): def test_plugins(self): 'Test plugin semantics' class Plugin(object): + def __init__(self): self.running = Event() self.event = Event() self.port = None + def start(self, loop): self.running.set() self.port = loop.bound_address[1] self.event.wait() self.running.clear() + def stop(self): self.event.set() @@ -124,6 +128,7 @@ class LoopTest(BaseTest): def test_ring_buffer(self): 'Test the ring buffer used for reads' class FakeSocket(object): + def __init__(self, data): self.data = data @@ -133,8 +138,10 @@ class LoopTest(BaseTest): return sz from calibre.srv.loop import ReadBuffer, READ, WRITE buf = ReadBuffer(100) + def write(data): return buf.recv_from(FakeSocket(data)) + def set(data, rpos, wpos, state): buf.ba = bytearray(data) buf.buf = memoryview(buf.ba) @@ -227,11 +234,14 @@ class LoopTest(BaseTest): 'Test the jobs manager' from calibre.srv.jobs import JobsManager O = namedtuple('O', 'max_jobs max_job_time') + class FakeLog(list): + def error(self, *args): self.append(' '.join(args)) s = ('waiting', 'running') jm = JobsManager(O(1, 5), FakeLog()) + def job_status(jid): return jm.job_status(jid)[0] diff --git a/src/calibre/srv/tests/main.py b/src/calibre/srv/tests/main.py index f0be245ae4..c8277cd17c 100644 --- a/src/calibre/srv/tests/main.py +++ b/src/calibre/srv/tests/main.py @@ -9,6 +9,7 @@ __copyright__ = '2015, Kovid Goyal ' import os from calibre.utils.run_tests import find_tests_in_dir, run_tests + def find_tests(): base = os.path.dirname(os.path.abspath(__file__)) return find_tests_in_dir(base) diff --git a/src/calibre/srv/tests/routes.py b/src/calibre/srv/tests/routes.py index 29d6ed293a..9b7bdf9210 100644 --- a/src/calibre/srv/tests/routes.py +++ b/src/calibre/srv/tests/routes.py @@ -8,11 +8,13 @@ __copyright__ = '2015, Kovid Goyal ' from calibre.srv.tests.base import BaseTest + class TestRouter(BaseTest): def test_route_construction(self): ' Test route construction ' from calibre.srv.routes import Route, endpoint, RouteError + def makeroute(route, func=lambda c,d:None, **kwargs): return Route(endpoint(route, **kwargs)(func)) diff --git a/src/calibre/srv/tests/web_sockets.py b/src/calibre/srv/tests/web_sockets.py index 7479643164..af7a01e635 100644 --- a/src/calibre/srv/tests/web_sockets.py +++ b/src/calibre/srv/tests/web_sockets.py @@ -27,6 +27,7 @@ Sec-WebSocket-Version: 13\r Frame = namedtuple('Frame', 'fin opcode payload') + class WSClient(object): def __init__(self, port, timeout=5): @@ -164,6 +165,7 @@ class WSTestServer(TestServer): def connect(self): return WSClient(self.address[1]) + class WebSocketTest(BaseTest): def simple_test(self, server, msgs, expected=(), close_code=NORMAL_CLOSE, send_close=True, close_reason=b'NORMAL CLOSE', ignore_send_failures=False): diff --git a/src/calibre/srv/users.py b/src/calibre/srv/users.py index 651d9fda58..118d201302 100644 --- a/src/calibre/srv/users.py +++ b/src/calibre/srv/users.py @@ -12,15 +12,18 @@ import apsw from calibre.constants import config_dir from calibre.utils.config import to_json, from_json + def as_json(data): return json.dumps(data, ensure_ascii=False, default=to_json) + def load_json(raw): try: return json.loads(raw, object_hook=from_json) except Exception: return {} + class UserManager(object): lock = RLock() diff --git a/src/calibre/srv/utils.py b/src/calibre/srv/utils.py index 725ef94907..f98d3ad69e 100644 --- a/src/calibre/srv/utils.py +++ b/src/calibre/srv/utils.py @@ -30,9 +30,11 @@ HTTP1 = 'HTTP/1.0' HTTP11 = 'HTTP/1.1' DESIRED_SEND_BUFFER_SIZE = 16 * 1024 # windows 7 uses an 8KB sndbuf + def http_date(timeval=None): return type('')(formatdate(timeval=timeval, usegmt=True)) + class MultiDict(dict): # {{{ def __setitem__(self, key, val): @@ -105,6 +107,7 @@ class MultiDict(dict): # {{{ '%s: %s' % (k, (repr(v) if isinstance(v, bytes) else v)) for k, v in sorted(self.items(), key=itemgetter(0))) # }}} + def error_codes(*errnames): ''' Return error numbers for error names, ignoring non-existent names ''' ans = {getattr(errno, x, None) for x in errnames} @@ -129,14 +132,17 @@ socket_errors_socket_closed = error_codes( # errors indicating a disconnected c socket_errors_nonblocking = error_codes( 'EAGAIN', 'EWOULDBLOCK', 'WSAEWOULDBLOCK') + def start_cork(sock): if hasattr(socket, 'TCP_CORK'): sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_CORK, 1) + def stop_cork(sock): if hasattr(socket, 'TCP_CORK'): sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_CORK, 0) + def create_sock_pair(port=0): '''Create socket pair. Works also on windows by using an ephemeral TCP port.''' if hasattr(socket, 'socketpair'): @@ -174,6 +180,7 @@ def create_sock_pair(port=0): return client_sock, srv_sock + def parse_http_list(header_val): """Parse lists as described by RFC 2068 Section 2. @@ -219,6 +226,7 @@ def parse_http_list(header_val): if part: yield part.strip() + def parse_http_dict(header_val): 'Parse an HTTP comma separated header with items of the form a=1, b="xxx" into a dictionary' if not header_val: @@ -233,10 +241,12 @@ def parse_http_dict(header_val): ans[k] = v return ans + def sort_q_values(header_val): 'Get sorted items from an HTTP header of type: a;q=0.5, b;q=0.7...' if not header_val: return [] + def item(x): e, r = x.partition(';')[::2] p, v = r.partition('=')[::2] @@ -249,6 +259,7 @@ def sort_q_values(header_val): return e.strip(), q return tuple(map(itemgetter(0), sorted(map(item, parse_http_list(header_val)), key=itemgetter(1), reverse=True))) + def eintr_retry_call(func, *args, **kwargs): while True: try: @@ -258,6 +269,7 @@ def eintr_retry_call(func, *args, **kwargs): continue raise + def get_translator_for_lang(cache, bcp_47_code): try: return cache[bcp_47_code] @@ -266,19 +278,23 @@ def get_translator_for_lang(cache, bcp_47_code): cache[bcp_47_code] = ans = get_translator(bcp_47_code) return ans + def encode_path(*components): 'Encode the path specified as a list of path components using URL encoding' return '/' + '/'.join(urlquote(x.encode('utf-8'), '').decode('ascii') for x in components) + def encode_name(name): 'Encode a name (arbitrary string) as URL safe characters. See decode_name() also.' if isinstance(name, unicode): name = name.encode('utf-8') return hexlify(name) + def decode_name(name): return unhexlify(name).decode('utf-8') + class Cookie(SimpleCookie): def _BaseCookie__set(self, key, real_value, coded_value): @@ -286,6 +302,7 @@ class Cookie(SimpleCookie): key = key.encode('ascii') # Python 2.x cannot handle unicode keys return SimpleCookie._BaseCookie__set(self, key, real_value, coded_value) + def custom_fields_to_display(db): ckeys = set(db.field_metadata.ignorable_field_keys()) yes_fields = set(tweaks['content_server_will_display']) @@ -298,9 +315,11 @@ def custom_fields_to_display(db): # Logging {{{ + class ServerLog(ThreadSafeLog): exception_traceback_level = ThreadSafeLog.WARN + class RotatingStream(object): def __init__(self, filename, max_size=None, history=5): @@ -356,6 +375,7 @@ class RotatingStream(object): self.rename(self.filename, '%s.%d' % (self.filename, 1)) self.set_output() + class RotatingLog(ServerLog): def __init__(self, filename, max_size=None, history=5): @@ -367,6 +387,7 @@ class RotatingLog(ServerLog): o.flush() # }}} + class HandleInterrupt(object): # {{{ # On windows socket functions like accept(), recv(), send() are not @@ -415,6 +436,7 @@ class HandleInterrupt(object): # {{{ raise WindowsError() # }}} + class Accumulator(object): # {{{ 'Optimized replacement for BytesIO when the usage pattern is many writes followed by a single getvalue()' @@ -434,12 +456,14 @@ class Accumulator(object): # {{{ return ans # }}} + def get_db(ctx, rd, library_id): db = ctx.get_library(rd, library_id) if db is None: raise HTTPNotFound('Library %r not found' % library_id) return db + def get_library_data(ctx, rd): library_id = rd.query.get('library_id') library_map, default_library = ctx.library_info(rd) @@ -448,6 +472,7 @@ def get_library_data(ctx, rd): db = get_db(ctx, rd, library_id) return db, library_id, library_map, default_library + class Offsets(object): 'Calculate offsets for a paginated view' @@ -472,6 +497,7 @@ class Offsets(object): _use_roman = None + def get_use_roman(): global _use_roman if _use_roman is None: diff --git a/src/calibre/srv/web_socket.py b/src/calibre/srv/web_socket.py index d92cb39349..7305737417 100644 --- a/src/calibre/srv/web_socket.py +++ b/src/calibre/srv/web_socket.py @@ -56,6 +56,7 @@ UNEXPECTED_ERROR = 1011 RESERVED_CLOSE_CODES = (1004,1005,1006,) + class ReadFrame(object): # {{{ def __init__(self): @@ -176,6 +177,7 @@ class ReadFrame(object): # {{{ # Sending frames {{{ + def create_frame(fin, opcode, payload, mask=None, rsv=0): if isinstance(payload, type('')): payload = payload.encode('utf-8') @@ -235,6 +237,7 @@ class MessageWriter(object): conn_id = 0 + class UTF8Decoder(object): # {{{ def __init__(self): @@ -249,6 +252,7 @@ class UTF8Decoder(object): # {{{ self.codep = 0 # }}} + class WebSocketConnection(HTTPConnection): # Internal API {{{ @@ -528,6 +532,7 @@ class DummyHandler(object): # Run this file with calibre-debug and use wstest to run the Autobahn test # suite + class EchoHandler(object): def __init__(self, *args, **kwargs): @@ -551,6 +556,7 @@ class EchoHandler(object): def handle_websocket_close(self, connection_id): self.ws_connections.pop(connection_id, None) + def run_echo_server(): s = ServerLoop(create_http_handler(websocket_handler=EchoHandler())) with HandleInterrupt(s.wakeup): diff --git a/src/calibre/startup.py b/src/calibre/startup.py index c9362c9231..96db4d5ae4 100644 --- a/src/calibre/startup.py +++ b/src/calibre/startup.py @@ -117,6 +117,7 @@ if not _run_once: elif isosx: import fcntl FIOCLEX = 0x20006601 + def local_open(name, mode='r', bufsize=-1): ans = open(name, mode, bufsize) try: @@ -131,6 +132,7 @@ if not _run_once: except AttributeError: cloexec_flag = 1 supports_mode_e = False + def local_open(name, mode='r', bufsize=-1): global supports_mode_e mode += 'e' @@ -163,6 +165,7 @@ if not _run_once: pthread_setname_np.argtypes = [ctypes.c_void_p, ctypes.c_char_p] pthread_setname_np.restype = ctypes.c_int orig_start = threading.Thread.start + def new_start(self): orig_start(self) try: @@ -181,6 +184,7 @@ if not _run_once: pass # Don't care about failure to set name threading.Thread.start = new_start + def test_lopen(): from calibre.ptempfile import TemporaryDirectory from calibre import CurrentDir @@ -189,6 +193,7 @@ def test_lopen(): if iswindows: import msvcrt, win32api + def assert_not_inheritable(f): if win32api.GetHandleInformation(msvcrt.get_osfhandle(f.fileno())) & 0b1: raise SystemExit('File handle is inheritable!') diff --git a/src/calibre/test_build.py b/src/calibre/test_build.py index a345bf39d8..85829e526c 100644 --- a/src/calibre/test_build.py +++ b/src/calibre/test_build.py @@ -16,6 +16,7 @@ import os, ctypes, sys, unittest from calibre.constants import plugins, iswindows, islinux, isosx is_ci = os.environ.get('CI', '').lower() == 'true' + class BuildTest(unittest.TestCase): @unittest.skipUnless(iswindows and not is_ci, 'DLL loading needs testing only on windows (non-continuous integration)') @@ -230,6 +231,7 @@ class BuildTest(unittest.TestCase): if not cafile or not cafile.endswith('/mozilla-ca-certs.pem') or not os.access(cafile, os.R_OK): self.assert_('Mozilla CA certs not loaded') + def find_tests(): ans = unittest.defaultTestLoader.loadTestsFromTestCase(BuildTest) from calibre.utils.icu_test import find_tests @@ -240,6 +242,7 @@ def find_tests(): ans.addTests(find_tests()) return ans + def test(): from calibre.utils.run_tests import run_cli run_cli(find_tests()) diff --git a/src/calibre/translations/__init__.py b/src/calibre/translations/__init__.py index 16e24e0f43..0d21249f3b 100644 --- a/src/calibre/translations/__init__.py +++ b/src/calibre/translations/__init__.py @@ -58,6 +58,7 @@ def import_from_launchpad(url): subprocess.check_call('python setup.py translations'.split(), cwd=path) return 0 + def check_for_critical_bugs(): if os.path.exists('.errors'): shutil.rmtree('.errors') diff --git a/src/calibre/translations/dynamic.py b/src/calibre/translations/dynamic.py index e179b69f90..ff50a6609b 100644 --- a/src/calibre/translations/dynamic.py +++ b/src/calibre/translations/dynamic.py @@ -14,6 +14,7 @@ __all__ = ['translate'] _CACHE = {} + def translate(lang, text): trans = None if lang in _CACHE: diff --git a/src/calibre/translations/msgfmt.py b/src/calibre/translations/msgfmt.py index eeffc87429..d52d1c9788 100644 --- a/src/calibre/translations/msgfmt.py +++ b/src/calibre/translations/msgfmt.py @@ -35,6 +35,7 @@ __version__ = "1.1" MESSAGES = {} STATS = {'translated': 0, 'untranslated': 0} + def usage(code, msg=''): print >> sys.stderr, __doc__ if msg: diff --git a/src/calibre/utils/Zeroconf.py b/src/calibre/utils/Zeroconf.py index 46bbcc335d..60192897a9 100755 --- a/src/calibre/utils/Zeroconf.py +++ b/src/calibre/utils/Zeroconf.py @@ -185,49 +185,61 @@ _TYPES = {_TYPE_A : "a", # utility functions + def currentTimeMillis(): """Current system time in milliseconds""" return time.time() * 1000 + def ntop(address): """Convert address to its string representation""" af = len(address) == 4 and socket.AF_INET or socket.AF_INET6 return socket.inet_ntop(af, address) + def address_type(address): """Return appropriate record type for an address""" return len(address) == 4 and _TYPE_A or _TYPE_AAAA # Exceptions + class MalformedPacketException(Exception): pass + class NonLocalNameException(Exception): pass + class NonUniqueNameException(Exception): pass + class NamePartTooLongException(Exception): pass + class AbstractMethodException(Exception): pass + class BadTypeInNameException(Exception): pass + class BadDomainName(Exception): def __init__(self, pos): Exception.__init__(self, "at position " + str(pos)) + class BadDomainNameCircular(BadDomainName): pass # implementation classes + class DNSEntry(object): """A DNS entry""" @@ -277,6 +289,7 @@ class DNSEntry(object): result += "]" return result + class DNSQuestion(DNSEntry): """A DNS question entry""" @@ -357,6 +370,7 @@ class DNSRecord(DNSEntry): arg = "%s/%s,%s" % (self.ttl, self.getRemainingTTL(currentTimeMillis()), other) return DNSEntry.toString(self, "record", arg) + class DNSAddress(DNSRecord): """A DNS address record""" @@ -382,6 +396,7 @@ class DNSAddress(DNSRecord): except: return 'record[%s]' % self.address + class DNSHinfo(DNSRecord): """A DNS host information record""" @@ -406,6 +421,7 @@ class DNSHinfo(DNSRecord): """String representation""" return self.cpu + " " + self.os + class DNSPointer(DNSRecord): """A DNS pointer record""" @@ -428,6 +444,7 @@ class DNSPointer(DNSRecord): """String representation""" return self.toString(self.alias) + class DNSText(DNSRecord): """A DNS text record""" @@ -453,6 +470,7 @@ class DNSText(DNSRecord): else: return self.toString(self.text) + class DNSService(DNSRecord): """A DNS service record""" @@ -481,6 +499,7 @@ class DNSService(DNSRecord): """String representation""" return self.toString("%s:%s" % (self.server, self.port)) + class DNSIncoming(object): """Object representation of an incoming DNS packet""" @@ -929,6 +948,7 @@ class Engine(threading.Thread): self.condition.notify() self.condition.release() + class Listener(object): """A Listener is used by this module to listen on the multicast @@ -1284,6 +1304,7 @@ class Zeroconf(object): Supports registration, unregistration, queries and browsing. """ + def __init__(self, bindaddress=None): """Creates an instance of the Zeroconf class, establishing multicast communications, listening and reaping threads.""" diff --git a/src/calibre/utils/__init__.py b/src/calibre/utils/__init__.py index c46b374080..6c0ce113ff 100644 --- a/src/calibre/utils/__init__.py +++ b/src/calibre/utils/__init__.py @@ -9,6 +9,7 @@ Miscelleaneous utilities. from time import time + def join_with_timeout(q, timeout=2): ''' Join the queue q with a specified timeout. Blocks until all tasks on the queue are done or times out with a runtime error. ''' diff --git a/src/calibre/utils/apsw_shell.py b/src/calibre/utils/apsw_shell.py index 846e0f3368..ba2fb2bd91 100644 --- a/src/calibre/utils/apsw_shell.py +++ b/src/calibre/utils/apsw_shell.py @@ -849,6 +849,7 @@ Enter SQL statements terminated with a ";" cur=self.db.cursor() # we need to know when each new statement is executed state={'newsql': True, 'timing': None} + def et(cur, sql, bindings): state['newsql']=True # if time reporting, do so now @@ -1048,6 +1049,7 @@ Enter SQL statements terminated with a ";" # prefer not to emit them v={"virtuals": False, "foreigns": False} + def check(name, sql): if name.lower().startswith("sqlite_"): return False @@ -1386,6 +1388,7 @@ Enter SQL statements terminated with a ";" tablefilter=cmd[1] querytemplate=[] queryparams=[] + def qp(): # binding for current queryparams return "?"+str(len(queryparams)) s=cmd[0] @@ -1697,6 +1700,7 @@ Enter SQL statements terminated with a ";" # The types we support deducing def DateUS(v): # US formatted date with wrong ordering of day and month return DateWorld(v, switchdm=True) + def DateWorld(v, switchdm=False): # Sensibly formatted date as used anywhere else in the world y,m,d=self._getdate(v) if switchdm: @@ -1704,8 +1708,10 @@ Enter SQL statements terminated with a ";" if m<1 or m>12 or d<1 or d>31: raise ValueError return "%d-%02d-%02d" % (y,m,d) + def DateTimeUS(v): # US date and time return DateTimeWorld(v, switchdm=True) + def DateTimeWorld(v, switchdm=False): # Sensible date and time y,m,d,h,M,s=self._getdatetime(v) if switchdm: @@ -1713,6 +1719,7 @@ Enter SQL statements terminated with a ";" if m<1 or m>12 or d<1 or d>31 or h<0 or h>23 or M<0 or M>59 or s<0 or s>65: raise ValueError return "%d-%02d-%02dT%02d:%02d:%02d" % (y,m,d,h,M,s) + def Number(v): # we really don't want phone numbers etc to match # Python's float & int constructors allow whitespace which we don't if re.search(r"\s", v): @@ -2867,12 +2874,16 @@ Enter SQL statements terminated with a ";" def __init__(self, **kwargs): for k,v in kwargs.items(): setattr(self, k, v) + def __nonzero__(self): return True + def __str__(self): return "_colourscheme("+str(self.__dict__)+")" + def __getattr__(self, k): return "" + def colour_value(self, val, formatted): self.colour if val is None: @@ -2918,6 +2929,7 @@ Enter SQL statements terminated with a ";" except: pass + def main(): # Docstring must start on second line so dedenting works correctly """ diff --git a/src/calibre/utils/bibtex.py b/src/calibre/utils/bibtex.py index 18bd6a4e81..695eac0df4 100644 --- a/src/calibre/utils/bibtex.py +++ b/src/calibre/utils/bibtex.py @@ -2843,7 +2843,9 @@ entity_mapping = { '"':'{"}', } + class BibTeX: + def __init__(self): self.rep_utf8 = MReplace(utf8enc2latex_mapping) self.rep_ent = MReplace(entity_mapping) diff --git a/src/calibre/utils/browser.py b/src/calibre/utils/browser.py index 4136b0092c..8fcea244d4 100644 --- a/src/calibre/utils/browser.py +++ b/src/calibre/utils/browser.py @@ -10,6 +10,7 @@ from cookielib import CookieJar, Cookie from mechanize import Browser as B, HTTPSHandler + class ModernHTTPSHandler(HTTPSHandler): ssl_context = None @@ -20,6 +21,7 @@ class ModernHTTPSHandler(HTTPSHandler): req.get_full_url()) if cert_file: self.ssl_context.load_cert_chain(cert_file, key_file) + def conn_factory(hostport): return httplib.HTTPSConnection(hostport, context=self.ssl_context) return self.do_open(conn_factory, req) diff --git a/src/calibre/utils/certgen.py b/src/calibre/utils/certgen.py index 6391b4815d..1defebc8ff 100644 --- a/src/calibre/utils/certgen.py +++ b/src/calibre/utils/certgen.py @@ -11,9 +11,11 @@ certgen, err = plugins['certgen'] if err: raise ImportError('Failed to load teh certgen module with error: %s' % err) + def create_key_pair(size=2048): return certgen.create_rsa_keypair(size) + def create_cert_request( key_pair, common_name, country='IN', state='Maharashtra', locality='Mumbai', organization=None, @@ -28,21 +30,27 @@ def create_cert_request( *map(enc, (common_name, country, state, locality, organization, organizational_unit, email_address)) ) + def create_cert(req, ca_cert, ca_keypair, expire=365, not_before=0): return certgen.create_rsa_cert(req, ca_cert, ca_keypair, not_before, expire) + def create_ca_cert(req, ca_keypair, expire=365, not_before=0): return certgen.create_rsa_cert(req, None, ca_keypair, not_before, expire) + def serialize_cert(cert): return certgen.serialize_cert(cert) + def serialize_key(key_pair, password=None): return certgen.serialize_rsa_key(key_pair, password) + def cert_info(cert): return certgen.cert_info(cert).decode('utf-8') + def create_server_cert( domain, ca_cert_file=None, server_cert_file=None, server_key_file=None, expire=365, ca_key_file=None, ca_name='Dummy Certificate Authority', key_size=2048, diff --git a/src/calibre/utils/chm/chm.py b/src/calibre/utils/chm/chm.py index 6cbba522f6..e317144d94 100644 --- a/src/calibre/utils/chm/chm.py +++ b/src/calibre/utils/chm/chm.py @@ -188,6 +188,7 @@ locale_table = { 0x042a : ('cp1258', "Vietnamese", "Vietnamese") } + class CHMFile: "A class to manage access to CHM files." filename = "" diff --git a/src/calibre/utils/chm/chmlib.py b/src/calibre/utils/chm/chmlib.py index b730a48a75..bfa59c8c7d 100644 --- a/src/calibre/utils/chm/chmlib.py +++ b/src/calibre/utils/chm/chmlib.py @@ -9,6 +9,7 @@ _chmlib, chmlib_err = plugins['chmlib'] if chmlib_err: raise RuntimeError('Failed to load chmlib: '+chmlib_err) + def _swig_setattr(self,class_type,name,value): if (name == "this"): if isinstance(value, class_type): @@ -22,6 +23,7 @@ def _swig_setattr(self,class_type,name,value): return method(self,value) self.__dict__[name] = value + def _swig_getattr(self,class_type,name): method = class_type.__swig_getmethods__.get(name,None) if method: @@ -41,6 +43,8 @@ except AttributeError: CHM_UNCOMPRESSED = _chmlib.CHM_UNCOMPRESSED CHM_COMPRESSED = _chmlib.CHM_COMPRESSED CHM_MAX_PATHLEN = _chmlib.CHM_MAX_PATHLEN + + class chmUnitInfo(_object): __swig_setmethods__ = {} __setattr__ = lambda self, name, value: _swig_setattr(self, chmUnitInfo, name, value) @@ -62,18 +66,22 @@ class chmUnitInfo(_object): __swig_getmethods__["path"] = _chmlib.chmUnitInfo_path_get if _newclass: path = property(_chmlib.chmUnitInfo_path_get,_chmlib.chmUnitInfo_path_set) + def __init__(self,*args): _swig_setattr(self, chmUnitInfo, 'this', apply(_chmlib.new_chmUnitInfo,args)) _swig_setattr(self, chmUnitInfo, 'thisown', 1) + def __del__(self, destroy=_chmlib.delete_chmUnitInfo): try: if self.thisown: destroy(self) except: pass + def __repr__(self): return "" % (self.this,) + class chmUnitInfoPtr(chmUnitInfo): def __init__(self,this): diff --git a/src/calibre/utils/cleantext.py b/src/calibre/utils/cleantext.py index 5a68ff736b..e93241f8ed 100644 --- a/src/calibre/utils/cleantext.py +++ b/src/calibre/utils/cleantext.py @@ -18,6 +18,7 @@ else: _ascii_pat = None + def clean_ascii_chars(txt, charlist=None): r''' Remove ASCII control chars. @@ -39,15 +40,18 @@ def clean_ascii_chars(txt, charlist=None): pat = re.compile(u'|'.join(map(unichr, charlist))) return pat.sub('', txt) + def allowed(x): x = ord(x) return (x != 127 and (31 < x < 0xd7ff or x in (9, 10, 13))) or (0xe000 < x < 0xfffd) or (0x10000 < x < 0x10ffff) + def py_clean_xml_chars(unicode_string): return u''.join(filter(allowed, unicode_string)) clean_xml_chars = native_clean_xml_chars or py_clean_xml_chars + def test_clean_xml_chars(): raw = u'asd\x02a\U00010437x\ud801b\udffe\ud802' if native_clean_xml_chars(raw) != u'asda\U00010437xb': diff --git a/src/calibre/utils/complete.py b/src/calibre/utils/complete.py index 0ae7fe0887..1903f0b12d 100644 --- a/src/calibre/utils/complete.py +++ b/src/calibre/utils/complete.py @@ -14,6 +14,7 @@ completion. import sys, os, shlex, glob, re, cPickle + def prints(*args, **kwargs): ''' Print unicode arguments safely by encoding them to preferred_encoding @@ -73,6 +74,7 @@ def files_and_dirs(prefix, allowed_exts=[]): elif allowed_exts is None or ext in allowed_exts: yield i+' ' + def get_opts_from_parser(parser, prefix): def do_opt(opt): for x in opt._long_opts: @@ -89,6 +91,7 @@ def get_opts_from_parser(parser, prefix): for x in do_opt(o): yield x+' ' + def send(ans): pat = re.compile('([^0-9a-zA-Z_./-])') for x in sorted(set(ans)): diff --git a/src/calibre/utils/config.py b/src/calibre/utils/config.py index 7e628dc4a2..aedcc6e8ec 100644 --- a/src/calibre/utils/config.py +++ b/src/calibre/utils/config.py @@ -27,9 +27,11 @@ if False: OptionSet, ConfigInterface, read_tweaks, write_tweaks read_raw_tweaks, tweaks, plugin_dir, prefs + def check_config_write_access(): return os.access(config_dir, os.W_OK) and os.access(config_dir, os.X_OK) + class CustomHelpFormatter(optparse.IndentedHelpFormatter): def format_usage(self, usage): @@ -188,6 +190,7 @@ class OptionParser(optparse.OptionParser): args = [optparse.OptionGroup(self, *args, **kwargs)] + list(args[1:]) return optparse.OptionParser.add_option_group(self, *args, **kwargs) + class DynamicConfig(dict): ''' A replacement for QSettings that supports dynamic config keys. @@ -195,6 +198,7 @@ class DynamicConfig(dict): data is stored in a non human readable pickle file, so only use this class for preferences that you don't intend to have the users edit directly. ''' + def __init__(self, name='dynamic'): dict.__init__(self, {}) self.name = name @@ -256,6 +260,7 @@ class DynamicConfig(dict): dynamic = DynamicConfig() + class XMLConfig(dict): ''' @@ -362,6 +367,7 @@ class XMLConfig(dict): self.no_commit = False self.commit() + def to_json(obj): if isinstance(obj, bytearray): return {'__class__': 'bytearray', @@ -372,6 +378,7 @@ def to_json(obj): '__value__': isoformat(obj, as_utc=True)} raise TypeError(repr(obj) + ' is not JSON serializable') + def from_json(obj): if '__class__' in obj: if obj['__class__'] == 'bytearray': @@ -381,6 +388,7 @@ def from_json(obj): return parse_iso8601(obj['__value__'], assume_utc=True) return obj + class JSONConfig(XMLConfig): EXTENSION = '.json' @@ -407,6 +415,7 @@ class JSONConfig(XMLConfig): dict.__setitem__(self, key, val) self.commit() + class DevicePrefs: def __init__(self, global_prefs): diff --git a/src/calibre/utils/config_base.py b/src/calibre/utils/config_base.py index bfc8d7b92f..a9cd5ef6b6 100644 --- a/src/calibre/utils/config_base.py +++ b/src/calibre/utils/config_base.py @@ -15,10 +15,12 @@ from calibre.constants import config_dir, CONFIG_DIR_MODE plugin_dir = os.path.join(config_dir, 'plugins') + def make_config_dir(): if not os.path.exists(plugin_dir): os.makedirs(plugin_dir, mode=CONFIG_DIR_MODE) + class Option(object): def __init__(self, name, switches=[], help='', type=None, choices=None, @@ -52,11 +54,13 @@ class Option(object): def __str__(self): return repr(self) + class OptionValues(object): def copy(self): return deepcopy(self) + class OptionSet(object): OVERRIDE_PAT = re.compile(r'#{3,100} Override Options #{15}(.*?)#{3,100} End Override #{3,100}', @@ -237,6 +241,7 @@ class OptionSet(object): for name in [None] + self.group_list] return src + '\n\n'.join(groups) + class ConfigInterface(object): def __init__(self, description): @@ -311,6 +316,7 @@ class Config(ConfigInterface): except LockError: raise IOError('Could not lock config file: %s'%self.config_file_path) + class StringConfig(ConfigInterface): ''' A string based configuration @@ -331,6 +337,7 @@ class StringConfig(ConfigInterface): footer = self.option_set.get_override_section(self.src) self.src = self.option_set.serialize(opts)+ '\n\n' + footer + '\n' + class ConfigProxy(object): ''' A Proxy to minimize file reads for widely used config settings @@ -451,6 +458,8 @@ if prefs['installation_uuid'] is None: prefs['installation_uuid'] = str(uuid.uuid4()) # Read tweaks + + def read_raw_tweaks(): make_config_dir() default_tweaks = P('default_tweaks.py', data=True, @@ -462,6 +471,7 @@ def read_raw_tweaks(): with open(tweaks_file, 'rb') as f: return default_tweaks, f.read() + def read_tweaks(): default_tweaks, tweaks = read_raw_tweaks() l, g = {}, {} @@ -476,6 +486,7 @@ def read_tweaks(): dl.update(l) return dl + def write_tweaks(raw): make_config_dir() tweaks_file = os.path.join(config_dir, 'tweaks.py') @@ -485,6 +496,7 @@ def write_tweaks(raw): tweaks = read_tweaks() + def reset_tweaks_to_default(): default_tweaks = P('default_tweaks.py', data=True, allow_user_override=False) @@ -493,6 +505,7 @@ def reset_tweaks_to_default(): tweaks.clear() tweaks.update(dl) + class Tweak(object): def __init__(self, name, value): diff --git a/src/calibre/utils/date.py b/src/calibre/utils/date.py index 0f378e85ef..828cc00d84 100644 --- a/src/calibre/utils/date.py +++ b/src/calibre/utils/date.py @@ -56,6 +56,7 @@ else: DEFAULT_DATE = datetime(2000,1,1, tzinfo=utc_tz) EPOCH = datetime(1970, 1, 1, tzinfo=_utc_tz) + def is_date_undefined(qt_or_dt): d = qt_or_dt if d is None: @@ -73,12 +74,15 @@ def is_date_undefined(qt_or_dt): d.day == UNDEFINED_DATE.day) _iso_pat = None + + def iso_pat(): global _iso_pat if _iso_pat is None: _iso_pat = re.compile(r'\d{4}[/.-]\d{1,2}[/.-]\d{1,2}') return _iso_pat + def parse_date(date_string, assume_utc=False, as_utc=True, default=None): ''' Parse a date/time string into a timezone aware datetime object. The timezone @@ -107,6 +111,7 @@ def parse_date(date_string, assume_utc=False, as_utc=True, default=None): dt = dt.replace(tzinfo=_utc_tz if assume_utc else _local_tz) return dt.astimezone(_utc_tz if as_utc else _local_tz) + def fix_only_date(val): n = val + timedelta(days=1) if n.month > val.month: @@ -115,6 +120,7 @@ def fix_only_date(val): val = val.replace(day=2) return val + def parse_only_date(raw, assume_utc=True, as_utc=True): ''' Parse a date string that contains no time information in a manner that @@ -126,12 +132,14 @@ def parse_only_date(raw, assume_utc=True, as_utc=True): day=15) return fix_only_date(parse_date(raw, default=default, assume_utc=assume_utc, as_utc=as_utc)) + def strptime(val, fmt, assume_utc=False, as_utc=True): dt = datetime.strptime(val, fmt) if dt.tzinfo is None: dt = dt.replace(tzinfo=_utc_tz if assume_utc else _local_tz) return dt.astimezone(_utc_tz if as_utc else _local_tz) + def dt_factory(time_t, assume_utc=False, as_utc=True): dt = datetime(*(time_t[0:6])) if dt.tzinfo is None: @@ -140,6 +148,7 @@ def dt_factory(time_t, assume_utc=False, as_utc=True): safeyear = lambda x: min(max(x, MINYEAR), MAXYEAR) + def qt_to_dt(qdate_or_qdatetime, as_utc=True): o = qdate_or_qdatetime if hasattr(o, 'toUTC'): @@ -160,16 +169,19 @@ def qt_to_dt(qdate_or_qdatetime, as_utc=True): dt = datetime(safeyear(o.year()), o.month(), 1).replace(tzinfo=_local_tz) return dt.astimezone(_utc_tz if as_utc else _local_tz) + def fromtimestamp(ctime, as_utc=True): dt = datetime.utcfromtimestamp(ctime).replace(tzinfo=_utc_tz) if not as_utc: dt = dt.astimezone(_local_tz) return dt + def fromordinal(day, as_utc=True): return datetime.fromordinal(day).replace( tzinfo=_utc_tz if as_utc else _local_tz) + def isoformat(date_time, assume_utc=False, as_utc=True, sep='T'): if not hasattr(date_time, 'tzinfo'): return unicode(date_time.isoformat()) @@ -180,6 +192,7 @@ def isoformat(date_time, assume_utc=False, as_utc=True, sep='T'): # str(sep) because isoformat barfs with unicode sep on python 2.x return unicode(date_time.isoformat(str(sep))) + def as_local_time(date_time, assume_utc=True): if not hasattr(date_time, 'tzinfo'): return date_time @@ -188,11 +201,13 @@ def as_local_time(date_time, assume_utc=True): _local_tz) return date_time.astimezone(_local_tz) + def dt_as_local(dt): if dt.tzinfo is local_tz: return dt return dt.astimezone(local_tz) + def as_utc(date_time, assume_utc=True): if not hasattr(date_time, 'tzinfo'): return date_time @@ -201,9 +216,11 @@ def as_utc(date_time, assume_utc=True): _local_tz) return date_time.astimezone(_utc_tz) + def now(): return datetime.now().replace(tzinfo=_local_tz) + def utcnow(): return datetime.utcnow().replace(tzinfo=_utc_tz) @@ -222,11 +239,13 @@ def utcfromtimestamp(stamp): traceback.print_exc() return utcnow() + def timestampfromdt(dt, assume_utc=True): return (as_utc(dt, assume_utc=assume_utc) - EPOCH).total_seconds() # Format date functions {{{ + def fd_format_hour(dt, ampm, hr): l = len(hr) h = dt.hour @@ -236,24 +255,28 @@ def fd_format_hour(dt, ampm, hr): return '%d'%h return '%02d'%h + def fd_format_minute(dt, ampm, min): l = len(min) if l == 1: return '%d'%dt.minute return '%02d'%dt.minute + def fd_format_second(dt, ampm, sec): l = len(sec) if l == 1: return '%d'%dt.second return '%02d'%dt.second + def fd_format_ampm(dt, ampm, ap): res = strftime('%p', t=dt.timetuple()) if ap == 'AP': return res return res.lower() + def fd_format_day(dt, ampm, dy): l = len(dy) if l == 1: @@ -262,6 +285,7 @@ def fd_format_day(dt, ampm, dy): return '%02d'%dt.day return lcdata['abday' if l == 3 else 'day'][(dt.weekday() + 1) % 7] + def fd_format_month(dt, ampm, mo): l = len(mo) if l == 1: @@ -270,6 +294,7 @@ def fd_format_month(dt, ampm, mo): return '%02d'%dt.month return lcdata['abmon' if l == 3 else 'mon'][dt.month - 1] + def fd_format_year(dt, ampm, yr): if len(yr) == 2: return '%02d'%(dt.year % 100) @@ -285,12 +310,15 @@ fd_function_index = { 'a': fd_format_ampm, 'A': fd_format_ampm, } + + def fd_repl_func(dt, ampm, mo): s = mo.group(0) if not s: return '' return fd_function_index[s[0]](dt, ampm, s) + def format_date(dt, format, assume_utc=False, as_utc=False): ''' Return a date formatted as a string using a subset of Qt's formatting codes ''' if not format: @@ -320,26 +348,32 @@ def format_date(dt, format, assume_utc=False, as_utc=False): # Clean date functions {{{ + def cd_has_hour(tt, dt): tt['hour'] = dt.hour return '' + def cd_has_minute(tt, dt): tt['min'] = dt.minute return '' + def cd_has_second(tt, dt): tt['sec'] = dt.second return '' + def cd_has_day(tt, dt): tt['day'] = dt.day return '' + def cd_has_month(tt, dt): tt['mon'] = dt.month return '' + def cd_has_year(tt, dt): tt['year'] = dt.year return '' @@ -353,12 +387,14 @@ cd_function_index = { 's': cd_has_second } + def cd_repl_func(tt, dt, match_object): s = match_object.group(0) if not s: return '' return cd_function_index[s[0]](tt, dt) + def clean_date_for_sort(dt, fmt=None): ''' Return dt with fields not in shown in format set to a default ''' if not fmt: @@ -385,6 +421,7 @@ def clean_date_for_sort(dt, fmt=None): minute=tt['min'], second=tt['sec'], microsecond=0) # }}} + def replace_months(datestr, clang): # Replace months by english equivalent for parse_date frtoen = { diff --git a/src/calibre/utils/dbus_service.py b/src/calibre/utils/dbus_service.py index 1d246b2a07..42089493d0 100644 --- a/src/calibre/utils/dbus_service.py +++ b/src/calibre/utils/dbus_service.py @@ -46,6 +46,7 @@ from dbus.lowlevel import ErrorMessage, MethodReturnMessage, MethodCallMessage from dbus.proxies import LOCAL_PATH is_py2 = sys.version_info.major == 2 + class dbus_property(object): """A decorator used to mark properties of a `dbus.service.Object`. """ @@ -149,6 +150,7 @@ class _VariantSignature(object): It has no string representation. """ + def __iter__(self): """Return self.""" return self @@ -577,6 +579,7 @@ class PropertiesInterface(Interface): #: Object._connection if it's actually in more than one place _MANY = object() + class Object(Interface): r"""A base class for exporting your own Objects across the Bus. @@ -1005,6 +1008,7 @@ class Object(Interface): id(self)) __str__ = __repr__ + class FallbackObject(Object): """An object that implements an entire subtree of the object-path tree. diff --git a/src/calibre/utils/exim.py b/src/calibre/utils/exim.py index cf7aa204c2..27e27321f3 100644 --- a/src/calibre/utils/exim.py +++ b/src/calibre/utils/exim.py @@ -27,6 +27,7 @@ def send_file(from_obj, to_obj, chunksize=1<<20): to_obj.write(raw) return type('')(m.hexdigest()) + class FileDest(object): def __init__(self, key, exporter, mtime=None): @@ -149,6 +150,7 @@ class Exporter(object): self.add_file(f, key) files.append((key, rpath)) + def all_known_libraries(): from calibre.gui2 import gprefs lus = gprefs.get('library_usage_stats', {}) @@ -166,6 +168,7 @@ def all_known_libraries(): added[path] = lus.get(path, 1) return added + def export(destdir, library_paths=None, dbmap=None, progress1=None, progress2=None, abort=None): from calibre.db.cache import Cache from calibre.db.backend import DB @@ -205,6 +208,7 @@ def export(destdir, library_paths=None, dbmap=None, progress1=None, progress2=No # Import {{{ + class FileSource(object): def __init__(self, f, size, digest, description, mtime, importer): @@ -230,6 +234,7 @@ class FileSource(object): self.importer.corrupted_files.append(self.description) self.hasher = self.f = None + class Importer(object): def __init__(self, path_to_export_dir): @@ -311,6 +316,7 @@ class Importer(object): gprefs = JSONConfig('gui', base_path=base_dir) gprefs['library_usage_stats'] = dict(library_usage_stats) + def import_data(importer, library_path_map, config_location=None, progress1=None, progress2=None, abort=None): from calibre.db.cache import import_library config_location = config_location or config_dir @@ -358,6 +364,7 @@ def import_data(importer, library_path_map, config_location=None, progress1=None if progress1 is not None: progress1(_('Completed'), total, total) + def test_import(export_dir='/t/ex', import_dir='/t/imp'): importer = Importer(export_dir) if os.path.exists(import_dir): @@ -366,12 +373,14 @@ def test_import(export_dir='/t/ex', import_dir='/t/imp'): import_data(importer, {k:os.path.join(import_dir, os.path.basename(k)) for k in importer.metadata['libraries'] if 'largelib' not in k}, config_location=os.path.join(import_dir, 'calibre-config'), progress1=print, progress2=print) + def cli_report(*args, **kw): try: prints(*args, **kw) except EnvironmentError: pass + def run_exporter(): export_dir = raw_input('Enter path to an empty folder (all exported data will be saved inside it): ').decode(filesystem_encoding) if not os.path.exists(export_dir): @@ -389,6 +398,7 @@ def run_exporter(): else: raise SystemExit('No libraries selected for export') + def run_importer(): export_dir = raw_input('Enter path to folder containing previously exported data: ').decode(filesystem_encoding) if not os.path.isdir(export_dir): diff --git a/src/calibre/utils/file_associations.py b/src/calibre/utils/file_associations.py index b1cda4a3c3..4c09fc279c 100644 --- a/src/calibre/utils/file_associations.py +++ b/src/calibre/utils/file_associations.py @@ -6,6 +6,7 @@ from __future__ import (unicode_literals, division, absolute_import, __license__ = 'GPL v3' __copyright__ = '2013, Kovid Goyal ' + def file_assoc_windows(ft): # See the IQueryAssociations::GetString method documentation on MSDN from win32com.shell import shell, shellcon diff --git a/src/calibre/utils/filenames.py b/src/calibre/utils/filenames.py index f8ec7f6da2..1726c9d8d2 100644 --- a/src/calibre/utils/filenames.py +++ b/src/calibre/utils/filenames.py @@ -11,6 +11,7 @@ from calibre.constants import (preferred_encoding, iswindows, filesystem_encoding) from calibre.utils.localization import get_udc + def ascii_text(orig): udc = get_udc() try: @@ -32,6 +33,7 @@ def ascii_filename(orig, substitute='_'): ans.append(x) return sanitize_file_name(''.join(ans), substitute=substitute) + def supports_long_names(path): t = ('a'*300)+'.txt' try: @@ -43,6 +45,7 @@ def supports_long_names(path): else: return True + def shorten_component(s, by_what): l = len(s) if l < by_what: @@ -52,6 +55,7 @@ def shorten_component(s, by_what): return s return s[:l] + s[-l:] + def shorten_components_to(length, components, more_to_take=0, last_has_extension=True): filepath = os.sep.join(components) extra = len(filepath) - (length - more_to_take) @@ -85,6 +89,7 @@ def shorten_components_to(length, components, more_to_take=0, last_has_extension return shorten_components_to(length, components, more_to_take+2) return ans + def find_executable_in_path(name, path=None): if path is None: path = os.environ.get('PATH', '') @@ -96,6 +101,7 @@ def find_executable_in_path(name, path=None): if os.access(q, os.X_OK): return q + def is_case_sensitive(path): ''' Return True if the filesystem is case sensitive. @@ -116,6 +122,7 @@ def is_case_sensitive(path): os.remove(f1) return is_case_sensitive + def case_preserving_open_file(path, mode='wb', mkdir_mode=0o777): ''' Open the file pointed to by path with the specified mode. If any @@ -200,6 +207,7 @@ def case_preserving_open_file(path, mode='wb', mkdir_mode=0o777): fpath = os.path.join(cpath, fname) return ans, fpath + def windows_get_fileid(path): ''' The fileid uniquely identifies actual file contents (it is the same for all hardlinks to a file). Similar to inode number on linux. ''' @@ -218,6 +226,7 @@ def windows_get_fileid(path): return None return data[4], data[8], data[9] + def samefile_windows(src, dst): samestring = (os.path.normcase(os.path.abspath(src)) == os.path.normcase(os.path.abspath(dst))) @@ -229,6 +238,7 @@ def samefile_windows(src, dst): return False return a == b + def samefile(src, dst): ''' Check if two paths point to the same actual file on the filesystem. Handles @@ -255,6 +265,7 @@ def samefile(src, dst): os.path.normcase(os.path.abspath(dst))) return samestring + def windows_get_size(path): ''' On windows file sizes are only accurately stored in the actual file, not in the directory entry (which could be out of date). So we open the @@ -270,6 +281,7 @@ def windows_get_size(path): finally: win32file.CloseHandle(h) + def windows_hardlink(src, dest): import win32file, pywintypes try: @@ -295,6 +307,7 @@ def windows_hardlink(src, dest): msg = u'Creating hardlink from %s to %s failed: %%s' % (src, dest) raise OSError(msg % ('hardlink size: %d not the same as source size' % sz)) + def windows_fast_hardlink(src, dest): import win32file, pywintypes try: @@ -307,6 +320,7 @@ def windows_fast_hardlink(src, dest): msg = u'Creating hardlink from %s to %s failed: %%s' % (src, dest) raise OSError(msg % ('hardlink size: %d not the same as source size: %s' % (dsz, ssz))) + def windows_nlinks(path): import win32file dwFlagsAndAttributes = win32file.FILE_FLAG_BACKUP_SEMANTICS if os.path.isdir(path) else 0 @@ -318,6 +332,7 @@ def windows_nlinks(path): finally: handle.Close() + class WindowsAtomicFolderMove(object): ''' @@ -446,18 +461,21 @@ class WindowsAtomicFolderMove(object): win32file.DeleteFile(path) self.close_handles() + def hardlink_file(src, dest): if iswindows: windows_hardlink(src, dest) return os.link(src, dest) + def nlinks_file(path): ' Return number of hardlinks to the file ' if iswindows: return windows_nlinks(path) return os.stat(path).st_nlink + def atomic_rename(oldpath, newpath): '''Replace the file newpath with the file oldpath. Can fail if the files are on different volumes. If succeeds, guaranteed to be atomic. newpath may @@ -477,6 +495,7 @@ def atomic_rename(oldpath, newpath): else: os.rename(oldpath, newpath) + def remove_dir_if_empty(path, ignore_metadata_caches=False): ''' Remove a directory if it is empty or contains only the folder metadata caches from different OSes. To delete the folder if it contains only @@ -522,6 +541,7 @@ if iswindows: else: expanduser = os.path.expanduser + def format_permissions(st_mode): import stat for func, letter in (x.split(':') for x in 'REG:- DIR:d BLK:b CHR:c FIFO:p LNK:l SOCK:s'.split()): @@ -539,6 +559,7 @@ def format_permissions(st_mode): ans[9] = 't' if (st_mode & stat.S_IXUSR) else 'T' return ''.join(ans) + def copyfile(src, dest): shutil.copyfile(src, dest) try: @@ -546,6 +567,7 @@ def copyfile(src, dest): except Exception: pass + def get_hardlink_function(src, dest): if iswindows: import win32file, win32api @@ -561,6 +583,7 @@ def get_hardlink_function(src, dest): hardlink = os.link return hardlink + def copyfile_using_links(path, dest, dest_is_dir=True, filecopyfunc=copyfile): path, dest = os.path.abspath(path), os.path.abspath(dest) if dest_is_dir: @@ -571,6 +594,7 @@ def copyfile_using_links(path, dest, dest_is_dir=True, filecopyfunc=copyfile): except Exception: filecopyfunc(path, dest) + def copytree_using_links(path, dest, dest_is_parent=True, filecopyfunc=copyfile): path, dest = os.path.abspath(path), os.path.abspath(dest) if dest_is_parent: diff --git a/src/calibre/utils/fonts/free_type.py b/src/calibre/utils/fonts/free_type.py index ba604bbad2..a5f787ca78 100644 --- a/src/calibre/utils/fonts/free_type.py +++ b/src/calibre/utils/fonts/free_type.py @@ -13,6 +13,7 @@ from future_builtins import map from calibre.constants import plugins + class ThreadingViolation(Exception): def __init__(self): @@ -20,6 +21,7 @@ class ThreadingViolation(Exception): 'You cannot use the freetype plugin from a thread other than the ' ' thread in which startup() was called') + def same_thread(func): @wraps(func) def check_thread(self, *args, **kwargs): @@ -30,6 +32,7 @@ def same_thread(func): FreeTypeError = getattr(plugins['freetype'][0], 'FreeTypeError', Exception) + class Face(object): def __init__(self, face): @@ -63,6 +66,7 @@ class Face(object): for char in text: yield self.face.glyph_id(ord(char)) + class FreeType(object): def __init__(self): diff --git a/src/calibre/utils/fonts/metadata.py b/src/calibre/utils/fonts/metadata.py index a552503c21..b5ad0c4fd5 100644 --- a/src/calibre/utils/fonts/metadata.py +++ b/src/calibre/utils/fonts/metadata.py @@ -13,6 +13,7 @@ from collections import namedtuple from calibre.utils.fonts.utils import get_font_names2, get_font_characteristics + class UnsupportedFont(ValueError): pass @@ -21,6 +22,7 @@ FontCharacteristics = namedtuple('FontCharacteristics', FontNames = namedtuple('FontNames', 'family_name, subfamily_name, full_name, preferred_family_name, preferred_subfamily_name, wws_family_name, wws_subfamily_name') + class FontMetadata(object): def __init__(self, bytes_or_stream): diff --git a/src/calibre/utils/fonts/scanner.py b/src/calibre/utils/fonts/scanner.py index fa13316621..ddde3e0699 100644 --- a/src/calibre/utils/fonts/scanner.py +++ b/src/calibre/utils/fonts/scanner.py @@ -17,6 +17,7 @@ from calibre.constants import (config_dir, iswindows, isosx, plugins, DEBUG, from calibre.utils.fonts.metadata import FontMetadata, UnsupportedFont from calibre.utils.icu import sort_key + class NoFonts(ValueError): pass @@ -115,6 +116,7 @@ def font_dirs(): ] return fc_list() + class FontScanner(Thread): CACHE_VERSION = 1 @@ -385,6 +387,7 @@ class FontScanner(Thread): font_scanner = FontScanner() font_scanner.start() + def force_rescan(): font_scanner.join() font_scanner.force_rescan() diff --git a/src/calibre/utils/fonts/sfnt/__init__.py b/src/calibre/utils/fonts/sfnt/__init__.py index 3355f18447..2c3d7aef4a 100644 --- a/src/calibre/utils/fonts/sfnt/__init__.py +++ b/src/calibre/utils/fonts/sfnt/__init__.py @@ -9,6 +9,7 @@ __docformat__ = 'restructuredtext en' from datetime import datetime, timedelta + def align_block(raw, multiple=4, pad=b'\0'): ''' Return raw with enough pad bytes append to ensure its length is a multiple @@ -19,6 +20,7 @@ def align_block(raw, multiple=4, pad=b'\0'): return raw return raw + pad*(multiple - extra) + class UnknownTable(object): def __init__(self, raw): @@ -30,6 +32,7 @@ class UnknownTable(object): def __len__(self): return len(self.raw) + class DateTimeProperty(object): def __init__(self, name): @@ -43,6 +46,7 @@ class DateTimeProperty(object): td = val - datetime(1904, 1, 1) setattr(obj, self.name, int(td.total_seconds())) + class FixedProperty(object): def __init__(self, name): @@ -55,6 +59,7 @@ class FixedProperty(object): def __set__(self, obj, val): return int(round(val*(0x10000))) + def max_power_of_two(x): """ Return the highest exponent of two, so that @@ -66,6 +71,7 @@ Return the highest exponent of two, so that exponent += 1 return max(exponent - 1, 0) + def load_font(stream_or_path): raw = stream_or_path if hasattr(raw, 'read'): diff --git a/src/calibre/utils/fonts/sfnt/cff/table.py b/src/calibre/utils/fonts/sfnt/cff/table.py index 952c588a42..396ceb6404 100644 --- a/src/calibre/utils/fonts/sfnt/cff/table.py +++ b/src/calibre/utils/fonts/sfnt/cff/table.py @@ -20,6 +20,7 @@ from calibre.utils.fonts.sfnt.cff.constants import (cff_standard_strings, # http://www.adobe.com/content/dam/Adobe/en/devnet/font/pdfs/5176.CFF.pdf # http://www.adobe.com/content/dam/Adobe/en/devnet/font/pdfs/5177.Type2.pdf + class CFF(object): def __init__(self, raw): @@ -87,6 +88,7 @@ class CFF(object): # pprint.pprint(self.top_dict) # pprint.pprint(self.private_dict) + class Index(list): def __init__(self, raw, offset, prepend=()): @@ -119,12 +121,14 @@ class Index(list): except IndexError: self.pos = offset + class Strings(Index): def __init__(self, raw, offset): super(Strings, self).__init__(raw, offset, prepend=[x.encode('ascii') for x in cff_standard_strings]) + class Charset(list): def __init__(self, raw, offset, strings, num_glyphs, is_CID): @@ -175,12 +179,15 @@ class Charset(list): except (KeyError, IndexError, ValueError): return None + class Subrs(Index): pass + class CharStringsIndex(Index): pass + class CFFTable(UnknownTable): def decompile(self): diff --git a/src/calibre/utils/fonts/sfnt/cff/writer.py b/src/calibre/utils/fonts/sfnt/cff/writer.py index b61c55946b..e3872d4472 100644 --- a/src/calibre/utils/fonts/sfnt/cff/writer.py +++ b/src/calibre/utils/fonts/sfnt/cff/writer.py @@ -12,6 +12,7 @@ from collections import OrderedDict from calibre.utils.fonts.sfnt.cff.constants import cff_standard_strings + class Index(list): def __init__(self): @@ -48,6 +49,7 @@ class Index(list): self.raw = prefix + offsets + obj_data return self.raw + class Strings(Index): def __init__(self): @@ -62,6 +64,7 @@ class Strings(Index): self.append(x) return ans + class Dict(Index): def __init__(self, src, strings): @@ -72,6 +75,7 @@ class Dict(Index): self[:] = [self.src.compile(self.strings)] Index.compile(self) + class PrivateDict(object): def __init__(self, src, subrs, strings): @@ -90,6 +94,7 @@ class PrivateDict(object): self.raw = raw return raw + class Charsets(list): def __init__(self, strings): @@ -103,6 +108,7 @@ class Charsets(list): self.raw = ans return ans + class Subset(object): def __init__(self, cff, keep_charnames): diff --git a/src/calibre/utils/fonts/sfnt/cmap.py b/src/calibre/utils/fonts/sfnt/cmap.py index 7ffa88795a..b04665453d 100644 --- a/src/calibre/utils/fonts/sfnt/cmap.py +++ b/src/calibre/utils/fonts/sfnt/cmap.py @@ -17,6 +17,7 @@ from calibre.utils.fonts.utils import read_bmp_prefix from calibre.utils.fonts.sfnt import UnknownTable, max_power_of_two from calibre.utils.fonts.sfnt.errors import UnsupportedFont + def split_range(start_code, end_code, cmap): # {{{ # Try to split a range of character codes into subranges with consecutive # glyph IDs in such a way that the cmap4 subtable can be stored "most" @@ -95,6 +96,7 @@ def split_range(start_code, end_code, cmap): # {{{ return start, end # }}} + def set_id_delta(id_delta): # {{{ # The lowest gid in glyphIndexArray, after subtracting id_delta, must be 1. # id_delta is a short, and must be between -32K and 32K @@ -117,6 +119,7 @@ def set_id_delta(id_delta): # {{{ return id_delta # }}} + class BMPTable(object): def __init__(self, raw): @@ -164,6 +167,7 @@ class BMPTable(object): ans[code] = glyph_id return ans + class CmapTable(UnknownTable): def __init__(self, *args, **kwargs): diff --git a/src/calibre/utils/fonts/sfnt/common.py b/src/calibre/utils/fonts/sfnt/common.py index 26e391cd84..5534fe6e92 100644 --- a/src/calibre/utils/fonts/sfnt/common.py +++ b/src/calibre/utils/fonts/sfnt/common.py @@ -12,6 +12,7 @@ from collections import OrderedDict, namedtuple from calibre.utils.fonts.sfnt.errors import UnsupportedFont + class Unpackable(object): def __init__(self, raw, offset): @@ -26,6 +27,7 @@ class Unpackable(object): self.offset += calcsize(fmt) return ans + class SimpleListTable(list): 'A table that contains a list of subtables' @@ -50,6 +52,7 @@ class SimpleListTable(list): def read_extra_footer(self, data): pass + class ListTable(OrderedDict): 'A table that contains an ordered mapping of table tag to subtable' @@ -99,6 +102,7 @@ class IndexTable(list): def dump(self, prefix=''): print(prefix, self.__class__.__name__, sep='') + class LanguageSystemTable(IndexTable): def read_extra_header(self, data): @@ -107,6 +111,7 @@ class LanguageSystemTable(IndexTable): raise UnsupportedFont('This LanguageSystemTable has an unknown' ' lookup order: 0x%x'%self.lookup_order) + class ScriptTable(ListTable): child_class = LanguageSystemTable @@ -120,10 +125,12 @@ class ScriptTable(ListTable): self[b'default'] = (LanguageSystemTable(data.raw, start_pos + default_offset) if default_offset else None) + class ScriptListTable(ListTable): child_class = ScriptTable + class FeatureTable(IndexTable): def read_extra_header(self, data): @@ -133,10 +140,12 @@ class FeatureTable(IndexTable): raise UnsupportedFont( 'This FeatureTable has non NULL FeatureParams: 0x%x'%self.feature_params) + class FeatureListTable(ListTable): child_class = FeatureTable + class LookupTable(SimpleListTable): def read_extra_header(self, data): @@ -150,6 +159,7 @@ class LookupTable(SimpleListTable): if self.lookup_flag & 0x0010: self.mark_filtering_set = data.unpack('H') + def ExtensionSubstitution(raw, offset, subtable_map={}): data = Unpackable(raw, offset) subst_format, extension_lookup_type, offset = data.unpack('2HL') @@ -159,6 +169,7 @@ def ExtensionSubstitution(raw, offset, subtable_map={}): CoverageRange = namedtuple('CoverageRange', 'start end start_coverage_index') + class Coverage(object): def __init__(self, raw, offset, parent_table_name): @@ -195,6 +206,7 @@ class Coverage(object): ans[gid] = start_coverage_index + (gid-start) return ans + class UnknownLookupSubTable(object): formats = {} diff --git a/src/calibre/utils/fonts/sfnt/container.py b/src/calibre/utils/fonts/sfnt/container.py index 16a1f75ac5..d41a60e27c 100644 --- a/src/calibre/utils/fonts/sfnt/container.py +++ b/src/calibre/utils/fonts/sfnt/container.py @@ -28,6 +28,7 @@ from calibre.utils.fonts.sfnt.cff.table import CFFTable # OpenType spec: http://www.microsoft.com/typography/otspec/otff.htm + class Sfnt(object): TABLE_MAP = { @@ -150,6 +151,7 @@ class Sfnt(object): return stream.getvalue(), sizes + def test_roundtrip(ff=None): if ff is None: data = P('fonts/liberation/LiberationSerif-Regular.ttf', data=True) diff --git a/src/calibre/utils/fonts/sfnt/errors.py b/src/calibre/utils/fonts/sfnt/errors.py index bbce08fa83..b262fab60a 100644 --- a/src/calibre/utils/fonts/sfnt/errors.py +++ b/src/calibre/utils/fonts/sfnt/errors.py @@ -7,9 +7,11 @@ __license__ = 'GPL v3' __copyright__ = '2012, Kovid Goyal ' __docformat__ = 'restructuredtext en' + class UnsupportedFont(ValueError): pass + class NoGlyphs(ValueError): pass diff --git a/src/calibre/utils/fonts/sfnt/glyf.py b/src/calibre/utils/fonts/sfnt/glyf.py index 6ab86a752c..2b3cc49e60 100644 --- a/src/calibre/utils/fonts/sfnt/glyf.py +++ b/src/calibre/utils/fonts/sfnt/glyf.py @@ -26,6 +26,7 @@ OVERLAP_COMPOUND = 0x0400 # used by Apple in GX fonts SCALED_COMPONENT_OFFSET = 0x0800 # composite designed to have the component offset scaled (designed for Apple) UNSCALED_COMPONENT_OFFSET = 0x1000 # composite designed not to have the component offset scaled (designed for MS) + class SimpleGlyph(object): def __init__(self, num_of_countours, raw): @@ -42,6 +43,7 @@ class SimpleGlyph(object): def __call__(self): return self.raw + class CompositeGlyph(SimpleGlyph): def __init__(self, num_of_countours, raw): @@ -65,6 +67,7 @@ class CompositeGlyph(SimpleGlyph): elif flags & WE_HAVE_A_TWO_BY_TWO: offset += 8 + class GlyfTable(UnknownTable): def glyph_data(self, offset, length): diff --git a/src/calibre/utils/fonts/sfnt/gsub.py b/src/calibre/utils/fonts/sfnt/gsub.py index 6b74e57c9c..b3567f271b 100644 --- a/src/calibre/utils/fonts/sfnt/gsub.py +++ b/src/calibre/utils/fonts/sfnt/gsub.py @@ -16,6 +16,7 @@ from calibre.utils.fonts.sfnt.common import (ScriptListTable, FeatureListTable, SimpleListTable, LookupTable, ExtensionSubstitution, UnknownLookupSubTable) + class SingleSubstitution(UnknownLookupSubTable): formats = {1, 2} @@ -33,6 +34,7 @@ class SingleSubstitution(UnknownLookupSubTable): return {gid + self.delta for gid in gid_index_map} return {self.substitutes[i] for i in gid_index_map.itervalues()} + class MultipleSubstitution(UnknownLookupSubTable): formats = {1} @@ -48,9 +50,11 @@ class MultipleSubstitution(UnknownLookupSubTable): ans |= glyphs return ans + class AlternateSubstitution(MultipleSubstitution): pass + class LigatureSubstitution(UnknownLookupSubTable): formats = {1} @@ -73,6 +77,7 @@ class LigatureSubstitution(UnknownLookupSubTable): ans.add(glyph_id) return ans + class ContexttualSubstitution(UnknownLookupSubTable): formats = {1, 2, 3} @@ -104,6 +109,7 @@ class ChainingContextualSubstitution(UnknownLookupSubTable): # This table only defined substitution in terms of other tables return set() + class ReverseChainSingleSubstitution(UnknownLookupSubTable): formats = {1} @@ -135,6 +141,7 @@ subtable_map = { 8: ReverseChainSingleSubstitution, } + class GSUBLookupTable(LookupTable): def set_child_class(self): @@ -144,10 +151,12 @@ class GSUBLookupTable(LookupTable): else: self.child_class = subtable_map[self.lookup_type] + class LookupListTable(SimpleListTable): child_class = GSUBLookupTable + class GSUBTable(UnknownTable): version = FixedProperty('_version') diff --git a/src/calibre/utils/fonts/sfnt/head.py b/src/calibre/utils/fonts/sfnt/head.py index d0695a404e..f21bd0450e 100644 --- a/src/calibre/utils/fonts/sfnt/head.py +++ b/src/calibre/utils/fonts/sfnt/head.py @@ -13,6 +13,7 @@ from struct import unpack_from, pack, calcsize from calibre.utils.fonts.sfnt import UnknownTable, DateTimeProperty, FixedProperty from calibre.utils.fonts.sfnt.errors import UnsupportedFont + class HeadTable(UnknownTable): created = DateTimeProperty('_created') @@ -53,6 +54,7 @@ class HeadTable(UnknownTable): vals = [getattr(self, f) for f in self._fields] self.raw = pack(self._fmt, *vals) + class HorizontalHeader(UnknownTable): version_number = FixedProperty('_version_number') @@ -98,6 +100,7 @@ class HorizontalHeader(UnknownTable): entries = unpack_from(fmt.encode('ascii'), long_hor_metric) self.left_side_bearings = entries[1::2] + class OS2Table(UnknownTable): def read_data(self): @@ -154,6 +157,7 @@ class OS2Table(UnknownTable): self.raw = self.raw[:prefix] + b'\0\0' + self.raw[prefix+2:] self.fs_type = 0 + class PostTable(UnknownTable): version_number = FixedProperty('_version') diff --git a/src/calibre/utils/fonts/sfnt/kern.py b/src/calibre/utils/fonts/sfnt/kern.py index 2fbf146115..fe139e638c 100644 --- a/src/calibre/utils/fonts/sfnt/kern.py +++ b/src/calibre/utils/fonts/sfnt/kern.py @@ -13,6 +13,7 @@ from calibre.utils.fonts.sfnt import (UnknownTable, FixedProperty, max_power_of_two) from calibre.utils.fonts.sfnt.errors import UnsupportedFont + class KernTable(UnknownTable): version = FixedProperty('_version') diff --git a/src/calibre/utils/fonts/sfnt/loca.py b/src/calibre/utils/fonts/sfnt/loca.py index 557c31c500..05426be921 100644 --- a/src/calibre/utils/fonts/sfnt/loca.py +++ b/src/calibre/utils/fonts/sfnt/loca.py @@ -12,6 +12,7 @@ from operator import itemgetter from calibre.utils.fonts.sfnt import UnknownTable + class LocaTable(UnknownTable): def load_offsets(self, head_table, maxp_table): diff --git a/src/calibre/utils/fonts/sfnt/maxp.py b/src/calibre/utils/fonts/sfnt/maxp.py index 6c8d3b81be..03c8b1793a 100644 --- a/src/calibre/utils/fonts/sfnt/maxp.py +++ b/src/calibre/utils/fonts/sfnt/maxp.py @@ -13,6 +13,7 @@ from struct import unpack_from, pack from calibre.utils.fonts.sfnt import UnknownTable, FixedProperty from calibre.utils.fonts.sfnt.errors import UnsupportedFont + class MaxpTable(UnknownTable): version = FixedProperty('_version') diff --git a/src/calibre/utils/fonts/sfnt/metrics.py b/src/calibre/utils/fonts/sfnt/metrics.py index 088ec262fd..892617b9b6 100644 --- a/src/calibre/utils/fonts/sfnt/metrics.py +++ b/src/calibre/utils/fonts/sfnt/metrics.py @@ -11,6 +11,7 @@ from future_builtins import map from calibre.utils.fonts.utils import get_all_font_names from calibre.utils.fonts.sfnt.container import UnsupportedFont + class FontMetrics(object): ''' diff --git a/src/calibre/utils/fonts/sfnt/subset.py b/src/calibre/utils/fonts/sfnt/subset.py index ffb36b46d9..5f0e570a3c 100644 --- a/src/calibre/utils/fonts/sfnt/subset.py +++ b/src/calibre/utils/fonts/sfnt/subset.py @@ -18,6 +18,7 @@ from calibre.utils.fonts.sfnt.errors import UnsupportedFont, NoGlyphs # TrueType outlines {{{ + def resolve_glyphs(loca, glyf, character_map, extra_glyphs): unresolved_glyphs = set(character_map.itervalues()) | extra_glyphs unresolved_glyphs.add(0) # We always want the .notdef glyph @@ -37,6 +38,7 @@ def resolve_glyphs(loca, glyf, character_map, extra_glyphs): return OrderedDict(sorted(resolved_glyphs.iteritems(), key=itemgetter(0))) + def subset_truetype(sfnt, character_map, extra_glyphs): loca = sfnt[b'loca'] glyf = sfnt[b'glyf'] @@ -65,11 +67,13 @@ def subset_truetype(sfnt, character_map, extra_glyphs): # }}} + def subset_postscript(sfnt, character_map, extra_glyphs): cff = sfnt[b'CFF '] cff.decompile() cff.subset(character_map, extra_glyphs) + def do_warn(warnings, *args): for arg in args: for line in arg.splitlines(): @@ -82,6 +86,7 @@ def do_warn(warnings, *args): else: warnings.append('') + def pdf_subset(sfnt, glyphs): for tag in tuple(sfnt.tables): if tag not in {b'hhea', b'head', b'hmtx', b'maxp', @@ -99,6 +104,7 @@ def pdf_subset(sfnt, glyphs): raise UnsupportedFont('This font does not contain TrueType ' 'or PostScript outlines') + def subset(raw, individual_chars, ranges=(), warnings=None): warn = partial(do_warn, warnings) @@ -176,6 +182,8 @@ def subset(raw, individual_chars, ranges=(), warnings=None): return raw, old_sizes, new_sizes # CLI {{{ + + def option_parser(): import textwrap from calibre.utils.config import OptionParser @@ -195,6 +203,7 @@ def option_parser(): parser.prog = 'subset-font' return parser + def print_stats(old_stats, new_stats): from calibre import prints prints('========= Table comparison (original vs. subset) =========') @@ -262,6 +271,7 @@ def main(args): sf, old_stats, new_stats = subset(orig, individual, ranges) taken = time.time() - st reduced = (len(sf)/len(orig)) * 100 + def sz(x): return '%gKB'%(len(x)/1024.) print_stats(old_stats, new_stats) @@ -282,6 +292,8 @@ if __name__ == '__main__': # }}} # Tests {{{ + + def test_mem(): from calibre.utils.mem import memory import gc @@ -296,12 +308,14 @@ def test_mem(): gc.collect() print ('Leaked memory per call:', (memory() - start_mem)/calls*1024, 'KB') + def test(): raw = P('fonts/liberation/LiberationSerif-Regular.ttf', data=True) sf, old_stats, new_stats = subset(raw, set(('a', 'b', 'c')), ()) if len(sf) > 0.3 * len(raw): raise Exception('Subsetting failed') + def all(): from calibre.utils.fonts.scanner import font_scanner failed = [] diff --git a/src/calibre/utils/fonts/utils.py b/src/calibre/utils/fonts/utils.py index f3b2709e95..db3f0ef22c 100644 --- a/src/calibre/utils/fonts/utils.py +++ b/src/calibre/utils/fonts/utils.py @@ -11,18 +11,22 @@ import struct from io import BytesIO from collections import defaultdict + class UnsupportedFont(ValueError): pass + def get_printable_characters(text): import unicodedata return u''.join(x for x in unicodedata.normalize('NFC', text) if unicodedata.category(x)[0] not in {'C', 'Z', 'M'}) + def is_truetype_font(raw): sfnt_version = raw[:4] return (sfnt_version in {b'\x00\x01\x00\x00', b'OTTO'}, sfnt_version) + def get_tables(raw): num_tables = struct.unpack_from(b'>H', raw, 4)[0] offset = 4*3 # start of the table record entries @@ -33,6 +37,7 @@ def get_tables(raw): table_offset, table_checksum) offset += 4*4 + def get_table(raw, name): ''' Get the raw table bytes for the specified table in the font ''' name = bytes(name.lower()) @@ -41,6 +46,7 @@ def get_table(raw, name): return table, table_index, table_offset, table_checksum return None, None, None, None + def get_font_characteristics(raw, raw_is_table=False, return_all=False): ''' Return (weight, is_italic, is_bold, is_regular, fs_type, panose, width, @@ -85,6 +91,7 @@ def get_font_characteristics(raw, raw_is_table=False, return_all=False): return weight, is_italic, is_bold, is_regular, fs_type, panose, width, is_oblique, is_wws, version + def panose_to_css_generic_family(panose): proportion = panose[3] if proportion == 9: @@ -99,6 +106,7 @@ def panose_to_css_generic_family(panose): return 'sans-serif' return 'serif' + def decode_name_record(recs): ''' Get the English names of this font. See @@ -161,6 +169,7 @@ def decode_name_record(recs): return None + def _get_font_names(raw, raw_is_table=False): if raw_is_table: table = raw @@ -185,6 +194,7 @@ def _get_font_names(raw, raw_is_table=False): return records + def get_font_names(raw, raw_is_table=False): records = _get_font_names(raw, raw_is_table) family_name = decode_name_record(records[1]) @@ -193,6 +203,7 @@ def get_font_names(raw, raw_is_table=False): return family_name, subfamily_name, full_name + def get_font_names2(raw, raw_is_table=False): records = _get_font_names(raw, raw_is_table) @@ -209,6 +220,7 @@ def get_font_names2(raw, raw_is_table=False): return (family_name, subfamily_name, full_name, preferred_family_name, preferred_subfamily_name, wws_family_name, wws_subfamily_name) + def get_all_font_names(raw, raw_is_table=False): records = _get_font_names(raw, raw_is_table) ans = {} @@ -239,12 +251,14 @@ def get_all_font_names(raw, raw_is_table=False): return ans + def checksum_of_block(raw): extra = 4 - len(raw)%4 raw += b'\0'*extra num = len(raw)//4 return sum(struct.unpack(b'>%dI'%num, raw)) % (1<<32) + def verify_checksums(raw): head_table = None for table_tag, table, table_index, table_offset, table_checksum in get_tables(raw): @@ -269,6 +283,7 @@ def verify_checksums(raw): if q != checksum_adj: raise ValueError('Checksum of entire font incorrect') + def set_checksum_adjustment(f): offset = get_table(f.getvalue(), 'head')[2] offset += 8 @@ -279,6 +294,7 @@ def set_checksum_adjustment(f): f.seek(offset) f.write(struct.pack(b'>I', q)) + def set_table_checksum(f, name): table, table_index, table_offset, table_checksum = get_table(f.getvalue(), name) checksum = checksum_of_block(table) @@ -286,6 +302,7 @@ def set_table_checksum(f, name): f.seek(table_index + 4) f.write(struct.pack(b'>I', checksum)) + def remove_embed_restriction(raw): ok, sig = is_truetype_font(raw) if not ok: @@ -310,6 +327,7 @@ def remove_embed_restriction(raw): verify_checksums(raw) return raw + def read_bmp_prefix(table, bmp): length, language, segcount = struct.unpack_from(b'>3H', table, bmp+2) array_len = segcount //2 @@ -331,6 +349,7 @@ def read_bmp_prefix(table, bmp): return (start_count, end_count, range_offset, id_delta, glyph_id_len, glyph_id_map, array_len) + def get_bmp_glyph_ids(table, bmp, codes): (start_count, end_count, range_offset, id_delta, glyph_id_len, glyph_id_map, array_len) = read_bmp_prefix(table, bmp) @@ -355,6 +374,7 @@ def get_bmp_glyph_ids(table, bmp, codes): if not found: yield 0 + def get_glyph_ids(raw, text, raw_is_table=False): if not isinstance(text, unicode): raise TypeError('%r is not a unicode object'%text) @@ -380,6 +400,7 @@ def get_glyph_ids(raw, text, raw_is_table=False): for glyph_id in get_bmp_glyph_ids(table, bmp_table, map(ord, text)): yield glyph_id + def supports_text(raw, text, has_only_printable_chars=False): if not isinstance(text, unicode): raise TypeError('%r is not a unicode object'%text) @@ -393,6 +414,7 @@ def supports_text(raw, text, has_only_printable_chars=False): return False return True + def get_font_for_text(text, candidate_font_data=None): ok = False if candidate_font_data is not None: @@ -405,6 +427,7 @@ def get_font_for_text(text, candidate_font_data=None): candidate_font_data = f.read() return candidate_font_data + def test_glyph_ids(): from calibre.utils.fonts.free_type import FreeType data = P('fonts/liberation/LiberationSerif-Regular.ttf', data=True) @@ -416,6 +439,7 @@ def test_glyph_ids(): if ft_glyphs != glyphs: raise Exception('My code and FreeType differ on the glyph ids') + def test_supports_text(): data = P('fonts/calibreSymbols.otf', data=True) if not supports_text(data, '.★½'): @@ -423,6 +447,7 @@ def test_supports_text(): if supports_text(data, 'abc'): raise RuntimeError('Incorrectly claiming that text is supported') + def test_find_font(): from calibre.utils.fonts.scanner import font_scanner abcd = '诶比西迪' @@ -438,6 +463,7 @@ def test(): test_supports_text() test_find_font() + def main(): import sys, os for f in sys.argv[1:]: diff --git a/src/calibre/utils/fonts/win_fonts.py b/src/calibre/utils/fonts/win_fonts.py index 8e631910db..eec6118e6f 100644 --- a/src/calibre/utils/fonts/win_fonts.py +++ b/src/calibre/utils/fonts/win_fonts.py @@ -15,6 +15,7 @@ from calibre.constants import plugins, filesystem_encoding from calibre.utils.fonts.utils import (is_truetype_font, get_font_names, get_font_characteristics) + class WinFonts(object): def __init__(self, winfonts): @@ -137,12 +138,14 @@ class WinFonts(object): def remove_system_font(self, path): return self.w.remove_system_font(path) + def load_winfonts(): w, err = plugins['winfonts'] if w is None: raise RuntimeError('Failed to load the winfonts module: %s'%err) return WinFonts(w) + def test_ttf_reading(): for f in sys.argv[1:]: raw = open(f).read() @@ -150,6 +153,7 @@ def test_ttf_reading(): get_font_characteristics(raw) print() + def test(): base = os.path.abspath(__file__) d = os.path.dirname diff --git a/src/calibre/utils/formatter.py b/src/calibre/utils/formatter.py index 66c4f9f636..8522f5a580 100644 --- a/src/calibre/utils/formatter.py +++ b/src/calibre/utils/formatter.py @@ -15,6 +15,7 @@ from calibre.constants import DEBUG from calibre.utils.formatter_functions import formatter_functions, compile_user_function from calibre.utils.config import tweaks + class _Parser(object): LEX_OP = 1 LEX_ID = 2 @@ -307,6 +308,7 @@ class _CompileParser(_Parser): compile_counter = 0 + class TemplateFormatter(string.Formatter): ''' Provides a format function that substitutes '' for any missing value @@ -531,10 +533,12 @@ class TemplateFormatter(string.Formatter): ans = error_value + ' ' + e.message return ans + class ValidateFormatter(TemplateFormatter): ''' Provides a formatter that substitutes the validation string for every value ''' + def get_value(self, key, args, kwargs): return self._validation_string @@ -545,10 +549,12 @@ class ValidateFormatter(TemplateFormatter): validation_formatter = ValidateFormatter() + class EvalFormatter(TemplateFormatter): ''' A template formatter that uses a simple dict instead of an mi instance ''' + def get_value(self, key, args, kwargs): if key == '': return '' diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py index a664f6536b..10f311d58a 100644 --- a/src/calibre/utils/formatter_functions.py +++ b/src/calibre/utils/formatter_functions.py @@ -23,6 +23,7 @@ from calibre.utils.icu import capitalize, strcmp, sort_key from calibre.utils.date import parse_date, format_date, now, UNDEFINED_DATE from calibre.utils.localization import calibre_langcode_to_name, canonicalize_lang + class FormatterFunctions(object): error_function_body = ('def evaluate(self, formatter, kwargs, mi, locals):\n' @@ -109,10 +110,12 @@ class FormatterFunctions(object): _ff = FormatterFunctions() + def formatter_functions(): global _ff return _ff + class FormatterFunction(object): doc = _('No documentation provided') @@ -133,6 +136,7 @@ class FormatterFunction(object): if isinstance(ret, (int, float, bool)): return unicode(ret) + class BuiltinFormatterFunction(FormatterFunction): def __init__(self): @@ -145,6 +149,7 @@ class BuiltinFormatterFunction(FormatterFunction): lines = [] self.program_text = ''.join(lines) + class BuiltinStrcmp(BuiltinFormatterFunction): name = 'strcmp' arg_count = 5 @@ -161,6 +166,7 @@ class BuiltinStrcmp(BuiltinFormatterFunction): return eq return gt + class BuiltinCmp(BuiltinFormatterFunction): name = 'cmp' category = 'Relational' @@ -177,6 +183,7 @@ class BuiltinCmp(BuiltinFormatterFunction): return eq return gt + class BuiltinFirstMatchingCmp(BuiltinFormatterFunction): name = 'first_matching_cmp' category = 'Relational' @@ -198,6 +205,7 @@ class BuiltinFirstMatchingCmp(BuiltinFormatterFunction): return args[i+1] return args[len(args)-1] + class BuiltinStrcat(BuiltinFormatterFunction): name = 'strcat' arg_count = -1 @@ -212,6 +220,7 @@ class BuiltinStrcat(BuiltinFormatterFunction): res += args[i] return res + class BuiltinStrlen(BuiltinFormatterFunction): name = 'strlen' arg_count = 1 @@ -225,6 +234,7 @@ class BuiltinStrlen(BuiltinFormatterFunction): except: return -1 + class BuiltinAdd(BuiltinFormatterFunction): name = 'add' arg_count = 2 @@ -236,6 +246,7 @@ class BuiltinAdd(BuiltinFormatterFunction): y = float(y if y and y != 'None' else 0) return unicode(x + y) + class BuiltinSubtract(BuiltinFormatterFunction): name = 'subtract' arg_count = 2 @@ -247,6 +258,7 @@ class BuiltinSubtract(BuiltinFormatterFunction): y = float(y if y and y != 'None' else 0) return unicode(x - y) + class BuiltinMultiply(BuiltinFormatterFunction): name = 'multiply' arg_count = 2 @@ -258,6 +270,7 @@ class BuiltinMultiply(BuiltinFormatterFunction): y = float(y if y and y != 'None' else 0) return unicode(x * y) + class BuiltinDivide(BuiltinFormatterFunction): name = 'divide' arg_count = 2 @@ -269,6 +282,7 @@ class BuiltinDivide(BuiltinFormatterFunction): y = float(y if y and y != 'None' else 0) return unicode(x / y) + class BuiltinTemplate(BuiltinFormatterFunction): name = 'template' arg_count = 1 @@ -288,6 +302,7 @@ class BuiltinTemplate(BuiltinFormatterFunction): template = template.replace('[[', '{').replace(']]', '}') return formatter.__class__().safe_format(template, kwargs, 'TEMPLATE', mi) + class BuiltinEval(BuiltinFormatterFunction): name = 'eval' arg_count = 1 @@ -307,6 +322,7 @@ class BuiltinEval(BuiltinFormatterFunction): template = template.replace('[[', '{').replace(']]', '}') return EvalFormatter().safe_format(template, locals, 'EVAL', None) + class BuiltinAssign(BuiltinFormatterFunction): name = 'assign' arg_count = 2 @@ -318,6 +334,7 @@ class BuiltinAssign(BuiltinFormatterFunction): locals[target] = value return value + class BuiltinPrint(BuiltinFormatterFunction): name = 'print' arg_count = -1 @@ -330,6 +347,7 @@ class BuiltinPrint(BuiltinFormatterFunction): print args return '' + class BuiltinField(BuiltinFormatterFunction): name = 'field' arg_count = 1 @@ -339,6 +357,7 @@ class BuiltinField(BuiltinFormatterFunction): def evaluate(self, formatter, kwargs, mi, locals, name): return formatter.get_value(name, [], kwargs) + class BuiltinRawField(BuiltinFormatterFunction): name = 'raw_field' arg_count = 1 @@ -355,6 +374,7 @@ class BuiltinRawField(BuiltinFormatterFunction): return fm['is_multiple']['list_to_ui'].join(res) return unicode(res) + class BuiltinRawList(BuiltinFormatterFunction): name = 'raw_list' arg_count = 2 @@ -369,6 +389,7 @@ class BuiltinRawList(BuiltinFormatterFunction): return "%s is not a list" % name return separator.join(res) + class BuiltinSubstr(BuiltinFormatterFunction): name = 'substr' arg_count = 3 @@ -383,6 +404,7 @@ class BuiltinSubstr(BuiltinFormatterFunction): def evaluate(self, formatter, kwargs, mi, locals, str_, start_, end_): return str_[int(start_): len(str_) if int(end_) == 0 else int(end_)] + class BuiltinLookup(BuiltinFormatterFunction): name = 'lookup' arg_count = -1 @@ -411,6 +433,7 @@ class BuiltinLookup(BuiltinFormatterFunction): return formatter.vformat('{'+args[i+1].strip() + '}', [], kwargs) i += 2 + class BuiltinTest(BuiltinFormatterFunction): name = 'test' arg_count = 3 @@ -424,6 +447,7 @@ class BuiltinTest(BuiltinFormatterFunction): else: return value_not_set + class BuiltinContains(BuiltinFormatterFunction): name = 'contains' arg_count = 4 @@ -440,6 +464,7 @@ class BuiltinContains(BuiltinFormatterFunction): else: return value_if_not + class BuiltinSwitch(BuiltinFormatterFunction): name = 'switch' arg_count = -1 @@ -461,6 +486,7 @@ class BuiltinSwitch(BuiltinFormatterFunction): return args[i+1] i += 2 + class BuiltinStrcatMax(BuiltinFormatterFunction): name = 'strcat_max' arg_count = -1 @@ -495,6 +521,7 @@ class BuiltinStrcatMax(BuiltinFormatterFunction): pass return result.strip() + class BuiltinInList(BuiltinFormatterFunction): name = 'in_list' arg_count = 5 @@ -513,6 +540,7 @@ class BuiltinInList(BuiltinFormatterFunction): return fv return nfv + class BuiltinStrInList(BuiltinFormatterFunction): name = 'str_in_list' arg_count = 5 @@ -534,6 +562,7 @@ class BuiltinStrInList(BuiltinFormatterFunction): return fv return nfv + class BuiltinIdentifierInList(BuiltinFormatterFunction): name = 'identifier_in_list' arg_count = 4 @@ -560,6 +589,7 @@ class BuiltinIdentifierInList(BuiltinFormatterFunction): return fv return nfv + class BuiltinRe(BuiltinFormatterFunction): name = 're' arg_count = 3 @@ -572,6 +602,7 @@ class BuiltinRe(BuiltinFormatterFunction): def evaluate(self, formatter, kwargs, mi, locals, val, pattern, replacement): return re.sub(pattern, replacement, val, flags=re.I) + class BuiltinReGroup(BuiltinFormatterFunction): name = 're_group' arg_count = -1 @@ -606,6 +637,7 @@ class BuiltinReGroup(BuiltinFormatterFunction): return res return re.sub(pattern, repl, val, flags=re.I) + class BuiltinSwapAroundComma(BuiltinFormatterFunction): name = 'swap_around_comma' arg_count = 1 @@ -618,6 +650,7 @@ class BuiltinSwapAroundComma(BuiltinFormatterFunction): def evaluate(self, formatter, kwargs, mi, locals, val): return re.sub(r'^(.*?),\s*(.*$)', r'\2 \1', val, flags=re.I).strip() + class BuiltinIfempty(BuiltinFormatterFunction): name = 'ifempty' arg_count = 2 @@ -631,6 +664,7 @@ class BuiltinIfempty(BuiltinFormatterFunction): else: return value_if_empty + class BuiltinShorten(BuiltinFormatterFunction): name = 'shorten' arg_count = 4 @@ -657,6 +691,7 @@ class BuiltinShorten(BuiltinFormatterFunction): else: return val + class BuiltinCount(BuiltinFormatterFunction): name = 'count' arg_count = 2 @@ -669,6 +704,7 @@ class BuiltinCount(BuiltinFormatterFunction): def evaluate(self, formatter, kwargs, mi, locals, val, sep): return unicode(len([v for v in val.split(sep) if v])) + class BuiltinListitem(BuiltinFormatterFunction): name = 'list_item' arg_count = 3 @@ -690,6 +726,7 @@ class BuiltinListitem(BuiltinFormatterFunction): except: return '' + class BuiltinSelect(BuiltinFormatterFunction): name = 'select' arg_count = 2 @@ -708,6 +745,7 @@ class BuiltinSelect(BuiltinFormatterFunction): return v[len(key)+1:] return '' + class BuiltinApproximateFormats(BuiltinFormatterFunction): name = 'approximate_formats' arg_count = 0 @@ -735,6 +773,7 @@ class BuiltinApproximateFormats(BuiltinFormatterFunction): return ','.join(v.upper() for v in data) return _('This function can be used only in the GUI') + class BuiltinFormatsModtimes(BuiltinFormatterFunction): name = 'formats_modtimes' arg_count = 1 @@ -755,6 +794,7 @@ class BuiltinFormatsModtimes(BuiltinFormatterFunction): return ','.join(k.upper()+':'+format_date(v['mtime'], fmt) for k,v in data) + class BuiltinFormatsSizes(BuiltinFormatterFunction): name = 'formats_sizes' arg_count = 0 @@ -771,6 +811,7 @@ class BuiltinFormatsSizes(BuiltinFormatterFunction): fmt_data = mi.get('format_metadata', {}) return ','.join(k.upper()+':'+str(v['size']) for k,v in fmt_data.iteritems()) + class BuiltinFormatsPaths(BuiltinFormatterFunction): name = 'formats_paths' arg_count = 0 @@ -786,6 +827,7 @@ class BuiltinFormatsPaths(BuiltinFormatterFunction): fmt_data = mi.get('format_metadata', {}) return ','.join(k.upper()+':'+str(v['path']) for k,v in fmt_data.iteritems()) + class BuiltinHumanReadable(BuiltinFormatterFunction): name = 'human_readable' arg_count = 1 @@ -800,6 +842,7 @@ class BuiltinHumanReadable(BuiltinFormatterFunction): except: return '' + class BuiltinFormatNumber(BuiltinFormatterFunction): name = 'format_number' arg_count = 2 @@ -831,6 +874,7 @@ class BuiltinFormatNumber(BuiltinFormatterFunction): pass return '' + class BuiltinSublist(BuiltinFormatterFunction): name = 'sublist' arg_count = 4 @@ -866,6 +910,7 @@ class BuiltinSublist(BuiltinFormatterFunction): except: return '' + class BuiltinSubitems(BuiltinFormatterFunction): name = 'subitems' arg_count = 3 @@ -910,6 +955,7 @@ class BuiltinSubitems(BuiltinFormatterFunction): pass return ', '.join(sorted(rv, key=sort_key)) + class BuiltinFormatDate(BuiltinFormatterFunction): name = 'format_date' arg_count = 2 @@ -947,6 +993,7 @@ class BuiltinFormatDate(BuiltinFormatterFunction): s = 'BAD DATE' return s + class BuiltinUppercase(BuiltinFormatterFunction): name = 'uppercase' arg_count = 1 @@ -956,6 +1003,7 @@ class BuiltinUppercase(BuiltinFormatterFunction): def evaluate(self, formatter, kwargs, mi, locals, val): return val.upper() + class BuiltinLowercase(BuiltinFormatterFunction): name = 'lowercase' arg_count = 1 @@ -965,6 +1013,7 @@ class BuiltinLowercase(BuiltinFormatterFunction): def evaluate(self, formatter, kwargs, mi, locals, val): return val.lower() + class BuiltinTitlecase(BuiltinFormatterFunction): name = 'titlecase' arg_count = 1 @@ -974,6 +1023,7 @@ class BuiltinTitlecase(BuiltinFormatterFunction): def evaluate(self, formatter, kwargs, mi, locals, val): return titlecase(val) + class BuiltinCapitalize(BuiltinFormatterFunction): name = 'capitalize' arg_count = 1 @@ -983,6 +1033,7 @@ class BuiltinCapitalize(BuiltinFormatterFunction): def evaluate(self, formatter, kwargs, mi, locals, val): return capitalize(val) + class BuiltinBooksize(BuiltinFormatterFunction): name = 'booksize' arg_count = 0 @@ -1006,6 +1057,7 @@ class BuiltinBooksize(BuiltinFormatterFunction): return '' return _('This function can be used only in the GUI') + class BuiltinOndevice(BuiltinFormatterFunction): name = 'ondevice' arg_count = 0 @@ -1024,6 +1076,7 @@ class BuiltinOndevice(BuiltinFormatterFunction): return '' return _('This function can be used only in the GUI') + class BuiltinSeriesSort(BuiltinFormatterFunction): name = 'series_sort' arg_count = 0 @@ -1035,6 +1088,7 @@ class BuiltinSeriesSort(BuiltinFormatterFunction): return title_sort(mi.series) return '' + class BuiltinHasCover(BuiltinFormatterFunction): name = 'has_cover' arg_count = 0 @@ -1047,6 +1101,7 @@ class BuiltinHasCover(BuiltinFormatterFunction): return _('Yes') return '' + class BuiltinFirstNonEmpty(BuiltinFormatterFunction): name = 'first_non_empty' arg_count = -1 @@ -1064,6 +1119,7 @@ class BuiltinFirstNonEmpty(BuiltinFormatterFunction): i += 1 return '' + class BuiltinAnd(BuiltinFormatterFunction): name = 'and' arg_count = -1 @@ -1081,6 +1137,7 @@ class BuiltinAnd(BuiltinFormatterFunction): i += 1 return '1' + class BuiltinOr(BuiltinFormatterFunction): name = 'or' arg_count = -1 @@ -1098,6 +1155,7 @@ class BuiltinOr(BuiltinFormatterFunction): i += 1 return '' + class BuiltinNot(BuiltinFormatterFunction): name = 'not' arg_count = 1 @@ -1110,6 +1168,7 @@ class BuiltinNot(BuiltinFormatterFunction): def evaluate(self, formatter, kwargs, mi, locals, val): return '' if val else '1' + class BuiltinListUnion(BuiltinFormatterFunction): name = 'list_union' arg_count = 3 @@ -1134,6 +1193,7 @@ class BuiltinListUnion(BuiltinFormatterFunction): return ', '.join(res) return separator.join(res) + class BuiltinListDifference(BuiltinFormatterFunction): name = 'list_difference' arg_count = 3 @@ -1155,6 +1215,7 @@ class BuiltinListDifference(BuiltinFormatterFunction): return ', '.join(res) return separator.join(res) + class BuiltinListIntersection(BuiltinFormatterFunction): name = 'list_intersection' arg_count = 3 @@ -1176,6 +1237,7 @@ class BuiltinListIntersection(BuiltinFormatterFunction): return ', '.join(res) return separator.join(res) + class BuiltinListSort(BuiltinFormatterFunction): name = 'list_sort' arg_count = 3 @@ -1191,6 +1253,7 @@ class BuiltinListSort(BuiltinFormatterFunction): return ', '.join(sorted(res, key=sort_key, reverse=direction != "0")) return separator.join(sorted(res, key=sort_key, reverse=direction != "0")) + class BuiltinListEquals(BuiltinFormatterFunction): name = 'list_equals' arg_count = 6 @@ -1209,6 +1272,7 @@ class BuiltinListEquals(BuiltinFormatterFunction): return yes_val return no_val + class BuiltinListRe(BuiltinFormatterFunction): name = 'list_re' arg_count = 4 @@ -1234,6 +1298,7 @@ class BuiltinListRe(BuiltinFormatterFunction): return ', '.join(res) return separator.join(res) + class BuiltinListReGroup(BuiltinFormatterFunction): name = 'list_re_group' arg_count = -1 @@ -1273,6 +1338,7 @@ class BuiltinListReGroup(BuiltinFormatterFunction): return ', '.join(res) return separator.join(res) + class BuiltinToday(BuiltinFormatterFunction): name = 'today' arg_count = 0 @@ -1281,9 +1347,11 @@ class BuiltinToday(BuiltinFormatterFunction): 'return a date string for today. This value is designed for use in ' 'format_date or days_between, but can be manipulated like any ' 'other string. The date is in ISO format.') + def evaluate(self, formatter, kwargs, mi, locals): return format_date(now(), 'iso') + class BuiltinDaysBetween(BuiltinFormatterFunction): name = 'days_between' arg_count = 2 @@ -1293,6 +1361,7 @@ class BuiltinDaysBetween(BuiltinFormatterFunction): 'positive if date1 is greater than date2, otherwise negative. If ' 'either date1 or date2 are not dates, the function returns the ' 'empty string.') + def evaluate(self, formatter, kwargs, mi, locals, date1, date2): try: d1 = parse_date(date1) @@ -1306,6 +1375,7 @@ class BuiltinDaysBetween(BuiltinFormatterFunction): i = d1 - d2 return '%.1f'%(i.days + (i.seconds/(24.0*60.0*60.0))) + class BuiltinLanguageStrings(BuiltinFormatterFunction): name = 'language_strings' arg_count = 2 @@ -1315,6 +1385,7 @@ class BuiltinLanguageStrings(BuiltinFormatterFunction): 'If localize is zero, return the strings in English. If ' 'localize is not zero, return the strings in the language of ' 'the current locale. Lang_codes is a comma-separated list.') + def evaluate(self, formatter, kwargs, mi, locals, lang_codes, localize): retval = [] for c in [c.strip() for c in lang_codes.split(',') if c.strip()]: @@ -1326,6 +1397,7 @@ class BuiltinLanguageStrings(BuiltinFormatterFunction): pass return ', '.join(retval) + class BuiltinLanguageCodes(BuiltinFormatterFunction): name = 'language_codes' arg_count = 1 @@ -1334,6 +1406,7 @@ class BuiltinLanguageCodes(BuiltinFormatterFunction): 'return the language codes for the strings passed in lang_strings. ' 'The strings must be in the language of the current locale. ' 'Lang_strings is a comma-separated list.') + def evaluate(self, formatter, kwargs, mi, locals, lang_strings): retval = [] for c in [c.strip() for c in lang_strings.split(',') if c.strip()]: @@ -1345,6 +1418,7 @@ class BuiltinLanguageCodes(BuiltinFormatterFunction): pass return ', '.join(retval) + class BuiltinCurrentLibraryName(BuiltinFormatterFunction): name = 'current_library_name' arg_count = 0 @@ -1353,10 +1427,12 @@ class BuiltinCurrentLibraryName(BuiltinFormatterFunction): 'return the last name on the path to the current calibre library. ' 'This function can be called in template program mode using the ' 'template "{:\'current_library_name()\'}".') + def evaluate(self, formatter, kwargs, mi, locals): from calibre.library import current_library_name return current_library_name() + class BuiltinCurrentLibraryPath(BuiltinFormatterFunction): name = 'current_library_path' arg_count = 0 @@ -1365,10 +1441,12 @@ class BuiltinCurrentLibraryPath(BuiltinFormatterFunction): 'return the path to the current calibre library. This function can ' 'be called in template program mode using the template ' '"{:\'current_library_path()\'}".') + def evaluate(self, formatter, kwargs, mi, locals): from calibre.library import current_library_path return current_library_path() + class BuiltinFinishFormatting(BuiltinFormatterFunction): name = 'finish_formatting' arg_count = 4 @@ -1385,6 +1463,7 @@ class BuiltinFinishFormatting(BuiltinFormatterFunction): return val return prefix + formatter._do_format(val, fmt) + suffix + class BuiltinVirtualLibraries(BuiltinFormatterFunction): name = 'virtual_libraries' arg_count = 0 @@ -1402,6 +1481,7 @@ class BuiltinVirtualLibraries(BuiltinFormatterFunction): return mi._proxy_metadata.virtual_libraries return _('This function can be used only in the GUI') + class BuiltinUserCategories(BuiltinFormatterFunction): name = 'user_categories' arg_count = 0 @@ -1421,6 +1501,7 @@ class BuiltinUserCategories(BuiltinFormatterFunction): return ', '.join(cats) return _('This function can be used only in the GUI') + class BuiltinTransliterate(BuiltinFormatterFunction): name = 'transliterate' arg_count = 1 @@ -1461,6 +1542,7 @@ class BuiltinAuthorLinks(BuiltinFormatterFunction): return pair_sep.join(n + val_sep + link_data[n] for n in names) return _('This function can be used only in the GUI') + class BuiltinAuthorSorts(BuiltinFormatterFunction): name = 'author_sorts' arg_count = 1 @@ -1507,6 +1589,7 @@ _formatter_builtins = [ BuiltinUserCategories(), BuiltinVirtualLibraries() ] + class FormatterUserFunction(FormatterFunction): def __init__(self, name, doc, arg_count, program_text): @@ -1516,6 +1599,8 @@ class FormatterUserFunction(FormatterFunction): self.program_text = program_text tabs = re.compile(r'^\t*') + + def compile_user_function(name, doc, arg_count, eval_func): def replace_func(mo): return mo.group().replace('\t', ' ') @@ -1554,5 +1639,6 @@ def load_user_template_functions(library_uuid, funcs): traceback.print_exc() formatter_functions().register_functions(library_uuid, compiled_funcs) + def unload_user_template_functions(library_uuid): formatter_functions().unregister_functions(library_uuid) diff --git a/src/calibre/utils/html2text.py b/src/calibre/utils/html2text.py index 99f221951f..d81b1854a5 100644 --- a/src/calibre/utils/html2text.py +++ b/src/calibre/utils/html2text.py @@ -38,6 +38,7 @@ SKIP_INTERNAL_LINKS = True # ## Entity Nonsense ### + def name2cp(k): if k == 'apos': return ord("'") @@ -63,6 +64,7 @@ unifiable_n = {} for k in unifiable.keys(): unifiable_n[name2cp(k)] = unifiable[k] + def charref(name): if name[0] in ['x','X']: c = int(name[1:], 16) @@ -74,6 +76,7 @@ def charref(name): else: return unichr(c) + def entityref(c): if not UNICODE_SNOB and c in unifiable.keys(): return unifiable[c] @@ -85,6 +88,7 @@ def entityref(c): else: return unichr(name2cp(c)) + def replaceEntities(s): s = s.group(1) if s[0] == "#": @@ -93,9 +97,12 @@ def replaceEntities(s): return entityref(s) r_unescape = re.compile(r"&(#?[xX]?(?:[0-9a-fA-F]+|\w{1,8}));") + + def unescape(s): return r_unescape.sub(replaceEntities, s) + def fixattrs(attrs): # Fix bug in sgmllib.py if not attrs: @@ -107,6 +114,7 @@ def fixattrs(attrs): # ## End Entity Nonsense ### + def onlywhite(line): """Return true if the line does only consist of whitespace characters.""" for c in line: @@ -114,6 +122,7 @@ def onlywhite(line): return c is ' ' return line + def optwrap(text): """Wrap all paragraphs in the provided text.""" if not BODY_WIDTH: @@ -139,6 +148,7 @@ def optwrap(text): newlines += 1 return result + def hn(tag): if tag[0] == 'h' and len(tag) == 2: try: @@ -148,6 +158,7 @@ def hn(tag): except ValueError: return 0 + class _html2text(sgmllib.SGMLParser): def __init__(self, out=None, baseurl=''): @@ -409,15 +420,18 @@ class _html2text(sgmllib.SGMLParser): def unknown_decl(self, data): pass + def wrapwrite(text): sys.stdout.write(text.encode('utf8')) + def html2text_file(html, out=wrapwrite, baseurl=''): h = _html2text(out, baseurl) h.feed(html) h.feed("") return h.close() + def html2text(html, baseurl=''): return optwrap(html2text_file(html, None, baseurl)) diff --git a/src/calibre/utils/https.py b/src/calibre/utils/https.py index 2a1fd5a7fc..b32b4248af 100644 --- a/src/calibre/utils/https.py +++ b/src/calibre/utils/https.py @@ -13,6 +13,7 @@ from calibre import get_proxies from calibre.constants import ispy3 has_ssl_verify = hasattr(ssl, 'create_default_context') and hasattr(ssl, '_create_unverified_context') + class HTTPError(ValueError): def __init__(self, url, code): @@ -154,6 +155,7 @@ else: self.sock = ssl.wrap_socket(sock, cert_reqs=ssl.CERT_REQUIRED, ca_certs=self.cert_file, ssl_version=self.calibre_ssl_version) getattr(ssl, 'match_hostname', match_hostname)(self.sock.getpeercert(), self.host) + def get_https_resource_securely( url, cacerts='calibre-ebook-root-CA.crt', timeout=60, max_redirects=5, ssl_version=None, headers=None, get_response=False): ''' diff --git a/src/calibre/utils/icu.py b/src/calibre/utils/icu.py index 26fd42e923..3efd1f4954 100644 --- a/src/calibre/utils/icu.py +++ b/src/calibre/utils/icu.py @@ -31,6 +31,8 @@ icu_unicode_version = getattr(_icu, 'unicode_version', None) _nmodes = {m:getattr(_icu, 'UNORM_'+m, None) for m in ('NFC', 'NFD', 'NFKC', 'NFKD', 'NONE', 'DEFAULT', 'FCD')} # Ensure that the python internal filesystem and default encodings are not ASCII + + def is_ascii(name): try: return codecs.lookup(name).name == b'ascii' @@ -51,6 +53,7 @@ except: traceback.print_exc() del is_ascii + def collator(): global _collator, _locale if _collator is None: @@ -67,11 +70,13 @@ def collator(): _collator = _icu.Collator('en') return _collator + def change_locale(locale=None): global _locale, _collator, _primary_collator, _sort_collator, _numeric_collator, _case_sensitive_collator _collator = _primary_collator = _sort_collator = _numeric_collator = _case_sensitive_collator = None _locale = locale + def primary_collator(): 'Ignores case differences and accented characters' global _primary_collator @@ -80,6 +85,7 @@ def primary_collator(): _primary_collator.strength = _icu.UCOL_PRIMARY return _primary_collator + def sort_collator(): 'Ignores case differences and recognizes numbers in strings (if the tweak is set)' global _sort_collator @@ -89,6 +95,7 @@ def sort_collator(): _sort_collator.numeric = tweaks['numeric_collation'] return _sort_collator + def numeric_collator(): 'Uses natural sorting for numbers inside strings so something2 will sort before something10' global _numeric_collator @@ -98,6 +105,7 @@ def numeric_collator(): _numeric_collator.numeric = True return _numeric_collator + def case_sensitive_collator(): 'Always sorts upper case letter before lower case' global _case_sensitive_collator @@ -171,6 +179,7 @@ def {name}(x): raise ''' + def _make_func(template, name, **kwargs): l = globals() kwargs['name'] = name @@ -206,6 +215,7 @@ lower = _make_func(_change_case_template, 'lower', which='LOWER_CASE') title_case = _make_func(_change_case_template, 'title_case', which='TITLE_CASE') + def capitalize(x): try: return upper(x[0]) + lower(x[1:]) @@ -233,18 +243,21 @@ safe_chr = _icu.chr ord_string = _icu.ord_string + def character_name(string): try: return _icu.character_name(unicode(string)) or None except (TypeError, ValueError, KeyError): pass + def character_name_from_code(code): try: return _icu.character_name_from_code(code) or '' except (TypeError, ValueError, KeyError): return '' + def normalize(text, mode='NFC'): # This is very slightly slower than using unicodedata.normalize, so stick with # that unless you have very good reasons not too. Also, it's speed @@ -252,6 +265,7 @@ def normalize(text, mode='NFC'): # representation is slower. return _icu.normalize(_nmodes[mode], unicode(text)) + def contractions(col=None): global _cmap col = col or _collator @@ -264,6 +278,7 @@ def contractions(col=None): _cmap[col] = ans return ans + def partition_by_first_letter(items, reverse=False, key=lambda x:x): # Build a list of 'equal' first letters by noticing changes # in ICU's 'ordinal' for the first letter. diff --git a/src/calibre/utils/icu_test.py b/src/calibre/utils/icu_test.py index 51fe8275df..caa11d217a 100644 --- a/src/calibre/utils/icu_test.py +++ b/src/calibre/utils/icu_test.py @@ -21,6 +21,7 @@ def make_collation_func(name, locale, numeric=True, template='_sort_key_template yield icu._make_func(getattr(icu, template), name, collator=cname, collator_func='not_used_xxx', func=func) delattr(icu, cname) + class TestICU(unittest.TestCase): ae = unittest.TestCase.assertEqual @@ -197,17 +198,21 @@ class TestICU(unittest.TestCase): fpos = index_of(needle, haystack) self.ae(pos, fpos, 'Failed to find index of %r in %r (%d != %d)' % (needle, haystack, pos, fpos)) + def find_tests(): return unittest.defaultTestLoader.loadTestsFromTestCase(TestICU) + class TestRunner(unittest.main): def createTests(self): self.test = find_tests() + def run(verbosity=4): TestRunner(verbosity=verbosity, exit=False) + def test_build(): result = TestRunner(verbosity=0, buffer=True, catchbreak=True, failfast=True, argv=sys.argv[:1], exit=False).result if not result.wasSuccessful(): diff --git a/src/calibre/utils/img.py b/src/calibre/utils/img.py index 4f16678235..7097488dbd 100644 --- a/src/calibre/utils/img.py +++ b/src/calibre/utils/img.py @@ -22,15 +22,18 @@ if imageops is None: ' get it by installing the betas from: http://www.mobileread.com/forums/showthread.php?t=274030') raise RuntimeError(imageops_err) + class NotImage(ValueError): pass + def normalize_format_name(fmt): fmt = fmt.lower() if fmt == 'jpg': fmt = 'jpeg' return fmt + def get_exe_path(name): from calibre.ebooks.pdf.pdftohtml import PDFTOHTML base = os.path.dirname(PDFTOHTML) @@ -43,10 +46,12 @@ def get_exe_path(name): # Loading images {{{ + def null_image(): ' Create an invalid image. For internal use. ' return QImage() + def image_from_data(data): ' Create an image object from data, which should be a bytestring. ' if isinstance(data, QImage): @@ -56,11 +61,13 @@ def image_from_data(data): raise NotImage('Not a valid image') return i + def image_from_path(path): ' Load an image from the specified path. ' with lopen(path, 'rb') as f: return image_from_data(f.read()) + def image_from_x(x): ' Create an image from a bytestring or a path or a file like object. ' if isinstance(x, type('')): @@ -75,6 +82,7 @@ def image_from_x(x): return x.toImage() raise TypeError('Unknown image src type: %s' % type(x)) + def image_and_format_from_data(data): ' Create an image object from the specified data which should be a bytsestring and also return the format of the image ' ba = QByteArray(data) @@ -87,6 +95,7 @@ def image_and_format_from_data(data): # Saving images {{{ + def image_to_data(img, compression_quality=95, fmt='JPEG', png_compression_level=9, jpeg_optimized=True, jpeg_progressive=False): ''' Serialize image to bytestring in the specified format. @@ -128,6 +137,7 @@ def image_to_data(img, compression_quality=95, fmt='JPEG', png_compression_level raise ValueError('Failed to export image as ' + fmt + ' with error: ' + w.errorString()) return ba.data() + def save_image(img, path, **kw): ''' Save image to the specified path. Image format is taken from the file extension. You can pass the same keyword arguments as for the @@ -137,6 +147,7 @@ def save_image(img, path, **kw): with lopen(path, 'wb') as f: f.write(image_to_data(image_from_data(img), **kw)) + def save_cover_data_to(data, path=None, bgcolor='#ffffff', resize_to=None, compression_quality=90, minify_to=None, grayscale=False, data_fmt='jpeg'): ''' Saves image in data to path, in the format specified by the path @@ -188,6 +199,7 @@ def save_cover_data_to(data, path=None, bgcolor='#ffffff', resize_to=None, compr # Overlaying images {{{ + def blend_on_canvas(img, width, height, bgcolor='#ffffff'): ' Blend the `img` onto a canvas with the specified background color and size ' w, h = img.width(), img.height() @@ -200,6 +212,7 @@ def blend_on_canvas(img, width, height, bgcolor='#ffffff'): overlay_image(img, canvas, (width - w)//2, (height - h)//2) return canvas + class Canvas(object): def __init__(self, width, height, bgcolor='#ffffff'): @@ -219,6 +232,7 @@ class Canvas(object): def export(self, fmt='JPEG', compression_quality=95): return image_to_data(self.img, compression_quality=compression_quality, fmt=fmt) + def create_canvas(width, height, bgcolor='#ffffff'): 'Create a blank canvas of the specified size and color ' img = QImage(width, height, QImage.Format_RGB32) @@ -235,12 +249,14 @@ def overlay_image(img, canvas=None, left=0, top=0): imageops.overlay(img, canvas, left, top) return canvas + def texture_image(canvas, texture): ' Repeatedly tile the image `texture` across and down the image `canvas` ' if canvas.hasAlphaChannel(): canvas = blend_image(canvas) return imageops.texture_image(canvas, texture) + def blend_image(img, bgcolor='#ffffff'): ' Used to convert images that have semi-transparent pixels to opaque by blending with the specified color ' canvas = QImage(img.size(), QImage.Format_RGB32) @@ -251,6 +267,7 @@ def blend_image(img, bgcolor='#ffffff'): # Image borders {{{ + def add_borders_to_image(img, left=0, top=0, right=0, bottom=0, border_color='#ffffff'): img = image_from_data(img) if not (left > 0 or right > 0 or top > 0 or bottom > 0): @@ -260,6 +277,7 @@ def add_borders_to_image(img, left=0, top=0, right=0, bottom=0, border_color='#f overlay_image(img, canvas, left, top) return canvas + def remove_borders_from_image(img, fuzz=None): ''' Try to auto-detect and remove any borders from the image. Returns the image itself if no borders could be removed. `fuzz` is a measure of @@ -273,9 +291,11 @@ def remove_borders_from_image(img, fuzz=None): # Cropping/scaling of images {{{ + def resize_image(img, width, height): return img.scaled(int(width), int(height), Qt.IgnoreAspectRatio, Qt.SmoothTransformation) + def resize_to_fit(img, width, height): img = image_from_data(img) resize_needed, nw, nh = fit_image(img.width(), img.height(), width, height) @@ -283,11 +303,13 @@ def resize_to_fit(img, width, height): resize_image(img, nw, nh) return resize_needed, img + def clone_image(img): ''' Returns a shallow copy of the image. However, the underlying data buffer will be automatically copied-on-write ''' return QImage(img) + def scale_image(data, width=60, height=80, compression_quality=70, as_png=False, preserve_aspect_ratio=True): ''' Scale an image, returning it as either JPEG or PNG data (bytestring). Transparency is alpha blended with white when converting to JPEG. Is thread @@ -306,6 +328,7 @@ def scale_image(data, width=60, height=80, compression_quality=70, as_png=False, w, h = img.width(), img.height() return w, h, image_to_data(img, compression_quality=compression_quality, fmt=fmt) + def crop_image(img, x, y, width, height): ''' Return the specified section of the image. @@ -324,18 +347,22 @@ def crop_image(img, x, y, width, height): # Image transformations {{{ + def grayscale_image(img): return imageops.grayscale(image_from_data(img)) + def set_image_opacity(img, alpha=0.5): ''' Change the opacity of `img`. Note that the alpha value is multiplied to any existing alpha values, so you cannot use this function to convert a semi-transparent image to an opaque one. For that use `blend_image()`. ''' return imageops.set_opacity(image_from_data(img), alpha) + def flip_image(img, horizontal=False, vertical=False): return image_from_data(img).mirrored(horizontal, vertical) + def image_has_transparent_pixels(img): ' Return True iff the image has at least one semi-transparent pixel ' img = image_from_data(img) @@ -343,26 +370,33 @@ def image_has_transparent_pixels(img): return False return imageops.has_transparent_pixels(img) + def rotate_image(img, degrees): t = QTransform() t.rotate(degrees) return image_from_data(img).transformed(t) + def gaussian_sharpen_image(img, radius=0, sigma=3, high_quality=True): return imageops.gaussian_sharpen(image_from_data(img), max(0, radius), sigma, high_quality) + def gaussian_blur_image(img, radius=-1, sigma=3): return imageops.gaussian_blur(image_from_data(img), max(0, radius), sigma) + def despeckle_image(img): return imageops.despeckle(image_from_data(img)) + def oil_paint_image(img, radius=-1, high_quality=True): return imageops.oil_paint(image_from_data(img), radius, high_quality) + def normalize_image(img): return imageops.normalize(image_from_data(img)) + def quantize_image(img, max_colors=256, dither=True, palette=''): ''' Quantize the image to contain a maximum of `max_colors` colors. By default a palette is chosen automatically, if you want to use a fixed @@ -385,6 +419,7 @@ def quantize_image(img, max_colors=256, dither=True, palette=''): # Optimization of images {{{ + def run_optimizer(file_path, cmd, as_filter=False, input_data=None): file_path = os.path.abspath(file_path) cwd = os.path.dirname(file_path) @@ -398,6 +433,7 @@ def run_optimizer(file_path, cmd, as_filter=False, input_data=None): else: os.close(fd) iname, oname = os.path.basename(file_path), os.path.basename(outfile) + def repl(q, r): cmd[cmd.index(q)] = r if not as_filter: @@ -416,6 +452,7 @@ def run_optimizer(file_path, cmd, as_filter=False, input_data=None): stderr = p.stderr if as_filter else p.stdout if as_filter: src = input_data or open(file_path, 'rb') + def copy(src, dest): try: shutil.copyfileobj(src, dest) @@ -453,16 +490,19 @@ def run_optimizer(file_path, cmd, as_filter=False, input_data=None): if err.errno != errno.ENOENT: raise + def optimize_jpeg(file_path): exe = get_exe_path('jpegtran') cmd = [exe] + '-copy none -optimize -progressive -maxmemory 100M -outfile'.split() + [False, True] return run_optimizer(file_path, cmd) + def optimize_png(file_path): exe = get_exe_path('optipng') cmd = [exe] + '-fix -clobber -strip all -o7 -out'.split() + [False, True] return run_optimizer(file_path, cmd) + def encode_jpeg(file_path, quality=80): from calibre.utils.speedups import ReadOnlyFileBuffer quality = max(0, min(100, int(quality))) @@ -479,6 +519,7 @@ def encode_jpeg(file_path, quality=80): return run_optimizer(file_path, cmd, as_filter=True, input_data=ReadOnlyFileBuffer(ba.data())) # }}} + def test(): # {{{ from calibre.ptempfile import TemporaryDirectory from calibre import CurrentDir diff --git a/src/calibre/utils/imghdr.py b/src/calibre/utils/imghdr.py index d8c827c021..fbb827d0fa 100644 --- a/src/calibre/utils/imghdr.py +++ b/src/calibre/utils/imghdr.py @@ -12,6 +12,7 @@ from calibre.utils.speedups import ReadOnlyFileBuffer HSIZE = 120 + def what(file, h=None): ' Recognize image headers ' if h is None: @@ -34,6 +35,7 @@ def what(file, h=None): return 'jpeg' return None + def identify(src): ''' Recognize file format and sizes. Returns format, width, height. width and height will be -1 if not found and fmt will be None if the image is not @@ -90,6 +92,7 @@ def identify(src): tests = [] + def test_jpeg(h): """JPEG data in JFIF format (Changed by Kovid to mimic the file utility, the original code was failing with some jpegs that included ICC_PROFILE @@ -101,6 +104,7 @@ def test_jpeg(h): if b'JFIF' in q or b'8BIM' in q: return 'jpeg' + def jpeg_dimensions(stream): # A JPEG marker is two bytes of the form 0xff x where 0 < x < 0xff # See section B.1.1.2 of https://www.w3.org/Graphics/JPEG/itu-t81.pdf @@ -144,12 +148,14 @@ def jpeg_dimensions(stream): tests.append(test_jpeg) + def test_png(h): if h[:8] == b"\211PNG\r\n\032\n": return 'png' tests.append(test_png) + def test_gif(h): """GIF ('87 and '89 variants)""" if h[:6] in (b'GIF87a', b'GIF89a'): @@ -157,6 +163,7 @@ def test_gif(h): tests.append(test_gif) + def test_tiff(h): """TIFF (can be in Motorola or Intel byte order)""" if h[:2] in (b'MM', b'II'): @@ -164,12 +171,14 @@ def test_tiff(h): tests.append(test_tiff) + def test_webp(h): if h[:4] == b'RIFF' and h[8:12] == b'WEBP': return 'webp' tests.append(test_webp) + def test_rgb(h): """SGI image library""" if h[:2] == b'\001\332': @@ -177,6 +186,7 @@ def test_rgb(h): tests.append(test_rgb) + def test_pbm(h): """PBM (portable bitmap)""" if len(h) >= 3 and \ @@ -185,6 +195,7 @@ def test_pbm(h): tests.append(test_pbm) + def test_pgm(h): """PGM (portable graymap)""" if len(h) >= 3 and \ @@ -193,6 +204,7 @@ def test_pgm(h): tests.append(test_pgm) + def test_ppm(h): """PPM (portable pixmap)""" if len(h) >= 3 and \ @@ -201,6 +213,7 @@ def test_ppm(h): tests.append(test_ppm) + def test_rast(h): """Sun raster file""" if h[:4] == b'\x59\xA6\x6A\x95': @@ -208,6 +221,7 @@ def test_rast(h): tests.append(test_rast) + def test_xbm(h): """X bitmap (X10 or X11)""" s = b'#define ' @@ -216,24 +230,28 @@ def test_xbm(h): tests.append(test_xbm) + def test_bmp(h): if h[:2] == b'BM': return 'bmp' tests.append(test_bmp) + def test_emf(h): if h[:4] == b'\x01\0\0\0' and h[40:44] == b' EMF': return 'emf' tests.append(test_emf) + def test_jpeg2000(h): if h[:12] == b'\x00\x00\x00\x0cjP \r\n\x87\n': return 'jpeg2000' tests.append(test_jpeg2000) + def test_svg(h): if h[:4] == b' for the flags defined below @@ -196,9 +202,11 @@ class INotify(object): 'Return True iff there are events waiting to be read. Blocks if timeout is None. Polls if timeout is 0.' return len((select.select([self._inotify_fd], [], []) if timeout is None else select.select([self._inotify_fd], [], [], timeout))[0]) > 0 + def realpath(path): return os.path.abspath(os.path.realpath(path)) + class INotifyTreeWatcher(INotify): is_dummy = False diff --git a/src/calibre/utils/ip_routing.py b/src/calibre/utils/ip_routing.py index f54fa20ec2..a6476b1cd2 100644 --- a/src/calibre/utils/ip_routing.py +++ b/src/calibre/utils/ip_routing.py @@ -7,6 +7,7 @@ from __future__ import (unicode_literals, division, absolute_import, import subprocess, re from calibre.constants import iswindows, isosx + def get_address_of_default_gateway(family='AF_INET'): import netifaces ip = netifaces.gateways()['default'][getattr(netifaces, family)][0] @@ -14,6 +15,7 @@ def get_address_of_default_gateway(family='AF_INET'): ip = ip.decode('ascii') return ip + def get_addresses_for_interface(name, family='AF_INET'): import netifaces for entry in netifaces.ifaddresses(name)[getattr(netifaces, family)]: diff --git a/src/calibre/utils/ipc/__init__.py b/src/calibre/utils/ipc/__init__.py index 45d5f5ea4a..2dca6bf5f9 100644 --- a/src/calibre/utils/ipc/__init__.py +++ b/src/calibre/utils/ipc/__init__.py @@ -15,6 +15,7 @@ from calibre.utils.filenames import ascii_filename ADDRESS = VADDRESS = None + def eintr_retry_call(func, *args, **kwargs): while True: try: @@ -24,6 +25,7 @@ def eintr_retry_call(func, *args, **kwargs): continue raise + def gui_socket_address(): global ADDRESS if ADDRESS is None: diff --git a/src/calibre/utils/ipc/job.py b/src/calibre/utils/ipc/job.py index 5f0e68298e..34a2a24d20 100644 --- a/src/calibre/utils/ipc/job.py +++ b/src/calibre/utils/ipc/job.py @@ -14,6 +14,7 @@ from Queue import Queue, Empty from calibre import prints from calibre.constants import DEBUG + class BaseJob(object): WAITING = 0 diff --git a/src/calibre/utils/ipc/launch.py b/src/calibre/utils/ipc/launch.py index 2cb4ee3300..5dfcf4f880 100644 --- a/src/calibre/utils/ipc/launch.py +++ b/src/calibre/utils/ipc/launch.py @@ -22,12 +22,14 @@ if iswindows: ' corrupted windows. You should contact Microsoft' ' for assistance and/or follow the steps described here: http://bytes.com/topic/net/answers/264804-compile-error-null-device-missing') + def renice(niceness): try: os.nice(niceness) except: pass + class Worker(object): ''' Platform independent object for launching child processes. All processes diff --git a/src/calibre/utils/ipc/pool.py b/src/calibre/utils/ipc/pool.py index dbd8bde274..a36de998cd 100644 --- a/src/calibre/utils/ipc/pool.py +++ b/src/calibre/utils/ipc/pool.py @@ -49,6 +49,7 @@ if iswindows: from calibre.utils.ipc.launch import windows_null_file worker_kwargs['stdout'] = worker_kwargs['stderr'] = windows_null_file + def get_stdout(process): import time while process.poll() is None: @@ -64,6 +65,7 @@ def get_stdout(process): except (EOFError, EnvironmentError): break + def start_worker(code, name=''): from calibre.utils.ipc.simple_worker import start_pipe_worker if name: @@ -75,6 +77,7 @@ def start_worker(code, name=''): t.start() return p + class Failure(Exception): def __init__(self, tf): @@ -83,6 +86,7 @@ class Failure(Exception): self.job_id = tf.job_id self.failure_message = tf.message + class Worker(object): def __init__(self, p, conn, events, name): @@ -295,6 +299,7 @@ class Pool(Thread): pass # If the process has already been killed workers = [w.process for w in self.available_workers + list(self.busy_workers)] aw = list(self.available_workers) + def join(): for w in aw: try: @@ -329,6 +334,7 @@ class Pool(Thread): except EnvironmentError: pass + def worker_main(conn): from importlib import import_module common_data = None @@ -378,6 +384,7 @@ def worker_main(conn): return 1 return 0 + def run_main(func): from multiprocessing.connection import Client from contextlib import closing @@ -385,9 +392,11 @@ def run_main(func): with closing(Client(address, authkey=key)) as conn: raise SystemExit(func(conn)) + def test_write(): print ('Printing to stdout in worker') + def test(): def get_results(pool, ignore_fail=False): ans = {} diff --git a/src/calibre/utils/ipc/proxy.py b/src/calibre/utils/ipc/proxy.py index 25aea22636..b58cda06e7 100644 --- a/src/calibre/utils/ipc/proxy.py +++ b/src/calibre/utils/ipc/proxy.py @@ -17,12 +17,14 @@ from calibre import as_unicode, prints from calibre.constants import iswindows, DEBUG from calibre.utils.ipc import eintr_retry_call + def _encode(msg): raw = cPickle.dumps(msg, -1) size = len(raw) header = struct.pack('!Q', size) return header + raw + def _decode(raw): sz = struct.calcsize('!Q') if len(raw) < sz: @@ -81,6 +83,7 @@ class Writer(Thread): def __exit__(self, exc_type, exc_val, exc_tb): self.close() + class Server(Thread): def __init__(self, dispatcher): diff --git a/src/calibre/utils/ipc/server.py b/src/calibre/utils/ipc/server.py index 76e7e5a811..48463c1c69 100644 --- a/src/calibre/utils/ipc/server.py +++ b/src/calibre/utils/ipc/server.py @@ -23,6 +23,7 @@ from calibre.ptempfile import base_dir _counter = 0 + class ConnectedWorker(Thread): def __init__(self, worker, conn, rfile): @@ -81,6 +82,7 @@ class ConnectedWorker(Thread): self._returncode = r return r + class CriticalError(Exception): pass @@ -88,6 +90,7 @@ _name_counter = itertools.count() if islinux: import fcntl + class LinuxListener(Listener): def __init__(self, *args, **kwargs): @@ -168,6 +171,7 @@ else: if err.errno != errno.EADDRINUSE: raise + class Server(Thread): def __init__(self, notify_on_job_done=lambda x: x, pool_size=None, diff --git a/src/calibre/utils/ipc/simple_worker.py b/src/calibre/utils/ipc/simple_worker.py index c57b72f010..8408905c12 100644 --- a/src/calibre/utils/ipc/simple_worker.py +++ b/src/calibre/utils/ipc/simple_worker.py @@ -17,12 +17,14 @@ from calibre.constants import iswindows from calibre.utils.ipc import eintr_retry_call from calibre.utils.ipc.launch import Worker + class WorkerError(Exception): def __init__(self, msg, orig_tb=''): Exception.__init__(self, msg) self.orig_tb = orig_tb + class ConnectedWorker(Thread): def __init__(self, listener, args): @@ -52,6 +54,7 @@ class ConnectedWorker(Thread): except BaseException: self.tb = traceback.format_exc() + class OffloadWorker(object): def __init__(self, listener, worker): @@ -86,6 +89,7 @@ class OffloadWorker(object): def is_alive(self): return self.worker.is_alive or self.kill_thread.is_alive() + def communicate(ans, worker, listener, args, timeout=300, heartbeat=None, abort=None): cw = ConnectedWorker(listener, args) @@ -118,6 +122,7 @@ def communicate(ans, worker, listener, args, timeout=300, heartbeat=None, raise WorkerError('Worker failed', cw.res['tb']) ans['result'] = cw.res['result'] + def create_worker(env, priority='normal', cwd=None, func='main'): from calibre.utils.ipc.server import create_listener auth_key = os.urandom(32) @@ -134,6 +139,7 @@ def create_worker(env, priority='normal', cwd=None, func='main'): w(cwd=cwd, priority=priority) return listener, w + def start_pipe_worker(command, env=None, priority='normal', **process_args): import subprocess from functools import partial @@ -162,6 +168,7 @@ def start_pipe_worker(command, env=None, priority='normal', **process_args): p = subprocess.Popen(cmd + ['--pipe-worker', command], **args) return p + def fork_job(mod_name, func_name, args=(), kwargs={}, timeout=300, # seconds cwd=None, priority='normal', env={}, no_output=False, heartbeat=None, abort=None, module_is_source_code=False): @@ -233,10 +240,12 @@ def fork_job(mod_name, func_name, args=(), kwargs={}, timeout=300, # seconds ans['stdout_stderr'] = w.log_path return ans + def offload_worker(env={}, priority='normal', cwd=None): listener, w = create_worker(env=env, priority=priority, cwd=cwd, func='offload') return OffloadWorker(listener, w) + def compile_code(src): import re, io if not isinstance(src, unicode): @@ -254,6 +263,7 @@ def compile_code(src): exec src in namespace return namespace + def main(): # The entry point for the simple worker process address = cPickle.loads(unhexlify(os.environ['CALIBRE_WORKER_ADDRESS'])) @@ -283,6 +293,7 @@ def main(): # Maybe EINTR conn.send(res) + def offload(): # The entry point for the offload worker process address = cPickle.loads(unhexlify(os.environ['CALIBRE_WORKER_ADDRESS'])) diff --git a/src/calibre/utils/ipc/worker.py b/src/calibre/utils/ipc/worker.py index 20b54d7f37..27fa48c3de 100644 --- a/src/calibre/utils/ipc/worker.py +++ b/src/calibre/utils/ipc/worker.py @@ -50,6 +50,7 @@ PARALLEL_FUNCS = { ('calibre.utils.ipc.worker', 'arbitrary_n', 'notification'), } + class Progress(Thread): def __init__(self, conn): @@ -71,6 +72,7 @@ class Progress(Thread): except: break + def arbitrary(module_name, func_name, args, kwargs={}): ''' An entry point that allows arbitrary functions to be run in a parallel @@ -110,6 +112,7 @@ def arbitrary(module_name, func_name, args, kwargs={}): func = getattr(module, func_name) return func(*args, **kwargs) + def arbitrary_n(module_name, func_name, args, kwargs={}, notification=lambda x, y: y): ''' @@ -129,6 +132,7 @@ def arbitrary_n(module_name, func_name, args, kwargs={}, kwargs['notification'] = notification return func(*args, **kwargs) + def get_func(name): module, func, notification = PARALLEL_FUNCS[name] try: @@ -142,6 +146,7 @@ def get_func(name): func = getattr(module, func) return func, notification + def main(): if iswindows: if '--multiprocessing-fork' in sys.argv: diff --git a/src/calibre/utils/iphlpapi.py b/src/calibre/utils/iphlpapi.py index e0ea0370b6..dd525b8416 100644 --- a/src/calibre/utils/iphlpapi.py +++ b/src/calibre/utils/iphlpapi.py @@ -15,6 +15,7 @@ from calibre.constants import is64bit # Wraps (part of) the IPHelper API, useful to enumerate the network routes and # adapters on the local machine + class GUID(ctypes.Structure): _fields_ = [ ("data1", wintypes.DWORD), @@ -35,6 +36,7 @@ class GUID(ctypes.Structure): self.data4[6] = b7 self.data4[7] = b8 + class SOCKADDR(ctypes.Structure): _fields_ = [ ('sa_family', wintypes.USHORT), @@ -145,6 +147,7 @@ class IP_ADAPTER_PREFIX(ctypes.Structure): ('PrefixLength', wintypes.ULONG), ] + class IP_ADAPTER_DNS_SUFFIX(ctypes.Structure): _fields_ = [ ('Next', wintypes.LPVOID), @@ -264,12 +267,14 @@ GAA_FLAG_INCLUDE_PREFIX = 0x0010 Ws2_32 = windll.Ws2_32 Ws2_32.inet_ntoa.restype = ctypes.c_char_p + def _heap_alloc(heap, size): table_mem = HeapAlloc(heap, 0, ctypes.c_size_t(size.value)) if not table_mem: raise MemoryError('Unable to allocate memory for the IP forward table') return table_mem + @contextmanager def _get_forward_table(): heap = GetProcessHeap() @@ -298,6 +303,7 @@ def _get_forward_table(): if p_forward_table is not None: HeapFree(heap, 0, p_forward_table) + @contextmanager def _get_adapters(): heap = GetProcessHeap() @@ -328,6 +334,7 @@ def _get_adapters(): Adapter = namedtuple('Adapter', 'name if_index if_index6 friendly_name status transmit_speed receive_speed') + def adapters(): ''' A list of adapters on this machine ''' ans = [] @@ -353,6 +360,7 @@ def adapters(): Route = namedtuple('Route', 'destination gateway netmask interface metric flags') + def routes(): ''' A list of routes on this machine ''' ans = [] diff --git a/src/calibre/utils/ipython.py b/src/calibre/utils/ipython.py index 3fe137a22e..bb0792f305 100644 --- a/src/calibre/utils/ipython.py +++ b/src/calibre/utils/ipython.py @@ -14,6 +14,7 @@ ipydir = os.path.join(cache_dir(), 'ipython') BANNER = ('Welcome to the interactive calibre shell!\n') + def setup_pyreadline(): config = ''' #Bind keys for exit (keys only work on empty lines @@ -129,6 +130,7 @@ history_length(2000) #value of -1 means no limit # Override completer from rlcompleter to disable automatic ( on callable completer_obj = rlcompleter.Completer() + def nop(val, word): return word completer_obj._callable_postfix = nop @@ -140,6 +142,7 @@ history_length(2000) #value of -1 means no limit atexit.register(readline.write_history_file) del readline, rlcompleter, atexit + def simple_repl(user_ns={}): if iswindows: setup_pyreadline() @@ -158,6 +161,7 @@ def simple_repl(user_ns={}): import code code.interact(BANNER, raw_input, user_ns) + def ipython(user_ns=None): os.environ['IPYTHONDIR'] = ipydir try: diff --git a/src/calibre/utils/iso8601.py b/src/calibre/utils/iso8601.py index 93f9cedb98..f91f119f09 100644 --- a/src/calibre/utils/iso8601.py +++ b/src/calibre/utils/iso8601.py @@ -13,6 +13,7 @@ speedup, err = plugins['speedup'] if not speedup: raise RuntimeError(err) + class SafeLocalTimeZone(tzlocal): def _isdst(self, dt): @@ -29,6 +30,7 @@ local_tz = SafeLocalTimeZone() del tzutc, tzlocal UNDEFINED_DATE = datetime(101,1,1, tzinfo=utc_tz) + def parse_iso8601(date_string, assume_utc=False, as_utc=True): if not date_string: return UNDEFINED_DATE diff --git a/src/calibre/utils/linux_trash.py b/src/calibre/utils/linux_trash.py index a855eb2491..d8c189c4d1 100644 --- a/src/calibre/utils/linux_trash.py +++ b/src/calibre/utils/linux_trash.py @@ -36,19 +36,23 @@ uid = os.getuid() TOPDIR_TRASH = '.Trash' TOPDIR_FALLBACK = '.Trash-%s'%uid + def uniquote(raw): if isinstance(raw, unicode): raw = raw.encode('utf-8') return quote(raw).decode('utf-8') + def is_parent(parent, path): path = op.realpath(path) # In case it's a symlink parent = op.realpath(parent) return path.startswith(parent) + def format_date(date): return date.strftime("%Y-%m-%dT%H:%M:%S") + def info_for(src, topdir): # ...it MUST not include a ".."" directory, and for files not "under" that # directory, absolute pathnames must be used. [2] @@ -62,11 +66,13 @@ def info_for(src, topdir): info += "DeletionDate=" + format_date(datetime.now()) + "\n" return info + def check_create(dir): # use 0700 for paths [3] if not op.exists(dir): os.makedirs(dir, 0o700) + def trash_move(src, dst, topdir=None): filename = op.basename(src) filespath = op.join(dst, FILES_DIR) @@ -86,6 +92,7 @@ def trash_move(src, dst, topdir=None): with open(op.join(infopath, destname + INFO_SUFFIX), 'wb') as f: f.write(info_for(src, topdir)) + def find_mount_point(path): # Even if something's wrong, "/" is a mount point, so the loop will exit. # Use realpath in case it's a symlink @@ -94,6 +101,7 @@ def find_mount_point(path): path = op.split(path)[0] return path + def find_ext_volume_global_trash(volume_root): # from [2] Trash directories (1) check for a .Trash dir with the right # permissions set. @@ -114,6 +122,7 @@ def find_ext_volume_global_trash(volume_root): return None return trash_dir + def find_ext_volume_fallback_trash(volume_root): # from [2] Trash directories (1) create a .Trash-$uid dir. trash_dir = op.join(volume_root, TOPDIR_FALLBACK) @@ -122,6 +131,7 @@ def find_ext_volume_fallback_trash(volume_root): check_create(trash_dir) return trash_dir + def find_ext_volume_trash(volume_root): trash_dir = find_ext_volume_global_trash(volume_root) if trash_dir is None: @@ -129,9 +139,12 @@ def find_ext_volume_trash(volume_root): return trash_dir # Pull this out so it's easy to stub (to avoid stubbing lstat itself) + + def get_dev(path): return os.lstat(path).st_dev + def send2trash(path): if not op.exists(path): raise OSError("File not found: %s" % path) diff --git a/src/calibre/utils/localization.py b/src/calibre/utils/localization.py index 1017bc3bf7..67a3f66145 100644 --- a/src/calibre/utils/localization.py +++ b/src/calibre/utils/localization.py @@ -11,6 +11,7 @@ from gettext import GNUTranslations, NullTranslations _available_translations = None + def available_translations(): global _available_translations if _available_translations is None: @@ -22,6 +23,7 @@ def available_translations(): _available_translations = [x for x in stats if stats[x] > 0.1] return _available_translations + def get_system_locale(): from calibre.constants import iswindows, isosx, plugins lang = None @@ -81,9 +83,11 @@ def get_lang(): lang = 'en' return lang + def is_rtl(): return get_lang()[:2].lower() in {'he', 'ar'} + def get_lc_messages_path(lang): hlang = None if zf_exists(): @@ -95,12 +99,14 @@ def get_lc_messages_path(lang): hlang = xlang return hlang + def zf_exists(): return os.path.exists(P('localization/locales.zip', allow_user_override=False)) _lang_trans = None + def get_all_translators(): from zipfile import ZipFile with ZipFile(P('localization/locales.zip', allow_user_override=False), 'r') as zf: @@ -110,12 +116,14 @@ def get_all_translators(): buf = cStringIO.StringIO(zf.read(mpath + '/messages.mo')) yield lang, GNUTranslations(buf) + def get_single_translator(mpath): from zipfile import ZipFile with ZipFile(P('localization/locales.zip', allow_user_override=False), 'r') as zf: buf = cStringIO.StringIO(zf.read(mpath + '/messages.mo')) return GNUTranslations(buf) + def get_translator(bcp_47_code): parts = bcp_47_code.replace('-', '_').split('_')[:2] parts[0] = lang_as_iso639_1(parts[0].lower()) or 'en' @@ -151,6 +159,7 @@ lcdata = { u'yesexpr': u'^[yY].*' } + def load_po(path): from calibre.translations.msgfmt import make buf = cStringIO.StringIO() @@ -302,6 +311,7 @@ _lcase_map = {} for k in _extra_lang_codes: _lcase_map[k.lower()] = k + def _load_iso639(): global _iso639 if _iso639 is None: @@ -310,6 +320,7 @@ def _load_iso639(): _iso639 = cPickle.load(f) return _iso639 + def get_language(lang): translate = _ lang = _lcase_map.get(lang, lang) @@ -332,6 +343,7 @@ def get_language(lang): except AttributeError: return translate(ans) + def calibre_langcode_to_name(lc, localize=True): iso639 = _load_iso639() translate = _ if localize else lambda x: x @@ -341,6 +353,7 @@ def calibre_langcode_to_name(lc, localize=True): pass return lc + def canonicalize_lang(raw): if not raw: return None @@ -369,6 +382,7 @@ def canonicalize_lang(raw): _lang_map = None + def lang_map(): ' Return mapping of ISO 639 3 letter codes to localized language names ' iso639 = _load_iso639() @@ -378,6 +392,7 @@ def lang_map(): _lang_map = {k:translate(v) for k, v in iso639['by_3t'].iteritems()} return _lang_map + def langnames_to_langcodes(names): ''' Given a list of localized language names return a mapping of the names to 3 @@ -400,6 +415,7 @@ def langnames_to_langcodes(names): return ans + def lang_as_iso639_1(name_or_code): code = canonicalize_lang(name_or_code) if code is not None: @@ -408,6 +424,7 @@ def lang_as_iso639_1(name_or_code): _udc = None + def get_udc(): global _udc if _udc is None: @@ -415,6 +432,7 @@ def get_udc(): _udc = Unihandecoder(lang=get_lang()) return _udc + def localize_user_manual_link(url): lc = lang_as_iso639_1(get_lang()) if lc == 'en': diff --git a/src/calibre/utils/localunzip.py b/src/calibre/utils/localunzip.py index 40c4dd5751..1cbe843bc6 100644 --- a/src/calibre/utils/localunzip.py +++ b/src/calibre/utils/localunzip.py @@ -31,6 +31,7 @@ LocalHeader = namedtuple('LocalHeader', 'crc32 compressed_size uncompressed_size filename_length extra_length ' 'filename extra') + def decode_arcname(name): if isinstance(name, bytes): from calibre.ebooks.chardet import detect @@ -45,6 +46,7 @@ def decode_arcname(name): name = name.decode('utf-8', 'replace') return name + def find_local_header(f): pos = f.tell() raw = f.read(50*1024) @@ -62,6 +64,7 @@ def find_local_header(f): return header f.seek(pos) + def find_data_descriptor(f): pos = f.tell() DD = namedtuple('DataDescriptor', 'crc32 compressed_size uncompressed_size') @@ -83,6 +86,7 @@ def find_data_descriptor(f): finally: f.seek(pos) + def read_local_file_header(f): pos = f.tell() raw = f.read(local_header_sz) @@ -132,10 +136,12 @@ def read_local_file_header(f): header[:-2] + (fname, extra) )) + def read_compressed_data(f, header): cdata = f.read(header.compressed_size) return cdata + def copy_stored_file(src, size, dest): read = 0 amt = min(size, 20*1024) @@ -146,6 +152,7 @@ def copy_stored_file(src, size, dest): dest.write(raw) read += len(raw) + def copy_compressed_file(src, size, dest): d = zlib.decompressobj(-15) read = 0 @@ -165,6 +172,7 @@ def copy_compressed_file(src, size, dest): raise ValueError('This ZIP file contains a ZIP bomb in %s'% os.path.basename(dest.name)) + def _extractall(f, path=None, file_info=None): found = False while True: diff --git a/src/calibre/utils/lock.py b/src/calibre/utils/lock.py index 15519522ea..34c8849f8c 100644 --- a/src/calibre/utils/lock.py +++ b/src/calibre/utils/lock.py @@ -9,9 +9,11 @@ Secure access to locked files from multiple processes. from calibre.constants import iswindows, __appname__, islinux, win32api, win32event, winerror, fcntl import time, atexit, os, stat, errno + class LockError(Exception): pass + class WindowsExclFile(object): def __init__(self, path, timeout=20): @@ -104,6 +106,7 @@ class WindowsExclFile(object): def closed(self): return self._handle is None + def unix_open(path): # We cannot use open(a+b) directly because Fedora apparently ships with a # broken libc that causes seek(0) followed by truncate() to not work for @@ -129,6 +132,7 @@ def unix_open(path): fcntl.fcntl(fd, fcntl.F_SETFD, fcntl.FD_CLOEXEC) return os.fdopen(fd, 'r+b') + class ExclusiveFile(object): def __init__(self, path, timeout=15): @@ -155,6 +159,7 @@ class ExclusiveFile(object): def __exit__(self, type, value, traceback): self.file.close() + def test_exclusive_file(path=None): if path is None: import tempfile @@ -180,6 +185,7 @@ def test_exclusive_file(path=None): except Exception as err: return str(err) + def _clean_lock_file(file): try: file.close() diff --git a/src/calibre/utils/logging.py b/src/calibre/utils/logging.py index 7b88c9cd11..eae37e7c65 100644 --- a/src/calibre/utils/logging.py +++ b/src/calibre/utils/logging.py @@ -16,6 +16,7 @@ from threading import Lock from calibre import isbytestring, force_unicode, as_unicode, prints + class Stream(object): def __init__(self, stream=None): @@ -50,6 +51,7 @@ class ANSIStream(Stream): def flush(self): self.stream.flush() + class FileStream(Stream): def __init__(self, stream=None): @@ -58,6 +60,7 @@ class FileStream(Stream): def prints(self, level, *args, **kwargs): self._prints(*args, **kwargs) + class HTMLStream(Stream): color = { @@ -80,6 +83,7 @@ class HTMLStream(Stream): def flush(self): self.stream.flush() + class UnicodeHTMLStream(HTMLStream): def __init__(self): @@ -170,12 +174,14 @@ class Log(object): def __exit__(self, *args): self.filter_level = self.orig_filter_level + class DevNull(Log): def __init__(self): Log.__init__(self, level=Log.ERROR) self.outputs = [] + class ThreadSafeLog(Log): exception_traceback_level = Log.DEBUG @@ -193,6 +199,7 @@ class ThreadSafeLog(Log): Log.prints(self, ERROR, *args, **kwargs) Log.prints(self, self.exception_traceback_level, traceback.format_exc(limit)) + class ThreadSafeWrapper(Log): def __init__(self, other_log): @@ -204,6 +211,7 @@ class ThreadSafeWrapper(Log): with self._lock: Log.prints(self, *args, **kwargs) + class GUILog(ThreadSafeLog): ''' diff --git a/src/calibre/utils/lru_cache.py b/src/calibre/utils/lru_cache.py index d83945d371..9bbd27e286 100644 --- a/src/calibre/utils/lru_cache.py +++ b/src/calibre/utils/lru_cache.py @@ -8,6 +8,7 @@ from __future__ import (unicode_literals, division, absolute_import, # Based on https://github.com/jlhutch/pylru/blob/master/pylru.py (which is # licensed GPL v2+) + class DoublyLinkedNode(object): __slots__ = 'empty prev next key value'.split() @@ -16,6 +17,7 @@ class DoublyLinkedNode(object): self.empty = True self.prev = self.next = self.key = self.value = None + class lru_cache(object): ''' diff --git a/src/calibre/utils/magick/__init__.py b/src/calibre/utils/magick/__init__.py index 0332e26ed5..bbef22b5f4 100644 --- a/src/calibre/utils/magick/__init__.py +++ b/src/calibre/utils/magick/__init__.py @@ -10,6 +10,7 @@ from calibre.utils.magick.legacy import Image, PixelWand if False: PixelWand + def create_canvas(width, height, bgcolor='#ffffff'): canvas = Image() canvas.create_canvas(int(width), int(height), str(bgcolor)) diff --git a/src/calibre/utils/magick/draw.py b/src/calibre/utils/magick/draw.py index 94724b50d5..290a11e6b7 100644 --- a/src/calibre/utils/magick/draw.py +++ b/src/calibre/utils/magick/draw.py @@ -12,6 +12,7 @@ from calibre.utils.img import save_cover_data_to as _save_cover_data_to, image_t from calibre.utils.imghdr import identify as _identify from calibre import fit_image + def _data_to_image(data): if isinstance(data, Image): img = data @@ -20,6 +21,7 @@ def _data_to_image(data): img.load(data) return img + def minify_image(data, minify_to=(1200, 1600), preserve_aspect_ratio=True): ''' Minify image to specified size if image is bigger than specified @@ -40,6 +42,7 @@ def minify_image(data, minify_to=(1200, 1600), preserve_aspect_ratio=True): img.size = (nwidth, nheight) return img + def save_cover_data_to(data, path, bgcolor='#ffffff', resize_to=None, return_data=False, compression_quality=90, minify_to=None, grayscale=False): @@ -69,6 +72,7 @@ def save_cover_data_to(data, path, bgcolor='#ffffff', resize_to=None, return _save_cover_data_to( data, path, bgcolor=bgcolor, resize_to=resize_to, compression_quality=compression_quality, minify_to=minify_to, grayscale=grayscale, data_fmt=fmt) + def thumbnail(data, width=120, height=120, bgcolor='#ffffff', fmt='jpg', preserve_aspect_ratio=True, compression_quality=70): img = Image() @@ -91,6 +95,7 @@ def thumbnail(data, width=120, height=120, bgcolor='#ffffff', fmt='jpg', data = image_to_data(canvas.img, compression_quality=compression_quality) return (canvas.size[0], canvas.size[1], data) + def identify_data(data): ''' Identify the image in data. Returns a 3-tuple @@ -100,6 +105,7 @@ def identify_data(data): fmt, width, height = _identify(data) return width, height, fmt + def identify(path): ''' Identify the image at path. Returns a 3-tuple @@ -110,6 +116,7 @@ def identify(path): fmt, width, height = _identify(f) return width, height, fmt + def add_borders_to_image(img_data, left=0, top=0, right=0, bottom=0, border_color='#ffffff', fmt='jpg'): img = abti(img_data, left=left, top=top, right=right, bottom=bottom, border_color=border_color) diff --git a/src/calibre/utils/magick/legacy.py b/src/calibre/utils/magick/legacy.py index 32af317f2a..388f7ee9e1 100644 --- a/src/calibre/utils/magick/legacy.py +++ b/src/calibre/utils/magick/legacy.py @@ -16,11 +16,13 @@ from calibre.utils.img import ( ) from calibre.utils.imghdr import identify + class PixelWand(object): def __init__(self): self.color = '#ffffff' + class Image(object): def __init__(self): @@ -61,6 +63,7 @@ class Image(object): if len(self.img.colorTable()) > 0: return 'PaletteType' return 'TrueColorType' + def fset(self, t): if t == 'GrayscaleType': self.img = grayscale_image(self.img) @@ -72,6 +75,7 @@ class Image(object): def format(self): def fget(self): return self.write_format or self.read_format + def fset(self, val): self.write_format = val return property(fget=fget, fset=fset) @@ -80,6 +84,7 @@ class Image(object): def colorspace(self): def fget(self): return 'RGBColorspace' + def fset(self, val): raise NotImplementedError('Changing image colorspace is not supported') return property(fget=fget, fset=fset) @@ -88,6 +93,7 @@ class Image(object): def size(self): def fget(self): return self.img.width(), self.img.height() + def fset(self, val): w, h = val[:2] self.img = resize_image(self.img, w, h) diff --git a/src/calibre/utils/matcher.py b/src/calibre/utils/matcher.py index 5cfa12092c..8defe9049d 100644 --- a/src/calibre/utils/matcher.py +++ b/src/calibre/utils/matcher.py @@ -26,9 +26,11 @@ DEFAULT_LEVEL1 = '/' DEFAULT_LEVEL2 = '-_ 0123456789' DEFAULT_LEVEL3 = '.' + class PluginFailed(RuntimeError): pass + class Worker(Thread): daemon = True @@ -53,6 +55,7 @@ class Worker(Thread): wlock = Lock() workers = [] + def split(tasks, pool_size): ''' Split a list into a list of sub lists, with the number of sub lists being @@ -69,12 +72,14 @@ def split(tasks, pool_size): ans.append(section) return ans + def default_scorer(*args, **kwargs): try: return CScorer(*args, **kwargs) except PluginFailed: return PyScorer(*args, **kwargs) + class Matcher(object): def __init__(self, items, level1=DEFAULT_LEVEL1, level2=DEFAULT_LEVEL2, level3=DEFAULT_LEVEL3, scorer=None): @@ -123,6 +128,7 @@ class Matcher(object): del items[limit:] return OrderedDict(x[1:] for x in filter(itemgetter(0), items)) + def get_items_from_dir(basedir, acceptq=lambda x: True): if isinstance(basedir, bytes): basedir = basedir.decode(filesystem_encoding) @@ -136,12 +142,15 @@ def get_items_from_dir(basedir, acceptq=lambda x: True): x = x.replace(os.sep, '/') yield x + class FilesystemMatcher(Matcher): def __init__(self, basedir, *args, **kwargs): Matcher.__init__(self, get_items_from_dir(basedir), *args, **kwargs) # Python implementation of the scoring algorithm {{{ + + def calc_score_for_char(ctx, prev, current, distance): factor = 1.0 ans = ctx.max_score_per_char @@ -157,6 +166,7 @@ def calc_score_for_char(ctx, prev, current, distance): return ans * factor + def process_item(ctx, haystack, needle): # non-recursive implementation using a stack stack = [(0, 0, 0, 0, [-1]*len(needle))] @@ -192,6 +202,7 @@ def process_item(ctx, haystack, needle): final_positions = positions return final_score, final_positions + class PyScorer(object): __slots__ = ('level1', 'level2', 'level3', 'max_score_per_char', 'items', 'memory') @@ -207,6 +218,7 @@ class PyScorer(object): yield process_item(self, item, needle) # }}} + class CScorer(object): def __init__(self, items, level1=DEFAULT_LEVEL1, level2=DEFAULT_LEVEL2, level3=DEFAULT_LEVEL3): @@ -220,6 +232,7 @@ class CScorer(object): for score, pos in izip(scores, positions): yield score, pos + def test(return_tests=False): import unittest @@ -230,6 +243,7 @@ def test(return_tests=False): from calibre.utils.mem import get_memory as memory m = Matcher(['a'], scorer=CScorer) m('a') + def doit(c): m = Matcher([c+'im/one.gif', c+'im/two.gif', c+'text/one.html',], scorer=CScorer) m('one') @@ -270,6 +284,7 @@ else: chs = 2 if ('\ud800' <= string[pos] <= '\udbff') else 1 # UTF-16 surrogate pair in python narrow builds return string[pos:pos+chs] + def main(basedir=None, query=None): from calibre import prints from calibre.utils.terminal import ColoredStream diff --git a/src/calibre/utils/mdns.py b/src/calibre/utils/mdns.py index 2a4fe745d8..bfc579d89d 100644 --- a/src/calibre/utils/mdns.py +++ b/src/calibre/utils/mdns.py @@ -14,6 +14,7 @@ _server = None _all_ip_addresses = dict() + class AllIpAddressesGetter(Thread): def get_all_ips(self): @@ -43,6 +44,7 @@ class AllIpAddressesGetter(Thread): _ip_address_getter_thread = None + def get_all_ips(reinitialize=False): global _all_ip_addresses, _ip_address_getter_thread if not _ip_address_getter_thread or (reinitialize and not @@ -53,6 +55,7 @@ def get_all_ips(reinitialize=False): _ip_address_getter_thread.start() return _all_ip_addresses + def _get_external_ip(): 'Get IP address of interface used to connect to the outside world' try: @@ -72,6 +75,7 @@ def _get_external_ip(): # print 'ipaddr: %s' % ipaddr return ipaddr + def verify_ipV4_address(ip_address): result = None if ip_address != '0.0.0.0' and ip_address != '::': @@ -86,6 +90,8 @@ def verify_ipV4_address(ip_address): return result _ext_ip = None + + def get_external_ip(): global _ext_ip if _ext_ip is None: @@ -96,6 +102,7 @@ def get_external_ip(): _ext_ip = _get_external_ip() return _ext_ip + def start_server(): global _server if _server is None: @@ -110,6 +117,7 @@ def start_server(): return _server + def create_service(desc, type, port, properties, add_hostname, use_ip_address=None): port = int(port) try: @@ -155,6 +163,7 @@ def publish(desc, type, port, properties=None, add_hostname=True, use_ip_address server.registerService(service) return service + def unpublish(desc, type, port, properties=None, add_hostname=True): ''' Unpublish a service. @@ -167,6 +176,7 @@ def unpublish(desc, type, port, properties=None, add_hostname=True): if server.countRegisteredServices() == 0: stop_server() + def stop_server(): global _server if _server is not None: diff --git a/src/calibre/utils/mem.py b/src/calibre/utils/mem.py index 25e21b5785..34d5e87c50 100644 --- a/src/calibre/utils/mem.py +++ b/src/calibre/utils/mem.py @@ -15,18 +15,21 @@ value. import gc, os + def get_memory(): 'Return memory usage in bytes' # See https://pythonhosted.org/psutil/#psutil.Process.memory_info import psutil return psutil.Process(os.getpid()).memory_info().rss + def memory(since=0.0): 'Return memory used in MB. The value of since is subtracted from the used memory' ans = get_memory() ans /= float(1024**2) return ans - since + def gc_histogram(): """Returns per-class counts of existing objects.""" result = {} @@ -36,6 +39,7 @@ def gc_histogram(): result[t] = count + 1 return result + def diff_hists(h1, h2): """Prints differences between two results of gc_histogram().""" for k in h1: diff --git a/src/calibre/utils/mreplace.py b/src/calibre/utils/mreplace.py index c817177ed2..09dca051d0 100644 --- a/src/calibre/utils/mreplace.py +++ b/src/calibre/utils/mreplace.py @@ -6,6 +6,7 @@ __docformat__ = 'restructuredtext en' import re from UserDict import UserDict + class MReplace(UserDict): def __init__(self, data=None, case_sensitive=True): diff --git a/src/calibre/utils/network.py b/src/calibre/utils/network.py index 081d7a811b..bb38245c70 100644 --- a/src/calibre/utils/network.py +++ b/src/calibre/utils/network.py @@ -8,6 +8,7 @@ __docformat__ = 'restructuredtext en' from calibre.constants import iswindows, islinux, isbsd + class LinuxNetworkStatus(object): def __init__(self): @@ -30,6 +31,7 @@ class LinuxNetworkStatus(object): except: return True + class WindowsNetworkStatus(object): def __init__(self): @@ -41,6 +43,7 @@ class WindowsNetworkStatus(object): return True return self.winutil.internet_connected() + class DummyNetworkStatus(object): def __call__(self): @@ -50,5 +53,6 @@ _network_status = WindowsNetworkStatus() if iswindows else \ LinuxNetworkStatus() if (islinux or isbsd) else \ DummyNetworkStatus() + def internet_connected(): return _network_status() diff --git a/src/calibre/utils/open_with/linux.py b/src/calibre/utils/open_with/linux.py index 5fbd70503d..05038da9c8 100644 --- a/src/calibre/utils/open_with/linux.py +++ b/src/calibre/utils/open_with/linux.py @@ -14,6 +14,7 @@ from calibre.constants import filesystem_encoding, cache_dir from calibre.utils.icu import numeric_sort_key as sort_key from calibre.utils.localization import canonicalize_lang, get_lang + def parse_localized_key(key): name, rest = key.partition('[')[0::2] if not rest: @@ -22,10 +23,12 @@ def parse_localized_key(key): lang = re.split(r'[_.@]', rest)[0] return name, canonicalize_lang(lang) + def unquote_exec(val): val = val.replace(r'\\', '\\') return shlex.split(val) + def parse_desktop_file(path): gpat = re.compile(r'^\[(.+?)\]\s*$') kpat = re.compile(r'^([-a-zA-Z0-9\[\]@_.]+)\s*=\s*(.+)$') @@ -72,6 +75,7 @@ def parse_desktop_file(path): icon_data = None + def find_icons(): global icon_data if icon_data is not None: @@ -157,10 +161,12 @@ def find_icons(): icon_data = {k:v[0][1] for k, v in ans.iteritems()} return icon_data + def localize_string(data): lang = canonicalize_lang(get_lang()) return data.get(lang, data.get(None)) or '' + def find_programs(extensions): extensions = {ext.lower() for ext in extensions} data_dirs = [os.environ.get('XDG_DATA_HOME') or os.path.expanduser('~/.local/share')] @@ -201,15 +207,18 @@ def find_programs(extensions): ans.sort(key=lambda d:sort_key(d.get('Name'))) return ans + def entry_sort_key(entry): return sort_key(entry['Name']) + def entry_to_cmdline(entry, path): path = os.path.abspath(path) rmap = { 'f':path, 'F':path, 'u':'file://'+path, 'U':'file://'+path, '%':'%', 'c':entry.get('Name', ''), 'k':entry.get('desktop_file_path', ''), } + def replace(match): char = match.group()[-1] repl = rmap.get(char) diff --git a/src/calibre/utils/open_with/osx.py b/src/calibre/utils/open_with/osx.py index d62460d648..dd98e9e644 100644 --- a/src/calibre/utils/open_with/osx.py +++ b/src/calibre/utils/open_with/osx.py @@ -15,6 +15,8 @@ from calibre.utils.icu import numeric_sort_key application_locations = ('/Applications', '~/Applications', '~/Desktop') # Public UTI MAP {{{ + + def generate_public_uti_map(): from lxml import etree import html5lib, urllib @@ -205,6 +207,7 @@ PUBLIC_UTI_RMAP = dict(PUBLIC_UTI_RMAP) # }}} + def find_applications_in(base): try: entries = os.listdir(base) @@ -219,12 +222,14 @@ def find_applications_in(base): for app in find_applications_in(path): yield app + def find_applications(): for base in application_locations: base = os.path.expanduser(base) for app in find_applications_in(base): yield app + def get_extensions_from_utis(utis, plist): declared_utis = defaultdict(set) for key in ('UTExportedTypeDeclarations', 'UTImportedTypeDeclarations'): @@ -248,6 +253,7 @@ def get_extensions_from_utis(utis, plist): ans |= PUBLIC_UTI_RMAP.get(uti, set()) return ans + def get_bundle_data(path): path = os.path.abspath(path) info = os.path.join(path, 'Contents', 'Info.plist') @@ -285,6 +291,7 @@ def get_bundle_data(path): extensions.add(ext.lower()) return ans + def find_programs(extensions): extensions = frozenset(extensions) ans = [] @@ -299,6 +306,7 @@ def find_programs(extensions): ans.append(app) return ans + def get_icon(path, pixmap_to_data=None, as_data=False, size=64): if not path: return @@ -329,6 +337,7 @@ def get_icon(path, pixmap_to_data=None, as_data=False, size=64): ans = pixmap_to_data(ans) return ans + def entry_to_cmdline(entry, path): app = entry['path'] if os.path.isdir(app): diff --git a/src/calibre/utils/open_with/windows.py b/src/calibre/utils/open_with/windows.py index 3435945976..372ef0e57b 100644 --- a/src/calibre/utils/open_with/windows.py +++ b/src/calibre/utils/open_with/windows.py @@ -19,9 +19,11 @@ from calibre.utils.winreg.default_programs import split_commandline ICON_SIZE = 64 + def hicon_to_pixmap(hicon): return QtWin.fromHICON(hicon) + def pixmap_to_data(pixmap): ba = QByteArray() buf = QBuffer(ba) @@ -29,11 +31,13 @@ def pixmap_to_data(pixmap): pixmap.save(buf, 'PNG') return bytearray(ba.data()) + def copy_to_size(pixmap, size=ICON_SIZE): if pixmap.width() > ICON_SIZE: return pixmap.scaled(ICON_SIZE, ICON_SIZE, transformMode=Qt.SmoothTransformation) return pixmap.copy() + def simple_load_icon(module, index, as_data=False, size=ICON_SIZE): ' Use the win32 API ExtractIcon to load the icon. This restricts icon size to 32x32, but has less chance of failing ' try: @@ -105,6 +109,7 @@ def load_icon(module, index, as_data=False, size=ICON_SIZE): finally: win32api.FreeLibrary(handle) + def load_icon_resource(icon_resource, as_data=False, size=ICON_SIZE): if not icon_resource: return diff --git a/src/calibre/utils/opensearch/description.py b/src/calibre/utils/opensearch/description.py index 0021cb8cc9..77f9ff0ebc 100644 --- a/src/calibre/utils/opensearch/description.py +++ b/src/calibre/utils/opensearch/description.py @@ -16,6 +16,7 @@ from lxml import etree from calibre import browser from calibre.utils.opensearch.url import URL + class Description(object): ''' A class for representing OpenSearch Description files. diff --git a/src/calibre/utils/opensearch/query.py b/src/calibre/utils/opensearch/query.py index 057d14fe1f..da639c122e 100644 --- a/src/calibre/utils/opensearch/query.py +++ b/src/calibre/utils/opensearch/query.py @@ -9,6 +9,7 @@ __docformat__ = 'restructuredtext en' from urlparse import urlparse, urlunparse, parse_qs from urllib import urlencode + class Query(object): ''' Represents an opensearch query Really this class is just a diff --git a/src/calibre/utils/opensearch/url.py b/src/calibre/utils/opensearch/url.py index f05ec3205a..7dfe6e6c10 100644 --- a/src/calibre/utils/opensearch/url.py +++ b/src/calibre/utils/opensearch/url.py @@ -6,6 +6,7 @@ __license__ = 'GPL 3' __copyright__ = '2006, Ed Summers ' __docformat__ = 'restructuredtext en' + class URL(object): ''' Class for representing a URL in an opensearch v1.1 query diff --git a/src/calibre/utils/podofo/__init__.py b/src/calibre/utils/podofo/__init__.py index 55ff0ce9bb..a617c02964 100644 --- a/src/calibre/utils/podofo/__init__.py +++ b/src/calibre/utils/podofo/__init__.py @@ -13,12 +13,14 @@ from calibre.ebooks.metadata import authors_to_string from calibre.ptempfile import TemporaryDirectory from calibre.utils.ipc.simple_worker import fork_job, WorkerError + def get_podofo(): podofo, podofo_err = plugins['podofo'] if podofo is None: raise RuntimeError('Failed to load podofo: %s'%podofo_err) return podofo + def prep(val): if not val: return u'' @@ -26,6 +28,7 @@ def prep(val): val = val.decode(preferred_encoding, 'replace') return val.strip() + def set_metadata(stream, mi): with TemporaryDirectory(u'_podofo_set_metadata') as tdir: with open(os.path.join(tdir, u'input.pdf'), 'wb') as f: @@ -50,6 +53,7 @@ def set_metadata(stream, mi): stream.flush() stream.seek(0) + def set_metadata_(tdir, title, authors, bkp, tags, xmp_packet): podofo = get_podofo() os.chdir(tdir) @@ -94,6 +98,7 @@ def set_metadata_(tdir, title, authors, bkp, tags, xmp_packet): return touched + def delete_all_but(path, pages): ''' Delete all the pages in the pdf except for the specified ones. Negative numbers are counted from the end of the PDF. ''' @@ -111,6 +116,7 @@ def delete_all_but(path, pages): with open(path, 'wb') as f: f.save_to_fileobj(path) + def get_xmp_metadata(path): podofo = get_podofo() p = podofo.PDFDoc() @@ -119,6 +125,7 @@ def get_xmp_metadata(path): p.load(raw) return p.get_xmp_metadata() + def get_image_count(path): podofo = get_podofo() p = podofo.PDFDoc() @@ -127,6 +134,7 @@ def get_image_count(path): p.load(raw) return p.image_count() + def test_outline(src): podofo = get_podofo() p = podofo.PDFDoc() @@ -143,6 +151,7 @@ def test_outline(src): f.write(raw) print 'Outlined PDF:', out + def test_save_to(src, dest): podofo = get_podofo() p = podofo.PDFDoc() @@ -153,6 +162,7 @@ def test_save_to(src, dest): p.save_to_fileobj(out) print ('Wrote PDF of size:', out.tell()) + def test_podofo(): from io import BytesIO from calibre.ebooks.metadata.book.base import Metadata diff --git a/src/calibre/utils/rapydscript.py b/src/calibre/utils/rapydscript.py index 54bdaf183c..0ec6edbdaa 100644 --- a/src/calibre/utils/rapydscript.py +++ b/src/calibre/utils/rapydscript.py @@ -21,10 +21,13 @@ from calibre.utils.terminal import ANSIStream COMPILER_PATH = 'rapydscript/compiler.js.xz' + def abspath(x): return os.path.realpath(os.path.abspath(x)) # Update RapydScript {{{ + + def update_rapydscript(): d = os.path.dirname base = d(d(d(d(d(abspath(__file__)))))) @@ -46,9 +49,11 @@ def update_rapydscript(): # Compiler {{{ tls = local() + def to_dict(obj): return dict(zip(obj.keys(), obj.values())) + def compiler(): c = getattr(tls, 'compiler', None) if c is None: @@ -59,14 +64,17 @@ def compiler(): c.eval(buf.getvalue(), fname=COMPILER_PATH, noreturn=True) return c + class CompileFailure(ValueError): pass + def default_lib_dir(): return P('rapydscript/lib', allow_user_override=False) _cache_dir = None + def module_cache_dir(): global _cache_dir if _cache_dir is None: @@ -119,6 +127,7 @@ def compile_pyj(data, filename='', beautify=True, private_scope=True, lib has_external_compiler = None + def detect_external_compiler(): from calibre.utils.filenames import find_executable_in_path rs = find_executable_in_path('rapydscript') @@ -136,6 +145,7 @@ def detect_external_compiler(): return rs return False + def compile_fast(data, filename=None, beautify=True, private_scope=True, libdir=None, omit_baselib=False): global has_external_compiler if has_external_compiler is None: @@ -159,6 +169,7 @@ def compile_fast(data, filename=None, beautify=True, private_scope=True, libdir= raise CompileFailure(force_unicode(stderr, 'utf-8')) return js.decode('utf-8') + def compile_srv(): d = os.path.dirname base = d(d(d(d(os.path.abspath(__file__))))) @@ -191,6 +202,7 @@ def compile_srv(): # Translations {{{ + def create_pot(source_files): ctx = compiler() ctx.g.gettext_options = { @@ -209,6 +221,7 @@ def create_pot(source_files): ctx.eval('exports.gettext_output(catalog, gettext_options, pywrite)') return ''.join(buf) + def msgfmt(po_data_as_string): ctx = compiler() ctx.g.po_data = po_data_as_string @@ -217,12 +230,16 @@ def msgfmt(po_data_as_string): # }}} # REPL {{{ + + def leading_whitespace(line): return line[:len(line) - len(line.lstrip())] + def format_error(data): return ':'.join(map(type(''), (data['file'], data['line'], data['col'], data['message']))) + class Repl(Thread): LINE_CONTINUATION_CHARS = r'\:' @@ -372,6 +389,7 @@ class Repl(Thread): # }}} + def main(args=sys.argv): import argparse ver = compiler().g.exports.rs_version @@ -398,6 +416,7 @@ def main(args=sys.argv): except CompileFailure as e: raise SystemExit(e.message) + def entry(): main(sys.argv[1:]) diff --git a/src/calibre/utils/recycle_bin.py b/src/calibre/utils/recycle_bin.py index 35f334b171..7575203aca 100644 --- a/src/calibre/utils/recycle_bin.py +++ b/src/calibre/utils/recycle_bin.py @@ -19,6 +19,7 @@ if iswindows: from threading import Lock recycler = None rlock = Lock() + def start_recycler(): global recycler if recycler is None: @@ -95,6 +96,7 @@ elif isosx: recycle = osx_recycle elif islinux: from calibre.utils.linux_trash import send2trash + def fdo_recycle(path): if isbytestring(path): path = path.decode(filesystem_encoding) @@ -104,14 +106,17 @@ elif islinux: can_recycle = callable(recycle) + def nuke_recycle(): global can_recycle can_recycle = False + def restore_recyle(): global can_recycle can_recycle = callable(recycle) + def delete_file(path, permanent=False): if not permanent and can_recycle: try: @@ -122,6 +127,7 @@ def delete_file(path, permanent=False): traceback.print_exc() os.remove(path) + def delete_tree(path, permanent=False): if permanent: try: diff --git a/src/calibre/utils/resources.py b/src/calibre/utils/resources.py index a7b14ea4d9..943775cbdf 100644 --- a/src/calibre/utils/resources.py +++ b/src/calibre/utils/resources.py @@ -11,6 +11,7 @@ import __builtin__, sys, os from calibre import config_dir + class PathResolver(object): def __init__(self): @@ -65,6 +66,7 @@ class PathResolver(object): _resolver = PathResolver() + def get_path(path, data=False, allow_user_override=True): fpath = _resolver(path, allow_user_override=allow_user_override) if data: @@ -86,12 +88,14 @@ else: return get_path('images', allow_user_override=allow_user_override) return get_path('images/'+path, data=data, allow_user_override=allow_user_override) + def js_name_to_path(name, ext='.coffee'): path = (u'/'.join(name.split('.'))) + ext d = os.path.dirname base = d(d(os.path.abspath(__file__))) return os.path.join(base, path) + def _compile_coffeescript(name): from calibre.utils.serve_coffee import compile_coffeescript src = js_name_to_path(name) @@ -104,6 +108,7 @@ def _compile_coffeescript(name): ': %s'%src) return cs + def compiled_coffeescript(name, dynamic=False): import zipfile zipf = get_path('compiled_coffeescript.zip', allow_user_override=False) diff --git a/src/calibre/utils/rss_gen.py b/src/calibre/utils/rss_gen.py index 47fd0810b4..d270f3dbf4 100644 --- a/src/calibre/utils/rss_gen.py +++ b/src/calibre/utils/rss_gen.py @@ -9,6 +9,8 @@ _generator_name = __name__ + "-" + ".".join(map(str, __version__)) import datetime # Could make this the base class; will need to add 'publish' + + class WriteXmlMixin: def write_xml(self, outfile, encoding="iso-8859-1"): @@ -41,6 +43,7 @@ def _element(handler, name, obj, d={}): # It better know how to emit the correct XML. obj.publish(handler) + def _opt_element(handler, name, obj): if obj is None: return @@ -79,14 +82,17 @@ class IntElement: to text for XML.) """ element_attrs = {} + def __init__(self, name, val): self.name = name self.val = val + def publish(self, handler): handler.startElement(self.name, self.element_attrs) handler.characters(str(self.val)) handler.endElement(self.name) + class DateElement: """implements the 'publish' API for a datetime.datetime @@ -94,26 +100,33 @@ class DateElement: Converts the datetime to RFC 2822 timestamp (4-digit year). """ + def __init__(self, name, dt): self.name = name self.dt = dt + def publish(self, handler): _element(handler, self.name, _format_date(self.dt)) #### + class Category: """Publish a category element""" + def __init__(self, category, domain=None): self.category = category self.domain = domain + def publish(self, handler): d = {} if self.domain is not None: d["domain"] = self.domain _element(handler, "category", self.category, d) + class Cloud: """Publish a cloud""" + def __init__(self, domain, port, path, registerProcedure, protocol): self.domain = domain @@ -121,6 +134,7 @@ class Cloud: self.path = path self.registerProcedure = registerProcedure self.protocol = protocol + def publish(self, handler): _element(handler, "cloud", None, { "domain": self.domain, @@ -129,9 +143,11 @@ class Cloud: "registerProcedure": self.registerProcedure, "protocol": self.protocol}) + class Image: """Publish a channel Image""" element_attrs = {} + def __init__(self, url, title, link, width=None, height=None, description=None): self.url = url @@ -162,15 +178,18 @@ class Image: handler.endElement("image") + class Guid: """Publish a guid Defaults to being a permalink, which is the assumption if it's omitted. Hence strings are always permalinks. """ + def __init__(self, guid, isPermaLink=1): self.guid = guid self.isPermaLink = isPermaLink + def publish(self, handler): d = {} if self.isPermaLink: @@ -179,12 +198,14 @@ class Guid: d["isPermaLink"] = "false" _element(handler, "guid", self.guid, d) + class TextInput: """Publish a textInput Apparently this is rarely used. """ element_attrs = {} + def __init__(self, title, description, name, link): self.title = title self.description = description @@ -202,10 +223,12 @@ class TextInput: class Enclosure: """Publish an enclosure""" + def __init__(self, url, length, type): self.url = url self.length = length self.type = type + def publish(self, handler): _element(handler, "enclosure", None, {"url": self.url, @@ -213,22 +236,28 @@ class Enclosure: "type": self.type, }) + class Source: """Publish the item's original source, used by aggregators""" + def __init__(self, name, url): self.name = name self.url = url + def publish(self, handler): _element(handler, "source", self.name, {"url": self.url}) + class SkipHours: """Publish the skipHours This takes a list of hours, as integers. """ element_attrs = {} + def __init__(self, hours): self.hours = hours + def publish(self, handler): if self.hours: handler.startElement("skipHours", self.element_attrs) @@ -236,14 +265,17 @@ class SkipHours: _element(handler, "hour", str(hour)) handler.endElement("skipHours") + class SkipDays: """Publish the skipDays This takes a list of days as strings. """ element_attrs = {} + def __init__(self, days): self.days = days + def publish(self, handler): if self.days: handler.startElement("skipDays", self.element_attrs) @@ -251,6 +283,7 @@ class SkipDays: _element(handler, "day", day) handler.endElement("skipDays") + class RSS2(WriteXmlMixin): """The main RSS class. @@ -260,6 +293,7 @@ class RSS2(WriteXmlMixin): rss_attrs = {"version": "2.0"} element_attrs = {} + def __init__(self, title, link, @@ -380,6 +414,7 @@ class RSS2(WriteXmlMixin): class RSSItem(WriteXmlMixin): """Publish an RSS Item""" element_attrs = {} + def __init__(self, title=None, # string link=None, # url as string diff --git a/src/calibre/utils/run_tests.py b/src/calibre/utils/run_tests.py index e11e38eac3..144682510a 100644 --- a/src/calibre/utils/run_tests.py +++ b/src/calibre/utils/run_tests.py @@ -7,6 +7,7 @@ from __future__ import (unicode_literals, division, absolute_import, import unittest, functools, os, importlib, zipfile from calibre.utils.monotonic import monotonic + def no_endl(f): @functools.wraps(f) def func(*args, **kwargs): @@ -19,6 +20,7 @@ def no_endl(f): self.stream.writeln = orig return func + class TestResult(unittest.TextTestResult): def __init__(self, *args, **kwargs): @@ -51,6 +53,7 @@ class TestResult(unittest.TextTestResult): if len(slowest) > 1: self.stream.writeln('\nSlowest tests: %s' % ' '.join(slowest)) + def find_tests_in_dir(path, excludes=('main.py',)): if not os.path.exists(path) and '.zip' in path: idx = path.rfind('.zip') @@ -72,6 +75,7 @@ def find_tests_in_dir(path, excludes=('main.py',)): suits.append(unittest.defaultTestLoader.loadTestsFromModule(m)) return unittest.TestSuite(suits) + def itertests(suite): stack = [suite] while stack: @@ -84,12 +88,14 @@ def itertests(suite): raise Exception('Failed to import a test module: %s' % test) yield test + def init_env(): from calibre.utils.config_base import reset_tweaks_to_default from calibre.ebooks.metadata.book.base import reset_field_metadata reset_tweaks_to_default() reset_field_metadata() + def filter_tests(suite, test_ok): ans = unittest.TestSuite() added = set() @@ -99,6 +105,7 @@ def filter_tests(suite, test_ok): added.add(test) return ans + def filter_tests_by_name(suite, *names): names = {x if x.startswith('test_') else 'test_' + x for x in names} @@ -106,13 +113,16 @@ def filter_tests_by_name(suite, *names): return test._testMethodName in names return filter_tests(suite, q) + def filter_tests_by_module(suite, *names): names = frozenset(names) + def q(test): m = test.__class__.__module__.rpartition('.')[-1] return m in names return filter_tests(suite, q) + def run_tests(find_tests, verbosity=4): import argparse parser = argparse.ArgumentParser() @@ -129,6 +139,7 @@ def run_tests(find_tests, verbosity=4): raise SystemExit('No test named %s found' % args.name) run_cli(tests, verbosity) + def run_cli(suite, verbosity=4): r = unittest.TextTestRunner r.resultclass = unittest.TextTestResult if verbosity < 2 else TestResult diff --git a/src/calibre/utils/search_query_parser.py b/src/calibre/utils/search_query_parser.py index d87d50a67e..fe04af0b34 100644 --- a/src/calibre/utils/search_query_parser.py +++ b/src/calibre/utils/search_query_parser.py @@ -28,6 +28,8 @@ This class manages access to the preference holding the saved search queries. It exists to ensure that unicode is used throughout, and also to permit adding other fields, such as whether the search is a 'favorite' ''' + + class SavedSearchQueries(object): queries = {} opt_name = '' @@ -90,14 +92,17 @@ are common across all instances of the parser (devices, library, etc). ''' ss = SavedSearchQueries(None, None) + def set_saved_searches(db, opt_name): global ss ss = SavedSearchQueries(db, opt_name) + def saved_searches(): global ss return ss + def global_lookup_saved_search(name): return ss.lookup(name) @@ -127,6 +132,8 @@ location_expression ::= base_token | ( '(' or_expression ')' ) base_token ::= a sequence of letters and colons, perhaps quoted ''' + + class Parser(object): def __init__(self): @@ -260,6 +267,7 @@ class Parser(object): return ['token', 'all', ':'.join(words)] + class ParseException(Exception): @property @@ -268,6 +276,7 @@ class ParseException(Exception): return self.args[0] return "" + class SearchQueryParser(object): ''' Parses a search query. @@ -425,6 +434,7 @@ class SearchQueryParser(object): # Testing {{{ + class Tester(SearchQueryParser): texts = { diff --git a/src/calibre/utils/serve_coffee.py b/src/calibre/utils/serve_coffee.py index 40bb02b723..0b7246a09b 100644 --- a/src/calibre/utils/serve_coffee.py +++ b/src/calibre/utils/serve_coffee.py @@ -26,6 +26,7 @@ from SimpleHTTPServer import SimpleHTTPRequestHandler tls = local() + def compiler(): ans = getattr(tls, 'compiler', None) if ans is None: @@ -34,6 +35,7 @@ def compiler(): c.eval(P('coffee-script.js', data=True).decode('utf-8')) return tls.compiler + def compile_coffeescript(raw, filename=None): from duktape import JSError jc = compiler() @@ -46,6 +48,7 @@ def compile_coffeescript(raw, filename=None): # }}} + def check_coffeescript(filename): with open(filename, 'rb') as f: raw = f.read() @@ -54,6 +57,7 @@ def check_coffeescript(filename): print('\n'.join(errs)) raise Exception('Compilation failed') + class HTTPRequestHandler(SimpleHTTPRequestHandler): # {{{ ''' @@ -175,6 +179,7 @@ class HTTPRequestHandler(SimpleHTTPRequestHandler): # {{{ return self.send_file(f, ctype, fs.st_mtime) # }}} + class Handler(HTTPRequestHandler): # {{{ class NoCoffee(Exception): @@ -249,6 +254,7 @@ class Handler(HTTPRequestHandler): # {{{ # }}} + class Server(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer): # {{{ daemon_threads = True @@ -264,6 +270,7 @@ class Server(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer): # {{{ print ('-'*40) # }}} + def serve(resources={}, port=8000, host='0.0.0.0'): Handler.special_resources = resources Handler.compiler = compile_coffeescript diff --git a/src/calibre/utils/sftp.py b/src/calibre/utils/sftp.py index 9b56d97b78..0d816af3d0 100644 --- a/src/calibre/utils/sftp.py +++ b/src/calibre/utils/sftp.py @@ -13,6 +13,7 @@ from binascii import hexlify import paramiko + def agent_auth(transport, username): """ Attempt to authenticate to the given transport using any of the private @@ -34,10 +35,12 @@ def agent_auth(transport, username): print '... failed.' return False + def portable_getpass(username, hostname, retry): return getpass.getpass('%sPlease enter the password for %s on %s: '%( 'Incorrect password. ' if retry else '', username, hostname)) + def password_auth(transport, username, hostname, getpw=portable_getpass): for i in range(3): pw = getpw(username, hostname, i>0) diff --git a/src/calibre/utils/shared_file.py b/src/calibre/utils/shared_file.py index 394c0cd86d..f089c9c612 100644 --- a/src/calibre/utils/shared_file.py +++ b/src/calibre/utils/shared_file.py @@ -33,9 +33,11 @@ if not speedup: valid_modes = {'a', 'a+', 'a+b', 'ab', 'r', 'rb', 'r+', 'r+b', 'w', 'wb', 'w+', 'w+b'} + def validate_mode(mode): return mode in valid_modes + class FlagConstants(object): def __init__(self): @@ -47,6 +49,7 @@ class FlagConstants(object): setattr(self, x, getattr(os, x, 0)) fc = FlagConstants() + def flags_from_mode(mode): if not validate_mode(mode): raise ValueError('The mode is invalid') @@ -172,6 +175,7 @@ else: def raise_winerror(x): raise NotImplementedError(), None, sys.exc_info()[2] + def find_tests(): import unittest from calibre.ptempfile import TemporaryDirectory diff --git a/src/calibre/utils/short_uuid.py b/src/calibre/utils/short_uuid.py index ce1ff7b8a0..76ea575a84 100644 --- a/src/calibre/utils/short_uuid.py +++ b/src/calibre/utils/short_uuid.py @@ -11,6 +11,7 @@ Generate UUID encoded using a user specified alphabet. import string, math, uuid as _uuid + def num_to_string(number, alphabet, alphabet_len, pad_to_length=None): ans = [] number = max(0, number) @@ -21,12 +22,14 @@ def num_to_string(number, alphabet, alphabet_len, pad_to_length=None): ans.append(alphabet[0] * (pad_to_length - len(ans))) return ''.join(ans) + def string_to_num(string, alphabet_map, alphabet_len): ans = 0 for char in reversed(string): ans = ans * alphabet_len + alphabet_map[char] return ans + class ShortUUID(object): def __init__(self, alphabet=None): diff --git a/src/calibre/utils/smartypants.py b/src/calibre/utils/smartypants.py index e9de316290..9ef0548c50 100644 --- a/src/calibre/utils/smartypants.py +++ b/src/calibre/utils/smartypants.py @@ -887,6 +887,7 @@ def _tokenize(str): return tokens + def run_tests(return_tests=False): import unittest sp = smartyPants diff --git a/src/calibre/utils/smtp.py b/src/calibre/utils/smtp.py index ef8c4e8021..ab7c93d189 100644 --- a/src/calibre/utils/smtp.py +++ b/src/calibre/utils/smtp.py @@ -12,6 +12,7 @@ This module implements a simple commandline SMTP client that supports: import sys, traceback, os, socket, encodings.idna as idna from calibre import isbytestring, force_unicode + def create_mail(from_, to, subject, text=None, attachment_data=None, attachment_type=None, attachment_name=None): assert text or attachment_data @@ -52,6 +53,7 @@ def create_mail(from_, to, subject, text=None, attachment_data=None, return outer.as_string() + def get_mx(host, verbose=0): import dns.resolver if verbose: @@ -61,6 +63,7 @@ def get_mx(host, verbose=0): int(getattr(y, 'preference', sys.maxint)))) return [str(x.exchange) for x in answers if hasattr(x, 'exchange')] + def safe_localhost(): # RFC 2821 says we should use the fqdn in the EHLO/HELO verb, and # if that can't be calculated, that we should use a domain literal @@ -83,6 +86,7 @@ def safe_localhost(): local_hostname = '[%s]' % addr return local_hostname + def sendmail_direct(from_, to, msg, timeout, localhost, verbose, debug_output=None): import calibre.utils.smtplib as smtplib @@ -143,6 +147,7 @@ def sendmail(msg, from_, to, localhost=None, verbose=0, timeout=None, pass # Ignore so as to not hide original error return ret + def option_parser(): try: from calibre.utils.config import OptionParser @@ -196,10 +201,12 @@ are only used in the SMTP negotiation, the message headers are not modified. help=_('Be more verbose')) return parser + def extract_email_address(raw): from email.utils import parseaddr return parseaddr(raw)[-1] + def compose_mail(from_, to, text, subject=None, attachment=None, attachment_name=None): attachment_type = attachment_data = None @@ -220,6 +227,7 @@ def compose_mail(from_, to, text, subject=None, attachment=None, attachment_data=attachment_data, attachment_type=attachment_type, attachment_name=attachment_name) + def main(args=sys.argv): parser = option_parser() opts, args = parser.parse_args(args) @@ -274,6 +282,7 @@ def main(args=sys.argv): raise return 0 + def config(defaults=None): from calibre.utils.config import Config, StringConfig desc = _('Control email delivery') diff --git a/src/calibre/utils/smtplib.py b/src/calibre/utils/smtplib.py index 386341b6a4..83a0a7280c 100755 --- a/src/calibre/utils/smtplib.py +++ b/src/calibre/utils/smtplib.py @@ -69,6 +69,7 @@ OLDSTYLE_AUTH = re.compile(r"auth=(.*)", re.I) class SMTPException(Exception): """Base class for all exceptions raised by this module.""" + class SMTPServerDisconnected(SMTPException): """Not connected to any SMTP server. @@ -77,6 +78,7 @@ class SMTPServerDisconnected(SMTPException): connecting it to a server. """ + class SMTPResponseException(SMTPException): """Base class for all exceptions that include an SMTP error code. @@ -91,6 +93,7 @@ class SMTPResponseException(SMTPException): self.smtp_error = msg self.args = (code, msg) + class SMTPSenderRefused(SMTPResponseException): """Sender address refused. @@ -104,6 +107,7 @@ class SMTPSenderRefused(SMTPResponseException): self.sender = sender self.args = (code, msg, sender) + class SMTPRecipientsRefused(SMTPException): """All recipient addresses refused. @@ -120,12 +124,15 @@ class SMTPRecipientsRefused(SMTPException): class SMTPDataError(SMTPResponseException): """The SMTP server didn't accept the data.""" + class SMTPConnectError(SMTPResponseException): """Error during connection establishment.""" + class SMTPHeloError(SMTPResponseException): """The server refused our HELO reply.""" + class SMTPAuthenticationError(SMTPResponseException): """Authentication error. @@ -153,6 +160,7 @@ def quoteaddr(addr): else: return "<%s>" % m + def _addr_only(addrstring): displayname, addr = email.utils.parseaddr(addrstring) if (displayname, addr) == ('', ''): @@ -160,6 +168,7 @@ def _addr_only(addrstring): return addrstring return addr + def quotedata(data): """Quote data for email. @@ -180,6 +189,7 @@ else: It only supports what is needed in smtplib. """ + def __init__(self, sslobj): self.sslobj = sslobj @@ -202,6 +212,7 @@ else: _have_ssl = True + class SMTP: """This class manages a connection to an SMTP or ESMTP server. SMTP Objects: @@ -826,6 +837,7 @@ if _have_ssl: # LMTP_PORT = 2003 + class LMTP(SMTP): """LMTP - Local Mail Transfer Protocol diff --git a/src/calibre/utils/socket_inheritance.py b/src/calibre/utils/socket_inheritance.py index 439977d1c5..4d73045feb 100644 --- a/src/calibre/utils/socket_inheritance.py +++ b/src/calibre/utils/socket_inheritance.py @@ -14,6 +14,7 @@ modified to make it work from calibre.constants import iswindows + def get_socket_inherit(socket): ''' Returns True if the socket has been set to allow inheritance across @@ -32,6 +33,7 @@ def get_socket_inherit(socket): import traceback traceback.print_exc() + def set_socket_inherit(sock, inherit): ''' Mark a socket as inheritable or non-inheritable to child processes. @@ -65,6 +67,7 @@ def set_socket_inherit(sock, inherit): import traceback traceback.print_exc() + def test(): import socket s = socket.socket() diff --git a/src/calibre/utils/soupparser.py b/src/calibre/utils/soupparser.py index b1d7c44aed..1f77cc1d1d 100644 --- a/src/calibre/utils/soupparser.py +++ b/src/calibre/utils/soupparser.py @@ -22,6 +22,7 @@ def fromstring(data, beautifulsoup=None, makeelement=None, **bsargs): """ return _parse(data, beautifulsoup, makeelement, **bsargs) + def parse(file, beautifulsoup=None, makeelement=None, **bsargs): """Parse a file into an ElemenTree using the BeautifulSoup parser. @@ -36,6 +37,7 @@ def parse(file, beautifulsoup=None, makeelement=None, **bsargs): root = _parse(file, beautifulsoup, makeelement, **bsargs) return etree.ElementTree(root) + def convert_tree(beautiful_soup_tree, makeelement=None): """Convert a BeautifulSoup tree to a list of Element trees. @@ -71,12 +73,14 @@ def _parse(source, beautifulsoup, makeelement, **bsargs): root.tag = "html" return root + def _convert_tree(beautiful_soup_tree, makeelement): root = makeelement(beautiful_soup_tree.name, attrib=dict(beautiful_soup_tree.attrs)) _convert_children(root, beautiful_soup_tree, makeelement) return root + def _convert_children(parent, beautiful_soup_tree, makeelement): SubElement = etree.SubElement et_child = None @@ -96,6 +100,7 @@ def _convert_children(parent, beautiful_soup_tree, makeelement): else: # CData _append_text(parent, et_child, unescape(child)) + def _append_text(parent, element, text): if element is None: parent.text = (parent.text or '') + text @@ -114,10 +119,12 @@ import re handle_entities = re.compile("&(\w+);").sub + def unescape(string): if not string: return '' # work around oddities in BeautifulSoup's entity handling + def unescape_entity(m): try: return unichr(name2codepoint[m.group(1)]) diff --git a/src/calibre/utils/speedups.py b/src/calibre/utils/speedups.py index f516d614fb..7dcec14dd4 100644 --- a/src/calibre/utils/speedups.py +++ b/src/calibre/utils/speedups.py @@ -7,6 +7,7 @@ from __future__ import (unicode_literals, division, absolute_import, import os + class ReadOnlyFileBuffer(object): ''' A zero copy implementation of a file like object. Uses memoryviews for efficiency. ''' @@ -43,6 +44,7 @@ class ReadOnlyFileBuffer(object): def close(self): pass + def svg_path_to_painter_path(d): ''' Convert a tiny SVG 1.2 path into a QPainterPath. diff --git a/src/calibre/utils/terminal.py b/src/calibre/utils/terminal.py index ebe79bc10c..ac0225a7bd 100644 --- a/src/calibre/utils/terminal.py +++ b/src/calibre/utils/terminal.py @@ -14,6 +14,7 @@ from calibre.constants import iswindows if iswindows: import ctypes.wintypes + class CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure): _fields_ = [ ('dwSize', ctypes.wintypes._COORD), @@ -23,6 +24,7 @@ if iswindows: ('dwMaximumWindowSize', ctypes.wintypes._COORD) ] + def fmt(code): return ('\033[%dm'%code).encode('ascii') @@ -84,6 +86,7 @@ if iswindows: val |= (WCOLORS[bg] << 4) return val + def colored(text, fg=None, bg=None, bold=False): prefix = [] if fg is not None: @@ -99,6 +102,7 @@ def colored(text, fg=None, bg=None, bold=False): suffix = suffix.decode('ascii') return prefix + text + suffix + class Detect(object): def __init__(self, stream): @@ -172,6 +176,7 @@ class Detect(object): if not ignore_errors: raise ctypes.WinError(err) + class ColoredStream(Detect): def __init__(self, stream=None, fg=None, bg=None, bold=False): @@ -208,6 +213,7 @@ class ColoredStream(Detect): elif self.set_console is not None: self.set_console(self.file_handle, self.default_console_text_attributes) + class ANSIStream(Detect): ANSI_RE = re.compile(br'\033\[((?:\d|;)*)([a-zA-Z])') @@ -298,6 +304,7 @@ class ANSIStream(Detect): else: self.set_console(self.file_handle, self.default_console_text_attributes) + def windows_terminfo(): from ctypes import Structure, byref from ctypes.wintypes import SHORT, WORD @@ -340,6 +347,7 @@ def windows_terminfo(): raise Exception('stdout is not a console?') return csbi + def get_term_geometry(): import fcntl, termios, struct @@ -383,6 +391,7 @@ def geometry(): pass return 80, 25 + def test(): s = ANSIStream() diff --git a/src/calibre/utils/text2int.py b/src/calibre/utils/text2int.py index 68dbce25c6..6fb266151f 100755 --- a/src/calibre/utils/text2int.py +++ b/src/calibre/utils/text2int.py @@ -13,6 +13,7 @@ import re numwords = {} + def text2int(textnum): if not numwords: diff --git a/src/calibre/utils/threadpool.py b/src/calibre/utils/threadpool.py index 6a3eb7e038..1bafd7a2ff 100644 --- a/src/calibre/utils/threadpool.py +++ b/src/calibre/utils/threadpool.py @@ -50,15 +50,20 @@ import threading import Queue # exceptions + + class NoResultsPending(Exception): """All work requests have been processed.""" pass + class NoWorkersAvailable(Exception): """No worker threads available to process remaining requests.""" pass # classes + + class WorkerThread(threading.Thread): """Background thread connected to the requests/results queues. @@ -231,6 +236,8 @@ class ThreadPool: break # helper functions + + def makeRequests(callable, args_list, callback=None, exc_callback=None): """Create several work requests for same callable with different arguments. diff --git a/src/calibre/utils/titlecase.py b/src/calibre/utils/titlecase.py index 2e188dac17..b1d1959716 100755 --- a/src/calibre/utils/titlecase.py +++ b/src/calibre/utils/titlecase.py @@ -31,6 +31,7 @@ UC_INITIALS = re.compile(r"^(?:[A-Z]{1}\.{1}|[A-Z]{1}\.{1}[A-Z]{1})+$") _lang = None + def lang(): global _lang if _lang is None: @@ -38,8 +39,8 @@ def lang(): _lang = get_lang().lower() return _lang -def titlecase(text): +def titlecase(text): """ Titlecases input text diff --git a/src/calibre/utils/unrar.py b/src/calibre/utils/unrar.py index 307ba327fe..11e294ef2e 100644 --- a/src/calibre/utils/unrar.py +++ b/src/calibre/utils/unrar.py @@ -13,13 +13,17 @@ from io import BytesIO from calibre import force_unicode from calibre.constants import filesystem_encoding, isosx + class UNRARError(Exception): pass + class DevNull: + def write(self, x): pass + class RARStream(object): def __init__(self, stream, unrar, get_comment=False): @@ -93,6 +97,7 @@ def RARFile(stream, get_comment=False): %err) return RARStream(stream, unrar, get_comment=get_comment) + class SaveStream(object): def __init__(self, stream): @@ -104,6 +109,7 @@ class SaveStream(object): def __exit__(self, *args): self.stream.seek(0) + def safe_path(base, relpath): base = os.path.abspath(base) path = os.path.abspath(os.path.join(base, relpath)) @@ -112,10 +118,12 @@ def safe_path(base, relpath): return None return path + def is_useful(h): return not (h['is_label'] or h['is_symlink'] or h['has_password'] or h['is_directory']) + def stream_extract(stream, location): location = os.path.abspath(location) if not os.path.exists(location): @@ -146,10 +154,12 @@ def stream_extract(stream, location): with open(path, 'wb') as dest: f.process_current_item(dest) + def extract(path, location): with open(path, 'rb') as stream: stream_extract(stream, location) + def names(stream): with SaveStream(stream): f = RARFile(stream) @@ -162,6 +172,7 @@ def names(stream): if is_useful(h): yield h['filename'] + def extract_member(stream, match=re.compile(r'\.(jpg|jpeg|gif|png)\s*$', re.I), name=None): @@ -184,12 +195,14 @@ def extract_member(stream, match=re.compile(r'\.(jpg|jpeg|gif|png)\s*$', re.I), f.process_current_item(et) return h['filename'], et.getvalue() + def extract_first_alphabetically(stream): from calibre.libunzip import sort_key names_ = sorted([x for x in names(stream) if os.path.splitext(x)[1][1:].lower() in {'png', 'jpg', 'jpeg', 'gif', 'webp'}], key=sort_key) return extract_member(stream, name=names_[0], match=None) + def extract_cover_image(stream): from calibre.libunzip import sort_key, name_ok for name in sorted(names(stream), key=sort_key): @@ -197,6 +210,8 @@ def extract_cover_image(stream): return extract_member(stream, name=name, match=None) # Test normal RAR file {{{ + + def test_basic(): stream = BytesIO( @@ -244,6 +259,7 @@ def test_basic(): del f for i in xrange(3): gc.collect() + def get_mem_use(num): start = memory() s = SaveStream(stream) @@ -261,6 +277,7 @@ def test_basic(): raise ValueError('Leaked %s MB for %d calls'%(b - a, 100)) # }}} + def test_rar(path): with open(path, 'rb') as stream: f = RARFile(stream) diff --git a/src/calibre/utils/winreg/dde.py b/src/calibre/utils/winreg/dde.py index d8c4533cd5..54a99c059d 100644 --- a/src/calibre/utils/winreg/dde.py +++ b/src/calibre/utils/winreg/dde.py @@ -66,21 +66,26 @@ PCONVCONTEXT = c_void_p XCLASS_FLAGS = 0x4000 XTYP_EXECUTE = (0x0050 | XCLASS_FLAGS) + class DDEError(ValueError): pass + def init_errcheck(result, func, args): if result != 0: raise DDEError('Failed to initialize DDE client with return code: %x' % result) return args + def no_errcheck(result, func, args): return args + def dde_error(instance): errcode = GetLastError(instance) raise DDEError(DML_ERRORS.get(errcode, 'Unknown DDE error code: %x' % errcode)) + def default_errcheck(result, func, args): if (isinstance(result, (int, long)) and result == 0) or (getattr(result, 'value', False) is None): dde_error(args[0]) @@ -88,6 +93,7 @@ def default_errcheck(result, func, args): null = object() + class a(object): def __init__(self, name, typ, default=null, in_arg=True): @@ -97,6 +103,7 @@ class a(object): else: self.spec=((1 if in_arg else 2), name, default) + def cwrap(name, restype, *args, **kw): params=(restype,) + tuple(x.typ for x in args) paramflags=tuple(x.spec for x in args) @@ -117,6 +124,7 @@ FreeDataHandle = cwrap('DdeFreeDataHandle', BOOL, a('data', HDDEDATA), errcheck= Disconnect = cwrap('DdeDisconnect', BOOL, a('conversation', HCONV), errcheck=no_errcheck) Uninitialize = cwrap('DdeUninitialize', BOOL, a('instance', DWORD), errcheck=no_errcheck) + def send_dde_command(service, topic, command): instance = DWORD(0) diff --git a/src/calibre/utils/winreg/default_programs.py b/src/calibre/utils/winreg/default_programs.py index 076fa04fa7..c35a378456 100644 --- a/src/calibre/utils/winreg/default_programs.py +++ b/src/calibre/utils/winreg/default_programs.py @@ -18,6 +18,7 @@ from calibre.utils.winreg.lib import Key, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE # See https://msdn.microsoft.com/en-us/library/windows/desktop/cc144154(v=vs.85).aspx + def default_programs(): return { 'calibre.exe': { @@ -45,6 +46,7 @@ def default_programs(): }, } + def extensions(basename): if basename == 'calibre.exe': from calibre.ebooks import BOOK_EXTENSIONS @@ -59,9 +61,11 @@ def extensions(basename): from calibre.ebooks.oeb.polish.import_book import IMPORTABLE return SUPPORTED | IMPORTABLE + class NotAllowed(ValueError): pass + def check_allowed(): if not isfrozen: raise NotAllowed('Not allowed to create associations for non-frozen installs') @@ -72,6 +76,7 @@ def check_allowed(): if b'CALIBRE_NO_DEFAULT_PROGRAMS' in os.environ: raise NotAllowed('Disabled by the CALIBRE_NO_DEFAULT_PROGRAMS environment variable') + def create_prog_id(ext, prog_id, ext_map, exe): with Key(r'Software\Classes\%s' % prog_id) as key: type_name = _('%s Document') % ext.upper() @@ -85,12 +90,15 @@ def create_prog_id(ext, prog_id, ext_map, exe): with Key(r'Software\Classes\.%s\OpenWithProgIDs' % ext) as key: key.set(prog_id) + def progid_name(assoc_name, ext): return '%s.AssocFile.%s' % (assoc_name, ext.upper()) + def cap_path(data): return r'Software\calibre\%s\Capabilities' % data['capability_name'] + def register(): base = os.path.dirname(sys.executable) @@ -123,6 +131,7 @@ def register(): from win32com.shell import shell, shellcon shell.SHChangeNotify(shellcon.SHCNE_ASSOCCHANGED, shellcon.SHCNF_DWORD | shellcon.SHCNF_FLUSH, 0, 0) + def unregister(): for program, data in default_programs().iteritems(): capabilities_path = cap_path(data).rpartition('\\')[0] @@ -181,6 +190,7 @@ class Register(Thread): # application very quickly self.join(4.0) + def get_prog_id_map(base, key_path): desc, ans = None, {} try: @@ -197,6 +207,7 @@ def get_prog_id_map(base, key_path): ans[ext[1:].lower()] = prog_id return desc, ans + def get_open_data(base, prog_id): try: k = Key(open_at=r'Software\Classes\%s' % prog_id, root=base) @@ -218,6 +229,7 @@ LocalFree = ctypes.windll.kernel32.LocalFree LocalFree.res_type = HLOCAL LocalFree.arg_types = [HLOCAL] + def split_commandline(commandline): # CommandLineToArgvW returns path to executable if called with empty string. if not commandline.strip(): @@ -231,6 +243,7 @@ def split_commandline(commandline): LocalFree(result_pointer) return result + def friendly_app_name(prog_id=None, exe=None): try: from win32com.shell import shell, shellcon @@ -241,6 +254,7 @@ def friendly_app_name(prog_id=None, exe=None): import traceback traceback.print_exc() + def find_programs(extensions): extensions = frozenset(extensions) ans = [] diff --git a/src/calibre/utils/winreg/lib.py b/src/calibre/utils/winreg/lib.py index cc4fee5af3..7f12f4eb2b 100644 --- a/src/calibre/utils/winreg/lib.py +++ b/src/calibre/utils/winreg/lib.py @@ -32,15 +32,19 @@ RRF_RT_ANY = 0x0000ffff RRF_NOEXPAND = 0x10000000 RRF_ZEROONFAILURE = 0x20000000 + class FILETIME(ctypes.Structure): _fields_ = [("dwLowDateTime", DWORD), ("dwHighDateTime", DWORD)] + def default_errcheck(result, func, args): if result != getattr(winerror, 'ERROR_SUCCESS', 0): # On shutdown winerror becomes None raise ctypes.WinError(result) return args null = object() + + class a(object): def __init__(self, name, typ, default=null, in_arg=True): @@ -50,6 +54,7 @@ class a(object): else: self.spec = ((1 if in_arg else 2), name, default) + def cwrap(name, restype, *args, **kw): params = (restype,) + tuple(x.typ for x in args) paramflags = tuple(x.spec for x in args) @@ -64,6 +69,7 @@ RegCreateKey = cwrap( a('access', ULONG, KEY_ALL_ACCESS), a('security', ctypes.c_void_p, 0), a('result', PHKEY, in_arg=False), a('disposition', LPDWORD, in_arg=False)) RegCloseKey = cwrap('RegCloseKey', LONG, a('key', HKEY)) + def enum_value_errcheck(result, func, args): if result == winerror.ERROR_SUCCESS: return args @@ -76,6 +82,7 @@ RegEnumValue = cwrap( 'RegEnumValueW', LONG, a('key', HKEY), a('index', DWORD), a('value_name', LPWSTR), a('value_name_size', LPDWORD), a('reserved', LPDWORD), a('value_type', LPDWORD), a('data', LPBYTE), a('data_size', LPDWORD), errcheck=enum_value_errcheck) + def last_error_errcheck(result, func, args): if result == 0: raise ctypes.WinError() @@ -83,11 +90,13 @@ def last_error_errcheck(result, func, args): ExpandEnvironmentStrings = cwrap( 'ExpandEnvironmentStringsW', DWORD, a('src', LPCWSTR), a('dest', LPWSTR), a('size', DWORD), errcheck=last_error_errcheck, lib=ctypes.windll.kernel32) + def expand_environment_strings(src): buf = ctypes.create_unicode_buffer(32 * 1024) ExpandEnvironmentStrings(src, buf, len(buf)) return buf.value + def convert_to_registry_data(value, has_expansions=False): if value is None: return None, winreg.REG_NONE, 0 @@ -106,6 +115,7 @@ def convert_to_registry_data(value, has_expansions=False): return buf, dtype, len(buf) raise ValueError('Unknown data type: %r' % value) + def convert_registry_data(raw, size, dtype): if dtype == winreg.REG_NONE: return None @@ -136,6 +146,7 @@ except Exception: raise RuntimeError('calibre requires Windows Vista or newer to run, the last version of calibre' ' that could run on Windows XP is version 1.48, available from: http://download.calibre-ebook.com/') + def delete_value_errcheck(result, func, args): if result == winerror.ERROR_FILE_NOT_FOUND: return args @@ -150,6 +161,8 @@ RegEnumKeyEx = cwrap( 'RegEnumKeyExW', LONG, a('key', HKEY), a('index', DWORD), a('name', LPWSTR), a('name_size', LPDWORD), a('reserved', LPDWORD, None), a('cls', LPWSTR, None), a('cls_size', LPDWORD, None), a('last_write_time', ctypes.POINTER(FILETIME), in_arg=False), errcheck=enum_value_errcheck) + + def get_value_errcheck(result, func, args): if result == winerror.ERROR_SUCCESS: return args @@ -176,6 +189,7 @@ def filetime_to_datettime(ft): # }}} + class Key(object): def __init__(self, create_at=None, open_at=None, root=HKEY_CURRENT_USER, open_mode=KEY_READ): diff --git a/src/calibre/utils/wmf/__init__.py b/src/calibre/utils/wmf/__init__.py index f0e2f98f4a..ee9b608729 100644 --- a/src/calibre/utils/wmf/__init__.py +++ b/src/calibre/utils/wmf/__init__.py @@ -7,9 +7,11 @@ __docformat__ = 'restructuredtext en' import struct + class Unavailable(Exception): pass + class NoRaster(Exception): pass @@ -56,6 +58,7 @@ def create_bmp_from_dib(raw): pixel_array_offset)] return b''.join(parts) + raw + def to_png(bmp): from PyQt5.Qt import QImage, QByteArray, QBuffer i = QImage() diff --git a/src/calibre/utils/wmf/emf.py b/src/calibre/utils/wmf/emf.py index d7f400c93a..9ff8cc2a9a 100644 --- a/src/calibre/utils/wmf/emf.py +++ b/src/calibre/utils/wmf/emf.py @@ -35,6 +35,7 @@ StretchDiBits = namedtuple( ' bmp_bits_size usage op dest_width dest_height') # }}} + class EMF(object): def __init__(self, raw, verbose=0): @@ -75,6 +76,7 @@ class EMF(object): bmp = bmps[-1] return to_png(bmp) + def emf_unwrap(raw, verbose=0): ''' Return the largest embedded raster image in the EMF. diff --git a/src/calibre/utils/wmf/parse.py b/src/calibre/utils/wmf/parse.py index 8e04f02ab2..83cae92e73 100644 --- a/src/calibre/utils/wmf/parse.py +++ b/src/calibre/utils/wmf/parse.py @@ -9,6 +9,7 @@ import sys, struct from calibre.utils.wmf import create_bmp_from_dib, to_png + class WMFHeader(object): ''' @@ -34,6 +35,7 @@ class WMFHeader(object): self.records_start_at = header_size * 2 + class WMF(object): def __init__(self, log=None, verbose=0): @@ -205,6 +207,7 @@ class WMF(object): bmp = bmps[-1] return to_png(bmp) + def wmf_unwrap(wmf_data, verbose=0): ''' Return the largest embedded raster image in the WMF. diff --git a/src/calibre/utils/wordcount.py b/src/calibre/utils/wordcount.py index ae17481c89..e711061b14 100644 --- a/src/calibre/utils/wordcount.py +++ b/src/calibre/utils/wordcount.py @@ -27,6 +27,7 @@ __author__ = "Ryan Ginstrom" IDEOGRAPHIC_SPACE = 0x3000 + def is_asian(char): """Is the character Asian?""" @@ -34,12 +35,14 @@ def is_asian(char): # Anything over is an Asian character return ord(char) > IDEOGRAPHIC_SPACE + def filter_jchars(c): """Filters Asian characters to spaces""" if is_asian(c): return ' ' return c + def nonj_len(word): u"""Returns number of non-Asian words in {word} - 日本語AアジアンB -> 2 @@ -55,6 +58,7 @@ def nonj_len(word): chars = [filter_jchars(c) for c in word] return len(u''.join(chars).split()) + def get_wordcount(text): """Get the word/character count for text @@ -73,13 +77,16 @@ def get_wordcount(text): non_asian_words=non_asian_words, words=words) + def dict2obj(dictionary): """Transform a dictionary into an object""" class Obj(object): + def __init__(self, dictionary): self.__dict__.update(dictionary) return Obj(dictionary) + def get_wordcount_obj(text): """Get the wordcount as an object rather than a dictionary""" return dict2obj(get_wordcount(text)) diff --git a/src/calibre/utils/zipfile.py b/src/calibre/utils/zipfile.py index 3171b89bd8..62bcb564f8 100644 --- a/src/calibre/utils/zipfile.py +++ b/src/calibre/utils/zipfile.py @@ -21,6 +21,7 @@ except ImportError: __all__ = ["BadZipfile", "error", "ZIP_STORED", "ZIP_DEFLATED", "is_zipfile", "ZipInfo", "ZipFile", "PyZipFile", "LargeZipFile"] + class BadZipfile(Exception): pass @@ -137,6 +138,7 @@ _CD64_NUMBER_ENTRIES_TOTAL = 7 _CD64_DIRECTORY_SIZE = 8 _CD64_OFFSET_START_CENTDIR = 9 + def decode_arcname(name): if not isinstance(name, unicode): try: @@ -152,11 +154,14 @@ def decode_arcname(name): # Added by Kovid to reset timestamp to default if it overflows the DOS # limits + + def fixtimevar(val): if val < 0 or val > 0xffff: val = 0 return val + def _check_zipfile(fp): try: if _EndRecData(fp): @@ -165,6 +170,7 @@ def _check_zipfile(fp): pass return False + def is_zipfile(filename): """Quickly see if a file is a ZIP file by checking the magic number. @@ -181,6 +187,7 @@ def is_zipfile(filename): pass return result + def _EndRecData64(fpin, offset, endrec): """ Read the ZIP64 end-of-archive records and use that to update endrec @@ -488,6 +495,7 @@ class _ZipDecrypter: self._UpdateKeys(c) return c + class ZipExtFile(io.BufferedIOBase): """File-like object for reading an archive member. @@ -1447,6 +1455,7 @@ class ZipFile: self.fp.close() self.fp = None + def safe_replace(zipstream, name, datastream, extra_replacements={}, add_missing=False): ''' @@ -1498,6 +1507,7 @@ def safe_replace(zipstream, name, datastream, extra_replacements={}, shutil.copyfileobj(temp, zipstream) zipstream.flush() + class PyZipFile(ZipFile): """Class to create ZIP archives with Python library files and packages.""" diff --git a/src/calibre/web/__init__.py b/src/calibre/web/__init__.py index ac125f40e7..f3b328cbe6 100644 --- a/src/calibre/web/__init__.py +++ b/src/calibre/web/__init__.py @@ -5,6 +5,7 @@ __copyright__ = '2008, Kovid Goyal ' class Recipe(object): pass + def get_download_filename_from_response(response): from urlparse import urlparse from urllib2 import unquote as urllib2_unquote diff --git a/src/calibre/web/feeds/__init__.py b/src/calibre/web/feeds/__init__.py index 1b39202cf8..8c9d7484bb 100644 --- a/src/calibre/web/feeds/__init__.py +++ b/src/calibre/web/feeds/__init__.py @@ -12,6 +12,7 @@ from calibre import entity_to_unicode, strftime, force_unicode from calibre.utils.date import dt_factory, utcnow, local_tz from calibre.utils.cleantext import clean_ascii_chars, clean_xml_chars + class Article(object): def __init__(self, id, title, url, author, summary, published, content): @@ -74,6 +75,7 @@ class Article(object): if not isinstance(t, unicode) and hasattr(t, 'decode'): t = t.decode('utf-8', 'replace') return t + def fset(self, val): self._title = clean_ascii_chars(val) return property(fget=fget, fset=fset) @@ -278,6 +280,7 @@ class Feed(object): except ValueError: pass + class FeedCollection(list): def __init__(self, feeds): @@ -342,6 +345,7 @@ def feed_from_xml(raw_xml, title=None, oldest_article=7, max_articles_per_feed=max_articles_per_feed) return pfeed + def feeds_from_index(index, oldest_article=7, max_articles_per_feed=100, log=default_log): ''' diff --git a/src/calibre/web/feeds/news.py b/src/calibre/web/feeds/news.py index e69f973e99..4d85b1dc30 100644 --- a/src/calibre/web/feeds/news.py +++ b/src/calibre/web/feeds/news.py @@ -32,12 +32,15 @@ from calibre.utils.img import save_cover_data_to, add_borders_to_image, image_to from calibre.utils.localization import canonicalize_lang from calibre.utils.logging import ThreadSafeWrapper + class LoginFailed(ValueError): pass + class DownloadDenied(ValueError): pass + class BasicNewsRecipe(Recipe): ''' Base class that contains logic needed in all recipes. By overriding @@ -1706,6 +1709,7 @@ class BasicNewsRecipe(Recipe): log.debug('Resolved internal URL: %s -> %s' % (url, arelpath)) seen.add(url) + class CustomIndexRecipe(BasicNewsRecipe): def custom_index(self): @@ -1738,10 +1742,12 @@ class CustomIndexRecipe(BasicNewsRecipe): self.create_opf() return res + class AutomaticNewsRecipe(BasicNewsRecipe): auto_cleanup = True + class CalibrePeriodical(BasicNewsRecipe): #: Set this to the slug for the calibre periodical diff --git a/src/calibre/web/feeds/recipes/__init__.py b/src/calibre/web/feeds/recipes/__init__.py index 1f782db213..f02d1a0fee 100644 --- a/src/calibre/web/feeds/recipes/__init__.py +++ b/src/calibre/web/feeds/recipes/__init__.py @@ -15,11 +15,13 @@ basic_recipes = (BasicNewsRecipe, AutomaticNewsRecipe, CustomIndexRecipe, custom_recipes = JSONConfig('custom_recipes/index.json') + def custom_recipe_filename(id_, title): from calibre.utils.filenames import ascii_filename return ascii_filename(title[:50]) + \ ('_%s.recipe'%id_) + def compile_recipe(src): ''' Compile the code in src and return a recipe object, if found. diff --git a/src/calibre/web/feeds/recipes/collection.py b/src/calibre/web/feeds/recipes/collection.py index ec0a8ebf79..43568838e8 100644 --- a/src/calibre/web/feeds/recipes/collection.py +++ b/src/calibre/web/feeds/recipes/collection.py @@ -21,6 +21,7 @@ from calibre.utils.recycle_bin import delete_file NS = 'http://calibre-ebook.com/recipe_collection' E = ElementMaker(namespace=NS, nsmap={None:NS}) + def iterate_over_builtin_recipe_files(): exclude = ['craigslist', 'toronto_sun'] d = os.path.dirname @@ -58,6 +59,7 @@ def serialize_recipe(urn, recipe_class): 'description' : attr('description', '') }) + def serialize_collection(mapping_of_recipe_classes): collection = E.recipe_collection() '''for u, x in mapping_of_recipe_classes.items(): @@ -80,6 +82,7 @@ def serialize_collection(mapping_of_recipe_classes): return etree.tostring(collection, encoding='utf-8', xml_declaration=True, pretty_print=True) + def serialize_builtin_recipes(): from calibre.web.feeds.recipes import compile_recipe recipe_mapping = {} @@ -95,9 +98,11 @@ def serialize_builtin_recipes(): return serialize_collection(recipe_mapping) + def get_builtin_recipe_collection(): return etree.parse(P('builtin_recipes.xml', allow_user_override=False)).getroot() + def get_custom_recipe_collection(*args): from calibre.web.feeds.recipes import compile_recipe, \ custom_recipes @@ -122,6 +127,7 @@ def get_custom_recipe_collection(*args): def update_custom_recipe(id_, title, script): update_custom_recipes([(id_, title, script)]) + def update_custom_recipes(script_ids): from calibre.web.feeds.recipes import custom_recipes, \ custom_recipe_filename @@ -151,6 +157,7 @@ def update_custom_recipes(script_ids): def add_custom_recipe(title, script): add_custom_recipes({title:script}) + def add_custom_recipes(script_map): from calibre.web.feeds.recipes import custom_recipes, \ custom_recipe_filename @@ -190,6 +197,7 @@ def remove_custom_recipe(id_): except: pass + def get_custom_recipe(id_): from calibre.web.feeds.recipes import custom_recipes id_ = str(int(id_)) @@ -200,9 +208,11 @@ def get_custom_recipe(id_): with open(os.path.join(bdir, fname), 'rb') as f: return f.read().decode('utf-8') + def get_builtin_recipe_titles(): return [r.get('title') for r in get_builtin_recipe_collection()] + def download_builtin_recipe(urn): from calibre.utils.config_base import prefs from calibre.utils.https import get_https_resource_securely @@ -210,10 +220,12 @@ def download_builtin_recipe(urn): return bz2.decompress(get_https_resource_securely( 'https://code.calibre-ebook.com/recipe-compressed/'+urn, headers={'CALIBRE-INSTALL-UUID':prefs['installation_uuid']})) + def get_builtin_recipe(urn): with zipfile.ZipFile(P('builtin_recipes.zip', allow_user_override=False), 'r') as zf: return zf.read(urn+'.recipe') + def get_builtin_recipe_by_title(title, log=None, download_recipe=False): for x in get_builtin_recipe_collection(): if x.get('title') == title: @@ -232,6 +244,7 @@ def get_builtin_recipe_by_title(title, log=None, download_recipe=False): 'Failed to download recipe, using builtin version') return get_builtin_recipe(urn) + def get_builtin_recipe_by_id(id_, log=None, download_recipe=False): for x in get_builtin_recipe_collection(): if x.get('id') == id_: @@ -250,6 +263,7 @@ def get_builtin_recipe_by_id(id_, log=None, download_recipe=False): 'Failed to download recipe, using builtin version') return get_builtin_recipe(urn) + class SchedulerConfig(object): def __init__(self): diff --git a/src/calibre/web/feeds/recipes/model.py b/src/calibre/web/feeds/recipes/model.py index 22bcc878b2..06fa5f5606 100644 --- a/src/calibre/web/feeds/recipes/model.py +++ b/src/calibre/web/feeds/recipes/model.py @@ -20,6 +20,7 @@ from calibre.web.feeds.recipes.collection import \ remove_custom_recipe, get_custom_recipe, get_builtin_recipe from calibre.utils.search_query_parser import ParseException + class NewsTreeItem(object): def __init__(self, builtin, custom, scheduler_config, parent=None): @@ -56,6 +57,7 @@ class NewsTreeItem(object): self.children.remove(child) child.parent = None + class NewsCategory(NewsTreeItem): def __init__(self, category, builtin, custom, scheduler_config, parent): @@ -126,11 +128,13 @@ class NewsItem(NewsTreeItem): def __cmp__(self, other): return cmp(self.title.lower(), getattr(other, 'title', '').lower()) + class AdaptSQP(SearchQueryParser): def __init__(self, *args, **kwargs): pass + class RecipeModel(QAbstractItemModel, AdaptSQP): LOCATIONS = ['all'] diff --git a/src/calibre/web/feeds/templates.py b/src/calibre/web/feeds/templates.py index 9d7479520d..fb9ddb078b 100644 --- a/src/calibre/web/feeds/templates.py +++ b/src/calibre/web/feeds/templates.py @@ -13,11 +13,14 @@ from lxml.html.builder import HTML, HEAD, TITLE, STYLE, DIV, BODY, \ from calibre import preferred_encoding, strftime, isbytestring + def CLASS(*args, **kwargs): # class is a reserved word in Python kwargs['class'] = ' '.join(args) return kwargs # Regular templates + + class Template(object): IS_HTML = True @@ -51,6 +54,7 @@ class Template(object): return etree.tostring(self.root, encoding='utf-8', xml_declaration=True, pretty_print=True) + class EmbeddedContent(Template): def _generate(self, article, style=None, extra_css=None): @@ -79,6 +83,7 @@ class EmbeddedContent(Template): elem = SPAN(elem) div.append(elem) + class IndexTemplate(Template): def _generate(self, title, masthead, datefmt, feeds, extra_css=None, style=None): @@ -106,6 +111,7 @@ class IndexTemplate(Template): if self.html_lang: self.root.set('lang', self.html_lang) + class FeedTemplate(Template): def get_navbar(self, f, feeds, top=True): diff --git a/src/calibre/web/fetch/simple.py b/src/calibre/web/fetch/simple.py index 682c0f2739..44f9fe1c24 100644 --- a/src/calibre/web/fetch/simple.py +++ b/src/calibre/web/fetch/simple.py @@ -23,12 +23,15 @@ from calibre.utils.img import image_from_data, image_to_data from calibre.utils.imghdr import what from calibre.web.fetch.utils import rescale_image + class AbortArticle(Exception): pass + class FetchError(Exception): pass + class closing(object): 'Context to automatically close something at the end of a block.' @@ -47,6 +50,8 @@ class closing(object): bad_url_counter = 0 + + def basename(url): try: parts = urlparse.urlsplit(url) @@ -60,6 +65,7 @@ def basename(url): return 'index.html' return res + def save_soup(soup, target): ns = BeautifulSoup('') nm = ns.find('meta') @@ -86,6 +92,7 @@ def save_soup(soup, target): with open(target, 'wb') as f: f.write(html.encode('utf-8')) + class response(str): def __new__(cls, *args): @@ -93,9 +100,11 @@ class response(str): obj.newurl = None return obj + def default_is_link_wanted(url, tag): raise NotImplementedError() + class RecursiveFetcher(object): LINK_FILTER = tuple(re.compile(i, re.IGNORECASE) for i in ('.exe\s*$', '.mp3\s*$', '.ogg\s*$', '^\s*mailto:', '^\s*$')) @@ -552,6 +561,7 @@ class RecursiveFetcher(object): print return res + def option_parser(usage=_('%prog URL\n\nWhere URL is for example http://google.com')): parser = OptionParser(usage=usage) parser.add_option('-d', '--base-dir', @@ -592,6 +602,7 @@ def create_fetcher(options, image_map={}, log=None): log = Log(level=Log.DEBUG) if options.verbose else Log() return RecursiveFetcher(options, log, image_map={}) + def main(args=sys.argv): parser = option_parser() options, args = parser.parse_args(args) diff --git a/src/calibre/web/fetch/utils.py b/src/calibre/web/fetch/utils.py index 5563049568..e57a0882e8 100644 --- a/src/calibre/web/fetch/utils.py +++ b/src/calibre/web/fetch/utils.py @@ -6,6 +6,7 @@ from __future__ import (unicode_literals, division, absolute_import, print_function) from calibre.utils.img import image_from_data, scale_image, image_to_data, blend_on_canvas + def rescale_image(data, scale_news_images, compress_news_images_max_size, compress_news_images_auto_size): orig_data = data # save it in case compression fails img = image_from_data(data) @@ -38,6 +39,7 @@ def rescale_image(data, scale_news_images, compress_news_images_max_size, compre return data + def prepare_masthead_image(path_to_image, out_path, mi_width, mi_height): with lopen(path_to_image, 'rb') as f: img = image_from_data(f.read())