First draft for image support.

This commit is contained in:
Christopher Ramírez 2014-09-07 15:24:48 -06:00
parent 75015dd248
commit d285ef7390
4 changed files with 156 additions and 14 deletions

15
samples/images/render.py Normal file
View file

@ -0,0 +1,15 @@
# /usr/bin/activate
import os
import sys
sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../../..')))
from secretary import Renderer
if __name__ == '__main__':
engine = Renderer(media_path='.')
template = open('template.odt', 'rb')
output = open('output.odt', 'wb')
output.write(engine.render(template, image='writer.png'))
print("Template rendering finished! Check output.odt file.")

BIN
samples/images/template.odt Normal file

Binary file not shown.

BIN
samples/images/writer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

View file

@ -22,6 +22,8 @@ import re
import sys import sys
import logging import logging
import zipfile import zipfile
from os import path
from mimetypes import guess_type, guess_extension
from uuid import uuid4 from uuid import uuid4
from xml.dom.minidom import parseString from xml.dom.minidom import parseString
from jinja2 import Environment, Undefined from jinja2 import Environment, Undefined
@ -89,11 +91,16 @@ class UndefinedSilently(Undefined):
# #
# ************************************************ # ************************************************
def media_loader(f):
def wrapper(*args, **kwargs):
Renderer.__media_loader__ = f
return wrapper
def pad_string(value, length=5): def pad_string(value, length=5):
value = str(value) value = str(value)
return value.zfill(length) return value.zfill(length)
class Renderer(object): class Renderer(object):
""" """
Main engine to convert and ODT document into a jinja Main engine to convert and ODT document into a jinja
@ -112,7 +119,6 @@ class Renderer(object):
result = engine.render() result = engine.render()
""" """
def __init__(self, environment=None, **kwargs): def __init__(self, environment=None, **kwargs):
""" """
Create a Renderer instance. Create a Renderer instance.
@ -137,6 +143,21 @@ class Renderer(object):
self.environment.filters['markdown'] = self.markdown_filter self.environment.filters['markdown'] = self.markdown_filter
self.environment.filters['image'] = self.image_filter self.environment.filters['image'] = self.image_filter
self.media_path = kwargs.pop('media_path', '')
self.media_callback = self.fs_loader
def media_loader(self, callback):
"""This sets the the media loader. A user defined function which
loads media. The function should take a template value, optionals
args and kwargs. Is media exists should return a tuple whose first
element if a file object type representing the media and its second
elements is the media mimetype.
See Renderer.fs_loader funcion for an example"""
self.media_callback = callback
return callback
def _unpack_template(self, template): def _unpack_template(self, template):
# And Open/libreOffice is just a ZIP file. Here we unarchive the file # And Open/libreOffice is just a ZIP file. Here we unarchive the file
# and return a dict with every file in the archive # and return a dict with every file in the archive
@ -151,7 +172,6 @@ class Renderer(object):
self.log.debug('Unpack completed') self.log.debug('Unpack completed')
def _pack_document(self, files): def _pack_document(self, files):
# Store to a zip files in files # Store to a zip files in files
self.log.debug('packing document') self.log.debug('packing document')
@ -312,10 +332,105 @@ class Renderer(object):
return xml_text return xml_text
def replace_images(self, xml_documet): def add_media_to_archive(self, media, mime, name=''):
"""Adds to "Pictures" archive folder the file in `media` and register
it into manifest file."""
extension = None
if hasattr(media, 'name') and not name:
extension = path.splitext(media.name)
name = extension[0]
extension = extension[1]
if not extension:
extension = guess_extension(mime)
media_path = 'Pictures/%s%s' % (name, extension)
self.files[media_path] = media.read(-1)
if hasattr(media, 'close'):
media.close()
files_node = self.manifest.getElementsByTagName('manifest:manifest')[0]
node = self.create_node(self.manifest, 'manifest:file-entry', files_node)
node.setAttribute('manifest:full-path', media_path)
node.setAttribute('manifest:media-type', mime)
return media_path
def fs_loader(self, media, *args, **kwargs):
"""Loads a file from the file system.
:param media: relative or absolute path of file to load.
:type media: unicode
"""
if path.isfile(media):
filename = media
else:
if not self.media_path:
self.log.debug('media_path property not specified to load images from.')
return
filename = path.join(self.media_path, media)
if not path.isfile(filename):
self.log.debug('Media file "%s" does not exists.' % filename)
return
mime = guess_type(filename)
return (open(filename, 'rb'), mime[0] if mime else None)
def replace_images(self, xml_document):
"""Perform images replacements""" """Perform images replacements"""
pass self.log.debug('Inserting images')
frames = xml_document.getElementsByTagName('draw:frame')
for frame in frames:
if not frame.hasChildNodes():
continue
key = frame.getAttribute('draw:name')
if key not in self.template_images:
continue
# Get frame attributes
frame_attrs = dict()
for i in xrange(frame.attributes.length):
attr = frame.attributes.item(i)
frame_attrs[attr.name] = attr.value
# Get child draw:image node and its attrs
image_node = frame.childNodes[0]
image_attrs = dict()
for i in xrange(image_node.attributes.length):
attr = image_node.attributes.item(i)
image_attrs[attr.name] = attr.value
# Request to media loader the image to use
image = self.media_callback(self.template_images[key]['value'],
*self.template_images[key]['args'],
frame_attrs=frame_attrs,
image_attrs=image_attrs,
**self.template_images[key]['kwargs'])
# Update frame and image node attrs (if they where updated in
# media_callback call)
for k, v in frame_attrs.items():
frame.setAttribute(k, v)
for k, v in image_attrs.items():
image_node.setAttribute(k, v)
# Keep original image reference value
frame.setAttribute('draw:name',
self.template_images[key]['value'])
# Does the madia loader returned something?
if not image:
continue
mname = self.add_media_to_archive(media=image[0], mime=image[1],
name=key)
if mname:
image_node.setAttribute('xlink:href', mname)
def _render_xml(self, xml_document, **kwargs): def _render_xml(self, xml_document, **kwargs):
# Prepare the xml object to be processed by jinja2 # Prepare the xml object to be processed by jinja2
@ -333,7 +448,7 @@ class Renderer(object):
if self.template_images: if self.template_images:
self.replace_images(final_xml) self.replace_images(final_xml)
return parseString(result.encode('ascii', 'xmlcharrefreplace')) return final_xml
except: except:
self.log.debug('Error rendering template:\n%s', self.log.debug('Error rendering template:\n%s',
@ -362,8 +477,9 @@ class Renderer(object):
# Keep content and styles object since many functions or # Keep content and styles object since many functions or
# filters may work with then # filters may work with then
self.content = parseString(self.files['content.xml']) self.content = parseString(self.files['content.xml'])
self.styles = parseString(self.files['styles.xml']) self.styles = parseString(self.files['styles.xml'])
self.manifest = parseString(self.files['META-INF/manifest.xml'])
# Render content.xml # Render content.xml
self.content = self._render_xml(self.content, **kwargs) self.content = self._render_xml(self.content, **kwargs)
@ -373,8 +489,11 @@ class Renderer(object):
self.log.debug('Template rendering finished') self.log.debug('Template rendering finished')
self.files['content.xml'] = self.content.toxml().encode('ascii', 'xmlcharrefreplace') self.files['content.xml'] = self.content.toxml().encode('ascii', 'xmlcharrefreplace')
self.files['styles.xml'] = self.styles.toxml().encode('ascii', 'xmlcharrefreplace') self.files['styles.xml'] = self.styles.toxml().encode('ascii', 'xmlcharrefreplace')
self.files['META-INF/manifest.xml'] = self.manifest.toxml().encode('ascii', 'xmlcharrefreplace')
document = self._pack_document(self.files)
document = self._pack_document(self.files) document = self._pack_document(self.files)
return document.getvalue() return document.getvalue()
@ -391,6 +510,14 @@ class Renderer(object):
else: else:
return None return None
def create_node(self, xml_document, node_type, parent=None):
"""Creates a node in `xml_document` of type `node_type` and specified,
as child of `parent`."""
node = xml_document.createElement(node_type)
if parent:
parent.appendChild(node)
return node
def create_text_span_node(self, xml_document, content): def create_text_span_node(self, xml_document, content):
span = xml_document.createElement('text:span') span = xml_document.createElement('text:span')
@ -602,9 +729,9 @@ if __name__ == "__main__":
] ]
render = Renderer() render = Renderer()
result = render.render('samples/images.odt', image="images/a.jpg") result = render.render('simple_template.odt', countries=countries, document=document)
output = open('samples/images.out.odt', 'wb') output = open('rendered.odt', 'wb')
output.write(result) output.write(result)
print("Template rendering finished! Check samples\images.out.odt file.") print("Template rendering finished! Check rendered.odt file.")