%PDF- %PDF-
| Direktori : /usr/lib/calibre/calibre/ebooks/pdf/render/ |
| Current File : //usr/lib/calibre/calibre/ebooks/pdf/render/serialize.py |
#!/usr/bin/env python3
__license__ = 'GPL v3'
__copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import hashlib, numbers
from polyglot.builtins import iteritems
from qt.core import QBuffer, QByteArray, QImage, Qt, QColor, qRgba, QPainter
from calibre.constants import (__appname__, __version__)
from calibre.ebooks.pdf.render.common import (
Reference, EOL, serialize, Stream, Dictionary, String, Name, Array,
fmtnum)
from calibre.ebooks.pdf.render.fonts import FontManager
from calibre.ebooks.pdf.render.links import Links
from calibre.utils.date import utcnow
from polyglot.builtins import as_unicode
PDFVER = b'%PDF-1.4' # 1.4 is needed for XMP metadata
class IndirectObjects:
def __init__(self):
self._list = []
self._map = {}
self._offsets = []
def __len__(self):
return len(self._list)
def add(self, o):
self._list.append(o)
ref = Reference(len(self._list), o)
self._map[id(o)] = ref
self._offsets.append(None)
return ref
def commit(self, ref, stream):
self.write_obj(stream, ref.num, ref.obj)
def write_obj(self, stream, num, obj):
stream.write(EOL)
self._offsets[num-1] = stream.tell()
stream.write('%d 0 obj'%num)
stream.write(EOL)
serialize(obj, stream)
if stream.last_char != EOL:
stream.write(EOL)
stream.write('endobj')
stream.write(EOL)
def __getitem__(self, o):
try:
return self._map[id(self._list[o] if isinstance(o, numbers.Integral) else o)]
except (KeyError, IndexError):
raise KeyError('The object %r was not found'%o)
def pdf_serialize(self, stream):
for i, obj in enumerate(self._list):
offset = self._offsets[i]
if offset is None:
self.write_obj(stream, i+1, obj)
def write_xref(self, stream):
self.xref_offset = stream.tell()
stream.write(b'xref'+EOL)
stream.write('0 %d'%(1+len(self._offsets)))
stream.write(EOL)
stream.write('%010d 65535 f '%0)
stream.write(EOL)
for offset in self._offsets:
line = '%010d 00000 n '%offset
stream.write(line.encode('ascii') + EOL)
return self.xref_offset
class Page(Stream):
def __init__(self, parentref, *args, **kwargs):
super().__init__(*args, **kwargs)
self.page_dict = Dictionary({
'Type': Name('Page'),
'Parent': parentref,
})
self.opacities = {}
self.fonts = {}
self.xobjects = {}
self.patterns = {}
def set_opacity(self, opref):
if opref not in self.opacities:
self.opacities[opref] = 'Opa%d'%len(self.opacities)
name = self.opacities[opref]
serialize(Name(name), self)
self.write(b' gs ')
def add_font(self, fontref):
if fontref not in self.fonts:
self.fonts[fontref] = 'F%d'%len(self.fonts)
return self.fonts[fontref]
def add_image(self, imgref):
if imgref not in self.xobjects:
self.xobjects[imgref] = 'Image%d'%len(self.xobjects)
return self.xobjects[imgref]
def add_pattern(self, patternref):
if patternref not in self.patterns:
self.patterns[patternref] = 'Pat%d'%len(self.patterns)
return self.patterns[patternref]
def add_resources(self):
r = Dictionary()
if self.opacities:
extgs = Dictionary()
for opref, name in iteritems(self.opacities):
extgs[name] = opref
r['ExtGState'] = extgs
if self.fonts:
fonts = Dictionary()
for ref, name in iteritems(self.fonts):
fonts[name] = ref
r['Font'] = fonts
if self.xobjects:
xobjects = Dictionary()
for ref, name in iteritems(self.xobjects):
xobjects[name] = ref
r['XObject'] = xobjects
if self.patterns:
r['ColorSpace'] = Dictionary({'PCSp':Array(
[Name('Pattern'), Name('DeviceRGB')])})
patterns = Dictionary()
for ref, name in iteritems(self.patterns):
patterns[name] = ref
r['Pattern'] = patterns
if r:
self.page_dict['Resources'] = r
def end(self, objects, stream):
contents = objects.add(self)
objects.commit(contents, stream)
self.page_dict['Contents'] = contents
self.add_resources()
ret = objects.add(self.page_dict)
# objects.commit(ret, stream)
return ret
class Path:
def __init__(self):
self.ops = []
def move_to(self, x, y):
self.ops.append((x, y, 'm'))
def line_to(self, x, y):
self.ops.append((x, y, 'l'))
def curve_to(self, x1, y1, x2, y2, x, y):
self.ops.append((x1, y1, x2, y2, x, y, 'c'))
def close(self):
self.ops.append(('h',))
class Catalog(Dictionary):
def __init__(self, pagetree):
super().__init__({'Type':Name('Catalog'),
'Pages': pagetree})
class PageTree(Dictionary):
def __init__(self, page_size):
super().__init__({'Type':Name('Pages'),
'MediaBox':Array([0, 0, page_size[0], page_size[1]]),
'Kids':Array(), 'Count':0,
})
def add_page(self, pageref):
self['Kids'].append(pageref)
self['Count'] += 1
def get_ref(self, num):
return self['Kids'][num-1]
def get_num(self, pageref):
try:
return self['Kids'].index(pageref) + 1
except ValueError:
return -1
class HashingStream:
def __init__(self, f):
self.f = f
self.tell = f.tell
self.hashobj = hashlib.sha256()
self.last_char = b''
def write(self, raw):
self.write_raw(raw if isinstance(raw, bytes) else raw.encode('ascii'))
def write_raw(self, raw):
self.f.write(raw)
self.hashobj.update(raw)
if raw:
self.last_char = raw[-1]
class Image(Stream):
def __init__(self, data, w, h, depth, mask, soft_mask, dct):
Stream.__init__(self)
self.width, self.height, self.depth = w, h, depth
self.mask, self.soft_mask = mask, soft_mask
if dct:
self.filters.append(Name('DCTDecode'))
else:
self.compress = True
self.write(data)
def add_extra_keys(self, d):
d['Type'] = Name('XObject')
d['Subtype']= Name('Image')
d['Width'] = self.width
d['Height'] = self.height
if self.depth == 1:
d['ImageMask'] = True
d['Decode'] = Array([1, 0])
else:
d['BitsPerComponent'] = 8
d['ColorSpace'] = Name('Device' + ('RGB' if self.depth == 32 else
'Gray'))
if self.mask is not None:
d['Mask'] = self.mask
if self.soft_mask is not None:
d['SMask'] = self.soft_mask
class Metadata(Stream):
def __init__(self, mi):
Stream.__init__(self)
from calibre.ebooks.metadata.xmp import metadata_to_xmp_packet
self.write(metadata_to_xmp_packet(mi))
def add_extra_keys(self, d):
d['Type'] = Name('Metadata')
d['Subtype'] = Name('XML')
class PDFStream:
PATH_OPS = {
# stroke fill fill-rule
(False, False, 'winding') : 'n',
(False, False, 'evenodd') : 'n',
(False, True, 'winding') : 'f',
(False, True, 'evenodd') : 'f*',
(True, False, 'winding') : 'S',
(True, False, 'evenodd') : 'S',
(True, True, 'winding') : 'B',
(True, True, 'evenodd') : 'B*',
}
def __init__(self, stream, page_size, compress=False, mark_links=False,
debug=print):
self.stream = HashingStream(stream)
self.compress = compress
self.write_line(PDFVER)
self.write_line('%íì¦"'.encode())
creator = ('%s %s [https://calibre-ebook.com]'%(__appname__,
__version__))
self.write_line('%% Created by %s'%creator)
self.objects = IndirectObjects()
self.objects.add(PageTree(page_size))
self.objects.add(Catalog(self.page_tree))
self.current_page = Page(self.page_tree, compress=self.compress)
self.info = Dictionary({
'Creator':String(creator),
'Producer':String(creator),
'CreationDate': utcnow(),
})
self.stroke_opacities, self.fill_opacities = {}, {}
self.font_manager = FontManager(self.objects, self.compress)
self.image_cache = {}
self.pattern_cache, self.shader_cache = {}, {}
self.debug = debug
self.page_size = page_size
self.links = Links(self, mark_links, page_size)
i = QImage(1, 1, QImage.Format.Format_ARGB32)
i.fill(qRgba(0, 0, 0, 255))
self.alpha_bit = i.constBits().asstring(4).find(b'\xff')
self.bw_image_color_table = frozenset((QColor(Qt.GlobalColor.black).rgba(), QColor(Qt.GlobalColor.white).rgba()))
@property
def page_tree(self):
return self.objects[0]
@property
def catalog(self):
return self.objects[1]
def get_pageref(self, pagenum):
return self.page_tree.obj.get_ref(pagenum)
def set_metadata(self, title=None, author=None, tags=None, mi=None):
if title:
self.info['Title'] = String(title)
if author:
self.info['Author'] = String(author)
if tags:
self.info['Keywords'] = String(tags)
if mi is not None:
self.metadata = self.objects.add(Metadata(mi))
self.catalog.obj['Metadata'] = self.metadata
def write_line(self, byts=b''):
byts = byts if isinstance(byts, bytes) else byts.encode('ascii')
self.stream.write(byts + EOL)
def transform(self, *args):
if len(args) == 1:
m = args[0]
vals = [m.m11(), m.m12(), m.m21(), m.m22(), m.dx(), m.dy()]
else:
vals = args
cm = ' '.join(map(fmtnum, vals))
self.current_page.write_line(cm + ' cm')
def save_stack(self):
self.current_page.write_line('q')
def restore_stack(self):
self.current_page.write_line('Q')
def reset_stack(self):
self.current_page.write_line('Q q')
def draw_rect(self, x, y, width, height, stroke=True, fill=False):
self.current_page.write('%s re '%' '.join(map(fmtnum, (x, y, width, height))))
self.current_page.write_line(self.PATH_OPS[(stroke, fill, 'winding')])
def write_path(self, path):
for i, op in enumerate(path.ops):
if i != 0:
self.current_page.write_line()
for x in op:
self.current_page.write(
(fmtnum(x) if isinstance(x, numbers.Number) else x) + ' ')
def draw_path(self, path, stroke=True, fill=False, fill_rule='winding'):
if not path.ops:
return
self.write_path(path)
self.current_page.write_line(self.PATH_OPS[(stroke, fill, fill_rule)])
def add_clip(self, path, fill_rule='winding'):
if not path.ops:
return
self.write_path(path)
op = 'W' if fill_rule == 'winding' else 'W*'
self.current_page.write_line(op + ' ' + 'n')
def serialize(self, o):
serialize(o, self.current_page)
def set_stroke_opacity(self, opacity):
if opacity not in self.stroke_opacities:
op = Dictionary({'Type':Name('ExtGState'), 'CA': opacity})
self.stroke_opacities[opacity] = self.objects.add(op)
self.current_page.set_opacity(self.stroke_opacities[opacity])
def set_fill_opacity(self, opacity):
opacity = float(opacity)
if opacity not in self.fill_opacities:
op = Dictionary({'Type':Name('ExtGState'), 'ca': opacity})
self.fill_opacities[opacity] = self.objects.add(op)
self.current_page.set_opacity(self.fill_opacities[opacity])
def end_page(self, drop_page=False):
if not drop_page:
pageref = self.current_page.end(self.objects, self.stream)
self.page_tree.obj.add_page(pageref)
self.current_page = Page(self.page_tree, compress=self.compress)
def draw_glyph_run(self, transform, size, font_metrics, glyphs):
glyph_ids = {x[-1] for x in glyphs}
fontref = self.font_manager.add_font(font_metrics, glyph_ids)
name = self.current_page.add_font(fontref)
self.current_page.write(b'BT ')
serialize(Name(name), self.current_page)
self.current_page.write(' %s Tf '%fmtnum(size))
self.current_page.write('%s Tm '%' '.join(map(fmtnum, transform)))
for x, y, glyph_id in glyphs:
self.current_page.write_raw(('%s %s Td <%04X> Tj '%(
fmtnum(x), fmtnum(y), glyph_id)).encode('ascii'))
self.current_page.write_line(b' ET')
def get_image(self, cache_key):
return self.image_cache.get(cache_key, None)
def write_image(self, data, w, h, depth, dct=False, mask=None,
soft_mask=None, cache_key=None):
imgobj = Image(data, w, h, depth, mask, soft_mask, dct)
self.image_cache[cache_key] = r = self.objects.add(imgobj)
self.objects.commit(r, self.stream)
return r
def add_jpeg_image(self, img_data, w, h, cache_key=None, depth=32):
return self.write_image(img_data, w, h, depth, dct=True)
def add_image(self, img, cache_key):
ref = self.get_image(cache_key)
if ref is not None:
return ref
fmt = img.format()
image = QImage(img)
if image.depth() == 1 and frozenset(img.colorTable()) == self.bw_image_color_table:
if fmt == QImage.Format.Format_MonoLSB:
image = image.convertToFormat(QImage.Format.Format_Mono)
fmt = QImage.Format.Format_Mono
else:
if (fmt != QImage.Format.Format_RGB32 and fmt != QImage.Format.Format_ARGB32):
image = image.convertToFormat(QImage.Format.Format_ARGB32)
fmt = QImage.Format.Format_ARGB32
w = image.width()
h = image.height()
d = image.depth()
if fmt == QImage.Format.Format_Mono:
bytes_per_line = (w + 7) >> 3
data = image.constBits().asstring(bytes_per_line * h)
return self.write_image(data, w, h, d, cache_key=cache_key)
has_alpha = False
soft_mask = None
if fmt == QImage.Format.Format_ARGB32:
tmask = image.constBits().asstring(4*w*h)[self.alpha_bit::4]
sdata = bytearray(tmask)
vals = set(sdata)
vals.discard(255) # discard opaque pixels
has_alpha = bool(vals)
if has_alpha:
# Blend image onto a white background as otherwise Qt will render
# transparent pixels as black
background = QImage(image.size(), QImage.Format.Format_ARGB32_Premultiplied)
background.fill(Qt.GlobalColor.white)
painter = QPainter(background)
painter.drawImage(0, 0, image)
painter.end()
image = background
ba = QByteArray()
buf = QBuffer(ba)
image.save(buf, 'jpeg', 94)
data = ba.data()
if has_alpha:
soft_mask = self.write_image(tmask, w, h, 8)
return self.write_image(data, w, h, 32, dct=True,
soft_mask=soft_mask, cache_key=cache_key)
def add_pattern(self, pattern):
if pattern.cache_key not in self.pattern_cache:
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):
self.draw_image_with_transform(imgref, scaling=(width, -height), translation=(x, y + height))
def draw_image_with_transform(self, imgref, translation=(0, 0), scaling=(1, 1)):
name = self.current_page.add_image(imgref)
self.current_page.write('q {} 0 0 {} {} {} cm '.format(*(tuple(scaling) + tuple(translation))))
serialize(Name(name), self.current_page)
self.current_page.write_line(' Do Q')
def apply_color_space(self, color, pattern, stroke=False):
wl = self.current_page.write_line
if color is not None and pattern is None:
wl(' '.join(map(fmtnum, color)) + (' RG' if stroke else ' rg'))
elif color is None and pattern is not None:
wl('/Pattern %s /%s %s'%('CS' if stroke else 'cs', pattern,
'SCN' if stroke else 'scn'))
elif color is not None and pattern is not None:
col = ' '.join(map(fmtnum, color))
wl('/PCSp %s %s /%s %s'%('CS' if stroke else 'cs', col, pattern,
'SCN' if stroke else 'scn'))
def apply_fill(self, color=None, pattern=None, opacity=None):
if opacity is not None:
self.set_fill_opacity(opacity)
self.apply_color_space(color, pattern)
def apply_stroke(self, color=None, pattern=None, opacity=None):
if opacity is not None:
self.set_stroke_opacity(opacity)
self.apply_color_space(color, pattern, stroke=True)
def end(self):
if self.current_page.getvalue():
self.end_page()
self.font_manager.embed_fonts(self.debug)
inforef = self.objects.add(self.info)
self.links.add_links()
self.objects.pdf_serialize(self.stream)
self.write_line()
startxref = self.objects.write_xref(self.stream)
file_id = String(as_unicode(self.stream.hashobj.hexdigest()))
self.write_line('trailer')
trailer = Dictionary({'Root':self.catalog, 'Size':len(self.objects)+1,
'ID':Array([file_id, file_id]), 'Info':inforef})
serialize(trailer, self.stream)
self.write_line('startxref')
self.write_line('%d'%startxref)
self.stream.write('%%EOF')