1 # -*- coding: utf-8 -*-
3 # This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
4 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
9 from subprocess import call, PIPE
10 from tempfile import NamedTemporaryFile, mkdtemp
11 from lxml import etree
12 from urllib import urlretrieve
13 from StringIO import StringIO
14 from Texml.processor import process
15 from librarian import DCNS, XMLNamespace
16 from librarian.formats import Format
17 from librarian.output import OutputFile
18 from librarian.renderers import Register, TreeRenderer
19 from librarian.utils import Context, get_resource, extend_element
20 from librarian import core
22 from ..html import Silent
25 TexmlNS = XMLNamespace('http://getfo.sourceforge.net/texml/ns1')
28 def texml_cmd(name, *parms, **kwargs):
29 cmd = etree.Element(TexmlNS('cmd'), name=name)
30 for opt in kwargs.get('opts', []):
31 etree.SubElement(cmd, TexmlNS('opt')).text = opt
33 etree.SubElement(cmd, TexmlNS('parm')).text = parm
37 class PdfFormat(Format):
41 style = get_resource('formats/pdf/res/default.sty')
44 get_resource('formats/pdf/res/coverimage.sty'),
45 get_resource('formats/pdf/res/insertimage.sty'),
48 renderers = Register()
50 def retrieve_file(self, url, save_as):
54 def add_file(self, ctx, filename, url=None, path=None, image=False):
55 from subprocess import call
57 save_as = os.path.join(ctx.workdir, filename)
59 ext = path.rsplit('.', 1)[-1]
62 call(['convert', path, save_as])
64 # JPEGs with bad density will break LaTeX with 'Dimension too large'.
65 call(['convert', '-units', 'PixelsPerInch', path, '-density', '300', save_as + '_.' + ext])
66 shutil.move(save_as + '_.' + ext, save_as)
68 shutil.copy(path, save_as)
69 elif not self.retrieve_file(url, save_as):
70 if url.startswith('file://'):
71 url = ctx.files_path + url[7:]
73 if url.startswith('/'):
74 url = 'http://milpeer.eu' + url
76 ext = url.rsplit('.', 1)[-1]
78 urlretrieve(url, save_as + '_.' + ext)
80 call(['convert', save_as + '_.' + ext, save_as])
82 # JPEGs with bad density will break LaTeX with 'Dimension too large'.
83 r = call(['convert', '-units', 'PixelsPerInch', save_as + '_.' + ext, '-density', '300', save_as + '_2.' + ext])
85 shutil.move(save_as + '_.' + ext, save_as)
87 shutil.move(save_as + '_2.' + ext, save_as)
89 urlretrieve(url, save_as)
91 def get_file(self, ctx, filename):
92 return os.path.join(ctx.workdir, filename)
94 def get_texml(self, build_ctx):
95 t = etree.Element(TexmlNS('TeXML'))
97 self.add_file(build_ctx, 'wl.cls', path=get_resource('formats/pdf/res/wl.cls'))
98 t.append(texml_cmd("documentclass", "wl"))
101 self.add_file(build_ctx, 'style.sty', path=self.style)
102 t.append(texml_cmd("usepackage", "style"))
103 t.append(texml_cmd("usepackage", "hyphenat"))
106 for i, package in enumerate(self.local_packages):
107 self.add_file(build_ctx, "librarianlocalpackage%s.sty" % i, path=package)
108 t.append(texml_cmd("usepackage", "librarianlocalpackage%s" % i))
110 author = ", ". join(self.doc.meta.get(DCNS('creator')) or '')
111 title = self.doc.meta.title()
112 t.append(texml_cmd("author", author))
113 t.append(texml_cmd("title", title))
115 doc = etree.SubElement(t, TexmlNS('env'), name="document")
116 doc.append(texml_cmd("thispagestyle", "empty"))
120 cover_url = self.doc.meta.get_one(DCNS('relation.coverimage.url'))
122 self.add_file(build_ctx, 'cover.png', cover_url, image=True)
124 img = Image.open(self.get_file(build_ctx, 'cover.png'))
127 if (size[1] > size[0]):
128 img = img.crop((0, 0, size[0], size[0]))
129 img.save(self.get_file(build_ctx, 'cover.png'), format=img.format, quality=90)
132 # TODO: hardcoded paper size here
133 height = 210.0 * size[1] / size[0]
134 doc.append(texml_cmd("makecover", "%fmm" % height))
136 doc.append(texml_cmd("vfill*"))
139 grp = etree.SubElement(doc, 'group')
140 grp.append(texml_cmd("raggedright"))
141 grp.append(texml_cmd("vfill"))
143 p = texml_cmd("par", "")
145 p[0].append(texml_cmd("Large"))
146 p[0].append(texml_cmd("noindent"))
147 p[0].append(texml_cmd("nohyphens", author))
148 p[0].append(texml_cmd("vspace", "1em"))
149 #p[0][-1].tail = author
151 p = texml_cmd("par", "")
153 p[0].append(texml_cmd("Huge"))
154 p[0].append(texml_cmd("noindent"))
155 p[0].append(texml_cmd("nohyphens", title))
156 #p[0][-1].tail = title
157 doc.append(texml_cmd("vfill"))
158 doc.append(texml_cmd("vfill"))
160 # IOFile probably would be better
161 cover_logo_url = getattr(build_ctx, 'cover_logo', None)
164 #cover_logo_url = 'http://milpeer.mdrn.pl/media/dynamic/people/logo/nowoczesnapolska.org.pl.png'
166 self.add_file(build_ctx, 'coverlogo.png', cover_logo_url, image=True)
167 size = Image.open(self.get_file(build_ctx, 'coverlogo.png')).size
168 p = texml_cmd("par", "")
170 p[0].append(texml_cmd("noindent"))
171 p[0].append(texml_cmd("insertimage", 'coverlogo.png', "%fcm" % (1.0 * size[0] / size[1]), "1cm"))
174 doc.append(texml_cmd("clearpage"))
176 ctx = Context(build_ctx, format=self, img=1)
177 doc.extend(self.render(self.doc.edoc.getroot(), ctx))
179 # Redakcyjna na końcu.
180 doc.append(texml_cmd("clearpage"))
182 doc.append(texml_cmd("section*", "Information about the resource"))
183 doc.append(texml_cmd("vspace", "1em"))
186 ('Publisher: ', DCNS('publisher')),
187 ('Rights: ', DCNS('rights')),
188 ('Intended audience: ', DCNS('audience')),
189 ('', DCNS('description')),
191 v = self.doc.meta.get_one(f)
193 e = texml_cmd("par", "")
194 e[0].append(texml_cmd("noindent"))
195 e[0][0].tail = "%s%s" % (m, v)
197 doc.append(texml_cmd("vspace", "1em"))
200 e = texml_cmd("par", "")
201 e[0].append(texml_cmd("noindent"))
202 e[0][0].tail = "Resource prepared using "
203 e[0].append(texml_cmd("href", "http://milpeer.eu", "MIL/PEER"))
204 e[0][-1].tail = " editing platform. "
207 source_url = getattr(build_ctx, 'source_url', None)
208 #source_url = 'http://milpeer.mdrn.pl/documents/27/'
210 e = texml_cmd("par", "")
212 e[0].append(texml_cmd("noindent"))
213 e[0][0].tail = "Source available at "
214 e[0].append(texml_cmd("href", source_url, source_url))
218 def get_tex_dir(self, ctx):
219 ctx.workdir = mkdtemp('-wl2pdf')
220 texml = self.get_texml(ctx)
221 tex_path = os.path.join(ctx.workdir, 'doc.tex')
222 with open(tex_path, 'w') as fout:
223 #print etree.tostring(texml)
224 process(StringIO(etree.tostring(texml)), fout, 'utf-8')
227 #~ shutil.copy(tex_path, self.save_tex)
231 #for sfile in ['wasysym.sty', 'uwasyvar.fd', 'uwasy.fd']:
232 # shutil.copy(get_resource(os.path.join('res/wasysym', sfile)), temp)
235 def build(self, ctx=None, verbose=False):
236 temp = self.get_tex_dir(ctx)
237 tex_path = os.path.join(temp, 'doc.tex')
245 for i in range(self.tex_passes):
246 p = call(['xelatex', tex_path])
248 for i in range(self.tex_passes):
249 p = call(['xelatex', '-interaction=batchmode', tex_path],
250 stdout=PIPE, stderr=PIPE)
252 #raise ParseError("Error parsing .tex file: %s" % tex_path)
253 raise RuntimeError("Error parsing .tex file: %s" % tex_path)
258 output_file = NamedTemporaryFile(prefix='librarian', suffix='.pdf', delete=False)
259 pdf_path = os.path.join(temp, 'doc.pdf')
260 shutil.move(pdf_path, output_file.name)
262 os.system("ls -l " + output_file.name)
263 return OutputFile.from_filename(output_file.name)
265 def render(self, element, ctx):
266 return self.renderers.get_for(element).render(element, ctx)
271 class CmdRenderer(TreeRenderer):
275 root = etree.Element(self.root_name)
276 root.append(texml_cmd(self.tag_name, *(self.parms() + [""])))
280 class EnvRenderer(TreeRenderer):
282 root = etree.Element(self.root_name)
283 inner = etree.SubElement(root, 'env', name=self.tag_name)
286 class GroupRenderer(CmdRenderer):
288 root = etree.Element(self.root_name)
289 inner = etree.SubElement(root, 'group')
291 inner.append(texml_cmd(self.tag_name, *(self.parms() + [""])))
295 class SectionRenderer(CmdRenderer):
296 def subcontext(self, element, ctx):
298 return Context(ctx, toc_level=getattr(ctx, 'toc_level', 1) + 2)
301 root = etree.Element(self.root_name)
302 root.append(texml_cmd('pagebreak', opts=['1']))
303 root.append(texml_cmd(self.tag_name, *(self.parms() + [""])))
307 PdfFormat.renderers.register(core.Section, None, SectionRenderer('par'))
310 PdfFormat.renderers.register(core.Header, None, CmdRenderer('section*'))
312 PdfFormat.renderers.register(core.Div, None, CmdRenderer('par'))
314 class ImgRenderer(CmdRenderer):
318 def render(self, element, ctx):
319 root = super(ImgRenderer, self).render(element, ctx)
320 url = element.get('src')
321 nr = getattr(ctx, 'img', 0)
323 ctx.format.add_file(ctx, 'f%d.png' % nr, url, image=True)
324 root[0][0].text = 'f%d.png' % nr
326 size = Image.open(ctx.format.get_file(ctx, 'f%d.png' % nr)).size
327 except IOError: # not an image
330 root[0][1].text = '15cm'
331 root[0][2].text = '%fcm' % (15.0 * size[1] / size[0])
334 PdfFormat.renderers.register(core.Div, 'img', ImgRenderer('insertimage'))
337 PdfFormat.renderers.register(core.Div, 'defined', CmdRenderer('textbf'))
338 PdfFormat.renderers.register(core.Div, 'item', CmdRenderer('item'))
339 PdfFormat.renderers.register(core.Div, 'list', EnvRenderer('itemize'))
340 PdfFormat.renderers.register(core.Div, 'list.enum', EnvRenderer('enumerate'))
344 PdfFormat.renderers.register(core.Span, None, TreeRenderer())
345 PdfFormat.renderers.register(core.Span, 'cite', CmdRenderer('emph'))
346 PdfFormat.renderers.register(core.Span, 'cite.code', CmdRenderer('texttt'))
347 PdfFormat.renderers.register(core.Span, 'emp', CmdRenderer('textbf'))
348 PdfFormat.renderers.register(core.Span, 'emph', CmdRenderer('emph'))
350 class SpanUri(CmdRenderer):
353 def render(self, element, ctx):
354 root = super(SpanUri, self).render(element, ctx)
356 if src.startswith('file://'):
357 src = ctx.files_path + src[7:]
358 root[0][0].text = src
360 PdfFormat.renderers.register(core.Span, 'uri', SpanUri('href'))
363 class SpanLink(CmdRenderer):
366 def render(self, element, ctx):
367 root = super(SpanLink, self).render(element, ctx)
368 src = element.attrib.get('href', '')
369 if src.startswith('file://'):
370 src = ctx.files_path + src[7:]
371 root[0][0].text = src
373 PdfFormat.renderers.register(core.Span, 'link', SpanLink('href'))
378 PdfFormat.renderers.register(core.Aside, None, TreeRenderer())
379 PdfFormat.renderers.register(core.Aside, 'editorial', CmdRenderer('editorialpage'))
380 PdfFormat.renderers.register(core.Aside, 'comment', Silent())