From ddf9932f4c42202deb9fe5419dc5e70703435b69 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 10 Jan 2013 00:00:52 +0530 Subject: [PATCH] Linear Gradients now work (modulo non padding spreads) --- src/calibre/ebooks/pdf/render/common.py | 4 +- src/calibre/ebooks/pdf/render/engine.py | 1 - src/calibre/ebooks/pdf/render/gradients.py | 236 +++++---------------- src/calibre/ebooks/pdf/render/graphics.py | 4 +- src/calibre/ebooks/pdf/render/test.py | 10 +- 5 files changed, 68 insertions(+), 187 deletions(-) diff --git a/src/calibre/ebooks/pdf/render/common.py b/src/calibre/ebooks/pdf/render/common.py index 03774e2d69..263ea961e5 100644 --- a/src/calibre/ebooks/pdf/render/common.py +++ b/src/calibre/ebooks/pdf/render/common.py @@ -65,14 +65,14 @@ def fmtnum(o): def serialize(o, stream): if isinstance(o, float): stream.write_raw(pdf_float(o).encode('ascii')) + elif isinstance(o, bool): + stream.write_raw(b'true' if o else b'false') elif isinstance(o, (int, long)): stream.write_raw(icb(o)) elif hasattr(o, 'pdf_serialize'): o.pdf_serialize(stream) elif o is None: stream.write_raw(b'null') - elif isinstance(o, bool): - stream.write_raw(b'true' if o else b'false') else: raise ValueError('Unknown object: %r'%o) diff --git a/src/calibre/ebooks/pdf/render/engine.py b/src/calibre/ebooks/pdf/render/engine.py index 9b5b12e86b..1be8613cea 100644 --- a/src/calibre/ebooks/pdf/render/engine.py +++ b/src/calibre/ebooks/pdf/render/engine.py @@ -52,7 +52,6 @@ class PdfEngine(QPaintEngine): FEATURES = QPaintEngine.AllFeatures & ~( QPaintEngine.PorterDuff | QPaintEngine.PerspectiveTransform | QPaintEngine.ObjectBoundingModeGradients - | QPaintEngine.LinearGradientFill | QPaintEngine.RadialGradientFill | QPaintEngine.ConicalGradientFill ) diff --git a/src/calibre/ebooks/pdf/render/gradients.py b/src/calibre/ebooks/pdf/render/gradients.py index a45c716d6f..917fa642be 100644 --- a/src/calibre/ebooks/pdf/render/gradients.py +++ b/src/calibre/ebooks/pdf/render/gradients.py @@ -7,172 +7,15 @@ __license__ = 'GPL v3' __copyright__ = '2013, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import sys -from math import floor, ceil from future_builtins import map +from collections import namedtuple import sip -from PyQt4.Qt import (QPointF, QGradient, QLinearGradient) +from PyQt4.Qt import QLinearGradient -from calibre.ebooks.pdf.render.common import Stream, Name, Array, Dictionary +from calibre.ebooks.pdf.render.common import Name, Array, Dictionary -def write_triple(data, val): - data.write(bytes(bytearray(( - (val >> 16) & 0xff, (val >> 8) & 0xff, val & 0xff)))) - -def write_byte(data, val): - data.write(bytes(bytearray([val&0xff]))) - -def write_triangle_line(data, xpos, ypos, xoff, yoff, rgb, flag, alpha): - for xo, yo in ( (0, 0), (xoff, yoff) ): - write_byte(data, flag) - write_triple(data, xpos+xo) - write_triple(data, ypos+yo) - if alpha: - write_byte(data, rgb[-1]) - else: - for x in rgb[:3]: - write_byte(data, x) - -class LinearShader(Stream): - - def __init__(self, is_transparent, xmin, xmax, ymin, ymax): - Stream.__init__(self, compress=False) - self.is_transparent = is_transparent - self.xmin, self.xmax, self.ymin, self.ymax = (xmin, xmax, ymin, ymax) - self.cache_key = None - - def add_extra_keys(self, d): - d['ShadingType'] = 4 - d['ColorSpace'] = Name('DeviceGray' if self.is_transparent else 'DeviceRGB') - d['AntiAlias'] = True - d['BitsPerCoordinate'] = 24 - d['BitsPerComponent'] = 8 - d['BitsPerFlag'] = 8 - a = ([0, 1] if self.is_transparent else [0, 1, 0, 1, 0, 1]) - d['Decode'] = Array([self.xmin, self.xmax, self.ymin, self.ymax]+a) - d['AntiAlias'] = True - -def generate_linear_gradient_shader(gradient, page_rect, is_transparent=False): - start = gradient.start() - stop = gradient.finalStop() - stops = list(map(list, gradient.stops())) - offset = stop - start - spread = gradient.spread() - - if gradient.spread() == QGradient.ReflectSpread: - offset *= 2 - for i in xrange(len(stops) - 2, -1, -1): - s = stops[i] - s[0] = 2. - s[0] - stops.append(s) - for i in xrange(len(stops)): - stops[i][0] /= 2. - - orthogonal = QPointF(offset.y(), -offset.x()) - length = offset.x()*offset.x() + offset.y()*offset.y() - - # find the max and min values in offset and orth direction that are needed to cover - # the whole page - off_min = sys.maxint - off_max = -sys.maxint - 1 - ort_min = sys.maxint - ort_max = -sys.maxint - 1 - for i in xrange(4): - off = ((page_rect[i].x() - start.x()) * offset.x() + (page_rect[i].y() - start.y()) * offset.y())/length - ort = ((page_rect[i].x() - start.x()) * orthogonal.x() + (page_rect[i].y() - start.y()) * orthogonal.y())/length - off_min = min(off_min, int(floor(off))) - off_max = max(off_max, int(ceil(off))) - ort_min = min(ort_min, ort) - ort_max = max(ort_max, ort) - ort_min -= 1 - ort_max += 1 - - start += off_min * offset + ort_min * orthogonal - orthogonal *= (ort_max - ort_min) - num = off_max - off_min - - gradient_rect = [ start, start + orthogonal, start + num*offset, start + - num*offset + orthogonal] - - xmin = gradient_rect[0].x() - xmax = gradient_rect[0].x() - ymin = gradient_rect[0].y() - ymax = gradient_rect[0].y() - for i in xrange(1, 4): - xmin = min(xmin, gradient_rect[i].x()) - xmax = max(xmax, gradient_rect[i].x()) - ymin = min(ymin, gradient_rect[i].y()) - ymax = max(ymax, gradient_rect[i].y()) - xmin -= 1000 - xmax += 1000 - ymin -= 1000 - ymax += 1000 - start -= QPointF(xmin, ymin) - factor_x = float(1<<24)/(xmax - xmin) - factor_y = float(1<<24)/(ymax - ymin) - xoff = int(orthogonal.x()*factor_x) - yoff = int(orthogonal.y()*factor_y) - - triangles = LinearShader(is_transparent, xmin, xmax, ymin, ymax) - if spread == QGradient.PadSpread: - if (off_min > 0 or off_max < 1): - # linear gradient outside of page - current_stop = stops[len(stops)-1] if off_min > 0 else stops[0] - rgb = current_stop[1].getRgb() - xpos = int(start.x()*factor_x) - ypos = int(start.y()*factor_y) - write_triangle_line(triangles, xpos, ypos, xoff, yoff, rgb, 0, - is_transparent) - start += num*offset - xpos = int(start.x()*factor_x) - ypos = int(start.y()*factor_y) - write_triangle_line(triangles, xpos, ypos, xoff, yoff, rgb, 1, - is_transparent) - else: - flag = 0 - if off_min < 0: - rgb = stops[0][1].getRgb() - xpos = int(start.x()*factor_x) - ypos = int(start.y()*factor_y) - write_triangle_line(triangles, xpos, ypos, xoff, yoff, rgb, flag, - is_transparent) - start -= off_min*offset - flag = 1 - for s, current_stop in enumerate(stops): - rgb = current_stop[1].getRgb() - xpos = int(start.x()*factor_x) - ypos = int(start.y()*factor_y) - write_triangle_line(triangles, xpos, ypos, xoff, yoff, rgb, flag, - is_transparent) - if s < len(stops)-1: - start += offset*(stops[s+1][0] - stops[s][0]) - flag = 1 - - if off_max > 1: - start += (off_max - 1)*offset - rgb = stops[len(stops)-1][1].getRgb() - xpos = int(start.x()*factor_x) - ypos = int(start.y()*factor_y) - write_triangle_line(triangles, xpos, ypos, xoff, yoff, rgb, flag, - is_transparent); - - else: - for i in xrange(num): - flag = 0 - for s in xrange(len(stops)): - rgb = stops[s][1].getRgb() - xpos = int(start.x()*factor_x) - ypos = int(start.y()*factor_y) - write_triangle_line(triangles, xpos, ypos, xoff, yoff, rgb, flag, - is_transparent) - if s < len(stops)-1: - start += offset*(stops[s+1][0] - stops[s][0]) - flag = 1 - - t = triangles - t.cache_key = (t.xmin, t.xmax, t.ymin, t.ymax, t.is_transparent, hash(t.getvalue())) - return triangles +Stop = namedtuple('Stop', 't color') class LinearGradientPattern(Dictionary): @@ -180,27 +23,64 @@ class LinearGradientPattern(Dictionary): self.matrix = (matrix.m11(), matrix.m12(), matrix.m21(), matrix.m22(), matrix.dx(), matrix.dy()) gradient = sip.cast(brush.gradient(), QLinearGradient) - inv = matrix.inverted()[0] - page_rect = tuple(map(inv.map, ( - QPointF(0, 0), QPointF(pixel_page_width, 0), QPointF(0, pixel_page_height), - QPointF(pixel_page_width, pixel_page_height)))) + # TODO: Handle spreads other than PadSpread by adding more stops to + # cover the entire page_rect + # inv = matrix.inverted()[0] + # page_rect = tuple(map(inv.map, ( + # QPointF(0, 0), QPointF(pixel_page_width, 0), QPointF(0, pixel_page_height), + # QPointF(pixel_page_width, pixel_page_height)))) - shader = generate_linear_gradient_shader(gradient, page_rect) - self.const_opacity = 1.0 - if not brush.isOpaque(): - # TODO: Handle colors with different opacities in the gradient - self.const_opacity = gradient.stops()[0][1].alphaF() + start = gradient.start() + stop = gradient.finalStop() + stops = tuple(map(lambda x: Stop(x[0], x[1].getRgbF()), gradient.stops())) - self.shaderref = pdf.add_shader(shader) + # TODO: Handle colors with different opacities + self.const_opacity = stops[0].color[-1] - d = {} - d['Type'] = Name('Pattern') - d['PatternType'] = 2 - d['Shading'] = self.shaderref - d['Matrix'] = Array(self.matrix) - Dictionary.__init__(self, d) + funcs = Array() + bounds = Array() + encode = Array() + + for i, current_stop in enumerate(stops): + if i < len(stops) - 1: + next_stop = stops[i+1] + func = Dictionary({ + 'FunctionType': 2, + 'Domain': Array([0, 1]), + 'C0': Array(current_stop.color[:3]), + 'C1': Array(next_stop.color[:3]), + 'N': 1, + }) + funcs.append(func) + encode.extend((0, 1)) + if i+1 < len(stops) - 1: + bounds.append(next_stop.t) + + func = Dictionary({ + 'FunctionType': 3, + 'Domain': Array([stops[0].t, stops[-1].t]), + 'Functions': funcs, + 'Bounds': bounds, + 'Encode': encode, + }) + + shader = Dictionary({ + 'ShadingType': 2, + 'ColorSpace': Name('DeviceRGB'), + 'AntiAlias': True, + 'Coords': Array([start.x(), start.y(), stop.x(), stop.y()]), + 'Function': func, + 'Extend': Array([True, True]), + }) + + Dictionary.__init__(self, { + 'Type': Name('Pattern'), + 'PatternType': 2, + 'Shading': shader, + 'Matrix': Array(self.matrix), + }) self.cache_key = (self.__class__.__name__, self.matrix, - repr(self.shaderref)) + tuple(shader['Coords']), stops) diff --git a/src/calibre/ebooks/pdf/render/graphics.py b/src/calibre/ebooks/pdf/render/graphics.py index 778ed54eed..25e23fcd0b 100644 --- a/src/calibre/ebooks/pdf/render/graphics.py +++ b/src/calibre/ebooks/pdf/render/graphics.py @@ -16,6 +16,7 @@ from PyQt4.Qt import ( from calibre.ebooks.pdf.render.common import ( Name, Array, fmtnum, Stream, Dictionary) from calibre.ebooks.pdf.render.serialize import Path +from calibre.ebooks.pdf.render.gradients import LinearGradientPattern def convert_path(path): # {{{ p = Path() @@ -382,8 +383,7 @@ class Graphics(object): opacity *= vals[-1] color = vals[:3] - elif False and style == Qt.LinearGradientPattern: - from calibre.ebooks.pdf.render.gradients import LinearGradientPattern + elif style == Qt.LinearGradientPattern: pat = LinearGradientPattern(brush, matrix, pdf, self.page_width_px, self.page_height_px) opacity *= pat.const_opacity diff --git a/src/calibre/ebooks/pdf/render/test.py b/src/calibre/ebooks/pdf/render/test.py index bd210186f6..d999fb378e 100644 --- a/src/calibre/ebooks/pdf/render/test.py +++ b/src/calibre/ebooks/pdf/render/test.py @@ -83,13 +83,15 @@ def run(dev, func): raise SystemExit(1) def brush(p, xmax, ymax): - x = xmax/3 + x = 0 y = 0 w = xmax/2 - g = QLinearGradient(QPointF(x, y), QPointF(x+w, y+w)) - g.setColorAt(0, QColor('#00f')) - g.setColorAt(1, QColor('#fff')) + g = QLinearGradient(QPointF(x, y), QPointF(x, y+w)) + g.setColorAt(0, QColor('#f00')) + g.setColorAt(0.5, QColor('#fff')) + g.setColorAt(1, QColor('#00f')) p.fillRect(x, y, w, w, QBrush(g)) + p.drawRect(x, y, w, w) def pen(p, xmax, ymax): pix = QPixmap(I('console.png'))