Linear Gradients now work (modulo non padding spreads)

This commit is contained in:
Kovid Goyal 2013-01-10 00:00:52 +05:30
parent 122552c8ce
commit ddf9932f4c
5 changed files with 68 additions and 187 deletions

View File

@ -65,14 +65,14 @@ def fmtnum(o):
def serialize(o, stream): def serialize(o, stream):
if isinstance(o, float): if isinstance(o, float):
stream.write_raw(pdf_float(o).encode('ascii')) 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)): elif isinstance(o, (int, long)):
stream.write_raw(icb(o)) stream.write_raw(icb(o))
elif hasattr(o, 'pdf_serialize'): elif hasattr(o, 'pdf_serialize'):
o.pdf_serialize(stream) o.pdf_serialize(stream)
elif o is None: elif o is None:
stream.write_raw(b'null') stream.write_raw(b'null')
elif isinstance(o, bool):
stream.write_raw(b'true' if o else b'false')
else: else:
raise ValueError('Unknown object: %r'%o) raise ValueError('Unknown object: %r'%o)

View File

@ -52,7 +52,6 @@ class PdfEngine(QPaintEngine):
FEATURES = QPaintEngine.AllFeatures & ~( FEATURES = QPaintEngine.AllFeatures & ~(
QPaintEngine.PorterDuff | QPaintEngine.PerspectiveTransform QPaintEngine.PorterDuff | QPaintEngine.PerspectiveTransform
| QPaintEngine.ObjectBoundingModeGradients | QPaintEngine.ObjectBoundingModeGradients
| QPaintEngine.LinearGradientFill
| QPaintEngine.RadialGradientFill | QPaintEngine.RadialGradientFill
| QPaintEngine.ConicalGradientFill | QPaintEngine.ConicalGradientFill
) )

View File

@ -7,172 +7,15 @@ __license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import sys
from math import floor, ceil
from future_builtins import map from future_builtins import map
from collections import namedtuple
import sip 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): Stop = namedtuple('Stop', 't color')
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
class LinearGradientPattern(Dictionary): class LinearGradientPattern(Dictionary):
@ -180,27 +23,64 @@ class LinearGradientPattern(Dictionary):
self.matrix = (matrix.m11(), matrix.m12(), matrix.m21(), matrix.m22(), self.matrix = (matrix.m11(), matrix.m12(), matrix.m21(), matrix.m22(),
matrix.dx(), matrix.dy()) matrix.dx(), matrix.dy())
gradient = sip.cast(brush.gradient(), QLinearGradient) gradient = sip.cast(brush.gradient(), QLinearGradient)
inv = matrix.inverted()[0]
page_rect = tuple(map(inv.map, ( # TODO: Handle spreads other than PadSpread by adding more stops to
QPointF(0, 0), QPointF(pixel_page_width, 0), QPointF(0, pixel_page_height), # cover the entire page_rect
QPointF(pixel_page_width, pixel_page_height)))) # 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) start = gradient.start()
self.const_opacity = 1.0 stop = gradient.finalStop()
if not brush.isOpaque(): stops = tuple(map(lambda x: Stop(x[0], x[1].getRgbF()), gradient.stops()))
# TODO: Handle colors with different opacities in the gradient
self.const_opacity = gradient.stops()[0][1].alphaF()
self.shaderref = pdf.add_shader(shader) # TODO: Handle colors with different opacities
self.const_opacity = stops[0].color[-1]
d = {} funcs = Array()
d['Type'] = Name('Pattern') bounds = Array()
d['PatternType'] = 2 encode = Array()
d['Shading'] = self.shaderref
d['Matrix'] = Array(self.matrix) for i, current_stop in enumerate(stops):
Dictionary.__init__(self, d) 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, self.cache_key = (self.__class__.__name__, self.matrix,
repr(self.shaderref)) tuple(shader['Coords']), stops)

View File

@ -16,6 +16,7 @@ from PyQt4.Qt import (
from calibre.ebooks.pdf.render.common import ( from calibre.ebooks.pdf.render.common import (
Name, Array, fmtnum, Stream, Dictionary) Name, Array, fmtnum, Stream, Dictionary)
from calibre.ebooks.pdf.render.serialize import Path from calibre.ebooks.pdf.render.serialize import Path
from calibre.ebooks.pdf.render.gradients import LinearGradientPattern
def convert_path(path): # {{{ def convert_path(path): # {{{
p = Path() p = Path()
@ -382,8 +383,7 @@ class Graphics(object):
opacity *= vals[-1] opacity *= vals[-1]
color = vals[:3] color = vals[:3]
elif False and style == Qt.LinearGradientPattern: elif style == Qt.LinearGradientPattern:
from calibre.ebooks.pdf.render.gradients import LinearGradientPattern
pat = LinearGradientPattern(brush, matrix, pdf, self.page_width_px, pat = LinearGradientPattern(brush, matrix, pdf, self.page_width_px,
self.page_height_px) self.page_height_px)
opacity *= pat.const_opacity opacity *= pat.const_opacity

View File

@ -83,13 +83,15 @@ def run(dev, func):
raise SystemExit(1) raise SystemExit(1)
def brush(p, xmax, ymax): def brush(p, xmax, ymax):
x = xmax/3 x = 0
y = 0 y = 0
w = xmax/2 w = xmax/2
g = QLinearGradient(QPointF(x, y), QPointF(x+w, y+w)) g = QLinearGradient(QPointF(x, y), QPointF(x, y+w))
g.setColorAt(0, QColor('#00f')) g.setColorAt(0, QColor('#f00'))
g.setColorAt(1, QColor('#fff')) g.setColorAt(0.5, QColor('#fff'))
g.setColorAt(1, QColor('#00f'))
p.fillRect(x, y, w, w, QBrush(g)) p.fillRect(x, y, w, w, QBrush(g))
p.drawRect(x, y, w, w)
def pen(p, xmax, ymax): def pen(p, xmax, ymax):
pix = QPixmap(I('console.png')) pix = QPixmap(I('console.png'))