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.
8 from subprocess import call, PIPE
9 from tempfile import NamedTemporaryFile, mkdtemp
10 from lxml import etree
11 from urllib import urlretrieve
12 from StringIO import StringIO
13 from Texml.processor import process
14 from librarian import DCNS, XMLNamespace
15 from librarian.formats import Format
16 from librarian.output import OutputFile
17 from librarian.renderers import Register, TreeRenderer
18 from librarian.utils import Context, get_resource
19 from librarian import core
21 from ..html import Silent
24 TexmlNS = XMLNamespace('http://getfo.sourceforge.net/texml/ns1')
27 def texml_cmd(name, *parms, **kwargs):
28 cmd = etree.Element(TexmlNS('cmd'), name=name)
29 for opt in kwargs.get('opts', []):
30 etree.SubElement(cmd, TexmlNS('opt')).text = opt
32 etree.SubElement(cmd, TexmlNS('parm')).text = parm
36 class PdfFormat(Format):
40 style = get_resource('formats/pdf/res/default.sty')
43 get_resource('formats/pdf/res/coverimage.sty'),
44 get_resource('formats/pdf/res/insertimage.sty'),
47 renderers = Register()
49 def retrieve_file(self, url, save_as):
53 def add_file(self, ctx, filename, url=None, path=None, image=False):
54 from subprocess import call
56 save_as = os.path.join(ctx.workdir, filename)
58 ext = path.rsplit('.', 1)[-1]
61 call(['convert', path, save_as])
63 # JPEGs with bad density will break LaTeX with 'Dimension too large'.
64 call(['convert', '-units', 'PixelsPerInch', path, '-density', '300', save_as + '_.' + ext])
65 shutil.move(save_as + '_.' + ext, save_as)
67 shutil.copy(path, save_as)
68 elif not self.retrieve_file(url, save_as):
69 if url.startswith('file://'):
70 url = ctx.files_path + url[7:]
72 if url.startswith('/'):
73 url = 'http://milpeer.eu' + url
75 ext = url.rsplit('.', 1)[-1]
77 urlretrieve(url, save_as + '_.' + ext)
79 call(['convert', save_as + '_.' + ext, save_as])
81 # JPEGs with bad density will break LaTeX with 'Dimension too large'.
82 r = call(['convert', '-units', 'PixelsPerInch', save_as + '_.' + ext, '-density', '300',
83 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'))):
190 v = self.doc.meta.get_one(f)
192 e = texml_cmd("par", "")
193 e[0].append(texml_cmd("noindent"))
194 e[0][0].tail = "%s%s" % (m, v)
196 doc.append(texml_cmd("vspace", "1em"))
198 e = texml_cmd("par", "")
199 e[0].append(texml_cmd("noindent"))
200 e[0][0].tail = "Resource prepared using "
201 e[0].append(texml_cmd("href", "http://milpeer.eu", "MIL/PEER"))
202 e[0][-1].tail = " editing platform. "
205 source_url = getattr(build_ctx, 'source_url', None)
206 # source_url = 'http://milpeer.mdrn.pl/documents/27/'
208 e = texml_cmd("par", "")
210 e[0].append(texml_cmd("noindent"))
211 e[0][0].tail = "Source available at "
212 e[0].append(texml_cmd("href", source_url, source_url))
216 def get_tex_dir(self, ctx):
217 ctx.workdir = mkdtemp('-wl2pdf')
218 texml = self.get_texml(ctx)
219 tex_path = os.path.join(ctx.workdir, 'doc.tex')
220 with open(tex_path, 'w') as fout:
221 # print etree.tostring(texml)
222 process(StringIO(etree.tostring(texml)), fout, 'utf-8')
225 # shutil.copy(tex_path, self.save_tex)
227 # for sfile in ['wasysym.sty', 'uwasyvar.fd', 'uwasy.fd']:
228 # shutil.copy(get_resource(os.path.join('res/wasysym', sfile)), temp)
231 def build(self, ctx=None, verbose=False):
232 temp = self.get_tex_dir(ctx)
233 tex_path = os.path.join(temp, 'doc.tex')
241 for i in range(self.tex_passes):
242 p = call(['xelatex', tex_path])
244 for i in range(self.tex_passes):
245 p = call(['xelatex', '-interaction=batchmode', tex_path],
246 stdout=PIPE, stderr=PIPE)
248 # raise ParseError("Error parsing .tex file: %s" % tex_path)
249 raise RuntimeError("Error parsing .tex file: %s" % tex_path)
254 output_file = NamedTemporaryFile(prefix='librarian', suffix='.pdf', delete=False)
255 pdf_path = os.path.join(temp, 'doc.pdf')
256 shutil.move(pdf_path, output_file.name)
258 os.system("ls -l " + output_file.name)
259 return OutputFile.from_filename(output_file.name)
261 def render(self, element, ctx):
262 return self.renderers.get_for(element).render(element, ctx)
265 class CmdRenderer(TreeRenderer):
270 root = etree.Element(self.root_name)
271 root.append(texml_cmd(self.tag_name, *(self.parms() + [""])))
276 class EnvRenderer(TreeRenderer):
278 root = etree.Element(self.root_name)
279 inner = etree.SubElement(root, 'env', name=self.tag_name)
283 class GroupRenderer(CmdRenderer):
285 root = etree.Element(self.root_name)
286 inner = etree.SubElement(root, 'group')
288 inner.append(texml_cmd(self.tag_name, *(self.parms() + [""])))
292 class SectionRenderer(CmdRenderer):
293 def subcontext(self, element, ctx):
295 return Context(ctx, toc_level=getattr(ctx, 'toc_level', 1) + 2)
298 root = etree.Element(self.root_name)
299 root.append(texml_cmd('pagebreak', opts=['1']))
300 root.append(texml_cmd(self.tag_name, *(self.parms() + [""])))
304 PdfFormat.renderers.register(core.Section, None, SectionRenderer('par'))
307 PdfFormat.renderers.register(core.Header, None, CmdRenderer('section*'))
309 PdfFormat.renderers.register(core.Div, None, CmdRenderer('par'))
312 class ImgRenderer(CmdRenderer):
316 def render(self, element, ctx):
317 root = super(ImgRenderer, self).render(element, ctx)
318 url = element.get('src')
319 nr = getattr(ctx, 'img', 0)
321 ctx.format.add_file(ctx, 'f%d.png' % nr, url, image=True)
322 root[0][0].text = 'f%d.png' % nr
324 size = Image.open(ctx.format.get_file(ctx, 'f%d.png' % nr)).size
325 except IOError: # not an image
328 root[0][1].text = '15cm'
329 root[0][2].text = '%fcm' % (15.0 * size[1] / size[0])
332 PdfFormat.renderers.register(core.Div, 'img', ImgRenderer('insertimage'))
335 class VideoRenderer(CmdRenderer):
336 def render(self, element, ctx):
337 root = super(VideoRenderer, self).render(element, ctx)
338 url = 'https://www.youtube.com/watch?v=%s' % element.attrib.get('videoid')
339 link = texml_cmd('href', url, url)
340 root[0][0].text = None
341 root[0][0].append(link)
344 PdfFormat.renderers.register(core.Div, 'video', VideoRenderer('par'))
347 PdfFormat.renderers.register(core.Div, 'defined', CmdRenderer('textbf'))
348 PdfFormat.renderers.register(core.Div, 'item', CmdRenderer('item'))
349 PdfFormat.renderers.register(core.Div, 'list', EnvRenderer('itemize'))
350 PdfFormat.renderers.register(core.Div, 'list.enum', EnvRenderer('enumerate'))
353 PdfFormat.renderers.register(core.Span, None, TreeRenderer())
354 PdfFormat.renderers.register(core.Span, 'cite', CmdRenderer('emph'))
355 PdfFormat.renderers.register(core.Span, 'cite.code', CmdRenderer('texttt'))
356 PdfFormat.renderers.register(core.Span, 'emp', CmdRenderer('textbf'))
357 PdfFormat.renderers.register(core.Span, 'emph', CmdRenderer('emph'))
360 class SpanUri(CmdRenderer):
364 def render(self, element, ctx):
365 root = super(SpanUri, self).render(element, ctx)
367 if src.startswith('file://'):
368 src = ctx.files_path + src[7:]
369 root[0][0].text = src
371 PdfFormat.renderers.register(core.Span, 'uri', SpanUri('href'))
374 class SpanLink(CmdRenderer):
378 def render(self, element, ctx):
379 root = super(SpanLink, self).render(element, ctx)
380 src = element.attrib.get('href', '')
381 if src.startswith('file://'):
382 src = ctx.files_path + src[7:]
383 root[0][0].text = src
385 PdfFormat.renderers.register(core.Span, 'link', SpanLink('href'))
388 PdfFormat.renderers.register(core.Aside, None, TreeRenderer())
389 PdfFormat.renderers.register(core.Aside, 'editorial', CmdRenderer('editorialpage'))
390 PdfFormat.renderers.register(core.Aside, 'comment', Silent())