From: Radek Czajka Date: Mon, 12 Dec 2011 10:25:15 +0000 (+0100) Subject: new cover scheme; Cover accepts BookInfo now; e-books include cover attribution,... X-Git-Tag: 1.7~183 X-Git-Url: https://git.mdrn.pl/librarian.git/commitdiff_plain/48acd2e1b4ad0a64661091dff97122b2a53e4685?hp=4077ffb3cbd868df95239898563508b64e6d6ecf new cover scheme; Cover accepts BookInfo now; e-books include cover attribution, if needed --- diff --git a/librarian/__init__.py b/librarian/__init__.py index 32a3af0..2e515dd 100644 --- a/librarian/__init__.py +++ b/librarian/__init__.py @@ -150,7 +150,6 @@ class DirDocProvider(DocProvider): def __init__(self, dir_): self.dir = dir_ self.files = {} - return super(DirDocProvider, self).__init__() def by_slug_and_lang(self, slug, lang=None): fname = WLURI.from_slug_and_lang(slug, lang).filename_stem() + '.xml' diff --git a/librarian/cover.py b/librarian/cover.py index f7d8f7d..f93e709 100644 --- a/librarian/cover.py +++ b/librarian/cover.py @@ -7,7 +7,79 @@ import Image, ImageFont, ImageDraw, ImageFilter from librarian import get_resource +class TextBox(object): + """Creates an Image with a series of centered strings.""" + + SHADOW_X = 3 + SHADOW_Y = 3 + SHADOW_BLUR = 3 + + def __init__(self, max_width, max_height, padding_x=None, padding_y=None): + if padding_x is None: + padding_x = self.SHADOW_X + self.SHADOW_BLUR + if padding_y is None: + padding_y = self.SHADOW_Y + self.SHADOW_BLUR + + self.max_width = max_width + self.max_text_width = max_width - 2 * padding_x + self.padding_y = padding_y + self.height = padding_y + self.img = Image.new('RGBA', (max_width, max_height)) + self.draw = ImageDraw.Draw(self.img) + self.shadow_img = None + self.shadow_draw = None + + def skip(self, height): + """Skips some vertical space.""" + self.height += height + + def text(self, text, color='#000', font=None, line_height=20, + shadow_color=None): + """Writes some centered text.""" + if shadow_color: + if not self.shadow_img: + self.shadow_img = Image.new('RGBA', self.img.size) + self.shadow_draw = ImageDraw.Draw(self.shadow_img) + while text: + line = text + line_width = self.draw.textsize(line, font=font)[0] + while line_width > self.max_text_width: + parts = line.rsplit(' ', 1) + if len(parts) == 1: + line_width = self.max_text_width + break + line = parts[0] + line_width = self.draw.textsize(line, font=font)[0] + line = line.strip() + ' ' + + pos_x = (self.max_width - line_width) / 2 + + if shadow_color: + self.shadow_draw.text( + (pos_x + self.SHADOW_X, self.height + self.SHADOW_Y), + line, font=font, fill=shadow_color + ) + + self.draw.text((pos_x, self.height), line, font=font, fill=color) + self.height += line_height + # go to next line + text = text[len(line):] + + def image(self): + """Creates the actual Image object.""" + image = Image.new('RGBA', (self.max_width, + self.height + self.padding_y)) + if self.shadow_img: + shadow = self.shadow_img.filter(ImageFilter.BLUR) + image.paste(shadow, (0, 0), shadow) + image.paste(self.img, (0, 0), self.img) + else: + image.paste(self.img, (0, 0)) + return image + + class Cover(object): + """Abstract base class for cover images generator.""" width = 600 height = 800 background_color = '#fff' @@ -31,10 +103,10 @@ class Cover(object): logo_bottom = None logo_width = None + uses_dc_cover = False format = 'JPEG' - exts = { 'JPEG': 'jpg', 'PNG': 'png', @@ -45,42 +117,16 @@ class Cover(object): 'PNG': 'image/png', } - - @staticmethod - def draw_centered_text(text, img, font, margin_left, width, pos_y, lineskip, color, shadow_color): - if shadow_color: - shadow_img = Image.new('RGBA', img.size) - shadow_draw = ImageDraw.Draw(shadow_img) - text_img = Image.new('RGBA', img.size) - text_draw = ImageDraw.Draw(text_img) - while text: - line = text - while text_draw.textsize(line, font=font)[0] > width: - try: - line, ext = line.rsplit(' ', 1) - except: - break - pos_x = margin_left + (width - text_draw.textsize(line, font=font)[0]) / 2 - if shadow_color: - shadow_draw.text((pos_x + 3, pos_y + 3), line, font=font, fill=shadow_color) - text_draw.text((pos_x, pos_y), line, font=font, fill=color) - pos_y += lineskip - text = text[len(line)+1:] - if shadow_color: - shadow_img = shadow_img.filter(ImageFilter.BLUR) - img.paste(shadow_img, None, shadow_img) - img.paste(text_img, None, text_img) - return pos_y - - - def __init__(self, author='', title=''): - self.author = author - self.title = title + def __init__(self, book_info): + self.author = book_info.author.readable() + self.title = book_info.title def pretty_author(self): + """Allows for decorating author's name.""" return self.author def pretty_title(self): + """Allows for decorating title.""" return self.title def image(self): @@ -97,14 +143,29 @@ class Cover(object): logo = logo.resize((self.logo_width, logo.size[1] * self.logo_width / logo.size[0])) img.paste(logo, ((self.width - self.logo_width) / 2, img.size[1] - logo.size[1] - self.logo_bottom)) - author_font = self.author_font or ImageFont.truetype(get_resource('fonts/DejaVuSerif.ttf'), 30) - title_y = self.draw_centered_text(self.pretty_author(), img, author_font, - self.author_margin_left, self.width - self.author_margin_left - self.author_margin_right, self.author_top, - self.author_lineskip, self.author_color, self.author_shadow) + self.title_top - title_font = self.title_font or ImageFont.truetype(get_resource('fonts/DejaVuSerif.ttf'), 40) - self.draw_centered_text(self.pretty_title(), img, title_font, - self.title_margin_left, self.width - self.title_margin_left - self.title_margin_right, title_y, - self.title_lineskip, self.title_color, self.title_shadow) + top = self.author_top + tbox = TextBox( + self.width - self.author_margin_left - self.author_margin_right, + self.height - top, + ) + author_font = self.author_font or ImageFont.truetype( + get_resource('fonts/DejaVuSerif.ttf'), 30) + tbox.text(self.pretty_author(), self.author_color, author_font, + self.author_lineskip, self.author_shadow) + text_img = tbox.image() + img.paste(text_img, (self.author_margin_left, top), text_img) + + top += text_img.size[1] + self.title_top + tbox = TextBox( + self.width - self.title_margin_left - self.title_margin_right, + self.height - top, + ) + title_font = self.author_font or ImageFont.truetype( + get_resource('fonts/DejaVuSerif.ttf'), 40) + tbox.text(self.pretty_title(), self.title_color, title_font, + self.title_lineskip, self.title_shadow) + text_img = tbox.image() + img.paste(text_img, (self.title_margin_left, top), text_img) return img @@ -119,10 +180,122 @@ class Cover(object): class WLCover(Cover): - author_font = ImageFont.truetype(get_resource('fonts/JunicodeWL-Italic.ttf'), 40) - title_font = ImageFont.truetype(get_resource('fonts/JunicodeWL-Italic.ttf'), 50) - logo_width = 250 - logo_bottom = 20 + """Default Wolne Lektury cover generator.""" + 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' + + epochs = { + u'Starożytność': 0, + u'Średniowiecze': 30, + u'Renesans': 60, + u'Barok': 90, + u'Oświecenie': 120, + u'Romantyzm': 150, + u'Pozytywizm': 180, + u'Modernizm': 210, + u'Dwudziestolecie międzywojenne': 240, + u'Współczesność': 270, + } + + def __init__(self, book_info): + super(WLCover, self).__init__(book_info) + self.kind = book_info.kind + self.epoch = book_info.epoch + if book_info.cover_url: + from urllib2 import urlopen + from StringIO import StringIO + + bg_src = urlopen(book_info.cover_url) + self.background_img = StringIO(bg_src.read()) + bg_src.close() + + def pretty_author(self): + return self.author.upper() + + def image(self): + from colorsys import hsv_to_rgb + + img = Image.new('RGB', (self.width, self.height), self.background_color) + draw = ImageDraw.Draw(img) + + if self.epoch in self.epochs: + epoch_color = tuple(int(255 * c) for c in hsv_to_rgb( + float(self.epochs[self.epoch]) / 360, .7, .7)) + 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/dcparser.py b/librarian/dcparser.py index 992029e..57308c6 100644 --- a/librarian/dcparser.py +++ b/librarian/dcparser.py @@ -147,6 +147,9 @@ class BookInfo(object): Field( DCNS('rights'), 'license_description'), Field( DCNS('language'), 'language'), Field( DCNS('description'), 'description', required=False), + Field( DCNS('relation.cover_image.url'), 'cover_url', required=False), + Field( DCNS('relation.cover_image.attribution'), 'cover_by', required=False), + Field( DCNS('relation.cover_image.source'), 'cover_source', required=False), ) @classmethod diff --git a/librarian/epub.py b/librarian/epub.py index b063380..80941eb 100644 --- a/librarian/epub.py +++ b/librarian/epub.py @@ -16,6 +16,7 @@ from tempfile import mkdtemp, NamedTemporaryFile from shutil import rmtree from librarian import RDFNS, WLNS, NCXNS, OPFNS, XHTMLNS, OutputFile +from librarian.cover import WLCover from librarian import functions, get_resource @@ -290,7 +291,7 @@ def transform(wldoc, verbose=False, """ produces a EPUB file sample=n: generate sample e-book (with at least n paragraphs) - cover: a cover.Cover object + cover: a cover.Cover object or True for default flags: less-advertising, without-fonts """ @@ -392,10 +393,17 @@ def transform(wldoc, verbose=False, style = get_resource('epub/style.css') zip.write(style, os.path.join('OPS', 'style.css')) - if cover: + if cover is True: + cover = WLCover + if cover.uses_dc_cover: + if document.book_info.cover_by: + document.edoc.getroot().set('data-cover-by', document.book_info.cover_by) + if document.book_info.cover_source: + document.edoc.getroot().set('data-cover-source', document.book_info.cover_source) + cover_file = StringIO() - c = cover(document.book_info.author.readable(), document.book_info.title) + c = cover(document.book_info) c.save(cover_file) c_name = 'cover.%s' % c.ext() zip.writestr(os.path.join('OPS', c_name), cover_file.getvalue()) diff --git a/librarian/epub/xsltLast.xsl b/librarian/epub/xsltLast.xsl index 67f6b0e..751f97a 100644 --- a/librarian/epub/xsltLast.xsl +++ b/librarian/epub/xsltLast.xsl @@ -9,7 +9,7 @@ - + @@ -63,6 +63,24 @@ + +

Okładka na podstawie: + + + + + + + + + + + + + +

+
+
Logo 1%
Przekaż 1% podatku na rozwój Wolnych Lektur.
diff --git a/librarian/mobi.py b/librarian/mobi.py index a93315e..1e7569b 100755 --- a/librarian/mobi.py +++ b/librarian/mobi.py @@ -3,6 +3,7 @@ # This file is part of Librarian, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # +from copy import deepcopy import os import subprocess from tempfile import NamedTemporaryFile @@ -22,19 +23,27 @@ def transform(wldoc, verbose=False, flags: less-advertising, """ - book_info = wldoc.book_info + document = deepcopy(wldoc) + del wldoc + book_info = document.book_info # provide a cover by default if not cover: cover = WLCover cover_file = NamedTemporaryFile(suffix='.png', delete=False) - c = cover(book_info.author.readable(), book_info.title) + c = cover(book_info) c.save(cover_file) + if cover.uses_dc_cover: + if document.book_info.cover_by: + document.edoc.getroot().set('data-cover-by', document.book_info.cover_by) + if document.book_info.cover_source: + document.edoc.getroot().set('data-cover-source', document.book_info.cover_source) + if not flags: flags = [] flags = list(flags) + ['without-fonts'] - epub = wldoc.as_epub(verbose=verbose, sample=sample, html_toc=True, + epub = document.as_epub(verbose=verbose, sample=sample, html_toc=True, flags=flags, style=get_resource('mobi/style.css')) if verbose: diff --git a/librarian/packagers.py b/librarian/packagers.py index ebeb5b3..36a7b60 100644 --- a/librarian/packagers.py +++ b/librarian/packagers.py @@ -130,10 +130,7 @@ class VirtualoEpubPackager(Packager): product_elem[4][0][1].text = cls.utf_trunc(info.author.last_name, 100) xml.append(product_elem) - cover.VirtualoCover( - u' '.join(info.author.first_names + (info.author.last_name,)), - info.title - ).save(os.path.join(outfile_dir, slug+'.jpg')) + cover.VirtualoCover(info).save(os.path.join(outfile_dir, slug+'.jpg')) outfile = os.path.join(outfile_dir, '1.epub') outfile_sample = os.path.join(outfile_dir, '1.sample.epub') doc.save_output_file(epub.transform(doc), diff --git a/librarian/pdf.py b/librarian/pdf.py index 02438a6..bcf8d9a 100644 --- a/librarian/pdf.py +++ b/librarian/pdf.py @@ -21,6 +21,7 @@ from librarian.dcparser import Person from librarian.parser import WLDocument from librarian import ParseError, DCNS, get_resource, OutputFile from librarian import functions +from librarian.cover import WLCover functions.reg_substitute_entities() @@ -189,8 +190,15 @@ def transform(wldoc, verbose=False, save_tex=None, morefloats=None, document = load_including_children(wldoc) if cover: + if cover is True: + cover = WLCover document.edoc.getroot().set('data-cover-width', str(cover.width)) document.edoc.getroot().set('data-cover-height', str(cover.height)) + if cover.uses_dc_cover: + if document.book_info.cover_by: + document.edoc.getroot().set('data-cover-by', document.book_info.cover_by) + if document.book_info.cover_source: + document.edoc.getroot().set('data-cover-source', document.book_info.cover_source) if flags: for flag in flags: document.edoc.getroot().set('flag-' + flag, 'yes') @@ -222,7 +230,7 @@ def transform(wldoc, verbose=False, save_tex=None, morefloats=None, temp = mkdtemp('-wl2pdf') if cover: - c = cover(document.book_info.author.readable(), document.book_info.title) + c = cover(document.book_info) with open(os.path.join(temp, 'cover.png'), 'w') as f: c.save(f) diff --git a/librarian/pdf/wl.cls b/librarian/pdf/wl.cls index 37d1d24..c9305ca 100644 --- a/librarian/pdf/wl.cls +++ b/librarian/pdf/wl.cls @@ -244,6 +244,9 @@ Letters={SmallCaps,UppercaseSmallCaps} \editors + \vspace{.6em} + \coverby + \color{black} } \end{figure} diff --git a/librarian/pdf/wl2tex.xslt b/librarian/pdf/wl2tex.xslt index 25d76d9..1a675ba 100644 --- a/librarian/pdf/wl2tex.xslt +++ b/librarian/pdf/wl2tex.xslt @@ -86,6 +86,22 @@ + + + \def\coverby{ + Okładka na podstawie: + + + \href{\datacoversource}{\datacoverby} + + + \datacoverby{} + + + + } + + diff --git a/scripts/book2cover b/scripts/book2cover new file mode 100755 index 0000000..d2befc3 --- /dev/null +++ b/scripts/book2cover @@ -0,0 +1,39 @@ +#!/usr/bin/env python +# -*- 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 os +import optparse + +from librarian import ParseError +from librarian.parser import WLDocument +from librarian.cover import WLCover + + +if __name__ == '__main__': + # Parse commandline arguments + usage = """Usage: %prog [options] SOURCE [SOURCE...] + Create cover images for SOURCE files.""" + + parser = optparse.OptionParser(usage=usage) + + parser.add_option('-v', '--verbose', action='store_true', dest='verbose', default=False, + help='print status messages to stdout') + + options, input_filenames = parser.parse_args() + + if len(input_filenames) < 1: + parser.print_help() + exit(1) + + # Do some real work + for input_filename in input_filenames: + if options.verbose: + print input_filename + + output_filename = os.path.splitext(input_filename)[0] + '.png' + + doc = WLDocument.from_file(input_filename) + WLCover(doc.book_info).save(output_filename) diff --git a/scripts/book2epub b/scripts/book2epub index 9af3692..bdb5ac6 100755 --- a/scripts/book2epub +++ b/scripts/book2epub @@ -20,6 +20,8 @@ if __name__ == '__main__': parser.add_option('-v', '--verbose', action='store_true', dest='verbose', default=False, help='print status messages to stdout') + parser.add_option('-c', '--with-cover', action='store_true', dest='with_cover', default=False, + help='create default cover') parser.add_option('-d', '--make-dir', action='store_true', dest='make_dir', default=False, help='create a directory for author and put the PDF in it') parser.add_option('-o', '--output-file', dest='output_file', metavar='FILE', @@ -47,7 +49,7 @@ if __name__ == '__main__': output_file = None doc = WLDocument.from_file(main_input, provider=provider) - epub = doc.as_epub() + epub = doc.as_epub(cover=options.with_cover) doc.save_output_file(epub, output_file, options.output_dir, options.make_dir, 'epub') diff --git a/scripts/book2pdf b/scripts/book2pdf index 171264b..258c20d 100755 --- a/scripts/book2pdf +++ b/scripts/book2pdf @@ -18,6 +18,8 @@ if __name__ == '__main__': parser = OptionParser(usage) parser.add_option('-v', '--verbose', action='store_true', dest='verbose', default=False, help='make lots of noise and revert to default interaction in LaTeX') + parser.add_option('-c', '--with-cover', action='store_true', dest='with_cover', default=False, + help='create default cover') parser.add_option('-d', '--make-dir', action='store_true', dest='make_dir', default=False, help='create a directory for author and put the PDF in it') parser.add_option('-t', '--save-tex', dest='save_tex', metavar='FILE', @@ -49,6 +51,7 @@ if __name__ == '__main__': doc = WLDocument.from_file(main_input, provider=provider) pdf = doc.as_pdf(save_tex=options.save_tex, + cover=options.with_cover, morefloats=options.morefloats) doc.save_output_file(pdf, diff --git a/setup.py b/setup.py index 023c943..4d8ec9b 100755 --- a/setup.py +++ b/setup.py @@ -39,6 +39,7 @@ setup( 'scripts/book2mobi', 'scripts/book2pdf', 'scripts/book2partner', + 'scripts/book2cover', 'scripts/bookfragments', 'scripts/genslugs'], tests_require=['nose>=0.11', 'coverage>=3.0.1'],