From a3cacb0eb1f0433604b761febb232500691735f6 Mon Sep 17 00:00:00 2001 From: Sven Steckmann Date: Sat, 15 Aug 2015 16:50:57 +0200 Subject: [PATCH 1/2] Add a possibility to tell ExporterMultirec not to open any files. This is necessary to use external libraries which process the files by its own and finally write the files to a given location. This makes the process of implementing such a plugin more easy. This also fixes a possible issue if self.out is not a string, then the program may crash because closing a string does not work. --- gourmet/exporters/exporter.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/gourmet/exporters/exporter.py b/gourmet/exporters/exporter.py index 1131eb163..9e8cf6e35 100644 --- a/gourmet/exporters/exporter.py +++ b/gourmet/exporters/exporter.py @@ -464,7 +464,7 @@ class ExporterMultirec (SuspendableThread, Pluggable): name = 'Exporter' - def __init__ (self, rd, recipes, out, one_file=True, + def __init__ (self, rd, recipes, out, one_file=True, open_files = True, ext='txt', conv=None, imgcount=1, @@ -474,7 +474,16 @@ def __init__ (self, rd, recipes, out, one_file=True, """Output all recipes in recipes into a document or multiple documents. if one_file, then everything is in one file. Otherwise, we treat 'out' as a directory and put - individual recipe files within it.""" + individual recipe files within it. + + @param one_file If True works in one_file mode. For this the class will + create the file which is accessible by self.ofi + If this is set to False a directory will be created and the + name of the directory will be passed via self.outdir and self.ofi + + @param open_files If this parameter is True the files will be created + otherwise to create the file is up to the user. + """ self.timer=TimeAction('exporterMultirec.__init__()') self.rd = rd self.recipes = recipes @@ -490,6 +499,7 @@ def __init__ (self, rd, recipes, out, one_file=True, convert.FRACTIONS_ASCII) self.DEFAULT_ENCODING = self.exporter.DEFAULT_ENCODING self.one_file = one_file + self.open_files = open_files def _grab_attr_ (self, obj, attr): if attr=='category': @@ -540,7 +550,11 @@ def do_run (self): self.outdir=self.unique_name(self.outdir) os.makedirs(self.outdir) else: os.makedirs(self.outdir) - if self.one_file and type(self.out)==str: + + oneFileOpenByMyself = self.one_file and type(self.out)==str and self.open_files + multiFileOpenByMyself = not self.one_file and self.open_files + + if oneFileOpenByMyself: self.ofi=open(self.out,'wb') else: self.ofi = self.out self.write_header() @@ -553,7 +567,7 @@ def do_run (self): msg = _("Exported %(number)s of %(total)s recipes")%{'number':self.rcount,'total':self.rlen} self.emit('progress',float(self.rcount)/float(self.rlen), msg) fn=None - if not self.one_file: + if multiFileOpenByMyself: fn=self.generate_filename(r,self.ext,add_id=True) self.ofi=open(fn,'wb') if self.padding and not first: @@ -562,12 +576,12 @@ def do_run (self): self.connect_subthread(e) e.do_run() self.recipe_hook(r,fn,e) - if not self.one_file: + if multiFileOpenByMyself: self.ofi.close() self.rcount += 1 first = False self.write_footer() - if self.one_file: + if oneFileOpenByMyself: self.ofi.close() self.timer.end() self.emit('progress',1,_("Export complete.")) From 76b8214d5594f7f8dfa3b9e275afc377fd880c62 Mon Sep 17 00:00:00 2001 From: Sven Steckmann Date: Sat, 15 Aug 2015 17:04:11 +0200 Subject: [PATCH 2/2] Add a plugin to support epub export of recipes. Now I can read the recipes on my tablet :) The plugin uses the ebooklib available at pip which makes creation of a epub very easy. In the futuere a mobie export is planned within this library too. For the moment this is a basic implementation, in the future we should implement some more things for e.g. to set the language or a book title, cover page, ordering, categories, etc. but this is the first step. refs #57 --- data/style/epubdefault.css | 15 + .../plugins/import_export/epub.gourmet-plugin | 8 + .../import_export/epub_plugin/__init__.py | 3 + .../epub_plugin/epub_exporter.py | 302 ++++++++++++++++++ .../epub_plugin/epub_exporter_plugin.py | 27 ++ 5 files changed, 355 insertions(+) create mode 100644 data/style/epubdefault.css create mode 100644 gourmet/plugins/import_export/epub.gourmet-plugin create mode 100644 gourmet/plugins/import_export/epub_plugin/__init__.py create mode 100644 gourmet/plugins/import_export/epub_plugin/epub_exporter.py create mode 100644 gourmet/plugins/import_export/epub_plugin/epub_exporter_plugin.py diff --git a/data/style/epubdefault.css b/data/style/epubdefault.css new file mode 100644 index 000000000..178a7edc3 --- /dev/null +++ b/data/style/epubdefault.css @@ -0,0 +1,15 @@ +div.recipe, div.index { font-style: Times, Serif; font-size: 12pt; margin-left: 7%; margin-right: 10%; padding: 1em; margin-top: 1em;} +div.recipe img {float: right; padding: 1em} +body {} +span.label { font-weight: bold;min-width: 10em; display: inline-block;} +p.title { font-size: 120%; text-align: center} +p.title span.label {display: none} +div.header p {margin-top: 0; margin-bottom: 0.2em} +div.ing { padding: 1em; border: solid 1px; background-color: #eef} +ul.ing { padding-left: 0.6em; } +li.ing { list-style: none; border-top: 0.3em } +div.ingamount{ display:inline-block; min-width:3em;} +div.ingunit{ display:inline-block; min-width:3em;} +img{ max-width: 90%; display: block; margin-left: auto; margin-right: auto; } +div.header{margin-top: 1em; margin-bottom: 1em;} +div.ing h2{margin-top: 0} diff --git a/gourmet/plugins/import_export/epub.gourmet-plugin b/gourmet/plugins/import_export/epub.gourmet-plugin new file mode 100644 index 000000000..0976066ce --- /dev/null +++ b/gourmet/plugins/import_export/epub.gourmet-plugin @@ -0,0 +1,8 @@ +[Gourmet Plugin] +Module=epub_plugin +Version=1.0 +API_Version=1.0 +_Name=EPub Export +_Comment=Create an epub book from recipes. +_Category=Importer/Exporter +Authors=Sven Steckmann diff --git a/gourmet/plugins/import_export/epub_plugin/__init__.py b/gourmet/plugins/import_export/epub_plugin/__init__.py new file mode 100644 index 000000000..e8fac24c0 --- /dev/null +++ b/gourmet/plugins/import_export/epub_plugin/__init__.py @@ -0,0 +1,3 @@ +import epub_exporter_plugin + +plugins = [epub_exporter_plugin.EpubExporterPlugin] diff --git a/gourmet/plugins/import_export/epub_plugin/epub_exporter.py b/gourmet/plugins/import_export/epub_plugin/epub_exporter.py new file mode 100644 index 000000000..4888fd3a4 --- /dev/null +++ b/gourmet/plugins/import_export/epub_plugin/epub_exporter.py @@ -0,0 +1,302 @@ +import os, re +import xml.sax.saxutils +from gettext import gettext as _ +from gourmet import convert,gglobals +from gourmet.exporters.exporter import ExporterMultirec, exporter_mult + +from ebooklib import epub +from string import Template + +RECIPE_HEADER = Template(''' + + + + $title + + +''') +RECIPE_FOOT= "" + +EPUB_DEFAULT_CSS="epubdefault.css" + + +class EpubWriter(): + """This class contains all things to write an epub and is a small wrapper + around the EbookLib which is capable of producing epub files and maybe + kindle in the future (it is under heavy development). + """ + def __init__(self, outFileName): + """ + @param outFileName The filename + path the ebook is written to on finish. + """ + self.outFileName = outFileName + self.lang = "en" + self.recipeCss = None + + self.imgCount = 0 + self.recipeCount = 0 + + self.ebook = epub.EpubBook() + self.spine = ['nav'] + self.toc = [] + + # set metadata + self.ebook.set_identifier("Cookme") # TODO: Something meaningful or time? + self.ebook.set_title("My Cookbook") + self.ebook.set_language(self.lang) + + # This is normally the publisher, makes less sense for the moment + # ebook.add_metadata('DC', 'publisher', "Gourmet") + + # TODO: Add real author from somewhere + self.ebook.add_author("Gourmet") + + # This adds the field also known as keywords in some programs. + self.ebook.add_metadata('DC', 'subject', "cooking") + + def addRecipeCssFromFile(self, filename): + """ Adds the CSS file from filename to the book. The style will be added + to the books root. + @param filename The file the css is read from and attached + @return The internal name within the ebook to reference this file. + """ + cssFileName = "Style/recipe.css" + + style = open(filename, 'rb').read() + recipe_css = epub.EpubItem( uid="style" + , file_name=cssFileName + , media_type="text/css" + , content=style) + self.ebook.add_item(recipe_css) + self.recipeCss = recipe_css + return cssFileName; + + def addJpegImage(self, imageData): + """Adds a jpeg image from the imageData array to the book and returns + the reference name for the image to be used in html. + @param imageData Image data in format jpeg + @return The name of the image to be used in html + """ + epimg = epub.EpubImage() + epimg.file_name = "grf/image_%i.jpg" % self.imgCount + self.imgCount += 1 + epimg.media_type = "image/jpeg" + epimg.set_content(imageData) + self.ebook.add_item(epimg) + return epimg.file_name; + + def getFileForRecipeID(self, id, ext=".xhtml"): + """ + Returns a filename to reference a specific recipe + @param id The id which is also passed during addRecipeText + @return A filename for reference + """ + return "recipe_%i%s" % (id,ext) + + def addRecipeText(self, uniqueId, title, text): + """ Adds the recipe text as a chapter. + """ + uniqueName = self.getFileForRecipeID(uniqueId, ext="") + fileName = self.getFileForRecipeID(uniqueId) + self.recipeCount += 1 + + c1 = epub.EpubHtml(title=title, file_name=fileName, lang=self.lang) + c1.content = text.encode('utf-8') + c1.add_item( self.recipeCss ) + + # add chapter + self.ebook.add_item(c1) + self.spine.append(c1) + + # define Table Of Contents + self.toc.append( epub.Link(fileName, title, uniqueName) ) + + def finish(self): + """ Finish the book and writes it to the disk. + """ + self.ebook.toc = self.toc + + # add default NCX and Nav file + self.ebook.add_item(epub.EpubNcx()) + self.ebook.add_item(epub.EpubNav()) + + self.ebook.spine = self.spine + + epub.write_epub(self.outFileName, self.ebook, {}) + +class epub_exporter (exporter_mult): + def __init__ (self, rd, r, out, conv=None, + doc=None, + # exporter_mult args + mult=1, + change_units=True, + ): + self.doc = doc + + # This document will be appended by the strings to join them in the + # last step and pass it to the ebookwriter. + self.preparedDocument = [] + + #self.link_generator=link_generator + exporter_mult.__init__(self, rd, r, out, + conv=conv, + imgcount=1, + mult=mult, + change_units=change_units, + do_markup=True, + use_ml=True) + + def htmlify (self, text): + t=text.strip() + #t=xml.sax.saxutils.escape(t) + t="

%s

"%t + t=re.sub('\n\n+','

',t) + t=re.sub('\n','
',t) + return t + + def get_title(self): + """Returns the title of the book in an unescaped format""" + title = self._grab_attr_(self.r,'title') + if not title: title = _('Recipe') + return unicode(title) + + def write_head (self): + self.preparedDocument.append( + RECIPE_HEADER.substitute(title=self.get_title()) ) + self.preparedDocument.append("

%s

" + % xml.sax.saxutils.escape(self.get_title())) + + def write_image (self, image): + imagePath = self.doc.addJpegImage( image) + self.preparedDocument.append('' % imagePath) + + def write_inghead (self): + self.preparedDocument.append('

%s

    '%_('Ingredients')) + + def write_text (self, label, text): + attr = gglobals.NAME_TO_ATTR.get(label,label) + if attr == 'instructions': + self.preparedDocument.append('

    %s

    %s
    ' % (attr,label,label,self.htmlify(text))) + else: + self.preparedDocument.append('

    %s

    %s
    ' % (attr,label,label,self.htmlify(text))) + + def handle_italic (self, chunk): return "" + chunk + "" + def handle_bold (self, chunk): return "" + chunk + "" + def handle_underline (self, chunk): return "" + chunk + "" + + def write_attr_head (self): + self.preparedDocument.append("
    ") + + def write_attr (self, label, text): + attr = gglobals.NAME_TO_ATTR.get(label,label) + if attr=='link': + webpage = text.strip('http://') + webpage = webpage.split('/')[0] + self.preparedDocument.append(''%text + + _('Original Page from %s')%webpage + + '\n') + elif attr == 'rating': + rating, rest = text.split('/', 1) + self.preparedDocument.append('

    %s: %s/%s

    \n' % (attr, label.capitalize(), rating, rest)) + else: + itemprop = None + if attr == 'title': + itemprop = 'name' + return # Title is already printed + elif attr == 'category': + itemprop = 'recipeCategory' + elif attr == 'cuisine': + itemprop = 'recipeCuisine' + elif attr == 'yields': + itemprop = 'recipeYield' + elif attr == 'preptime': + itemprop = 'prepTime' + elif attr == 'cooktime': + itemprop = 'cookTime' + elif attr == 'instructions': + itemprop = 'recipeInstructions' + if itemprop: + self.preparedDocument.append('

    %s: %s

    \n' % (attr, label.capitalize(), itemprop, xml.sax.saxutils.escape(text))) + else: + self.preparedDocument.append("

    %s: %s

    \n"%(attr, label.capitalize(), xml.sax.saxutils.escape(text))) + + def write_attr_foot (self): + self.preparedDocument.append("
    ") + + def write_grouphead (self, name): + self.preparedDocument.append("

    %s

    "%name) + + def write_groupfoot (self): + pass + + def _write_ing_impl(self, amount, unit, item, link, optional): + self.preparedDocument.append('
  • ') + + # Escape all incoming things first. + (amount, unit, item) = tuple([xml.sax.saxutils.escape("%s"%o) if o else "" for o in [amount, unit, item]]) + + self.preparedDocument.append('
    %s
    ' % (amount if len(amount) != 0 else " ")) + self.preparedDocument.append('
    %s
    ' % (unit if len(unit) != 0 else " ")) + + if item: + if link: + self.preparedDocument.append( "%s"% (link, item )) + else: + self.preparedDocument.append(item) + + if optional: + self.preparedDocument.append("(%s)"%_('optional')) + self.preparedDocument.append("
  • \n") + + def write_ingref (self, amount, unit, item, refid, optional): + refFile = self.doc.getFileForRecipeID(refid) + self._write_ing_impl(amount, unit, item, refFile, optional) + + def write_ing (self, amount=1, unit=None, + item=None, key=None, optional=False): + self._write_ing_impl(amount, unit, item, None, optional) + + def write_ingfoot (self): + self.preparedDocument.append('

\n
\n') + + def write_foot (self): + self.preparedDocument.append(RECIPE_FOOT) + + self._grab_attr_(self.r,'id') + self.doc.addRecipeText(self._grab_attr_(self.r,'id'), self.get_title(), "".join(self.preparedDocument) ) + +class website_exporter (ExporterMultirec): + def __init__ (self, rd, recipe_table, out, conv=None, ext='epub', copy_css=True, + css=os.path.join(gglobals.style_dir,EPUB_DEFAULT_CSS), + index_rows=['title','category','cuisine','rating','yields'], + change_units=False, + mult=1): + + self.doc = EpubWriter(out) + self.doc.addRecipeCssFromFile(css) + + self.ext=ext + + self.index_rows=index_rows + self.exportargs={ 'change_units':change_units, + 'mult':mult, + 'doc':self.doc} + + if conv: + self.exportargs['conv']=conv + ExporterMultirec.__init__(self, rd, recipe_table, out, + one_file=True, + open_files=False, + ext=self.ext, + exporter=epub_exporter, + exporter_kwargs=self.exportargs) + + def recipe_hook (self, rec, filename, exporter): + """Add index entry""" + # TODO: Do some cool things here. + pass + + def write_footer (self): + self.doc.finish() + diff --git a/gourmet/plugins/import_export/epub_plugin/epub_exporter_plugin.py b/gourmet/plugins/import_export/epub_plugin/epub_exporter_plugin.py new file mode 100644 index 000000000..6fbae03f2 --- /dev/null +++ b/gourmet/plugins/import_export/epub_plugin/epub_exporter_plugin.py @@ -0,0 +1,27 @@ +from gourmet.plugin import ExporterPlugin +import epub_exporter +from gettext import gettext as _ + +EPUBFILE = _('Epub File') + +class EpubExporterPlugin (ExporterPlugin): + label = _('Exporting epub') + sublabel = _('Exporting recipes an epub file in directory %(file)s') + single_completed_string = _('Recipe saved as epub file %(file)s') + filetype_desc = EPUBFILE + saveas_filters = [EPUBFILE,['application/epub+zip'],['*.epub']] + saveas_single_filters = [EPUBFILE,['application/epub+zip'],['*.epub']] + + def get_multiple_exporter (self, args): + return epub_exporter.website_exporter( + args['rd'], + args['rv'], + args['file'], + #args['conv'], + ) + + def do_single_export (self, args) : + print "Single export not supported!" + + def run_extra_prefs_dialog (self): + pass