title_body: How can I automate the creation of EPUB files from series of existing JPEG photos?
upvoted_answer: Part of the program I wrote for scanning and EPUB-ing scanned images can be reused for this purpose. I tried to make a minimal version, the output was tested with calibre book reader and a BookEen reading device and passed
  EpubCheck version 3.0.1 without errors.
  
  The program requires you to provide a Title (--title) and one or more JPEG files, which are taken as is (not scaled or rotated). Some extra options allow for specifying an output file name (otherwise constructed from the title), author/creator and series/series-index (as used by Calibre):
  
  jpeg2epub --title "My Photo Album" img*.jpg
  
  
  will create a file My_Photo_Album.epub in the current directory.
  
  The program works with Python 2.7 and standard libraries, but will not work without modification on Python 3.X. All processing is done in memory, the computers memory limits the size of your EPUB, but no intermediate files are written.
  
  
  
  #!/usr/bin/env python
  # coding: utf-8
  # jpeg2epub: copyright (C) 2013, RUAMEL bvba, A. van der Neut
  
  import os
  import sys
  from io import open
  from textwrap import dedent
  from cStringIO import StringIO
  import zipfile
  import uuid
  import datetime
  
  
  class Jpeg2Epub(object):
      """simple epub creator for series of JPEG image files
  
      creates the file epub file in memory
      """
      version = 1  # class version when used a s library
  
      def __init__(self, title, file_name=None, creator=None, title_sort=None,
                   series=None, series_idx=None, verbose=0):
          self._output_name = file_name if file_name else \
              title.replace(' ', '_') + '.epub'
          self._files = None
          self._zip = None  # the in memory zip file
          self._zip_data = None
          self._content = []
          self._count = 0
          self._series = series
          self._series_idx = series_idx
          self.d = dict(
              title=title,
              title_sort=title_sort if title_sort else title,
              creator=creator if creator else 'Unknown',
              opf_name="c.opf",
              toc_name="toc.ncx",
              ncx_ns='http://www.daisy.org/z3986/2005/ncx/',
              opf_ns='http://www.idpf.org/2007/opf',
              xsi_ns='http://www.w3.org/2001/XMLSchema-instance',
              dcterms_ns='http://purl.org/dc/terms/',
              dc_ns='http://purl.org/dc/elements/1.1/',
              cal_ns='http://calibre.kovidgoyal.net/2009/metadata',
              cont_urn='urn:oasis:names:tc:opendocument:xmlns:container',
              mt='application/oebps-package+xml',  # media-type
              style_sheet='stylesheet.css',
              uuid=None,
              nav_point=None,
              nav_uuid=None,
          )
  
      def __enter__(self):
          return self
  
      def __exit__(self, typ, value, traceback):
          if value is None:
              if isinstance(self._zip_data, basestring):
                  return
              self._write_toc()
              self._write_content()
              self._zip.close()
              self._zip = None
              self.d['nav_point'] = None
              with open(self._output_name, 'wb') as ofp:
                  ofp.write(self._zip_data.getvalue())
              # minimal test: listing contents of EPUB
              # os.system('unzip -lv ' + self._output_name)
              return True
          return False
  
      def add_image_file(self, file_name):
          self._add_image_file(file_name)
          self._count += 1
  
      def _write_toc(self):
          self._add_from_bytes(self.d['toc_name'], dedent("""\
          &lt;?xml version='1.0' encoding='utf-8'?&gt;
          &lt;ncx xmlns="{ncx_ns}" version="2005-1" xml:lang="eng"&gt;
            &lt;head&gt;
              &lt;meta content="{uuid}" name="dtb:uid"/&gt;
              &lt;meta content="2" name="dtb:depth"/&gt;
              &lt;meta content="ruamel.jpeg2epub (0.1)" name="dtb:generator"/&gt;
              &lt;meta content="0" name="dtb:totalPageCount"/&gt;
              &lt;meta content="0" name="dtb:maxPageNumber"/&gt;
            &lt;/head&gt;
            &lt;docTitle&gt;
              &lt;text&gt;xx&lt;/text&gt;
            &lt;/docTitle&gt;
            &lt;navMap&gt;
              &lt;navPoint id="{nav_uuid}" playOrder="1"&gt;
                &lt;navLabel&gt;
                  &lt;text&gt;Start&lt;/text&gt;
                &lt;/navLabel&gt;
                &lt;content src="{nav_point}"/&gt;
              &lt;/navPoint&gt;
            &lt;/navMap&gt;
          &lt;/ncx&gt;
          """).format(**self.d))
          self._content.append((self.d['toc_name'], 'ncx',
                                'application/x-dtbncx+xml'))
  
      def _write_content(self):
          d = self.d.copy()
          manifest = []
          spine = []
          d['manifest'] = ''
          d['spine'] = ''
          for f in self._content:
              manifest.append(
                  '&lt;item href="{}" id="{}" media-type="{}"/&gt;'.format(*f))
              if f[1].startswith('html'):
                  spine.append('&lt;itemref idref="{}"/&gt;'.format(f[1]))
          d['manifest'] = '\n    '.join(manifest)
          d['spine'] = '\n    '.join(spine)
          d['ts'] = datetime.datetime.utcnow().isoformat() + '+00:00'
          d['series'] = ''
          if self._series:
              d['series'] = \
                  u'\n' \
                  '&lt;meta name="calibre:series" content="{}"/&gt;' \
                  '&lt;meta name="calibre:series_index" content="{}"/&gt;'.format(
                      self._series, self._series_idx)
          self._add_from_bytes(self.d["opf_name"], dedent(u"""\
          &lt;?xml version='1.0' encoding='utf-8'?&gt;
          &lt;package xmlns="{opf_ns}" unique-identifier="uuid_id" version="2.0"&gt;
            &lt;metadata xmlns:xsi="{xsi_ns}" xmlns:opf="{opf_ns}"
                      xmlns:dcterms="{dcterms_ns}"
                      xmlns:calibre="{cal_ns}"
                      xmlns:dc="{dc_ns}"&gt;
              &lt;dc:language&gt;en&lt;/dc:language&gt;
              &lt;dc:creator&gt;{creator}&lt;/dc:creator&gt;
              &lt;meta name="calibre:timestamp" content="{ts}"/&gt;
              &lt;meta name="calibre:title_sort" content="{title_sort}"/&gt;
              &lt;meta name="cover" content="cover"/&gt;
              &lt;dc:date&gt;0101-01-01T00:00:00+00:00&lt;/dc:date&gt;
              &lt;dc:title&gt;{title}&lt;/dc:title&gt;{series}
              &lt;dc:identifier id="uuid_id" opf:scheme="uuid"&gt;{uuid}
              &lt;/dc:identifier&gt;
              &lt;dc:identifier opf:scheme="calibre"&gt;{uuid}&lt;/dc:identifier&gt;
            &lt;/metadata&gt;
            &lt;manifest&gt;
              {manifest}
            &lt;/manifest&gt;
            &lt;spine toc="ncx"&gt;
              {spine}
            &lt;/spine&gt;
          &lt;/package&gt;
          """).format(**d).encode('utf-8'))
  
      def _add_html(self, title):
          file_name = self._name(False)
          d = self.d.copy()
          d['title'] = title
          d['img_name'] = self._name()
          self._add_from_bytes(file_name, dedent(u"""\
          &lt;?xml version='1.0' encoding='utf-8'?&gt;
          &lt;html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en"&gt;
            &lt;head&gt;
              &lt;title&gt;{title}&lt;/title&gt;
              &lt;meta http-equiv="Content-Type" content="text/html; \
          charset=utf-8"/&gt;
            &lt;link href="{style_sheet}" rel="stylesheet" type="text/css"/&gt;
            &lt;/head&gt;
            &lt;body class="album"&gt;
              &lt;div&gt;
                &lt;img src="{img_name}" class="albumimg" alt="{title}"/&gt;
              &lt;/div&gt;
            &lt;/body&gt;
          &lt;/html&gt;
          """).format(**d).encode('utf-8'))
          self._content.append((file_name, 'html{}'.format(self._count),
                                'application/xhtml+xml'))
          if self.d['nav_point'] is None:
              self.d['nav_point'] = file_name
              self._write_style_sheet()
  
      def _write_style_sheet(self):
          file_name = self.d['style_sheet']
          self._add_from_bytes(file_name, dedent("""\
          .album {
              display: block;
              font-size: 1em;
              padding: 0;
              margin: 0;
          }
          .albumimg {
              height: auto;
              max-height: 100%;
              max-width: 100%;
              width: auto
          }
          """))
          self._content.append((file_name, 'css', 'text/css'))
  
      def _name(self, image=True):
          """no leading zero's necessary in zip internal filenames"""
          return '{}.{}'.format(self._count, 'jpg' if image else 'xhtml')
  
      def _add_image_file(self, file_name, width=None, height=None,
                          strip=None, max_strip_pixel=None, z=None):
          z = z if z else self.zip  # initializes if not done yet
          self._add_html(file_name)
          # you can compress JPEGs, but with little result (1-8%) and
          # more complex/slow decompression (zip then jpeg)
          # Gain 2.836 Mb -&gt; 2.798 Mb ( ~ 1% difference )
          if width:
              im = EpubImage(file_name)
              z.writestr(self._name(), im.read(), zipfile.ZIP_STORED)
          else:
              z.write(file_name, self._name())
          self._content.append((self._name(), 'img{}'.format(self._count),
                                'image/jpeg'))
  
      @property
      def zip(self):
          if self._zip is not None:
              return self._zip
          self._zip_data = StringIO()
          # create zip with default compression
          #self._zip_data = '/var/tmp/epubtmp/yy.zip'
          self._zip = zipfile.ZipFile(self._zip_data, "a",
                                      zipfile.ZIP_DEFLATED, False)
          self.d['uuid'] = uuid.uuid4()
          self.d['nav_uuid'] = uuid.uuid4()
          self._add_mimetype()
          self._add_container()
          return self._zip
  
      def _add_from_bytes(self, file_name, data, no_compression=False):
          self._zip.writestr(
              file_name, data,
              compress_type=zipfile.ZIP_STORED if no_compression else None)
  
      def _add_mimetype(self):
          self._add_from_bytes('mimetype', dedent("""\
          application/epub+zip
          """).rstrip(), no_compression=True)
  
      def _add_container(self):
          self._add_from_bytes('META-INF/container.xml', dedent("""\
          &lt;?xml version="1.0"?&gt;
             &lt;container version="1.0" xmlns="{cont_urn}"&gt;
            &lt;rootfiles&gt;
              &lt;rootfile full-path="{opf_name}" media-type="{mt}"/&gt;
            &lt;/rootfiles&gt;
          &lt;/container&gt;
          """).rstrip().format(**self.d))
  
  
  def main():
      import argparse
      parser = argparse.ArgumentParser()
      parser.add_argument("--title", "-t", required=True)
      parser.add_argument("--title-sort", help="alternative title for sorting")
      parser.add_argument(
          "--output", "-o",
          help="epub name if not specified, derived from title",
      )
      parser.add_argument("--series", help="series name")
      parser.add_argument("--index", help="series index")
      parser.add_argument("--creator", help="Creator/Author")
      parser.add_argument("file_names", nargs="+")
      args = parser.parse_args()
      with Jpeg2Epub(args.title, title_sort=args.title_sort,
                     file_name=args.output,
                     series=args.series, series_idx=args.index,
                     creator=args.creator,  verbose=0) as j2e:
          for file_name in args.file_names:
              j2e.add_image_file(file_name)
  
  
  if __name__ == "__main__":
      main()