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('' % (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_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