Skip to content

Commit

Permalink
Add a plugin to support epub export of recipes.
Browse files Browse the repository at this point in the history
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 thinkle#57
  • Loading branch information
Sven Steckmann committed Aug 15, 2015
1 parent 3e2d6d6 commit e53a2bb
Show file tree
Hide file tree
Showing 5 changed files with 355 additions and 0 deletions.
15 changes: 15 additions & 0 deletions data/style/epubdefault.css
Original file line number Diff line number Diff line change
@@ -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}
8 changes: 8 additions & 0 deletions gourmet/plugins/import_export/epub.gourmet-plugin
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>
3 changes: 3 additions & 0 deletions gourmet/plugins/import_export/epub_plugin/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import epub_exporter_plugin

plugins = [epub_exporter_plugin.EpubExporterPlugin]
302 changes: 302 additions & 0 deletions gourmet/plugins/import_export/epub_plugin/epub_exporter.py
Original file line number Diff line number Diff line change
@@ -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('''<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>$title</title>
</head>
<body>
''')
RECIPE_FOOT= "</body></html>"

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="<p>%s</p>"%t
t=re.sub('\n\n+','</p><p>',t)
t=re.sub('\n','<br>',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("<h1>%s</h1>"
% xml.sax.saxutils.escape(self.get_title()))

def write_image (self, image):
imagePath = self.doc.addJpegImage( image)
self.preparedDocument.append('<img src="%s" itemprop="image"/>' % imagePath)

def write_inghead (self):
self.preparedDocument.append('<div class="ing"><h2>%s</h2><ul class="ing">'%_('Ingredients'))

def write_text (self, label, text):
attr = gglobals.NAME_TO_ATTR.get(label,label)
if attr == 'instructions':
self.preparedDocument.append('<div class="%s"><h2 class="%s">%s</h2><div itemprop="recipeInstructions">%s</div></div>' % (attr,label,label,self.htmlify(text)))
else:
self.preparedDocument.append('<div class="%s"><h2 class="%s">%s</h2>%s</div>' % (attr,label,label,self.htmlify(text)))

def handle_italic (self, chunk): return "<em>" + chunk + "</em>"
def handle_bold (self, chunk): return "<strong>" + chunk + "</strong>"
def handle_underline (self, chunk): return "<u>" + chunk + "</u>"

def write_attr_head (self):
self.preparedDocument.append("<div class='header'>")

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('<a href="%s">'%text +
_('Original Page from %s')%webpage +
'</a>\n')
elif attr == 'rating':
rating, rest = text.split('/', 1)
self.preparedDocument.append('<p class="%s" itemprop="aggregateRating" itemscope itemtype="http://schema.org/AggregateRating"><span class="label">%s:</span> <span itemprop="ratingValue">%s</span><span>/%s</span></p>\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('<p class="%s"><span class="label">%s:</span> <span itemprop="%s">%s</span></p>\n' % (attr, label.capitalize(), itemprop, xml.sax.saxutils.escape(text)))
else:
self.preparedDocument.append("<p class='%s'><span class='label'>%s:</span> %s</p>\n"%(attr, label.capitalize(), xml.sax.saxutils.escape(text)))

def write_attr_foot (self):
self.preparedDocument.append("</div>")

def write_grouphead (self, name):
self.preparedDocument.append("<h3 class='inggroup'>%s<h3/>"%name)

def write_groupfoot (self):
pass

def _write_ing_impl(self, amount, unit, item, link, optional):
self.preparedDocument.append('<li class="ing" itemprop="ingredients">')

# 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('<div class="ingamount">%s</div>' % (amount if len(amount) != 0 else "&nbsp;"))
self.preparedDocument.append('<div class="ingunit">%s</div>' % (unit if len(unit) != 0 else "&nbsp;"))

if item:
if link:
self.preparedDocument.append( "<a href='%s'>%s</a>"% (link, item ))
else:
self.preparedDocument.append(item)

if optional:
self.preparedDocument.append("(%s)"%_('optional'))
self.preparedDocument.append("</li>\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('</ul>\n</div>\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()

27 changes: 27 additions & 0 deletions gourmet/plugins/import_export/epub_plugin/epub_exporter_plugin.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit e53a2bb

Please sign in to comment.