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, BuildError
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
55 if not url and not path:
56 raise BuildError('No URL or path for image')
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
77 raise BuildError('Linked file without extension: %s' % url)
78 ext = url.rsplit('.', 1)[-1]
80 urlretrieve(url, save_as + '_.' + ext)
82 call(['convert', save_as + '_.' + ext, save_as])
84 # JPEGs with bad density will break LaTeX with 'Dimension too large'.
85 r = call(['convert', '-units', 'PixelsPerInch', save_as + '_.' + ext, '-density', '300',
86 save_as + '_2.' + ext])
88 shutil.move(save_as + '_.' + ext, save_as)
90 shutil.move(save_as + '_2.' + ext, save_as)
92 urlretrieve(url, save_as)
94 def get_file(self, ctx, filename):
95 return os.path.join(ctx.workdir, filename)
97 def get_texml(self, build_ctx):
98 t = etree.Element(TexmlNS('TeXML'))
100 self.add_file(build_ctx, 'wl.cls', path=get_resource('formats/pdf/res/wl.cls'))
101 t.append(texml_cmd("documentclass", "wl"))
104 self.add_file(build_ctx, 'style.sty', path=self.style)
105 t.append(texml_cmd("usepackage", "style"))
106 t.append(texml_cmd("usepackage", "hyphenat"))
109 for i, package in enumerate(self.local_packages):
110 self.add_file(build_ctx, "librarianlocalpackage%s.sty" % i, path=package)
111 t.append(texml_cmd("usepackage", "librarianlocalpackage%s" % i))
113 author = ", ". join(self.doc.meta.get(DCNS('creator')) or '')
114 title = self.doc.meta.title()
115 t.append(texml_cmd("author", author))
116 t.append(texml_cmd("title", title))
117 t.append(texml_cmd("organization", build_ctx.organization))
119 doc = etree.SubElement(t, TexmlNS('env'), name="document")
122 title_field = texml_cmd("titlefield", "")
123 doc.append(title_field)
125 grp.append(texml_cmd("raggedright"))
126 grp.append(texml_cmd("vfill"))
128 p = texml_cmd("par", "")
130 p[0].append(texml_cmd("Large"))
131 p[0].append(texml_cmd("noindent"))
132 p[0].append(texml_cmd("nohyphens", author))
133 p[0].append(texml_cmd("vspace", "1em"))
134 # p[0][-1].tail = author
136 p = texml_cmd("par", "")
138 p[0].append(texml_cmd("Huge"))
139 p[0].append(texml_cmd("noindent"))
140 p[0].append(texml_cmd("nohyphens", title))
141 # p[0][-1].tail = title
143 # IOFile probably would be better
144 cover_logo_url = getattr(build_ctx, 'cover_logo', None)
147 # cover_logo_url = 'http://milpeer.mdrn.pl/media/dynamic/people/logo/nowoczesnapolska.org.pl.png'
149 self.add_file(build_ctx, 'coverlogo.png', cover_logo_url, image=True)
150 size = Image.open(self.get_file(build_ctx, 'coverlogo.png')).size
151 doc.append(texml_cmd("toplogo", 'coverlogo.png', "%fcm" % (2.0 * size[0] / size[1]), "2cm"))
153 doc.append(texml_cmd("vspace", "2em"))
155 ctx = Context(build_ctx, format=self, img=1)
156 root = self.doc.edoc.getroot()
158 doc.extend(self.render(root, ctx))
160 # Redakcyjna na końcu.
161 doc.append(texml_cmd("section*", "Information about the resource"))
162 doc.append(texml_cmd("vspace", "1em"))
164 for m, f, multiple in (
165 ('Publisher: ', DCNS('publisher'), False),
166 ('Rights: ', DCNS('rights'), False),
167 ('', DCNS('description'), False)):
169 v = ', '.join(self.doc.meta.get(f))
171 v = self.doc.meta.get_one(f)
173 e = texml_cmd("par", "")
174 e[0].append(texml_cmd("noindent"))
175 e[0][0].tail = "%s%s" % (m, v)
177 doc.append(texml_cmd("vspace", "1em"))
179 e = texml_cmd("par", "")
180 e[0].append(texml_cmd("noindent"))
181 e[0][0].tail = "Resource prepared using "
182 e[0].append(texml_cmd("href", "http://milpeer.eu", "MIL/PEER"))
183 e[0][-1].tail = " editing platform. "
186 source_url = getattr(build_ctx, 'source_url', None)
187 # source_url = 'http://milpeer.mdrn.pl/documents/27/'
189 e = texml_cmd("par", "")
191 e[0].append(texml_cmd("noindent"))
192 e[0][0].tail = "Source available at "
193 e[0].append(texml_cmd("href", source_url, source_url))
197 def get_tex_dir(self, ctx):
198 ctx.workdir = mkdtemp('-wl2pdf')
199 texml = self.get_texml(ctx)
200 tex_path = os.path.join(ctx.workdir, 'doc.tex')
201 with open(tex_path, 'w') as fout:
202 # print etree.tostring(texml)
203 process(StringIO(etree.tostring(texml)), fout, 'utf-8')
206 # shutil.copy(tex_path, self.save_tex)
208 # for sfile in ['wasysym.sty', 'uwasyvar.fd', 'uwasy.fd']:
209 # shutil.copy(get_resource(os.path.join('res/wasysym', sfile)), temp)
212 def build(self, ctx=None, verbose=False):
213 temp = self.get_tex_dir(ctx)
214 tex_path = os.path.join(temp, 'doc.tex')
222 for i in range(self.tex_passes):
223 p = call(['xelatex', tex_path])
225 for i in range(self.tex_passes):
226 p = call(['xelatex', '-interaction=batchmode', tex_path],
227 stdout=PIPE, stderr=PIPE)
229 # raise ParseError("Error parsing .tex file: %s" % tex_path)
230 raise RuntimeError("Error parsing .tex file: %s" % tex_path)
235 output_file = NamedTemporaryFile(prefix='librarian', suffix='.pdf', delete=False)
236 pdf_path = os.path.join(temp, 'doc.pdf')
237 shutil.move(pdf_path, output_file.name)
239 os.system("ls -l " + output_file.name)
240 return OutputFile.from_filename(output_file.name)
242 def render(self, element, ctx):
243 return self.renderers.get_for(element).render(element, ctx)
246 class CmdRenderer(TreeRenderer):
251 root = etree.Element(self.root_name)
252 root.append(texml_cmd(self.tag_name, *(self.parms() + [""])))
257 class EnvRenderer(TreeRenderer):
259 root = etree.Element(self.root_name)
260 inner = etree.SubElement(root, 'env', name=self.tag_name)
264 class GroupRenderer(CmdRenderer):
266 root = etree.Element(self.root_name)
267 inner = etree.SubElement(root, 'group')
269 inner.append(texml_cmd(self.tag_name, *(self.parms() + [""])))
273 class SectionRenderer(CmdRenderer):
274 def subcontext(self, element, ctx):
276 return Context(ctx, toc_level=getattr(ctx, 'toc_level', 1) + 2)
279 root = etree.Element(self.root_name)
280 root.append(texml_cmd('pagebreak', opts=['1']))
281 root.append(texml_cmd(self.tag_name, *(self.parms() + [""])))
285 PdfFormat.renderers.register(core.Section, None, SectionRenderer('par'))
288 PdfFormat.renderers.register(core.Header, None, CmdRenderer('section*'))
290 PdfFormat.renderers.register(core.Div, None, CmdRenderer('par'))
293 class ImgRenderer(CmdRenderer):
297 def render(self, element, ctx):
298 root = super(ImgRenderer, self).render(element, ctx)
299 url = element.get('src')
300 nr = getattr(ctx, 'img', 0)
302 ctx.format.add_file(ctx, 'f%d.png' % nr, url, image=True)
303 root[0][0].text = 'f%d.png' % nr
305 size = Image.open(ctx.format.get_file(ctx, 'f%d.png' % nr)).size
306 except IOError: # not an image
309 root[0][1].text = '15cm'
310 root[0][2].text = '%fcm' % (15.0 * size[1] / size[0])
313 PdfFormat.renderers.register(core.Div, 'img', ImgRenderer('insertimage'))
316 class VideoRenderer(CmdRenderer):
317 def render(self, element, ctx):
318 root = super(VideoRenderer, self).render(element, ctx)
319 url = 'https://www.youtube.com/watch?v=%s' % element.attrib.get('videoid')
320 link = texml_cmd('href', url, url)
321 root[0][0].text = None
322 root[0][0].append(link)
325 PdfFormat.renderers.register(core.Div, 'video', VideoRenderer('par'))
328 PdfFormat.renderers.register(core.Div, 'defined', CmdRenderer('textbf'))
329 PdfFormat.renderers.register(core.Div, 'item', CmdRenderer('item'))
330 PdfFormat.renderers.register(core.Span, 'item', CmdRenderer('item'))
331 PdfFormat.renderers.register(core.Div, 'list', EnvRenderer('itemize'))
332 PdfFormat.renderers.register(core.Div, 'list.enum', EnvRenderer('enumerate'))
335 PdfFormat.renderers.register(core.Span, None, TreeRenderer())
336 PdfFormat.renderers.register(core.Span, 'cite', CmdRenderer('emph'))
337 PdfFormat.renderers.register(core.Span, 'cite.code', CmdRenderer('texttt'))
338 PdfFormat.renderers.register(core.Span, 'emp', CmdRenderer('textbf'))
339 PdfFormat.renderers.register(core.Span, 'emph', CmdRenderer('emph'))
342 class SpanUri(CmdRenderer):
346 def render(self, element, ctx):
347 root = super(SpanUri, self).render(element, ctx)
349 if src.startswith('file://'):
350 src = ctx.files_path + src[7:]
351 root[0][0].text = src
353 PdfFormat.renderers.register(core.Span, 'uri', SpanUri('href'))
356 class SpanLink(CmdRenderer):
360 def render(self, element, ctx):
361 root = super(SpanLink, self).render(element, ctx)
362 src = element.attrib.get('href', '')
363 if src.startswith('file://'):
364 src = ctx.files_path + src[7:]
365 root[0][0].text = src
367 PdfFormat.renderers.register(core.Span, 'link', SpanLink('href'))
370 PdfFormat.renderers.register(core.Aside, None, TreeRenderer())
371 PdfFormat.renderers.register(core.Aside, 'editorial', CmdRenderer('editorialpage'))
372 PdfFormat.renderers.register(core.Aside, 'comment', Silent())