From 5a4d164347b4e04d38d4ace5d9064d70d664180f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 2 Oct 2012 13:36:16 +0530 Subject: [PATCH] PDF Output: Handle embedded fonts better on linux --- .../ebooks/conversion/plugins/pdf_output.py | 113 +++++++++++++----- src/calibre/utils/fonts/win_fonts.py | 17 ++- src/calibre/utils/fonts/winfonts.cpp | 54 +++++++-- 3 files changed, 144 insertions(+), 40 deletions(-) diff --git a/src/calibre/ebooks/conversion/plugins/pdf_output.py b/src/calibre/ebooks/conversion/plugins/pdf_output.py index e9046bcfeb..da66a9be0d 100644 --- a/src/calibre/ebooks/conversion/plugins/pdf_output.py +++ b/src/calibre/ebooks/conversion/plugins/pdf_output.py @@ -15,7 +15,6 @@ from calibre.customize.conversion import OutputFormatPlugin, \ OptionRecommendation from calibre.ptempfile import TemporaryDirectory from calibre.constants import iswindows -from calibre import walk UNITS = [ 'millimeter', @@ -138,6 +137,85 @@ class PDFOutput(OutputFormatPlugin): item = oeb.manifest.ids[cover_id] self.cover_data = item.data + def handle_embedded_fonts(self): + ''' + Because of QtWebKit's inability to handle embedded fonts correctly, we + remove the embedded fonts and make them available system wide instead. + If you ever move to Qt WebKit 2.3+ then this will be unnecessary. + ''' + from calibre.ebooks.oeb.base import urlnormalize + from calibre.gui2 import must_use_qt + from calibre.utils.fonts.utils import get_font_names, remove_embed_restriction + from PyQt4.Qt import QFontDatabase, QByteArray + + # First find all @font-face rules and remove them, adding the embedded + # fonts to Qt + family_map = {} + for item in list(self.oeb.manifest): + if not hasattr(item.data, 'cssRules'): continue + remove = set() + for i, rule in enumerate(item.data.cssRules): + if rule.type == rule.FONT_FACE_RULE: + remove.add(i) + try: + s = rule.style + src = s.getProperty('src').propertyValue[0].uri + font_family = s.getProperty('font-family').propertyValue[0].value + except: + continue + path = item.abshref(src) + ff = self.oeb.manifest.hrefs.get(urlnormalize(path), None) + if ff is None: + continue + + raw = ff.data + self.oeb.manifest.remove(ff) + try: + raw = remove_embed_restriction(raw) + except: + continue + must_use_qt() + QFontDatabase.addApplicationFontFromData(QByteArray(raw)) + try: + family_name = get_font_names(raw)[0] + except: + family_name = None + if family_name: + family_map[icu_lower(font_family)] = family_name + + for i in sorted(remove, reverse=True): + item.data.cssRules.pop(i) + + # Now map the font family name specified in the css to the actual + # family name of the embedded font (they may be different in general). + for item in self.oeb.manifest: + if not hasattr(item.data, 'cssRules'): continue + for i, rule in enumerate(item.data.cssRules): + if rule.type != rule.STYLE_RULE: continue + ff = rule.style.getProperty('font-family') + if ff is None: continue + val = ff.propertyValue + for i in xrange(val.length): + k = icu_lower(val[i].value) + if k in family_map: + val[i].value = family_map[k] + + def remove_font_specification(self): + # Qt produces image based pdfs on windows when non-generic fonts are specified + # This might change in Qt WebKit 2.3+ you will have to test. + for item in self.oeb.manifest: + if not hasattr(item.data, 'cssRules'): continue + for i, rule in enumerate(item.data.cssRules): + if rule.type != rule.STYLE_RULE: continue + ff = rule.style.getProperty('font-family') + if ff is None: continue + val = ff.propertyValue + for i in xrange(val.length): + k = icu_lower(val[i].value) + if k not in {'serif', 'sans', 'sans-serif', 'sansserif', + 'monospace', 'cursive', 'fantasy'}: + val[i].value = '' + def convert_text(self, oeb_book): from calibre.ebooks.pdf.writer import PDFWriter from calibre.ebooks.metadata.opf2 import OPF @@ -145,39 +223,16 @@ class PDFOutput(OutputFormatPlugin): self.log.debug('Serializing oeb input to disk for processing...') self.get_cover_data() + if iswindows: + self.remove_font_specification() + else: + self.handle_embedded_fonts() + with TemporaryDirectory('_pdf_out') as oeb_dir: from calibre.customize.ui import plugin_for_output_format oeb_output = plugin_for_output_format('oeb') oeb_output.convert(oeb_book, oeb_dir, self.input_plugin, self.opts, self.log) - if iswindows: - # from calibre.utils.fonts.utils import remove_embed_restriction - # On windows Qt generates an image based PDF if the html uses - # embedded fonts. See https://launchpad.net/bugs/1053906 - for f in walk(oeb_dir): - if f.rpartition('.')[-1].lower() in {'ttf', 'otf'}: - os.remove(f) - # It's not the font embedding restriction that causes - # this, even after removing the restriction, Qt still - # generates an image based document. Theoretically, it - # fixed = False - # with open(f, 'r+b') as s: - # raw = s.read() - # try: - # raw = remove_embed_restriction(raw) - # except: - # self.log.exception('Failed to remove embedding' - # ' restriction from font %s, ignoring it'% - # os.path.basename(f)) - # else: - # s.seek(0) - # s.truncate() - # s.write(raw) - # fixed = True - - # if not fixed: - # os.remove(f) - opfpath = glob.glob(os.path.join(oeb_dir, '*.opf'))[0] opf = OPF(opfpath, os.path.dirname(opfpath)) diff --git a/src/calibre/utils/fonts/win_fonts.py b/src/calibre/utils/fonts/win_fonts.py index bcfa40758b..747580d45e 100644 --- a/src/calibre/utils/fonts/win_fonts.py +++ b/src/calibre/utils/fonts/win_fonts.py @@ -7,11 +7,11 @@ __license__ = 'GPL v3' __copyright__ = '2012, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import os, sys +import os, sys, atexit from itertools import product -from calibre import prints -from calibre.constants import plugins +from calibre import prints, isbytestring +from calibre.constants import plugins, filesystem_encoding from calibre.utils.fonts.utils import (is_truetype_font, get_font_names, get_font_characteristics) @@ -97,6 +97,17 @@ class WinFonts(object): return ans + def add_system_font(self, path): + if isbytestring(path): + path = path.decode(filesystem_encoding) + path = os.path.abspath(path) + ret = self.w.add_system_font(path) + if ret > 0: + atexit.register(self.remove_system_font, path) + return ret + + def remove_system_font(self, path): + return self.w.remove_system_font(path) def load_winfonts(): w, err = plugins['winfonts'] diff --git a/src/calibre/utils/fonts/winfonts.cpp b/src/calibre/utils/fonts/winfonts.cpp index dcf99683a8..f678d021c4 100644 --- a/src/calibre/utils/fonts/winfonts.cpp +++ b/src/calibre/utils/fonts/winfonts.cpp @@ -161,21 +161,49 @@ static PyObject* font_data(PyObject *self, PyObject *args) { // }}} static PyObject* add_font(PyObject *self, PyObject *args) { - PyObject *pyname, *private_font; + char *data; + Py_ssize_t sz; + DWORD num = 0; + + if (!PyArg_ParseTuple(args, "s#", &data, &sz)) return NULL; + + AddFontMemResourceEx(data, sz, NULL, &num); + + return Py_BuildValue("k", num); +} + +static PyObject* add_system_font(PyObject *self, PyObject *args) { + PyObject *name; LPWSTR path; int num; - if (!PyArg_ParseTuple(args, "OO", &pyname, &private_font)) return NULL; - - path = unicode_to_wchar(pyname); + if (!PyArg_ParseTuple(args, "O", &name)) return NULL; + path = unicode_to_wchar(name); if (path == NULL) return NULL; - num = AddFontResourceEx(path, (PyObject_IsTrue(private_font)) ? FR_PRIVATE : 0, 0); + num = AddFontResource(path); + if (num > 0) + SendMessage(HWND_BROADCAST, WM_FONTCHANGE, 0, 0); free(path); - return Py_BuildValue("i", num); } +static PyObject* remove_system_font(PyObject *self, PyObject *args) { + PyObject *name, *ok = Py_False; + LPWSTR path; + + if (!PyArg_ParseTuple(args, "O", &name)) return NULL; + path = unicode_to_wchar(name); + if (path == NULL) return NULL; + + if (RemoveFontResource(path)) { + SendMessage(HWND_BROADCAST, WM_FONTCHANGE, 0, 0); + ok = Py_True; + } + free(path); + return Py_BuildValue("O", ok); +} + static PyMethodDef winfonts_methods[] = { {"enum_font_families", enum_font_families, METH_VARARGS, @@ -189,8 +217,18 @@ PyMethodDef winfonts_methods[] = { }, {"add_font", add_font, METH_VARARGS, - "add_font(filename, private)\n\n" - "Add the font(s) in filename to windows. If private is True, the font will only be available to this process and will not be installed system wide. Reeturns the number of fonts added." + "add_font(data)\n\n" + "Add the font(s) in the data (bytestring) to windows. Added fonts are always private. Returns the number of fonts added." + }, + + {"add_system_font", add_system_font, METH_VARARGS, + "add_system_font(data)\n\n" + "Add the font(s) in the specified file to the system font tables." + }, + + {"remove_system_font", remove_system_font, METH_VARARGS, + "remove_system_font(data)\n\n" + "Remove the font(s) in the specified file from the system font tables." }, {NULL, NULL, 0, NULL}