1#!/usr/local/bin/python3.8
2# vim:fileencoding=utf-8
3
4
5__license__ = 'GPL v3'
6__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
7
8import re, random, unicodedata, numbers
9from collections import namedtuple
10from contextlib import contextmanager
11from math import ceil, sqrt, cos, sin, atan2
12from polyglot.builtins import iteritems, itervalues, string_or_bytes
13from itertools import chain
14
15from qt.core import (
16    QImage, Qt, QFont, QPainter, QPointF, QTextLayout, QTextOption,
17    QFontMetrics, QTextCharFormat, QColor, QRect, QBrush, QLinearGradient,
18    QPainterPath, QPen, QRectF, QTransform, QRadialGradient
19)
20
21from calibre import force_unicode, fit_image
22from calibre.constants import __appname__, __version__
23from calibre.ebooks.metadata import fmt_sidx
24from calibre.ebooks.metadata.book.base import Metadata
25from calibre.ebooks.metadata.book.formatter import SafeFormat
26from calibre.gui2 import ensure_app, config, load_builtin_fonts, pixmap_to_data
27from calibre.utils.cleantext import clean_ascii_chars, clean_xml_chars
28from calibre.utils.config import JSONConfig
29
30# Default settings {{{
31cprefs = JSONConfig('cover_generation')
32cprefs.defaults['title_font_size'] = 120  # px
33cprefs.defaults['subtitle_font_size'] = 80  # px
34cprefs.defaults['footer_font_size'] = 80  # px
35cprefs.defaults['cover_width'] = 1200  # px
36cprefs.defaults['cover_height'] = 1600  # px
37cprefs.defaults['title_font_family'] = None
38cprefs.defaults['subtitle_font_family'] = None
39cprefs.defaults['footer_font_family'] = None
40cprefs.defaults['color_themes'] = {}
41cprefs.defaults['disabled_color_themes'] = []
42cprefs.defaults['disabled_styles'] = []
43cprefs.defaults['title_template'] = '<b>{title}'
44cprefs.defaults['subtitle_template'] = '''{series:'test($, strcat("<i>", $, "</i> - ", raw_field("formatted_series_index")), "")'}'''
45cprefs.defaults['footer_template'] = r'''program:
46# Show at most two authors, on separate lines.
47authors = field('authors');
48num = count(authors, ' &amp; ');
49authors = sublist(authors, 0, 2, ' &amp; ');
50authors = list_re(authors, ' &amp; ', '(.+)', '<b>\1');
51authors = re(authors, ' &amp; ', '<br>');
52re(authors, '&amp;&amp;', '&amp;')
53'''
54Prefs = namedtuple('Prefs', ' '.join(sorted(cprefs.defaults)))
55
56_use_roman = None
57
58
59def get_use_roman():
60    global _use_roman
61    if _use_roman is None:
62        return config['use_roman_numerals_for_series_number']
63    return _use_roman
64
65
66def set_use_roman(val):
67    global _use_roman
68    _use_roman = bool(val)
69
70# }}}
71
72
73# Draw text {{{
74Point = namedtuple('Point', 'x y')
75
76
77def parse_text_formatting(text):
78    pos = 0
79    tokens = []
80    for m in re.finditer(r'</?([a-zA-Z1-6]+)/?>', text):
81        q = text[pos:m.start()]
82        if q:
83            tokens.append((False, q))
84        tokens.append((True, (m.group(1).lower(), '/' in m.group()[:2])))
85        pos = m.end()
86    if tokens:
87        if text[pos:]:
88            tokens.append((False, text[pos:]))
89    else:
90        tokens = [(False, text)]
91
92    ranges, open_ranges, text = [], [], []
93    offset = 0
94    for is_tag, tok in tokens:
95        if is_tag:
96            tag, closing = tok
97            if closing:
98                if open_ranges:
99                    r = open_ranges.pop()
100                    r[-1] = offset - r[-2]
101                    if r[-1] > 0:
102                        ranges.append(r)
103            else:
104                if tag in {'b', 'strong', 'i', 'em'}:
105                    open_ranges.append([tag, offset, -1])
106        else:
107            offset += len(tok.replace('&amp;', '&'))
108            text.append(tok)
109    text = ''.join(text)
110    formats = []
111    for tag, start, length in chain(ranges, open_ranges):
112        fmt = QTextCharFormat()
113        if tag in {'b', 'strong'}:
114            fmt.setFontWeight(QFont.Weight.Bold)
115        elif tag in {'i', 'em'}:
116            fmt.setFontItalic(True)
117        else:
118            continue
119        if length == -1:
120            length = len(text) - start
121        if length > 0:
122            r = QTextLayout.FormatRange()
123            r.format = fmt
124            r.start, r.length = start, length
125            formats.append(r)
126    return text, formats
127
128
129class Block:
130
131    def __init__(self, text='', width=0, font=None, img=None, max_height=100, align=Qt.AlignmentFlag.AlignCenter):
132        self.layouts = []
133        self._position = Point(0, 0)
134        self.leading = self.line_spacing = 0
135        if font is not None:
136            fm = QFontMetrics(font, img)
137            self.leading = fm.leading()
138            self.line_spacing = fm.lineSpacing()
139        for text in text.split('<br>') if text else ():
140            text, formats = parse_text_formatting(sanitize(text))
141            l = QTextLayout(unescape_formatting(text), font, img)
142            l.setAdditionalFormats(formats)
143            to = QTextOption(align)
144            to.setWrapMode(QTextOption.WrapMode.WrapAtWordBoundaryOrAnywhere)
145            l.setTextOption(to)
146
147            l.beginLayout()
148            height = 0
149            while height + 3*self.leading < max_height:
150                line = l.createLine()
151                if not line.isValid():
152                    break
153                line.setLineWidth(width)
154                height += self.leading
155                line.setPosition(QPointF(0, height))
156                height += line.height()
157            max_height -= height
158            l.endLayout()
159            if self.layouts:
160                self.layouts.append(self.leading)
161            else:
162                self._position = Point(l.position().x(), l.position().y())
163            self.layouts.append(l)
164        if self.layouts:
165            self.layouts.append(self.leading)
166
167    @property
168    def height(self):
169        return int(ceil(sum(l if isinstance(l, numbers.Number) else l.boundingRect().height() for l in self.layouts)))
170
171    @property
172    def position(self):
173        return self._position
174
175    @position.setter
176    def position(self, new_pos):
177        (x, y) = new_pos
178        self._position = Point(x, y)
179        if self.layouts:
180            self.layouts[0].setPosition(QPointF(x, y))
181            y += self.layouts[0].boundingRect().height()
182            for l in self.layouts[1:]:
183                if isinstance(l, numbers.Number):
184                    y += l
185                else:
186                    l.setPosition(QPointF(x, y))
187                    y += l.boundingRect().height()
188
189    def draw(self, painter):
190        for l in self.layouts:
191            if hasattr(l, 'draw'):
192                # Etch effect for the text
193                painter.save()
194                painter.setRenderHints(QPainter.RenderHint.TextAntialiasing | QPainter.RenderHint.Antialiasing)
195                painter.save()
196                painter.setPen(QColor(255, 255, 255, 125))
197                l.draw(painter, QPointF(1, 1))
198                painter.restore()
199                l.draw(painter, QPointF())
200                painter.restore()
201
202
203def layout_text(prefs, img, title, subtitle, footer, max_height, style):
204    width = img.width() - 2 * style.hmargin
205    title, subtitle, footer = title, subtitle, footer
206    title_font = QFont(prefs.title_font_family or 'Liberation Serif')
207    title_font.setPixelSize(prefs.title_font_size)
208    title_font.setStyleStrategy(QFont.StyleStrategy.PreferAntialias)
209    title_block = Block(title, width, title_font, img, max_height, style.TITLE_ALIGN)
210    title_block.position = style.hmargin, style.vmargin
211    subtitle_block = Block()
212    if subtitle:
213        subtitle_font = QFont(prefs.subtitle_font_family or 'Liberation Sans')
214        subtitle_font.setPixelSize(prefs.subtitle_font_size)
215        subtitle_font.setStyleStrategy(QFont.StyleStrategy.PreferAntialias)
216        gap = 2 * title_block.leading
217        mh = max_height - title_block.height - gap
218        subtitle_block = Block(subtitle, width, subtitle_font, img, mh, style.SUBTITLE_ALIGN)
219        subtitle_block.position = style.hmargin, title_block.position.y + title_block.height + gap
220
221    footer_font = QFont(prefs.footer_font_family or 'Liberation Serif')
222    footer_font.setStyleStrategy(QFont.StyleStrategy.PreferAntialias)
223    footer_font.setPixelSize(prefs.footer_font_size)
224    footer_block = Block(footer, width, footer_font, img, max_height, style.FOOTER_ALIGN)
225    footer_block.position = style.hmargin, img.height() - style.vmargin - footer_block.height
226
227    return title_block, subtitle_block, footer_block
228
229# }}}
230
231# Format text using templates {{{
232
233
234def sanitize(s):
235    return unicodedata.normalize('NFC', clean_xml_chars(clean_ascii_chars(force_unicode(s or ''))))
236
237
238_formatter = None
239_template_cache = {}
240
241
242def escape_formatting(val):
243    return val.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
244
245
246def unescape_formatting(val):
247    return val.replace('&lt;', '<').replace('&gt;', '>').replace('&amp;', '&')
248
249
250class Formatter(SafeFormat):
251
252    def get_value(self, orig_key, args, kwargs):
253        ans = SafeFormat.get_value(self, orig_key, args, kwargs)
254        return escape_formatting(ans)
255
256
257def formatter():
258    global _formatter
259    if _formatter is None:
260        _formatter = Formatter()
261    return _formatter
262
263
264def format_fields(mi, prefs):
265    f = formatter()
266
267    def safe_format(field):
268        return f.safe_format(
269            getattr(prefs, field), mi, _('Template error'), mi, template_cache=_template_cache
270        )
271    return map(safe_format, ('title_template', 'subtitle_template', 'footer_template'))
272
273
274@contextmanager
275def preserve_fields(obj, fields):
276    if isinstance(fields, string_or_bytes):
277        fields = fields.split()
278    null = object()
279    mem = {f:getattr(obj, f, null) for f in fields}
280    try:
281        yield
282    finally:
283        for f, val in iteritems(mem):
284            if val is null:
285                delattr(obj, f)
286            else:
287                setattr(obj, f, val)
288
289
290def format_text(mi, prefs):
291    with preserve_fields(mi, 'authors formatted_series_index'):
292        mi.authors = [a for a in mi.authors if a != _('Unknown')]
293        mi.formatted_series_index = fmt_sidx(mi.series_index or 0, use_roman=get_use_roman())
294        return tuple(format_fields(mi, prefs))
295# }}}
296
297
298# Colors {{{
299ColorTheme = namedtuple('ColorTheme', 'color1 color2 contrast_color1 contrast_color2')
300
301
302def to_theme(x):
303    return {k:v for k, v in zip(ColorTheme._fields[:4], x.split())}
304
305
306fallback_colors = to_theme('ffffff 000000 000000 ffffff')
307
308default_color_themes = {
309    'Earth' : to_theme('e8d9ac c7b07b 564628 382d1a'),
310    'Grass' : to_theme('d8edb5 abc8a4 375d3b 183128'),
311    'Water' : to_theme('d3dcf2 829fe4 00448d 00305a'),
312    'Silver': to_theme('e6f1f5 aab3b6 6e7476 3b3e40'),
313}
314
315
316def theme_to_colors(theme):
317    colors = {k:QColor('#' + theme[k]) for k in ColorTheme._fields}
318    return ColorTheme(**colors)
319
320
321def load_color_themes(prefs):
322    t = default_color_themes.copy()
323    t.update(prefs.color_themes)
324    disabled = frozenset(prefs.disabled_color_themes)
325    ans = [theme_to_colors(v) for k, v in iteritems(t) if k not in disabled]
326    if not ans:
327        # Ignore disabled and return only the builtin color themes
328        ans = [theme_to_colors(v) for k, v in iteritems(default_color_themes)]
329    return ans
330
331
332def color(color_theme, name):
333    ans = getattr(color_theme, name)
334    if not ans.isValid():
335        ans = QColor('#' + fallback_colors[name])
336    return ans
337
338# }}}
339
340# Styles {{{
341
342
343class Style:
344
345    TITLE_ALIGN = SUBTITLE_ALIGN = FOOTER_ALIGN = Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop
346
347    def __init__(self, color_theme, prefs):
348        self.load_colors(color_theme)
349        self.calculate_margins(prefs)
350
351    def calculate_margins(self, prefs):
352        self.hmargin = int((50 / 600) * prefs.cover_width)
353        self.vmargin = int((50 / 800) * prefs.cover_height)
354
355    def load_colors(self, color_theme):
356        self.color1 = color(color_theme, 'color1')
357        self.color2 = color(color_theme, 'color2')
358        self.ccolor1 = color(color_theme, 'contrast_color1')
359        self.ccolor2 = color(color_theme, 'contrast_color2')
360
361
362class Cross(Style):
363
364    NAME = 'The Cross'
365    GUI_NAME = _('The Cross')
366
367    def __call__(self, painter, rect, color_theme, title_block, subtitle_block, footer_block):
368        painter.fillRect(rect, self.color1)
369        r = QRect(0, int(title_block.position.y), rect.width(),
370                  title_block.height + subtitle_block.height + subtitle_block.line_spacing // 2 + title_block.leading)
371        painter.save()
372        p = QPainterPath()
373        p.addRoundedRect(QRectF(r), 10, 10 * r.width()/r.height(), Qt.SizeMode.RelativeSize)
374        painter.setClipPath(p)
375        painter.setRenderHint(QPainter.RenderHint.Antialiasing)
376        painter.fillRect(r, self.color2)
377        painter.restore()
378        r = QRect(0, 0, int(title_block.position.x), rect.height())
379        painter.fillRect(r, self.color2)
380        return self.ccolor2, self.ccolor2, self.ccolor1
381
382
383class Half(Style):
384
385    NAME = 'Half and Half'
386    GUI_NAME = _('Half and half')
387
388    def __call__(self, painter, rect, color_theme, title_block, subtitle_block, footer_block):
389        g = QLinearGradient(QPointF(0, 0), QPointF(0, rect.height()))
390        g.setStops([(0, self.color1), (0.7, self.color2), (1, self.color1)])
391        painter.fillRect(rect, QBrush(g))
392        return self.ccolor1, self.ccolor1, self.ccolor1
393
394
395def rotate_vector(angle, x, y):
396    return x * cos(angle) - y * sin(angle), x * sin(angle) + y * cos(angle)
397
398
399def draw_curved_line(painter_path, dx, dy, c1_frac, c1_amp, c2_frac, c2_amp):
400    length = sqrt(dx * dx + dy * dy)
401    angle = atan2(dy, dx)
402    c1 = QPointF(*rotate_vector(angle, c1_frac * length, c1_amp * length))
403    c2 = QPointF(*rotate_vector(angle, c2_frac * length, c2_amp * length))
404    pos = painter_path.currentPosition()
405    painter_path.cubicTo(pos + c1, pos + c2, pos + QPointF(dx, dy))
406
407
408class Banner(Style):
409
410    NAME = 'Banner'
411    GUI_NAME = _('Banner')
412    GRADE = 0.07
413
414    def calculate_margins(self, prefs):
415        Style.calculate_margins(self, prefs)
416        self.hmargin = int(0.15 * prefs.cover_width)
417        self.fold_width = int(0.1 * prefs.cover_width)
418
419    def __call__(self, painter, rect, color_theme, title_block, subtitle_block, footer_block):
420        painter.fillRect(rect, self.color1)
421        top = title_block.position.y + 2
422        extra_spacing = subtitle_block.line_spacing // 2 if subtitle_block.line_spacing else title_block.line_spacing // 3
423        height = title_block.height + subtitle_block.height + extra_spacing + title_block.leading
424        right = rect.right() - self.hmargin
425        width = right - self.hmargin
426
427        # Draw main banner
428        p = main = QPainterPath(QPointF(self.hmargin, top))
429        draw_curved_line(p, rect.width() - 2 * self.hmargin, 0, 0.1, -0.1, 0.9, -0.1)
430        deltax = self.GRADE * height
431        p.lineTo(right + deltax, top + height)
432        right_corner = p.currentPosition()
433        draw_curved_line(p, - width - 2 * deltax, 0, 0.1, 0.05, 0.9, 0.05)
434        left_corner = p.currentPosition()
435        p.closeSubpath()
436
437        # Draw fold rectangles
438        rwidth = self.fold_width
439        yfrac = 0.1
440        width23 = int(0.67 * rwidth)
441        rtop = top + height * yfrac
442
443        def draw_fold(x, m=1, corner=left_corner):
444            ans = p = QPainterPath(QPointF(x, rtop))
445            draw_curved_line(p, rwidth*m, 0, 0.1, 0.1*m, 0.5, -0.2*m)
446            fold_upper = p.currentPosition()
447            p.lineTo(p.currentPosition() + QPointF(-deltax*m, height))
448            fold_corner = p.currentPosition()
449            draw_curved_line(p, -rwidth*m, 0, 0.2, -0.1*m, 0.8, -0.1*m)
450            draw_curved_line(p, deltax*m, -height, 0.2, 0.1*m, 0.8, 0.1*m)
451            p = inner_fold = QPainterPath(corner)
452            dp = fold_corner - p.currentPosition()
453            draw_curved_line(p, dp.x(), dp.y(), 0.5, 0.3*m, 1, 0*m)
454            p.lineTo(fold_upper), p.closeSubpath()
455            return ans, inner_fold
456
457        left_fold, left_inner = draw_fold(self.hmargin - width23)
458        right_fold, right_inner = draw_fold(right + width23, m=-1, corner=right_corner)
459
460        painter.save()
461        painter.setRenderHint(QPainter.RenderHint.Antialiasing)
462        pen = QPen(self.ccolor2)
463        pen.setWidth(3)
464        pen.setJoinStyle(Qt.PenJoinStyle.RoundJoin)
465        painter.setPen(pen)
466        for r in (left_fold, right_fold):
467            painter.fillPath(r, QBrush(self.color2))
468            painter.drawPath(r)
469        for r in (left_inner, right_inner):
470            painter.fillPath(r, QBrush(self.color2.darker()))
471            painter.drawPath(r)
472        painter.fillPath(main, QBrush(self.color2))
473        painter.drawPath(main)
474        painter.restore()
475        return self.ccolor2, self.ccolor2, self.ccolor1
476
477
478class Ornamental(Style):
479
480    NAME = 'Ornamental'
481    GUI_NAME = _('Ornamental')
482
483    # SVG vectors {{{
484    CORNER_VECTOR = "m 67.791903,64.260958 c -4.308097,-2.07925 -4.086719,-8.29575 0.334943,-9.40552 4.119758,-1.03399 8.732363,5.05239 5.393055,7.1162 -0.55,0.33992 -1,1.04147 -1,1.55902 0,1.59332 2.597425,1.04548 5.365141,-1.1316 1.999416,-1.57274 2.634859,-2.96609 2.634859,-5.7775 0,-9.55787 -9.827495,-13.42961 -24.43221,-9.62556 -3.218823,0.83839 -5.905663,1.40089 -5.970755,1.25 -0.06509,-0.1509 -0.887601,-1.19493 -1.827799,-2.32007 -1.672708,-2.00174 -1.636693,-2.03722 1.675668,-1.65052 1.861815,0.21736 6.685863,-0.35719 10.720107,-1.27678 12.280767,-2.79934 20.195487,-0.0248 22.846932,8.0092 3.187273,9.65753 -6.423297,17.7497 -15.739941,13.25313 z m 49.881417,-20.53932 c -3.19204,-2.701 -3.72967,-6.67376 -1.24009,-9.16334 2.48236,-2.48236 5.35141,-2.67905 7.51523,-0.51523 1.85966,1.85966 2.07045,6.52954 0.37143,8.22857 -2.04025,2.04024 3.28436,1.44595 6.92316,-0.77272 9.66959,-5.89579 0.88581,-18.22422 -13.0777,-18.35516 -5.28594,-0.0496 -10.31098,1.88721 -14.26764,5.4991 -1.98835,1.81509 -2.16454,1.82692 -2.7936,0.18763 -0.40973,-1.06774 0.12141,-2.82197 1.3628,-4.50104 2.46349,-3.33205 1.67564,-4.01299 -2.891784,-2.49938 -2.85998,0.94777 -3.81038,2.05378 -5.59837,6.51495 -1.184469,2.95536 -3.346819,6.86882 -4.805219,8.69657 -1.4584,1.82776 -2.65164,4.02223 -2.65164,4.87662 0,3.24694 -4.442667,0.59094 -5.872557,-3.51085 -1.361274,-3.90495 0.408198,-8.63869 4.404043,-11.78183 5.155844,-4.05558 1.612374,-3.42079 -9.235926,1.65457 -12.882907,6.02725 -16.864953,7.18038 -24.795556,7.18038 -8.471637,0 -13.38802,-1.64157 -17.634617,-5.88816 -2.832233,-2.83224 -3.849773,-4.81378 -4.418121,-8.6038 -1.946289,-12.9787795 8.03227,-20.91713135 19.767685,-15.7259993 5.547225,2.4538018 6.993631,6.1265383 3.999564,10.1557393 -5.468513,7.35914 -15.917883,-0.19431 -10.657807,-7.7041155 1.486298,-2.1219878 1.441784,-2.2225068 -0.984223,-2.2225068 -1.397511,0 -4.010527,1.3130878 -5.806704,2.9179718 -2.773359,2.4779995 -3.265777,3.5977995 -3.265777,7.4266705 0,5.10943 2.254112,8.84197 7.492986,12.40748 8.921325,6.07175 19.286666,5.61396 37.12088,-1.63946 15.35037,-6.24321 21.294999,-7.42408 34.886123,-6.92999 11.77046,0.4279 19.35803,3.05537 24.34054,8.42878 4.97758,5.3681 2.53939,13.58271 -4.86733,16.39873 -4.17361,1.58681 -11.00702,1.19681 -13.31978,-0.76018 z m 26.50156,-0.0787 c -2.26347,-2.50111 -2.07852,-7.36311 0.39995,-10.51398 2.68134,-3.40877 10.49035,-5.69409 18.87656,-5.52426 l 6.5685,0.13301 -7.84029,0.82767 c -8.47925,0.89511 -12.76997,2.82233 -16.03465,7.20213 -1.92294,2.57976 -1.96722,3.00481 -0.57298,5.5 1.00296,1.79495 2.50427,2.81821 4.46514,3.04333 2.92852,0.33623 2.93789,0.32121 1.08045,-1.73124 -1.53602,-1.69728 -1.64654,-2.34411 -0.61324,-3.58916 2.84565,-3.4288 7.14497,-0.49759 5.03976,3.43603 -1.86726,3.48903 -8.65528,4.21532 -11.3692,1.21647 z m -4.17462,-14.20302 c -0.38836,-0.62838 -0.23556,-1.61305 0.33954,-2.18816 1.3439,-1.34389 4.47714,-0.17168 3.93038,1.47045 -0.5566,1.67168 -3.38637,2.14732 -4.26992,0.71771 z m -8.48037,-9.1829 c -12.462,-4.1101 -12.53952,-4.12156 -25.49998,-3.7694 -24.020921,0.65269 -32.338219,0.31756 -37.082166,-1.49417 -5.113999,-1.95305 -8.192504,-6.3647405 -6.485463,-9.2940713 0.566827,-0.972691 1.020091,-1.181447 1.037211,-0.477701 0.01685,0.692606 1.268676,1.2499998 2.807321,1.2499998 1.685814,0 4.868609,1.571672 8.10041,4.0000015 4.221481,3.171961 6.182506,3.999221 9.473089,3.996261 l 4.149585,-0.004 -3.249996,-1.98156 c -3.056252,-1.863441 -4.051566,-3.8760635 -2.623216,-5.3044145 0.794,-0.794 6.188222,1.901516 9.064482,4.5295635 1.858669,1.698271 3.461409,1.980521 10.559493,1.859621 11.30984,-0.19266 20.89052,1.29095 31.97905,4.95208 7.63881,2.52213 11.51931,3.16471 22.05074,3.65141 7.02931,0.32486 13.01836,0.97543 13.30902,1.44571 0.29065,0.47029 -5.2356,0.83436 -12.28056,0.80906 -12.25942,-0.044 -13.34537,-0.2229 -25.30902,-4.16865 z"  # noqa
485    # }}}
486    PATH_CACHE = {}
487    VIEWPORT = (400, 500)
488
489    def calculate_margins(self, prefs):
490        self.hmargin = int((51 / self.VIEWPORT[0]) * prefs.cover_width)
491        self.vmargin = int((83 / self.VIEWPORT[1]) * prefs.cover_height)
492
493    def __call__(self, painter, rect, color_theme, title_block, subtitle_block, footer_block):
494        if not self.PATH_CACHE:
495            from calibre.utils.speedups import svg_path_to_painter_path
496            try:
497                self.__class__.PATH_CACHE['corner'] = svg_path_to_painter_path(self.CORNER_VECTOR)
498            except Exception:
499                import traceback
500                traceback.print_exc()
501        p = painter
502        painter.setRenderHint(QPainter.RenderHint.Antialiasing)
503        g = QRadialGradient(QPointF(rect.center()), rect.width())
504        g.setColorAt(0, self.color1), g.setColorAt(1, self.color2)
505        painter.fillRect(rect, QBrush(g))
506        painter.save()
507        painter.setWindow(0, 0, *self.VIEWPORT)
508        try:
509            path = self.PATH_CACHE['corner']
510        except KeyError:
511            path = QPainterPath()
512        pen = p.pen()
513        pen.setColor(self.ccolor1)
514        p.setPen(pen)
515
516        def corner():
517            b = QBrush(self.ccolor1)
518            p.fillPath(path, b)
519            p.rotate(90), p.translate(100, -100), p.scale(1, -1), p.translate(-103, -97)
520            p.fillPath(path, b)
521            p.setWorldTransform(QTransform())
522        # Top-left corner
523        corner()
524        # Top right corner
525        p.scale(-1, 1), p.translate(-400, 0), corner()
526        # Bottom left corner
527        p.scale(1, -1), p.translate(0, -500), corner()
528        # Bottom right corner
529        p.scale(-1, -1), p.translate(-400, -500), corner()
530        for y in (28.4, 471.7):
531            p.drawLine(QPointF(160, y), QPointF(240, y))
532        for x in (31.3, 368.7):
533            p.drawLine(QPointF(x, 155), QPointF(x, 345))
534        pen.setWidthF(1.8)
535        p.setPen(pen)
536        for y in (23.8, 476.7):
537            p.drawLine(QPointF(160, y), QPointF(240, y))
538        for x in (26.3, 373.7):
539            p.drawLine(QPointF(x, 155), QPointF(x, 345))
540        painter.restore()
541
542        return self.ccolor2, self.ccolor2, self.ccolor1
543
544
545class Blocks(Style):
546
547    NAME = 'Blocks'
548    GUI_NAME = _('Blocks')
549    FOOTER_ALIGN = Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignTop
550
551    def __call__(self, painter, rect, color_theme, title_block, subtitle_block, footer_block):
552        painter.fillRect(rect, self.color1)
553        y = rect.height() - rect.height() // 3
554        r = QRect(rect)
555        r.setBottom(y)
556        painter.fillRect(rect, self.color1)
557        r = QRect(rect)
558        r.setTop(y)
559        painter.fillRect(r, self.color2)
560        return self.ccolor1, self.ccolor1, self.ccolor2
561
562
563def all_styles():
564    return {
565        x.NAME for x in itervalues(globals()) if
566        isinstance(x, type) and issubclass(x, Style) and x is not Style
567    }
568
569
570def load_styles(prefs, respect_disabled=True):
571    disabled = frozenset(prefs.disabled_styles) if respect_disabled else ()
572    ans = tuple(x for x in itervalues(globals()) if
573            isinstance(x, type) and issubclass(x, Style) and x is not Style and x.NAME not in disabled)
574    if not ans and disabled:
575        # If all styles have been disabled, ignore the disabling and return all
576        # the styles
577        ans = load_styles(prefs, respect_disabled=False)
578    return ans
579
580# }}}
581
582
583def init_environment():
584    ensure_app()
585    load_builtin_fonts()
586
587
588def generate_cover(mi, prefs=None, as_qimage=False):
589    init_environment()
590    prefs = prefs or cprefs
591    prefs = {k:prefs.get(k) for k in cprefs.defaults}
592    prefs = Prefs(**prefs)
593    color_theme = random.choice(load_color_themes(prefs))
594    style = random.choice(load_styles(prefs))(color_theme, prefs)
595    title, subtitle, footer = format_text(mi, prefs)
596    img = QImage(prefs.cover_width, prefs.cover_height, QImage.Format.Format_ARGB32)
597    title_block, subtitle_block, footer_block = layout_text(
598        prefs, img, title, subtitle, footer, img.height() // 3, style)
599    p = QPainter(img)
600    rect = QRect(0, 0, img.width(), img.height())
601    colors = style(p, rect, color_theme, title_block, subtitle_block, footer_block)
602    for block, color in zip((title_block, subtitle_block, footer_block), colors):
603        p.setPen(color)
604        block.draw(p)
605    p.end()
606    img.setText('Generated cover', '%s %s' % (__appname__, __version__))
607    if as_qimage:
608        return img
609    return pixmap_to_data(img)
610
611
612def override_prefs(base_prefs, **overrides):
613    ans = {k:overrides.get(k, base_prefs[k]) for k in cprefs.defaults}
614    override_color_theme = overrides.get('override_color_theme')
615    if override_color_theme is not None:
616        all_themes = set(default_color_themes) | set(ans['color_themes'])
617        if override_color_theme in all_themes:
618            all_themes.discard(override_color_theme)
619            ans['disabled_color_themes'] = all_themes
620    override_style = overrides.get('override_style')
621    if override_style is not None:
622        styles = all_styles()
623        if override_style in styles:
624            styles.discard(override_style)
625            ans['disabled_styles'] = styles
626
627    return ans
628
629
630def create_cover(title, authors, series=None, series_index=1, prefs=None, as_qimage=False):
631    ' Create a cover from the specified title, author and series. Any user set'
632    ' templates are ignored, to ensure that the specified metadata is used. '
633    mi = Metadata(title, authors)
634    if series:
635        mi.series, mi.series_index = series, series_index
636    d = cprefs.defaults
637    prefs = override_prefs(
638        prefs or cprefs, title_template=d['title_template'], subtitle_template=d['subtitle_template'], footer_template=d['footer_template'])
639    return generate_cover(mi, prefs=prefs, as_qimage=as_qimage)
640
641
642def calibre_cover2(title, author_string='', series_string='', prefs=None, as_qimage=False, logo_path=None):
643    init_environment()
644    title, subtitle, footer = '<b>' + escape_formatting(title), '<i>' + escape_formatting(series_string), '<b>' + escape_formatting(author_string)
645    prefs = prefs or cprefs
646    prefs = {k:prefs.get(k) for k in cprefs.defaults}
647    scale = 800. / prefs['cover_height']
648    scale_cover(prefs, scale)
649    prefs = Prefs(**prefs)
650    img = QImage(prefs.cover_width, prefs.cover_height, QImage.Format.Format_ARGB32)
651    img.fill(Qt.GlobalColor.white)
652    # colors = to_theme('ffffff ffffff 000000 000000')
653    color_theme = theme_to_colors(fallback_colors)
654
655    class CalibeLogoStyle(Style):
656        NAME = GUI_NAME = 'calibre'
657
658        def __call__(self, painter, rect, color_theme, title_block, subtitle_block, footer_block):
659            top = title_block.position.y + 10
660            extra_spacing = subtitle_block.line_spacing // 2 if subtitle_block.line_spacing else title_block.line_spacing // 3
661            height = title_block.height + subtitle_block.height + extra_spacing + title_block.leading
662            top += height + 25
663            bottom = footer_block.position.y - 50
664            logo = QImage(logo_path or I('library.png'))
665            pwidth, pheight = rect.width(), bottom - top
666            scaled, width, height = fit_image(logo.width(), logo.height(), pwidth, pheight)
667            x, y = (pwidth - width) // 2, (pheight - height) // 2
668            rect = QRect(x, top + y, width, height)
669            painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform)
670            painter.drawImage(rect, logo)
671            return self.ccolor1, self.ccolor1, self.ccolor1
672    style = CalibeLogoStyle(color_theme, prefs)
673    title_block, subtitle_block, footer_block = layout_text(
674        prefs, img, title, subtitle, footer, img.height() // 3, style)
675    p = QPainter(img)
676    rect = QRect(0, 0, img.width(), img.height())
677    colors = style(p, rect, color_theme, title_block, subtitle_block, footer_block)
678    for block, color in zip((title_block, subtitle_block, footer_block), colors):
679        p.setPen(color)
680        block.draw(p)
681    p.end()
682    img.setText('Generated cover', '%s %s' % (__appname__, __version__))
683    if as_qimage:
684        return img
685    return pixmap_to_data(img)
686
687
688def message_image(text, width=500, height=400, font_size=20):
689    init_environment()
690    img = QImage(width, height, QImage.Format.Format_ARGB32)
691    img.fill(Qt.GlobalColor.white)
692    p = QPainter(img)
693    f = QFont()
694    f.setPixelSize(font_size)
695    p.setFont(f)
696    r = img.rect().adjusted(10, 10, -10, -10)
697    p.drawText(r, Qt.AlignmentFlag.AlignJustify | Qt.AlignmentFlag.AlignVCenter | Qt.TextFlag.TextWordWrap, text)
698    p.end()
699    return pixmap_to_data(img)
700
701
702def scale_cover(prefs, scale):
703    for x in ('cover_width', 'cover_height', 'title_font_size', 'subtitle_font_size', 'footer_font_size'):
704        prefs[x] = int(scale * prefs[x])
705
706
707def generate_masthead(title, output_path=None, width=600, height=60, as_qimage=False, font_family=None):
708    init_environment()
709    font_family = font_family or cprefs['title_font_family'] or 'Liberation Serif'
710    img = QImage(width, height, QImage.Format.Format_ARGB32)
711    img.fill(Qt.GlobalColor.white)
712    p = QPainter(img)
713    p.setRenderHints(QPainter.RenderHint.Antialiasing | QPainter.RenderHint.TextAntialiasing)
714    f = QFont(font_family)
715    f.setStyleStrategy(QFont.StyleStrategy.PreferAntialias)
716    f.setPixelSize((height * 3) // 4), f.setBold(True)
717    p.setFont(f)
718    p.drawText(img.rect(), Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, sanitize(title))
719    p.end()
720    if as_qimage:
721        return img
722    data = pixmap_to_data(img)
723    if output_path is None:
724        return data
725    with open(output_path, 'wb') as f:
726        f.write(data)
727
728
729def test(scale=0.25):
730    from qt.core import QLabel, QPixmap, QMainWindow, QWidget, QScrollArea, QGridLayout
731    from calibre.gui2 import Application
732    app = Application([])
733    mi = Metadata('Unknown', ['Kovid Goyal', 'John & Doe', 'Author'])
734    mi.series = 'A series & styles'
735    m = QMainWindow()
736    sa = QScrollArea(m)
737    w = QWidget(m)
738    sa.setWidget(w)
739    l = QGridLayout(w)
740    w.setLayout(l), l.setSpacing(30)
741    scale *= w.devicePixelRatioF()
742    labels = []
743    for r, color in enumerate(sorted(default_color_themes)):
744        for c, style in enumerate(sorted(all_styles())):
745            mi.series_index = c + 1
746            mi.title = 'An algorithmic cover [%s]' % color
747            prefs = override_prefs(cprefs, override_color_theme=color, override_style=style)
748            scale_cover(prefs, scale)
749            img = generate_cover(mi, prefs=prefs, as_qimage=True)
750            img.setDevicePixelRatio(w.devicePixelRatioF())
751            la = QLabel()
752            la.setPixmap(QPixmap.fromImage(img))
753            l.addWidget(la, r, c)
754            labels.append(la)
755    m.setCentralWidget(sa)
756    w.resize(w.sizeHint())
757    m.show()
758    app.exec()
759
760
761if __name__ == '__main__':
762    test()
763