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 PdfFormat.renderers.register(core.Div, 'defined', CmdRenderer('textbf'))
336 PdfFormat.renderers.register(core.Div, 'item', CmdRenderer('item'))
337 PdfFormat.renderers.register(core.Div, 'list', EnvRenderer('itemize'))
338 PdfFormat.renderers.register(core.Div, 'list.enum', EnvRenderer('enumerate'))
341 PdfFormat.renderers.register(core.Span, None, TreeRenderer())
342 PdfFormat.renderers.register(core.Span, 'cite', CmdRenderer('emph'))
343 PdfFormat.renderers.register(core.Span, 'cite.code', CmdRenderer('texttt'))
344 PdfFormat.renderers.register(core.Span, 'emp', CmdRenderer('textbf'))
345 PdfFormat.renderers.register(core.Span, 'emph', CmdRenderer('emph'))
348 class SpanUri(CmdRenderer):
352 def render(self, element, ctx):
353 root = super(SpanUri, self).render(element, ctx)
355 if src.startswith('file://'):
356 src = ctx.files_path + src[7:]
357 root[0][0].text = src
359 PdfFormat.renderers.register(core.Span, 'uri', SpanUri('href'))
362 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'))
376 PdfFormat.renderers.register(core.Aside, None, TreeRenderer())
377 PdfFormat.renderers.register(core.Aside, 'editorial', CmdRenderer('editorialpage'))
378 PdfFormat.renderers.register(core.Aside, 'comment', Silent())