From 4eae90e5c29434d170af00b563d09ff87f60ef79 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 25 Dec 2012 15:45:23 +0530 Subject: [PATCH] Fix text rendering by using private Qt APIs --- session.vim | 1 + setup/extensions.py | 10 ++ src/calibre/constants.py | 1 + src/calibre/ebooks/pdf/render/engine.py | 139 +++++++--------------- src/calibre/ebooks/pdf/render/qt_hack.cpp | 66 ++++++++++ src/calibre/ebooks/pdf/render/qt_hack.h | 34 ++++++ src/calibre/ebooks/pdf/render/qt_hack.sip | 28 +++++ src/calibre/utils/fonts/sfnt/container.py | 8 +- 8 files changed, 190 insertions(+), 97 deletions(-) create mode 100644 src/calibre/ebooks/pdf/render/qt_hack.cpp create mode 100644 src/calibre/ebooks/pdf/render/qt_hack.h create mode 100644 src/calibre/ebooks/pdf/render/qt_hack.sip diff --git a/session.vim b/session.vim index 9bcbbe7800..54c269978f 100644 --- a/session.vim +++ b/session.vim @@ -12,6 +12,7 @@ let g:syntastic_cpp_include_dirs = [ \'/usr/include/fontconfig', \'src/qtcurve/common', 'src/qtcurve', \'src/unrar', + \'src/qt-harfbuzz/src', \'/usr/include/ImageMagick', \] let g:syntastic_c_include_dirs = g:syntastic_cpp_include_dirs diff --git a/setup/extensions.py b/setup/extensions.py index c167916afb..8983063d55 100644 --- a/setup/extensions.py +++ b/setup/extensions.py @@ -183,6 +183,13 @@ extensions = [ sip_files = ['calibre/gui2/progress_indicator/QProgressIndicator.sip'] ), + Extension('qt_hack', + ['calibre/ebooks/pdf/render/qt_hack.cpp'], + inc_dirs = ['calibre/ebooks/pdf/render', 'qt-harfbuzz/src'], + headers = ['calibre/ebooks/pdf/render/qt_hack.h'], + sip_files = ['calibre/ebooks/pdf/render/qt_hack.sip'] + ), + Extension('unrar', ['unrar/%s.cpp'%(x.partition('.')[0]) for x in ''' rar.o strlist.o strfn.o pathfn.o savepos.o smallfn.o global.o file.o @@ -545,6 +552,9 @@ class Build(Command): VERSION = 1.0.0 CONFIG += %s ''')%(ext.name, ' '.join(ext.headers), ' '.join(ext.sources), archs) + if ext.inc_dirs: + idir = ' '.join(ext.inc_dirs) + pro += 'INCLUDEPATH = %s\n'%idir pro = pro.replace('\\', '\\\\') open(ext.name+'.pro', 'wb').write(pro) qmc = [QMAKE, '-o', 'Makefile'] diff --git a/src/calibre/constants.py b/src/calibre/constants.py index 613c280176..1dee51fd6a 100644 --- a/src/calibre/constants.py +++ b/src/calibre/constants.py @@ -100,6 +100,7 @@ class Plugins(collections.Mapping): 'freetype', 'woff', 'unrar', + 'qt_hack', ] if iswindows: plugins.extend(['winutil', 'wpd', 'winfonts']) diff --git a/src/calibre/ebooks/pdf/render/engine.py b/src/calibre/ebooks/pdf/render/engine.py index 723cff7a89..5f1d6b9602 100644 --- a/src/calibre/ebooks/pdf/render/engine.py +++ b/src/calibre/ebooks/pdf/render/engine.py @@ -7,15 +7,17 @@ __license__ = 'GPL v3' __copyright__ = '2012, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import sys, traceback, unicodedata +import sys, traceback from math import sqrt from collections import namedtuple -from functools import wraps +from functools import wraps, partial +import sip from PyQt4.Qt import (QPaintEngine, QPaintDevice, Qt, QApplication, QPainter, - QTransform, QPainterPath, QTextOption, QTextLayout, - QImage, QByteArray, QBuffer, qRgba) + QTransform, QPainterPath, QImage, QByteArray, QBuffer, + qRgba) +from calibre.constants import plugins from calibre.ebooks.pdf.render.serialize import (Color, PDFStream, Path) from calibre.ebooks.pdf.render.common import inch, A4 from calibre.utils.fonts.sfnt.container import Sfnt @@ -215,14 +217,15 @@ class PdfEngine(QPaintEngine): self.graphics_state = GraphicsState() self.errors_occurred = False self.errors, self.debug = errors, debug - self.text_option = QTextOption() - self.text_option.setWrapMode(QTextOption.NoWrap) self.fonts = {} i = QImage(1, 1, QImage.Format_ARGB32) i.fill(qRgba(0, 0, 0, 255)) self.alpha_bit = i.constBits().asstring(4).find(b'\xff') self.current_page_num = 1 self.current_page_inited = False + self.qt_hack, err = plugins['qt_hack'] + if err: + raise RuntimeError('Failed to load qt_hack with err: %s'%err) def init_page(self): self.pdf.transform(self.pdf_system) @@ -421,98 +424,48 @@ class PdfEngine(QPaintEngine): self.pdf.draw_rect(bl.x(), bl.y(), rect.width(), rect.height(), stroke=self.do_stroke, fill=self.do_fill) - def get_text_layout(self, text_item, text): - tl = QTextLayout(text, text_item.font(), self.paintDevice()) - self.text_option.setTextDirection(Qt.RightToLeft if - text_item.renderFlags() & text_item.RightToLeft else Qt.LeftToRight) - tl.setTextOption(self.text_option) - return tl - - def update_glyph_map(self, text, indices, text_item, glyph_map): - ''' - Map glyphs back to the unicode text they represent. - ''' - pos = 0 - tl = self.get_text_layout(text_item, '') - indices = list(indices) - - def get_glyphs(string): - tl.setText(string) - tl.beginLayout() - line = tl.createLine() - if not line.isValid(): - tl.endLayout() - return [] - line.setLineWidth(int(1e12)) - tl.endLayout() - ans = [] - for run in tl.glyphRuns(): - ans.extend(run.glyphIndexes()) - return ans - - ipos = 0 - while ipos < len(indices): - if indices[ipos] in glyph_map: - t = glyph_map[indices[ipos]] - if t == text[pos:pos+len(t)]: - pos += len(t) - ipos += 1 - continue - - found = False - for l in xrange(1, 10): - string = text[pos:pos+l] - g = get_glyphs(string) - if g and g[0] == indices[ipos]: - found = True - glyph_map[g[0]] = string - break - if not found: - self.debug( - 'Failed to find glyph->unicode mapping for text: %s'%text) - break - ipos += 1 - pos += l - - return text[pos:] + def create_sfnt(self, text_item): + get_table = partial(self.qt_hack.get_sfnt_table, text_item) + ans = Font(Sfnt(get_table)) + glyph_map = self.qt_hack.get_glyph_map(text_item) + gm = {} + for uc, glyph_id in enumerate(glyph_map): + if glyph_id not in gm: + gm[glyph_id] = unichr(uc) + ans.full_glyph_map = gm + return ans @store_error def drawTextItem(self, point, text_item): # super(PdfEngine, self).drawTextItem(point, text_item) self.graphics_state(self) - text = type(u'')(text_item.text()).replace('\n', ' ') - text = unicodedata.normalize('NFKC', text) - tl = self.get_text_layout(text_item, text) - tl.setPosition(point) - tl.beginLayout() - line = tl.createLine() - if not line.isValid(): - tl.endLayout() + gi = self.qt_hack.get_glyphs(point, text_item) + if not gi.indices: + sip.delete(gi) return - line.setLineWidth(int(1e12)) - tl.endLayout() - for run in tl.glyphRuns(): - rf = run.rawFont() - name = hash(bytes(rf.fontTable('name'))) - if name not in self.fonts: - self.fonts[name] = Font(Sfnt(rf)) - metrics = self.fonts[name] - indices = run.glyphIndexes() - text = self.update_glyph_map(text, indices, text_item, metrics.glyph_map) - glyphs = [] - pdf_pos = point - first_baseline = None - for i, pos in enumerate(run.positions()): - if first_baseline is None: - first_baseline = pos.y() - glyph_pos = point + pos - delta = glyph_pos - pdf_pos - glyphs.append((delta.x(), pos.y()-first_baseline, indices[i])) - pdf_pos = glyph_pos - - self.pdf.draw_glyph_run([1, 0, 0, -1, point.x(), - point.y()], rf.pixelSize(), metrics, glyphs) + name = hash(bytes(gi.name)) + if name not in self.fonts: + self.fonts[name] = self.create_sfnt(text_item) + metrics = self.fonts[name] + for glyph_id in gi.indices: + try: + metrics.glyph_map[glyph_id] = metrics.full_glyph_map[glyph_id] + except (KeyError, ValueError): + pass + glyphs = [] + pdf_pos = point + first_baseline = None + for i, pos in enumerate(gi.positions): + if first_baseline is None: + first_baseline = pos.y() + glyph_pos = pos + delta = glyph_pos - pdf_pos + glyphs.append((delta.x(), pos.y()-first_baseline, gi.indices[i])) + pdf_pos = glyph_pos + self.pdf.draw_glyph_run([1, 0, 0, -1, point.x(), + point.y()], gi.size, metrics, glyphs) + sip.delete(gi) @store_error def drawPolygon(self, points, mode): @@ -645,12 +598,12 @@ if __name__ == '__main__': # f.setUnderline(True) # f.setOverline(True) # f.setStrikeOut(True) - f.setFamily('DejaVu Sans') + f.setFamily('Calibri') p.setFont(f) # p.setPen(QColor(0, 0, 255)) # p.scale(2, 2) # p.rotate(45) - p.drawText(QPoint(0, 300), 'Some—text not By’s ū --- Д AV ff ff') + p.drawText(QPoint(300, 300), 'Some—text not By’s ū --- Д AV ff ff') finally: p.end() if dev.engine.errors_occurred: diff --git a/src/calibre/ebooks/pdf/render/qt_hack.cpp b/src/calibre/ebooks/pdf/render/qt_hack.cpp new file mode 100644 index 0000000000..f68f40c921 --- /dev/null +++ b/src/calibre/ebooks/pdf/render/qt_hack.cpp @@ -0,0 +1,66 @@ +/* + * qt_hack.cpp + * Copyright (C) 2012 Kovid Goyal + * + * Distributed under terms of the GPL3 license. + */ + +#include "qt_hack.h" + +#include + +#include "private/qtextengine_p.h" +#include "private/qfontengine_p.h" + +GlyphInfo* get_glyphs(QPointF &p, const QTextItem &text_item) { + QTextItemInt ti = static_cast(text_item); + QFontEngine *fe = ti.fontEngine; + qreal size = ti.fontEngine->fontDef.pixelSize; +#ifdef Q_WS_WIN + if (ti.fontEngine->type() == QFontEngine::Win) { + QFontEngineWin *fe = static_cast(ti.fontEngine); + size = fe->tm.tmHeight; + } +#endif + QVarLengthArray glyphs; + QVarLengthArray positions; + QTransform m = QTransform::fromTranslate(p.x(), p.y()); + fe->getGlyphPositions(ti.glyphs, m, ti.flags, glyphs, positions); + QVector points = QVector(positions.count()); + for (int i = 0; i < positions.count(); i++) { + points[i].setX(positions[i].x.toReal()); + points[i].setY(positions[i].y.toReal()); + } + + QVector indices = QVector(glyphs.count()); + for (int i = 0; i < glyphs.count(); i++) + indices[i] = (unsigned int)glyphs[i]; + + const quint32 *tag = reinterpret_cast("name"); + + return new GlyphInfo(fe->getSfntTable(qToBigEndian(*tag)), size, points, indices); +} + +GlyphInfo::GlyphInfo(const QByteArray& name, qreal size, const QVector &positions, const QVector &indices) :name(name), positions(positions), size(size), indices(indices) { +} + +QByteArray get_sfnt_table(const QTextItem &text_item, const char* tag_name) { + QTextItemInt ti = static_cast(text_item); + const quint32 *tag = reinterpret_cast(tag_name); + return ti.fontEngine->getSfntTable(qToBigEndian(*tag)); +} + +QVector* get_glyph_map(const QTextItem &text_item) { + QTextItemInt ti = static_cast(text_item); + QVector *ans = new QVector(0x10000); + QGlyphLayoutArray<10> glyphs; + int nglyphs = 10; + + for (uint uc = 0; uc < 0x10000; ++uc) { + QChar ch(uc); + ti.fontEngine->stringToCMap(&ch, 1, &glyphs, &nglyphs, QTextEngine::GlyphIndicesOnly); + (*ans)[uc] = glyphs.glyphs[0]; + } + return ans; +} + diff --git a/src/calibre/ebooks/pdf/render/qt_hack.h b/src/calibre/ebooks/pdf/render/qt_hack.h new file mode 100644 index 0000000000..d1cb5e208d --- /dev/null +++ b/src/calibre/ebooks/pdf/render/qt_hack.h @@ -0,0 +1,34 @@ +/* + * qt_hack.h + * Copyright (C) 2012 Kovid Goyal + * + * Distributed under terms of the GPL3 license. + */ + +#pragma once + +#include +#include +#include + + +class GlyphInfo { + public: + QByteArray name; + QVector positions; + qreal size; + QVector indices; + + GlyphInfo(const QByteArray &name, qreal size, const QVector &positions, const QVector &indices); + + private: + GlyphInfo(const GlyphInfo&); + GlyphInfo &operator=(const GlyphInfo&); +}; + +GlyphInfo* get_glyphs(QPointF &p, const QTextItem &text_item); + +QByteArray get_sfnt_table(const QTextItem &text_item, const char* tag_name); + +QVector* get_glyph_map(const QTextItem &text_item); + diff --git a/src/calibre/ebooks/pdf/render/qt_hack.sip b/src/calibre/ebooks/pdf/render/qt_hack.sip new file mode 100644 index 0000000000..b5a6fcf55e --- /dev/null +++ b/src/calibre/ebooks/pdf/render/qt_hack.sip @@ -0,0 +1,28 @@ +//Define the SIP wrapper to the qt_hack code +//Author - Kovid Goyal + +%Module(name=qt_hack, version=1) + +%Import QtCore/QtCoremod.sip +%Import QtGui/QtGuimod.sip + +class GlyphInfo { +%TypeHeaderCode +#include +%End +public: + QByteArray name; + qreal size; + QVector &positions; + QVector indices; + GlyphInfo(const QByteArray &name, qreal size, const QVector &positions, const QVector &indices); +private: + GlyphInfo(const GlyphInfo& g); + +}; + +GlyphInfo* get_glyphs(QPointF &p, const QTextItem &text_item); + +QByteArray get_sfnt_table(const QTextItem &text_item, const char* tag_name); + +QVector* get_glyph_map(const QTextItem &text_item); diff --git a/src/calibre/utils/fonts/sfnt/container.py b/src/calibre/utils/fonts/sfnt/container.py index 4514721d2b..932cd6a3d2 100644 --- a/src/calibre/utils/fonts/sfnt/container.py +++ b/src/calibre/utils/fonts/sfnt/container.py @@ -44,10 +44,10 @@ class Sfnt(object): b'post' : PostTable, } - def __init__(self, raw_or_qrawfont): + def __init__(self, raw_or_get_table): self.tables = {} - if isinstance(raw_or_qrawfont, bytes): - raw = raw_or_qrawfont + if isinstance(raw_or_get_table, bytes): + raw = raw_or_get_table self.sfnt_version = raw[:4] if self.sfnt_version not in {b'\x00\x01\x00\x00', b'OTTO', b'true', b'type1'}: @@ -62,7 +62,7 @@ class Sfnt(object): b'VORG', b'EBDT', b'EBLC', b'EBSC', b'BASE', b'GSUB', b'GPOS', b'GDEF', b'JSTF', b'gasp', b'hdmx', b'kern', b'LTSH', b'PCLT', b'VDMX', b'vhea', b'vmtx', b'MATH'}: - table = bytes(raw_or_qrawfont.fontTable(table_tag)) + table = bytes(raw_or_get_table(table_tag)) if table: self.tables[table_tag] = self.TABLE_MAP.get( table_tag, UnknownTable)(table)