From 5bc29f19f308610c944d63597962fb3b0f468c54 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Fri, 18 Jan 2013 13:28:30 +0100 Subject: [PATCH] Separate the general from the WL-specific: PDF --- librarian/__init__.py | 37 ++- librarian/cover.py | 203 +--------------- librarian/epub.py | 4 +- librarian/fb2.py | 4 +- librarian/functions.py | 14 ++ librarian/html.py | 4 +- librarian/mobi.py | 4 +- librarian/parser.py | 98 ++++---- librarian/pdf.py | 220 +++++++++--------- librarian/pdf/cover_image.sty | 27 +++ librarian/pdf/default.sty | 33 +++ librarian/pdf/wl.cls | 91 +------- librarian/pdf/wl2tex.xslt | 91 ++++---- .../styles/wolnelektury/pdf/wolnelektury.sty | 80 +++++++ librarian/styles/__init__.py | 0 librarian/styles/wolnelektury/__init__.py | 0 librarian/styles/wolnelektury/cover.py | 137 +++++++++++ .../styles/wolnelektury/partners/__init__.py | 0 .../styles/wolnelektury/partners/cover.py | 75 ++++++ librarian/styles/wolnelektury/pdf.py | 13 ++ librarian/text.py | 6 +- scripts/book2cover | 4 +- tests/test_html.py | 2 +- tests/test_iofile.py | 21 ++ 24 files changed, 656 insertions(+), 512 deletions(-) create mode 100755 librarian/pdf/cover_image.sty create mode 100755 librarian/pdf/default.sty mode change 100644 => 100755 librarian/pdf/wl.cls mode change 100644 => 100755 librarian/pdf/wl2tex.xslt create mode 100755 librarian/res/styles/wolnelektury/pdf/wolnelektury.sty create mode 100755 librarian/styles/__init__.py create mode 100644 librarian/styles/wolnelektury/__init__.py create mode 100644 librarian/styles/wolnelektury/cover.py create mode 100755 librarian/styles/wolnelektury/partners/__init__.py create mode 100755 librarian/styles/wolnelektury/partners/cover.py create mode 100644 librarian/styles/wolnelektury/pdf.py create mode 100644 tests/test_iofile.py diff --git a/librarian/__init__.py b/librarian/__init__.py index c46d5d1..3b811d3 100644 --- a/librarian/__init__.py +++ b/librarian/__init__.py @@ -205,32 +205,35 @@ def get_resource(path): return os.path.join(os.path.dirname(__file__), path) -class OutputFile(object): - """Represents a file returned by one of the converters.""" - +class IOFile(object): + """ Represents a file fed as input or returned as a result. """ _string = None _filename = None + _filename_tmp = False + + def __init__(self, attachments=None): + self.attachments = attachments or {} def __del__(self): - if self._filename: + if self._filename_tmp: os.unlink(self._filename) def __nonzero__(self): return self._string is not None or self._filename is not None @classmethod - def from_string(cls, string): + def from_string(cls, string, *args, **kwargs): """Converter returns contents of a file as a string.""" - instance = cls() + instance = cls(*args, **kwargs) instance._string = string return instance @classmethod - def from_filename(cls, filename): + def from_filename(cls, filename, *args, **kwargs): """Converter returns contents of a file as a named file.""" - instance = cls() + instance = cls(*args, **kwargs) instance._filename = filename return instance @@ -263,6 +266,7 @@ class OutputFile(object): temp.write(self._string) temp.close() self._filename = temp.name + self._filename_tmp = True return self._filename else: return None @@ -275,6 +279,23 @@ class OutputFile(object): os.makedirs(dirname) shutil.copy(self.get_filename(), path) + def dump_to(self, path, directory=None): + """ Path should be name for main file. """ + self.save_as(path) + dirname = os.path.dirname(os.path.abspath(path)) + for filename, attachment in self.attachments.items(): + attachment.save_as(os.path.join(dirname, filename)) + + +class Format(object): + """ Generic format class. """ + def __init__(self, wldoc, **kwargs): + self.wldoc = wldoc + self.customization = kwargs + + def build(self): + raise NotImplementedError + class URLOpener(urllib.FancyURLopener): version = 'FNP Librarian (http://github.com/fnp/librarian)' diff --git a/librarian/cover.py b/librarian/cover.py index be34e26..b53de30 100644 --- a/librarian/cover.py +++ b/librarian/cover.py @@ -6,7 +6,7 @@ import re import Image, ImageFont, ImageDraw, ImageFilter from StringIO import StringIO -from librarian import get_resource, OutputFile, URLOpener +from librarian import get_resource, IOFile class TextBox(object): @@ -186,200 +186,9 @@ class Cover(object): def output_file(self, *args, **kwargs): imgstr = StringIO() self.save(imgstr, *args, **kwargs) - return OutputFile.from_string(imgstr.getvalue()) + return IOFile.from_string(imgstr.getvalue()) - -class WLCover(Cover): - """Default Wolne Lektury cover generator.""" - width = 600 - height = 833 - uses_dc_cover = True - author_font = ImageFont.truetype( - get_resource('fonts/JunicodeWL-Regular.ttf'), 20) - author_lineskip = 30 - title_font = ImageFont.truetype( - get_resource('fonts/DejaVuSerif-Bold.ttf'), 30) - title_lineskip = 40 - title_box_width = 350 - bar_width = 35 - background_color = '#444' - author_color = '#444' - default_background = get_resource('res/cover.png') - format = 'JPEG' - - epoch_colors = { - u'Starożytność': '#9e3610', - u'Średniowiecze': '#564c09', - u'Renesans': '#8ca629', - u'Barok': '#a6820a', - u'Oświecenie': '#f2802e', - u'Romantyzm': '#db4b16', - u'Pozytywizm': '#961060', - u'Modernizm': '#7784e0', - u'Dwudziestolecie międzywojenne': '#3044cf', - u'Współczesność': '#06393d', - } - - def __init__(self, book_info, format=None, image_cache=None): - super(WLCover, self).__init__(book_info, format=format) - self.kind = book_info.kind - self.epoch = book_info.epoch - if book_info.cover_url: - url = book_info.cover_url - bg_src = None - if image_cache: - from urllib import quote - try: - bg_src = URLOpener().open(image_cache + quote(url, safe="")) - except: - pass - if bg_src is None: - bg_src = URLOpener().open(url) - self.background_img = StringIO(bg_src.read()) - bg_src.close() - else: - self.background_img = self.default_background - - def pretty_author(self): - return self.author.upper() - - def image(self): - img = Image.new('RGB', (self.width, self.height), self.background_color) - draw = ImageDraw.Draw(img) - - if self.epoch in self.epoch_colors: - epoch_color = self.epoch_colors[self.epoch] - else: - epoch_color = '#000' - draw.rectangle((0, 0, self.bar_width, self.height), fill=epoch_color) - - if self.background_img: - src = Image.open(self.background_img) - trg_size = (self.width - self.bar_width, self.height) - if src.size[0] * trg_size[1] < src.size[1] * trg_size[0]: - resized = ( - trg_size[0], - src.size[1] * trg_size[0] / src.size[0] - ) - cut = (resized[1] - trg_size[1]) / 2 - src = src.resize(resized) - src = src.crop((0, cut, src.size[0], src.size[1] - cut)) - else: - resized = ( - src.size[0] * trg_size[1] / src.size[1], - trg_size[1], - ) - cut = (resized[0] - trg_size[0]) / 2 - src = src.resize(resized) - src = src.crop((cut, 0, src.size[0] - cut, src.size[1])) - - img.paste(src, (self.bar_width, 0)) - del src - - box = TextBox(self.title_box_width, self.height, padding_y=20) - box.text(self.pretty_author(), - font=self.author_font, - line_height=self.author_lineskip, - color=self.author_color, - shadow_color=self.author_shadow, - ) - - box.skip(10) - box.draw.line((75, box.height, 275, box.height), - fill=self.author_color, width=2) - box.skip(15) - - box.text(self.pretty_title(), - line_height=self.title_lineskip, - font=self.title_font, - color=epoch_color, - shadow_color=self.title_shadow, - ) - box_img = box.image() - - if self.kind == 'Liryka': - # top - box_top = 100 - elif self.kind == 'Epika': - # bottom - box_top = self.height - 100 - box_img.size[1] - else: - # center - box_top = (self.height - box_img.size[1]) / 2 - - box_left = self.bar_width + (self.width - self.bar_width - - box_img.size[0]) / 2 - draw.rectangle((box_left, box_top, - box_left + box_img.size[0], box_top + box_img.size[1]), - fill='#fff') - img.paste(box_img, (box_left, box_top), box_img) - - return img - - - -class VirtualoCover(Cover): - width = 600 - height = 730 - author_top = 73 - title_top = 73 - logo_bottom = 25 - logo_width = 250 - - -class PrestigioCover(Cover): - width = 580 - height = 783 - background_img = get_resource('res/cover-prestigio.png') - - author_top = 446 - author_margin_left = 118 - author_margin_right = 62 - author_lineskip = 60 - author_color = '#fff' - author_shadow = '#000' - author_font = ImageFont.truetype(get_resource('fonts/JunicodeWL-Italic.ttf'), 50) - - title_top = 0 - title_margin_left = 118 - title_margin_right = 62 - title_lineskip = 60 - title_color = '#fff' - title_shadow = '#000' - title_font = ImageFont.truetype(get_resource('fonts/JunicodeWL-Italic.ttf'), 50) - - def pretty_title(self): - return u"„%s”" % self.title - - -class BookotekaCover(Cover): - width = 2140 - height = 2733 - background_img = get_resource('res/cover-bookoteka.png') - - author_top = 480 - author_margin_left = 307 - author_margin_right = 233 - author_lineskip = 156 - author_color = '#d9d919' - author_font = ImageFont.truetype(get_resource('fonts/JunicodeWL-Regular.ttf'), 130) - - title_top = 400 - title_margin_left = 307 - title_margin_right = 233 - title_lineskip = 168 - title_color = '#d9d919' - title_font = ImageFont.truetype(get_resource('fonts/JunicodeWL-Regular.ttf'), 140) - - format = 'PNG' - - -class GandalfCover(Cover): - width = 600 - height = 730 - background_img = get_resource('res/cover-gandalf.png') - author_font = ImageFont.truetype(get_resource('fonts/JunicodeWL-Regular.ttf'), 30) - title_font = ImageFont.truetype(get_resource('fonts/JunicodeWL-Regular.ttf'), 40) - logo_bottom = 25 - logo_width = 250 - format = 'PNG' + def for_pdf(self): + return IOFile.from_filename(get_resource('pdf/cover_image.sty'), { + 'cover.png': self.output_file(), + }) diff --git a/librarian/epub.py b/librarian/epub.py index 10922d4..0c7a16c 100644 --- a/librarian/epub.py +++ b/librarian/epub.py @@ -16,7 +16,7 @@ import zipfile from tempfile import mkdtemp, NamedTemporaryFile from shutil import rmtree -from librarian import RDFNS, WLNS, NCXNS, OPFNS, XHTMLNS, OutputFile +from librarian import RDFNS, WLNS, NCXNS, OPFNS, XHTMLNS, IOFile from librarian.cover import WLCover from librarian import functions, get_resource @@ -560,4 +560,4 @@ def transform(wldoc, verbose=False, zip.writestr('OPS/toc.ncx', etree.tostring(toc_file, pretty_print=True)) zip.close() - return OutputFile.from_filename(output_file.name) + return IOFile.from_filename(output_file.name) diff --git a/librarian/fb2.py b/librarian/fb2.py index d979566..1e110f5 100644 --- a/librarian/fb2.py +++ b/librarian/fb2.py @@ -7,7 +7,7 @@ import os.path from copy import deepcopy from lxml import etree -from librarian import functions, OutputFile +from librarian import functions, IOFile from .epub import replace_by_verse @@ -58,6 +58,6 @@ def transform(wldoc, verbose=False, result = document.transform(style) - return OutputFile.from_string(unicode(result).encode('utf-8')) + return IOFile.from_string(unicode(result).encode('utf-8')) # vim:et diff --git a/librarian/functions.py b/librarian/functions.py index 523b3d5..9490cbb 100644 --- a/librarian/functions.py +++ b/librarian/functions.py @@ -104,3 +104,17 @@ def reg_texcommand(): _register_function(texcommand) +def reg_get(format_): + def get(context, *args): + obj = format_ + for arg in args: + if hasattr(obj, arg): + obj = getattr(obj, arg) + else: + try: + obj = obj[arg] + except (TypeError, KeyError), e: + # Just raise proper AttributeError. + getattr(obj, arg) + return obj + _register_function(get) diff --git a/librarian/html.py b/librarian/html.py index c1a5e5b..985970a 100644 --- a/librarian/html.py +++ b/librarian/html.py @@ -8,7 +8,7 @@ import cStringIO import copy from lxml import etree -from librarian import XHTMLNS, ParseError, OutputFile +from librarian import XHTMLNS, ParseError, IOFile from librarian import functions from lxml.etree import XMLSyntaxError, XSLTApplyError @@ -59,7 +59,7 @@ def transform(wldoc, stylesheet='legacy', options=None, flags=None): add_anchors(result.getroot()) add_table_of_contents(result.getroot()) - return OutputFile.from_string(etree.tostring(result, method='html', + return IOFile.from_string(etree.tostring(result, method='html', xml_declaration=False, pretty_print=True, encoding='utf-8')) else: return None diff --git a/librarian/mobi.py b/librarian/mobi.py index d98b838..9558452 100644 --- a/librarian/mobi.py +++ b/librarian/mobi.py @@ -8,7 +8,7 @@ import os import subprocess from tempfile import NamedTemporaryFile -from librarian import OutputFile +from librarian import IOFile from librarian.cover import WLCover from librarian import get_resource @@ -57,4 +57,4 @@ def transform(wldoc, verbose=False, subprocess.check_call(['ebook-convert', epub.get_filename(), output_file.name, '--no-inline-toc', '--cover=%s' % cover_file.name], **kwargs) os.unlink(cover_file.name) - return OutputFile.from_filename(output_file.name) \ No newline at end of file + return IOFile.from_filename(output_file.name) \ No newline at end of file diff --git a/librarian/parser.py b/librarian/parser.py index a9e8c65..d330a72 100644 --- a/librarian/parser.py +++ b/librarian/parser.py @@ -4,8 +4,8 @@ # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # from librarian import ValidationError, NoDublinCore, ParseError, NoProvider -from librarian import RDFNS -from librarian.cover import WLCover +from librarian import RDFNS, IOFile +from librarian.styles.wolnelektury.cover import WLCover from librarian import dcparser from xml.parsers.expat import ExpatError @@ -20,58 +20,68 @@ class WLDocument(object): LINE_SWAP_EXPR = re.compile(r'/\s', re.MULTILINE | re.UNICODE) provider = None - def __init__(self, edoc, parse_dublincore=True, provider=None, - strict=False, meta_fallbacks=None): - self.edoc = edoc - self.provider = provider - - root_elem = edoc.getroot() - - dc_path = './/' + RDFNS('RDF') + _edoc = None + @property + def edoc(self): + if self._edoc is None: + data = self.source.get_string() + if not isinstance(data, unicode): + data = data.decode('utf-8') + data = data.replace(u'\ufeff', '') + try: + parser = etree.XMLParser(remove_blank_text=False) + self._edoc = etree.parse(StringIO(data.encode('utf-8')), parser) + except (ExpatError, XMLSyntaxError, XSLTApplyError), e: + raise ParseError(e) + return self._edoc + + _rdf_elem = None + @property + def rdf_elem(self): + if self._rdf_elem is None: + dc_path = './/' + RDFNS('RDF') + self._rdf_elem = self.edoc.getroot().find(dc_path) + if self._rdf_elem is None: + raise NoDublinCore('Document has no DublinCore - which is required.') + return self._rdf_elem - if root_elem.tag != 'utwor': + _book_info = None + @property + def book_info(self): + if not self.parse_dublincore: + return None + if self._book_info is None: + self._book_info = dcparser.BookInfo.from_element( + self.rdf_elem, fallbacks=self.meta_fallbacks, strict=self.strict) + return self._book_info + + def __init__(self, iofile, provider=None, + parse_dublincore=True, # shouldn't it be in a subclass? + strict=False, # ? + meta_fallbacks=None # ? + ): + self.source = iofile + self.provider = provider + self.parse_dublincore = parse_dublincore + self.strict = strict + self.meta_fallbacks = meta_fallbacks + if self.edoc.getroot().tag != 'utwor': raise ValidationError("Invalid root element. Found '%s', should be 'utwor'" % root_elem.tag) - if parse_dublincore: - self.rdf_elem = root_elem.find(dc_path) - - if self.rdf_elem is None: - raise NoDublinCore('Document has no DublinCore - which is required.') - - self.book_info = dcparser.BookInfo.from_element( - self.rdf_elem, fallbacks=meta_fallbacks, strict=strict) - else: - self.book_info = None + self.book_info @classmethod def from_string(cls, xml, *args, **kwargs): - return cls.from_file(StringIO(xml), *args, **kwargs) + return cls(IOFile.from_string(xml), *args, **kwargs) @classmethod def from_file(cls, xmlfile, *args, **kwargs): - - # first, prepare for parsing if isinstance(xmlfile, basestring): - file = open(xmlfile, 'rb') - try: - data = file.read() - finally: - file.close() + iofile = IOFile.from_filename(xmlfile) else: - data = xmlfile.read() - - if not isinstance(data, unicode): - data = data.decode('utf-8') + iofile = IOFile.from_file(xmlfile) + return cls(iofile, *args, **kwargs) - data = data.replace(u'\ufeff', '') - - try: - parser = etree.XMLParser(remove_blank_text=False) - tree = etree.parse(StringIO(data.encode('utf-8')), parser) - - return cls(tree, *args, **kwargs) - except (ExpatError, XMLSyntaxError, XSLTApplyError), e: - raise ParseError(e) def swap_endlines(self): """Converts line breaks in stanzas into
tags.""" @@ -95,10 +105,10 @@ class WLDocument(object): elem.text = chunks.pop(0) def parts(self): - if self.provider is None: - raise NoProvider('No document provider supplied.') if self.book_info is None: raise NoDublinCore('No Dublin Core in document.') + if self.book_info.parts and self.provider is None: + raise NoProvider('No document provider supplied.') for part_uri in self.book_info.parts: yield self.from_file(self.provider.by_uri(part_uri), provider=self.provider) diff --git a/librarian/pdf.py b/librarian/pdf.py index 9fb92b1..9308704 100644 --- a/librarian/pdf.py +++ b/librarian/pdf.py @@ -25,9 +25,8 @@ from lxml.etree import XMLSyntaxError, XSLTApplyError from librarian.dcparser import Person from librarian.parser import WLDocument -from librarian import ParseError, DCNS, get_resource, OutputFile +from librarian import ParseError, DCNS, get_resource, IOFile, Format from librarian import functions -from librarian.cover import WLCover functions.reg_substitute_entities() @@ -40,15 +39,6 @@ STYLESHEETS = { 'wl2tex': 'pdf/wl2tex.xslt', } -#CUSTOMIZATIONS = [ -# 'nofootnotes', -# 'nothemes', -# 'defaultleading', -# 'onehalfleading', -# 'doubleleading', -# 'nowlfont', -# ] - def insert_tags(doc, split_re, tagname, exclude=None): """ inserts for every occurence of `split_re' in text nodes in the `doc' tree @@ -83,7 +73,7 @@ def substitute_hyphens(doc): insert_tags(doc, re.compile("(?<=[^-\s])-(?=[^-\s])"), "dywiz", - exclude=[DCNS("identifier.url"), DCNS("rights.license")] + exclude=[DCNS("identifier.url"), DCNS("rights.license"), 'www'] ) @@ -183,52 +173,65 @@ def package_available(package, args='', verbose=False): return p == 0 -def transform(wldoc, verbose=False, save_tex=None, morefloats=None, - cover=None, flags=None, customizations=None): - """ produces a PDF file with XeLaTeX +def load_including_children(wldoc=None, provider=None, uri=None): + """ Makes one big xml file with children inserted at end. + + Either wldoc or provider and URI must be provided. + """ + + if uri and provider: + f = provider.by_uri(uri) + text = f.read().decode('utf-8') + f.close() + elif wldoc is not None: + text = etree.tostring(wldoc.edoc, encoding=unicode) + provider = wldoc.provider + else: + raise ValueError('Neither a WLDocument, nor provider and URI were provided.') + + text = re.sub(ur"([\u0400-\u04ff]+)", ur"\1", text) + + document = WLDocument.from_string(text, + parse_dublincore=True, provider=provider) + document.swap_endlines() + + for child_uri in document.book_info.parts: + child = load_including_children(provider=provider, uri=child_uri) + document.edoc.getroot().append(child.edoc.getroot()) + return document + + +class PDFFormat(Format): + """ Base PDF format. + + Available customization: + nofootnotes: Doesn't do footnotes. + nothemes: Doesn't do themes. + defaultleading: Default leading. + onehalfleading: Bigger leading. + doubleleading: Big leading. + nowlfont: Uses standard TeX font instead of JUnicodeWL. - wldoc: a WLDocument - verbose: prints all output from LaTeX - save_tex: path to save the intermediary LaTeX file to - morefloats (old/new/none): force specific morefloats - cover: a cover.Cover factory or True for default - flags: less-advertising, - customizations: user requested customizations regarding various formatting parameters (passed to wl LaTeX class) """ - # Parse XSLT - try: - book_info = wldoc.book_info - document = load_including_children(wldoc) - root = document.edoc.getroot() + cover_class = None + tex_passes = 1 + style = get_resource('pdf/default.sty') + cover = None + + @property + def has_cover(self): + """ For use in XSLT. """ + return self.cover is not None - if cover: - if cover is True: - cover = WLCover - bound_cover = cover(book_info) - root.set('data-cover-width', str(bound_cover.width)) - root.set('data-cover-height', str(bound_cover.height)) - if bound_cover.uses_dc_cover: - if book_info.cover_by: - root.set('data-cover-by', book_info.cover_by) - if book_info.cover_source: - root.set('data-cover-source', - book_info.cover_source) - if flags: - for flag in flags: - root.set('flag-' + flag, 'yes') - - # check for LaTeX packages - if morefloats: - root.set('morefloats', morefloats.lower()) - elif package_available('morefloats', 'maxfloats=19'): - root.set('morefloats', 'new') - - # add customizations - if customizations is not None: - root.set('customizations', u','.join(customizations)) - - # add editors info + @property + def customization_str(self): + """ For use in XSLT. """ + return u','.join(k for k, v in self.customization.items() if v) + + def get_document(self): + document = load_including_children(self.wldoc) + root = document.edoc.getroot() root.set('editors', u', '.join(sorted( editor.readable() for editor in document.editors()))) @@ -238,45 +241,51 @@ def transform(wldoc, verbose=False, save_tex=None, morefloats=None, parse_creator(document.edoc) substitute_hyphens(document.edoc) fix_hanging(document.edoc) + return document - # wl -> TeXML + def get_texml(self): style_filename = get_stylesheet("wl2tex") - style = etree.parse(style_filename) - - texml = document.transform(style) - - # TeXML -> LaTeX + functions.reg_get(self) + try: + style = etree.parse(style_filename) + texml = self.get_document().transform(style) + return texml + except (XMLSyntaxError, XSLTApplyError), e: + raise ParseError(e) + + def get_tex_dir(self): + texml = self.get_texml() temp = mkdtemp('-wl2pdf') - - if cover: - with open(os.path.join(temp, 'cover.png'), 'w') as f: - bound_cover.save(f) - - del document # no longer needed large object :) - + # Save TeX file tex_path = os.path.join(temp, 'doc.tex') - fout = open(tex_path, 'w') - process(StringIO(texml), fout, 'utf-8') - fout.close() - del texml - - if save_tex: - shutil.copy(tex_path, save_tex) - - # LaTeX -> PDF + with open(tex_path, 'w') as fout: + process(StringIO(texml), fout, 'utf-8') + if self.save_tex: + shutil.copy(tex_path, self.save_tex) + # Copy style shutil.copy(get_resource('pdf/wl.cls'), temp) - shutil.copy(get_resource('res/wl-logo.png'), temp) - + shutil.copy(self.style, os.path.join(temp, 'style.sty')) + # Save attachments + if self.cover: + self.cover.for_pdf().dump_to(os.path.join(temp, 'makecover.sty')) + return temp + + def get_pdf(self): + temp = self.get_tex_dir() + tex_path = os.path.join(temp, 'doc.tex') try: cwd = os.getcwd() except OSError: cwd = None os.chdir(temp) - if verbose: - p = call(['xelatex', tex_path]) + if self.verbose: + for i in range(self.tex_passes): + p = call(['xelatex', tex_path]) else: - p = call(['xelatex', '-interaction=batchmode', tex_path], stdout=PIPE, stderr=PIPE) + for i in range(self.tex_passes): + p = call(['xelatex', '-interaction=batchmode', tex_path], + stdout=PIPE, stderr=PIPE) if p: raise ParseError("Error parsing .tex file") @@ -287,35 +296,20 @@ def transform(wldoc, verbose=False, save_tex=None, morefloats=None, pdf_path = os.path.join(temp, 'doc.pdf') shutil.move(pdf_path, output_file.name) shutil.rmtree(temp) - return OutputFile.from_filename(output_file.name) - - except (XMLSyntaxError, XSLTApplyError), e: - raise ParseError(e) - - -def load_including_children(wldoc=None, provider=None, uri=None): - """ Makes one big xml file with children inserted at end. - - Either wldoc or provider and URI must be provided. - """ - - if uri and provider: - f = provider.by_uri(uri) - text = f.read().decode('utf-8') - f.close() - elif wldoc is not None: - text = etree.tostring(wldoc.edoc, encoding=unicode) - provider = wldoc.provider - else: - raise ValueError('Neither a WLDocument, nor provider and URI were provided.') - - text = re.sub(ur"([\u0400-\u04ff]+)", ur"\1", text) - - document = WLDocument.from_string(text, - parse_dublincore=True, provider=provider) - document.swap_endlines() - - for child_uri in document.book_info.parts: - child = load_including_children(provider=provider, uri=child_uri) - document.edoc.getroot().append(child.edoc.getroot()) - return document + return IOFile.from_filename(output_file.name) + + def build(self, verbose=False, save_tex=None, morefloats=None): + """ morefloats: new/old/none + """ + self.verbose = verbose + self.save_tex = save_tex + + if morefloats is None and package_available('morefloats', 'maxfloats=19'): + morefloats = 'new' + self.morefloats = morefloats + + book_info = self.wldoc.book_info + if self.cover_class: + self.cover = self.cover_class(book_info) + + return self.get_pdf() diff --git a/librarian/pdf/cover_image.sty b/librarian/pdf/cover_image.sty new file mode 100755 index 0000000..4ed4eed --- /dev/null +++ b/librarian/pdf/cover_image.sty @@ -0,0 +1,27 @@ +\newcommand{\makecover}[2]{ + \pdfpagewidth=#1 + \pdfpageheight=#2 + + \thispagestyle{empty} + \newlength{\PictHOffset} + \newlength{\PictVOffset} + \setlength{\PictHOffset}{1in} + \addtolength{\PictHOffset}{\hoffset} + \addtolength{\PictHOffset}{\oddsidemargin} + + \setlength{\PictVOffset}{1in} + \addtolength{\PictVOffset}{\voffset} + \addtolength{\PictVOffset}{\topmargin} + \addtolength{\PictVOffset}{\headheight} + \addtolength{\PictVOffset}{\headsep} + \addtolength{\PictVOffset}{\topskip} + \addtolength{\PictVOffset}{-\pdfpageheight} + + \noindent\hspace*{-\PictHOffset}% + \raisebox{\PictVOffset}[0pt][0pt]{\makebox[0pt][l]{% + \includegraphics[height=\pdfpageheight,width=\pdfpagewidth]{cover.png}}} + \clearpage + + \setlength{\pdfpagewidth}{210mm} + \setlength{\pdfpageheight}{297mm} +} diff --git a/librarian/pdf/default.sty b/librarian/pdf/default.sty new file mode 100755 index 0000000..40b867d --- /dev/null +++ b/librarian/pdf/default.sty @@ -0,0 +1,33 @@ +\usepackage[MeX]{polski} + +\newcommand{\rightsinfostr}[2][] { + \ifx&% + #2 + \else + Ten utwór jest udostępniony na licencji + \href{#1}{#2}. + \fi +} + + +\renewcommand{\maketitle}{} + +\newcommand{\editorialsection}{ + \begin{figure}[b!] + { + \footnotesize + \color{theme} + \noindent \rule{\linewidth}{0.4pt} + + \rightsinfo + \vspace{.6em} + + \editors + + \vspace{.6em} + \coverby + + \color{black} + } + \end{figure} +} diff --git a/librarian/pdf/wl.cls b/librarian/pdf/wl.cls old mode 100644 new mode 100755 index c387b03..53da8d5 --- a/librarian/pdf/wl.cls +++ b/librarian/pdf/wl.cls @@ -66,8 +66,6 @@ \usepackage{trace} -\usepackage[MeX]{polski} - \usepackage[xetex]{graphicx} \usepackage{fontspec} \usepackage{xunicode} @@ -75,6 +73,7 @@ \usepackage[overload]{textcase} \usepackage{scalefnt} +% TODO: link color is a style thing \usepackage[colorlinks=true,linkcolor=black,setpagesize=false,urlcolor=black,xetex]{hyperref} \ifenablewlfont @@ -168,91 +167,6 @@ Letters={SmallCaps,UppercaseSmallCaps} \usebox{\ximagebox}% \raisebox{0pt}[0pt][0pt]{\makebox[0pt][r]{\usebox{\xglyphbox}}}} -\newcommand{\makecover}[2]{ - \pdfpagewidth=#1 - \pdfpageheight=#2 - - \thispagestyle{empty} - \newlength{\PictHOffset} - \newlength{\PictVOffset} - \setlength{\PictHOffset}{1in} - \addtolength{\PictHOffset}{\hoffset} - \addtolength{\PictHOffset}{\oddsidemargin} - - \setlength{\PictVOffset}{1in} - \addtolength{\PictVOffset}{\voffset} - \addtolength{\PictVOffset}{\topmargin} - \addtolength{\PictVOffset}{\headheight} - \addtolength{\PictVOffset}{\headsep} - \addtolength{\PictVOffset}{\topskip} - \addtolength{\PictVOffset}{-\pdfpageheight} - - \noindent\hspace*{-\PictHOffset}% - \raisebox{\PictVOffset}[0pt][0pt]{\makebox[0pt][l]{% - \includegraphics[height=\pdfpageheight,width=\pdfpagewidth]{cover.png}}} - \clearpage - - \setlength{\pdfpagewidth}{210mm} - \setlength{\pdfpageheight}{297mm} -} - - -\renewcommand{\maketitle}{ - { - \thispagestyle{empty} - \footnotesize - \color{theme} - - \noindent \begin{minipage}[t]{.35\textwidth}\vspace{0pt} - \href{http://www.wolnelektury.pl}{\xbox{\includegraphics[width=\textwidth]{wl-logo.png}}} - \end{minipage} - \begin{minipage}[t]{.65\textwidth}\vspace{0pt} - - \ifflaglessadvertising - \else - \href{\bookurl}{Ta lektura}, podobnie jak tysiące innych, jest dostępna on-line na stronie - \href{http://www.wolnelektury.pl/}{wolnelektury.pl}. - \vspace{.5em} - \fi - - Utwór opracowany został w ramach projektu \href{http://www.wolnelektury.pl/}{Wolne Lektury} - przez \href{http://nowoczesnapolska.org.pl}{fundację Nowoczesna Polska}. - - \end{minipage} - \noindent \rule{\linewidth}{0.4pt} - - \vspace{.6em} - \color{black} - } -} - -\newcommand{\editorialsection}{ - \begin{figure}[b!] - { - \footnotesize - \color{theme} - \noindent \rule{\linewidth}{0.4pt} - - \rightsinfo - \vspace{.6em} - - Źródło: \href{\bookurl}{\bookurl} - - \vspace{.6em} - \sourceinfo - - \description - \vspace{.6em} - - \editors - - \vspace{.6em} - \coverby - - \color{black} - } - \end{figure} -} \newcommand{\typosubsubsection}[1]{% @@ -303,6 +217,7 @@ Letters={Uppercase} } \newcommand{\translator}[1]{% +% TODO: l10n is a style thing \subsection*{\typosubsubsection{tłum. #1}}% } @@ -430,7 +345,7 @@ Letters={Uppercase} } \ifshowfootnotes - \newcommand{\pa}[1]{\NoCaseChange{\footnote{#1 [przypis autorski]}}} + \newcommand{\pa}[1]{\NoCaseChange{\footnote{#1}}} \newcommand{\pe}[1]{\NoCaseChange{\footnote{#1}}} \newcommand{\pr}[1]{\NoCaseChange{\footnote{#1}}} \newcommand{\pt}[1]{\NoCaseChange{\footnote{#1}}} diff --git a/librarian/pdf/wl2tex.xslt b/librarian/pdf/wl2tex.xslt old mode 100644 new mode 100755 index 909cf4b..f36172d --- a/librarian/pdf/wl2tex.xslt +++ b/librarian/pdf/wl2tex.xslt @@ -15,36 +15,24 @@ - \documentclass[]{wl} - - - - \newif\ifflaglessadvertising - - - true - - - - - - \def\{} - - - + \documentclass[]{wl} + \usepackage{style} + + \usepackage{makecover} + - + \usepackage[maxfloats=64]{morefloats} - + \usepackage{morefloats} - + \IfFileExists{morefloats.sty}{ @@ -58,9 +46,9 @@ - + - mm + mm 210mm @@ -89,23 +77,32 @@ \def\coverby{ - Okładka na podstawie: + + + + Okładka na podstawie: - - \href{\datacoversource}{\datacoverby} + + + + + + - \datacoverby{} + + } - \def\editors{} + \def\editors{} + @@ -140,31 +137,31 @@ - \def\authors{} + \def\authors{} \author{\authors} - \title{} - \def\translatorsline{} - - \def\bookurl{} - - \def\rightsinfo{Ten utwór nie jest chroniony prawem autorskim i~znajduje się w~domenie - publicznej, co oznacza że możesz go swobodnie wykorzystywać, publikować - i~rozpowszechniać. Jeśli utwór opatrzony jest dodatkowymi materiałami - (przypisy, motywy literackie etc.), które podlegają prawu autorskiemu, to - te dodatkowe materiały udostępnione są na licencji - \href{http://creativecommons.org/licenses/by-sa/3.0/}{Creative Commons - Uznanie Autorstwa – Na Tych Samych Warunkach 3.0 PL}.} - - \def\rightsinfo{Ten utwór jest udostepniony na licencji - \href{}{}.} - + \title{} + \def\translatorsline{} + + \def\bookurl{} - \def\sourceinfo{ + \def\rightsinfo{ + + + + + + + } + + + \def\sourceinfo{ Tekst opracowany na podstawie: \vspace{.6em} - } - \def\description{} + + } + \def\description{ + } diff --git a/librarian/res/styles/wolnelektury/pdf/wolnelektury.sty b/librarian/res/styles/wolnelektury/pdf/wolnelektury.sty new file mode 100755 index 0000000..827d551 --- /dev/null +++ b/librarian/res/styles/wolnelektury/pdf/wolnelektury.sty @@ -0,0 +1,80 @@ +\usepackage[MeX]{polski} + +\newcommand{\rightsinfostr}[2][] { + \ifx&% + Ten utwór nie jest chroniony prawem autorskim i~znajduje się w~domenie + publicznej, co oznacza że możesz go swobodnie wykorzystywać, publikować + i~rozpowszechniać. Jeśli utwór opatrzony jest dodatkowymi materiałami + (przypisy, motywy literackie etc.), które podlegają prawu autorskiemu, to + te dodatkowe materiały udostępnione są na licencji + \href{http://creativecommons.org/licenses/by-sa/3.0/}{Creative Commons + Uznanie Autorstwa – Na Tych Samych Warunkach 3.0 PL}. + \else + Ten utwór jest udostępniony na licencji + \href{#1}{#2}. + \fi +} + +\renewcommand{\maketitle}{ + { + \thispagestyle{empty} + \footnotesize + \color{theme} + + \noindent \begin{minipage}[t]{.35\textwidth}\vspace{0pt} + \href{http://www.wolnelektury.pl}{\xbox{\includegraphics[width=\textwidth]{wl-logo.png}}} + \end{minipage} + \begin{minipage}[t]{.65\textwidth}\vspace{0pt} + + %\ifflaglessadvertising + %\else + \href{\bookurl}{Ta lektura}, podobnie jak tysiące innych, jest dostępna on-line na stronie + \href{http://www.wolnelektury.pl/}{wolnelektury.pl}. + \vspace{.5em} + %\fi + + Utwór opracowany został w ramach projektu \href{http://www.wolnelektury.pl/}{Wolne Lektury} + przez \href{http://nowoczesnapolska.org.pl}{fundację Nowoczesna Polska}. + + \end{minipage} + \noindent \rule{\linewidth}{0.4pt} + + \vspace{.6em} + \color{black} + } +} + +\newcommand{\editorialsection}{ + \begin{figure}[b!] + { + \footnotesize + \color{theme} + \noindent \rule{\linewidth}{0.4pt} + + \rightsinfo + \vspace{.6em} + + Źródło: \href{\bookurl}{\bookurl} + + \vspace{.6em} + \sourceinfo + + \description + \vspace{.6em} + + \editors + + \vspace{.6em} + \coverby + + \color{black} + } + \end{figure} +} + + +% Label as such. +\let\paorig\pa +\ifshowfootnotes + \renewcommand{\pa}[1]{\paorig{#1 [przypis autorski]}} +\fi \ No newline at end of file diff --git a/librarian/styles/__init__.py b/librarian/styles/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/librarian/styles/wolnelektury/__init__.py b/librarian/styles/wolnelektury/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/librarian/styles/wolnelektury/cover.py b/librarian/styles/wolnelektury/cover.py new file mode 100644 index 0000000..b417216 --- /dev/null +++ b/librarian/styles/wolnelektury/cover.py @@ -0,0 +1,137 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Librarian, licensed under GNU Affero GPLv3 or later. +# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. +# +import Image, ImageFont, ImageDraw +from StringIO import StringIO +from librarian import get_resource, URLOpener +from librarian.cover import Cover, TextBox + + +class WLCover(Cover): + """Default Wolne Lektury cover generator.""" + width = 600 + height = 833 + uses_dc_cover = True + author_font = ImageFont.truetype( + get_resource('fonts/JunicodeWL-Regular.ttf'), 20) + author_lineskip = 30 + title_font = ImageFont.truetype( + get_resource('fonts/DejaVuSerif-Bold.ttf'), 30) + title_lineskip = 40 + title_box_width = 350 + bar_width = 35 + background_color = '#444' + author_color = '#444' + default_background = get_resource('res/cover.png') + format = 'JPEG' + + epoch_colors = { + u'Starożytność': '#9e3610', + u'Średniowiecze': '#564c09', + u'Renesans': '#8ca629', + u'Barok': '#a6820a', + u'Oświecenie': '#f2802e', + u'Romantyzm': '#db4b16', + u'Pozytywizm': '#961060', + u'Modernizm': '#7784e0', + u'Dwudziestolecie międzywojenne': '#3044cf', + u'Współczesność': '#06393d', + } + + def __init__(self, book_info, format=None, image_cache=None): + super(WLCover, self).__init__(book_info, format=format) + self.kind = book_info.kind + self.epoch = book_info.epoch + if book_info.cover_url: + url = book_info.cover_url + bg_src = None + if image_cache: + from urllib import quote + try: + bg_src = URLOpener().open(image_cache + quote(url, safe="")) + except: + pass + if bg_src is None: + bg_src = URLOpener().open(url) + self.background_img = StringIO(bg_src.read()) + bg_src.close() + else: + self.background_img = self.default_background + + def pretty_author(self): + return self.author.upper() + + def image(self): + img = Image.new('RGB', (self.width, self.height), self.background_color) + draw = ImageDraw.Draw(img) + + if self.epoch in self.epoch_colors: + epoch_color = self.epoch_colors[self.epoch] + else: + epoch_color = '#000' + draw.rectangle((0, 0, self.bar_width, self.height), fill=epoch_color) + + if self.background_img: + src = Image.open(self.background_img) + trg_size = (self.width - self.bar_width, self.height) + if src.size[0] * trg_size[1] < src.size[1] * trg_size[0]: + resized = ( + trg_size[0], + src.size[1] * trg_size[0] / src.size[0] + ) + cut = (resized[1] - trg_size[1]) / 2 + src = src.resize(resized) + src = src.crop((0, cut, src.size[0], src.size[1] - cut)) + else: + resized = ( + src.size[0] * trg_size[1] / src.size[1], + trg_size[1], + ) + cut = (resized[0] - trg_size[0]) / 2 + src = src.resize(resized) + src = src.crop((cut, 0, src.size[0] - cut, src.size[1])) + + img.paste(src, (self.bar_width, 0)) + del src + + box = TextBox(self.title_box_width, self.height, padding_y=20) + box.text(self.pretty_author(), + font=self.author_font, + line_height=self.author_lineskip, + color=self.author_color, + shadow_color=self.author_shadow, + ) + + box.skip(10) + box.draw.line((75, box.height, 275, box.height), + fill=self.author_color, width=2) + box.skip(15) + + box.text(self.pretty_title(), + line_height=self.title_lineskip, + font=self.title_font, + color=epoch_color, + shadow_color=self.title_shadow, + ) + box_img = box.image() + + if self.kind == 'Liryka': + # top + box_top = 100 + elif self.kind == 'Epika': + # bottom + box_top = self.height - 100 - box_img.size[1] + else: + # center + box_top = (self.height - box_img.size[1]) / 2 + + box_left = self.bar_width + (self.width - self.bar_width - + box_img.size[0]) / 2 + draw.rectangle((box_left, box_top, + box_left + box_img.size[0], box_top + box_img.size[1]), + fill='#fff') + img.paste(box_img, (box_left, box_top), box_img) + + return img diff --git a/librarian/styles/wolnelektury/partners/__init__.py b/librarian/styles/wolnelektury/partners/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/librarian/styles/wolnelektury/partners/cover.py b/librarian/styles/wolnelektury/partners/cover.py new file mode 100755 index 0000000..28f3573 --- /dev/null +++ b/librarian/styles/wolnelektury/partners/cover.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Librarian, licensed under GNU Affero GPLv3 or later. +# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. +# +import ImageFont +from librarian import get_resource +from librarian.cover import Cover + + +class VirtualoCover(Cover): + width = 600 + height = 730 + author_top = 73 + title_top = 73 + logo_bottom = 25 + logo_width = 250 + + +class PrestigioCover(Cover): + width = 580 + height = 783 + background_img = get_resource('res/cover-prestigio.png') + + author_top = 446 + author_margin_left = 118 + author_margin_right = 62 + author_lineskip = 60 + author_color = '#fff' + author_shadow = '#000' + author_font = ImageFont.truetype(get_resource('fonts/JunicodeWL-Italic.ttf'), 50) + + title_top = 0 + title_margin_left = 118 + title_margin_right = 62 + title_lineskip = 60 + title_color = '#fff' + title_shadow = '#000' + title_font = ImageFont.truetype(get_resource('fonts/JunicodeWL-Italic.ttf'), 50) + + def pretty_title(self): + return u"„%s”" % self.title + + +class BookotekaCover(Cover): + width = 2140 + height = 2733 + background_img = get_resource('res/cover-bookoteka.png') + + author_top = 480 + author_margin_left = 307 + author_margin_right = 233 + author_lineskip = 156 + author_color = '#d9d919' + author_font = ImageFont.truetype(get_resource('fonts/JunicodeWL-Regular.ttf'), 130) + + title_top = 400 + title_margin_left = 307 + title_margin_right = 233 + title_lineskip = 168 + title_color = '#d9d919' + title_font = ImageFont.truetype(get_resource('fonts/JunicodeWL-Regular.ttf'), 140) + + format = 'PNG' + + +class GandalfCover(Cover): + width = 600 + height = 730 + background_img = get_resource('res/cover-gandalf.png') + author_font = ImageFont.truetype(get_resource('fonts/JunicodeWL-Regular.ttf'), 30) + title_font = ImageFont.truetype(get_resource('fonts/JunicodeWL-Regular.ttf'), 40) + logo_bottom = 25 + logo_width = 250 + format = 'PNG' diff --git a/librarian/styles/wolnelektury/pdf.py b/librarian/styles/wolnelektury/pdf.py new file mode 100644 index 0000000..6a43b0e --- /dev/null +++ b/librarian/styles/wolnelektury/pdf.py @@ -0,0 +1,13 @@ +import shutil +from librarian import get_resource +from librarian.pdf import PDFFormat +from librarian.styles.wolnelektury.cover import WLCover + +class WLPDFFormat(PDFFormat): + cover_class = WLCover + style = get_resource('res/styles/wolnelektury/pdf/wolnelektury.sty') + + def get_tex_dir(self): + temp = super(WLPDFFormat, self).get_tex_dir() + shutil.copy(get_resource('res/wl-logo.png'), temp) + return temp diff --git a/librarian/text.py b/librarian/text.py index d99e7cf..70f5f01 100644 --- a/librarian/text.py +++ b/librarian/text.py @@ -4,7 +4,7 @@ # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # import copy -from librarian import functions, OutputFile +from librarian import functions, IOFile from lxml import etree import os @@ -78,7 +78,7 @@ def transform(wldoc, flags=None, **options): license_description = "" source = "" contributors = "" - return OutputFile.from_string((TEMPLATE % { + return IOFile.from_string((TEMPLATE % { 'description': description, 'url': url, 'license_description': license_description, @@ -87,5 +87,5 @@ def transform(wldoc, flags=None, **options): 'contributors': contributors, }).encode('utf-8')) else: - return OutputFile.from_string(unicode(result).encode('utf-8')) + return IOFile.from_string(unicode(result).encode('utf-8')) diff --git a/scripts/book2cover b/scripts/book2cover index 3cc0ed7..a8b4871 100755 --- a/scripts/book2cover +++ b/scripts/book2cover @@ -4,9 +4,7 @@ # This file is part of Librarian, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # -from StringIO import StringIO -from librarian import OutputFile -from librarian.book2anything import Book2Anything, Option +from librarian.book2anything import Book2Anything class Book2Cover(Book2Anything): diff --git a/tests/test_html.py b/tests/test_html.py index 51d6acd..21adfb1 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -37,4 +37,4 @@ def test_empty(): assert not WLDocument.from_string( '', parse_dublincore=False, - ).as_html() + ).as_html().get_string() diff --git a/tests/test_iofile.py b/tests/test_iofile.py new file mode 100644 index 0000000..097a65a --- /dev/null +++ b/tests/test_iofile.py @@ -0,0 +1,21 @@ +import os +from StringIO import StringIO +from tempfile import NamedTemporaryFile +from nose.tools import * +from librarian import IOFile + +def test_iofile_from_string_reusable(): + some_file = IOFile.from_string("test") + some_file.get_file().read() + assert_equal(some_file.get_file().read(), "test") + +def test_iofile_from_filename_reusable(): + temp = NamedTemporaryFile(delete=False) + try: + temp.write('test') + temp.close() + some_file = IOFile.from_filename(temp.name) + some_file.get_file().read() + assert_equal(some_file.get_file().read(), "test") + finally: + os.unlink(temp.name) -- 2.20.1