diff --git a/src/calibre/ebooks/pdf/render/engine.py b/src/calibre/ebooks/pdf/render/engine.py index adbeec5293..9b5b12e86b 100644 --- a/src/calibre/ebooks/pdf/render/engine.py +++ b/src/calibre/ebooks/pdf/render/engine.py @@ -82,7 +82,7 @@ class PdfEngine(QPaintEngine): self.bottom_margin) / self.pixel_height self.pdf_system = QTransform(sx, 0, 0, -sy, dx, dy) - self.graphics = Graphics() + self.graphics = Graphics(self.pixel_width, self.pixel_height) self.errors_occurred = False self.errors, self.debug = errors, debug self.fonts = {} diff --git a/src/calibre/ebooks/pdf/render/gradients.py b/src/calibre/ebooks/pdf/render/gradients.py index b614ca128e..a45c716d6f 100644 --- a/src/calibre/ebooks/pdf/render/gradients.py +++ b/src/calibre/ebooks/pdf/render/gradients.py @@ -7,20 +7,179 @@ __license__ = 'GPL v3' __copyright__ = '2013, Kovid Goyal ' __docformat__ = 'restructuredtext en' +import sys +from math import floor, ceil from future_builtins import map -from PyQt4.Qt import (QPointF) +import sip +from PyQt4.Qt import (QPointF, QGradient, QLinearGradient) -from calibre.ebooks.pdf.render.common import Stream +from calibre.ebooks.pdf.render.common import Stream, 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): - pass + start = gradient.start() + stop = gradient.finalStop() + stops = list(map(list, gradient.stops())) + offset = stop - start + spread = gradient.spread() -class LinearGradient(Stream): + 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. - def __init__(self, brush, matrix, pixel_page_width, pixel_page_height): - is_opaque = brush.isOpaque() - gradient = brush.gradient() + 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 + +class LinearGradientPattern(Dictionary): + + def __init__(self, brush, matrix, pdf, pixel_page_width, pixel_page_height): + 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, ( @@ -28,10 +187,20 @@ class LinearGradient(Stream): QPointF(pixel_page_width, pixel_page_height)))) shader = generate_linear_gradient_shader(gradient, page_rect) - alpha_shader = None - if not is_opaque: - alpha_shader = generate_linear_gradient_shader(gradient, page_rect, True) + 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() - shader, alpha_shader + self.shaderref = pdf.add_shader(shader) + d = {} + d['Type'] = Name('Pattern') + d['PatternType'] = 2 + d['Shading'] = self.shaderref + d['Matrix'] = Array(self.matrix) + Dictionary.__init__(self, d) + + self.cache_key = (self.__class__.__name__, self.matrix, + repr(self.shaderref)) diff --git a/src/calibre/ebooks/pdf/render/graphics.py b/src/calibre/ebooks/pdf/render/graphics.py index 7fb87662b4..456194fb0b 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() @@ -280,10 +281,11 @@ class GraphicsState(object): class Graphics(object): - def __init__(self): + def __init__(self, page_width_px, page_height_px): self.base_state = GraphicsState() self.current_state = GraphicsState() self.pending_state = None + self.page_width_px, self.page_height_px = (page_width_px, page_height_px) def begin(self, pdf): self.pdf = pdf @@ -360,7 +362,7 @@ class Graphics(object): pdf = self.pdf pattern = color = pat = None - opacity = 1.0 + opacity = global_opacity do_fill = True matrix = (QTransform.fromTranslate(brush_origin.x(), brush_origin.y()) @@ -369,29 +371,30 @@ class Graphics(object): self.brushobj = None if style <= Qt.DiagCrossPattern: - opacity = global_opacity * vals[-1] + opacity *= vals[-1] color = vals[:3] if style > Qt.SolidPattern: pat = QtPattern(style, matrix) - pattern = pdf.add_pattern(pat) - - if opacity < 1e-4 or style == Qt.NoBrush: - do_fill = False elif style == Qt.TexturePattern: pat = TexturePattern(brush.texture(), matrix, pdf) - opacity = global_opacity if pat.paint_type == 2: opacity *= vals[-1] color = vals[:3] - pattern = pdf.add_pattern(pat) - if opacity < 1e-4 or style == Qt.NoBrush: - do_fill = False + elif False and style == Qt.LinearGradientPattern: + pat = LinearGradientPattern(brush, matrix, pdf, self.page_width_px, + self.page_height_px) + opacity *= pat.const_opacity + # TODO: Add support for radial/conical gradient fills + if opacity < 1e-4 or style == Qt.NoBrush: + do_fill = False self.brushobj = Brush(brush_origin, pat, color) - # TODO: Add support for gradient fills + + if pat is not None: + pattern = pdf.add_pattern(pat) return color, opacity, pattern, do_fill def apply_stroke(self, state, pdf_system, painter): diff --git a/src/calibre/ebooks/pdf/render/serialize.py b/src/calibre/ebooks/pdf/render/serialize.py index b2a17734db..3ef07eebc1 100644 --- a/src/calibre/ebooks/pdf/render/serialize.py +++ b/src/calibre/ebooks/pdf/render/serialize.py @@ -264,7 +264,7 @@ class PDFStream(object): self.stroke_opacities, self.fill_opacities = {}, {} self.font_manager = FontManager(self.objects, self.compress) self.image_cache = {} - self.pattern_cache = {} + self.pattern_cache, self.shader_cache = {}, {} self.debug = debug self.links = Links(self, mark_links, page_size) i = QImage(1, 1, QImage.Format_ARGB32) @@ -447,6 +447,11 @@ class PDFStream(object): self.pattern_cache[pattern.cache_key] = self.objects.add(pattern) return self.current_page.add_pattern(self.pattern_cache[pattern.cache_key]) + def add_shader(self, shader): + if shader.cache_key not in self.shader_cache: + self.shader_cache[shader.cache_key] = self.objects.add(shader) + return self.shader_cache[shader.cache_key] + def draw_image(self, x, y, width, height, imgref): name = self.current_page.add_image(imgref) self.current_page.write('q %s 0 0 %s %s %s cm '%(fmtnum(width), diff --git a/src/calibre/ebooks/pdf/render/test.py b/src/calibre/ebooks/pdf/render/test.py index 866c15f83f..bd210186f6 100644 --- a/src/calibre/ebooks/pdf/render/test.py +++ b/src/calibre/ebooks/pdf/render/test.py @@ -86,10 +86,10 @@ def brush(p, xmax, ymax): x = xmax/3 y = 0 w = xmax/2 - pix = QPixmap(I('console.png')) - p.fillRect(x, y, w, w, QBrush(pix)) - - p.fillRect(0, y+xmax/1.9, w, w, QBrush(pix)) + g = QLinearGradient(QPointF(x, y), QPointF(x+w, y+w)) + g.setColorAt(0, QColor('#00f')) + g.setColorAt(1, QColor('#fff')) + p.fillRect(x, y, w, w, QBrush(g)) def pen(p, xmax, ymax): pix = QPixmap(I('console.png')) @@ -110,7 +110,7 @@ def main(): app tdir = os.path.abspath('.') pdf = os.path.join(tdir, 'painter.pdf') - func = full + func = brush dpi = 100 with open(pdf, 'wb') as f: dev = PdfDevice(f, xdpi=dpi, ydpi=dpi, compress=False)