diff --git a/setup/extensions.py b/setup/extensions.py index f7d40ca72c..1827d32f4a 100644 --- a/setup/extensions.py +++ b/setup/extensions.py @@ -191,6 +191,12 @@ if iswindows: # needs_ddk=True, cflags=['/X'] ), + Extension('winfonts', + ['calibre/utils/fonts/winfonts.cpp'], + libraries=['Gdi32', 'User32'], + cflags=['/X'] + ), + ]) if isosx: diff --git a/src/calibre/constants.py b/src/calibre/constants.py index dd7abd89f5..899157c13b 100644 --- a/src/calibre/constants.py +++ b/src/calibre/constants.py @@ -91,7 +91,7 @@ class Plugins(collections.Mapping): 'speedup', ] if iswindows: - plugins.extend(['winutil', 'wpd']) + plugins.extend(['winutil', 'wpd', 'winfonts']) if isosx: plugins.append('usbobserver') if islinux or isosx: diff --git a/src/calibre/ebooks/conversion/plugins/pdf_output.py b/src/calibre/ebooks/conversion/plugins/pdf_output.py index b3eed763ac..3019255270 100644 --- a/src/calibre/ebooks/conversion/plugins/pdf_output.py +++ b/src/calibre/ebooks/conversion/plugins/pdf_output.py @@ -151,14 +151,28 @@ class PDFOutput(OutputFormatPlugin): 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'}: - self.log.warn('Found embedded font %s, removing it, as ' - 'embedded fonts on windows are not supported by ' - 'the PDF Output plugin'%os.path.basename(f)) - os.remove(f) + 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/embedflag.py b/src/calibre/utils/fonts/embedflag.py deleted file mode 100644 index 0c4e94bae6..0000000000 --- a/src/calibre/utils/fonts/embedflag.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env python -# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai -from __future__ import (unicode_literals, division, absolute_import, - print_function) - -__license__ = 'GPL v3' -__copyright__ = '2012, Kovid Goyal ' -__docformat__ = 'restructuredtext en' - -import sys, struct - -class UnsupportedFont(ValueError): - pass - -def remove_embed_restriction(raw): - sfnt_version = raw[:4] - if sfnt_version not in {b'\x00\x01\x00\x00', b'OTTO'}: - raise UnsupportedFont('Not a supported font, sfnt_version: %r'%sfnt_version) - - num_tables = struct.unpack_from(b'>H', raw, 4)[0] - - # Find OS/2 table - offset = 4 + 4*2 # Start of the Table record entries - os2_table_offset = None - for i in xrange(num_tables): - table_tag = raw[offset:offset+4] - offset += 16 # Size of a table record - if table_tag == b'OS/2': - os2_table_offset = struct.unpack_from(b'>I', raw, offset+8)[0] - break - if os2_table_offset is None: - raise UnsupportedFont('Not a supported font, has no OS/2 table') - - version, = struct.unpack_from(b'>H', raw, os2_table_offset) - - fs_type_offset = os2_table_offset + struct.calcsize(b'>HhHH') - fs_type = struct.unpack_from(b'>H', raw, fs_type_offset)[0] - if fs_type == 0: - return raw - - return raw[:fs_type_offset] + struct.pack(b'>H', 0) + raw[fs_type_offset+2:] - -if __name__ == '__main__': - remove_embed_restriction(open(sys.argv[-1], 'rb').read()) - diff --git a/src/calibre/utils/fonts/utils.py b/src/calibre/utils/fonts/utils.py new file mode 100644 index 0000000000..085373318b --- /dev/null +++ b/src/calibre/utils/fonts/utils.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2012, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import sys, struct + +class UnsupportedFont(ValueError): + pass + +def is_truetype_font(raw): + sfnt_version = raw[:4] + return (sfnt_version in {b'\x00\x01\x00\x00', b'OTTO'}, sfnt_version) + +def get_font_characteristics(raw): + num_tables = struct.unpack_from(b'>H', raw, 4)[0] + + # Find OS/2 table + offset = 4 + 4*2 # Start of the Table record entries + os2_table_offset = None + for i in xrange(num_tables): + table_tag = raw[offset:offset+4] + if table_tag == b'OS/2': + os2_table_offset = struct.unpack_from(b'>I', raw, offset+8)[0] + break + offset += 16 # Size of a table record + if os2_table_offset is None: + raise UnsupportedFont('Not a supported font, has no OS/2 table') + + common_fields = b'>HhHHHhhhhhhhhhhh' + (version, char_width, weight, width, fs_type, subscript_x_size, + subscript_y_size, subscript_x_offset, subscript_y_offset, + superscript_x_size, superscript_y_size, superscript_x_offset, + superscript_y_offset, strikeout_size, strikeout_position, + family_class) = struct.unpack_from(common_fields, + raw, os2_table_offset) + offset = os2_table_offset + struct.calcsize(common_fields) + panose = struct.unpack_from(b'>'+b'B'*10, raw, offset) + panose + offset += 10 + (range1,) = struct.unpack_from(b'>L', raw, offset) + offset += struct.calcsize(b'>L') + if version > 0: + range2, range3, range4 = struct.unpack_from(b'>LLL', raw, offset) + offset += struct.calcsize(b'>LLL') + vendor_id = raw[offset:offset+4] + vendor_id + offset += 4 + selection, = struct.unpack_from(b'>H', raw, offset) + + is_italic = (selection & 0b1) != 0 + is_bold = (selection & 0b100000) != 0 + is_regular = (selection & 0b1000000) != 0 + return weight, is_italic, is_bold, is_regular + +def remove_embed_restriction(raw): + sfnt_version = raw[:4] + if sfnt_version not in {b'\x00\x01\x00\x00', b'OTTO'}: + raise UnsupportedFont('Not a supported font, sfnt_version: %r'%sfnt_version) + + num_tables = struct.unpack_from(b'>H', raw, 4)[0] + + # Find OS/2 table + offset = 4 + 4*2 # Start of the Table record entries + os2_table_offset = None + for i in xrange(num_tables): + table_tag = raw[offset:offset+4] + if table_tag == b'OS/2': + os2_table_offset = struct.unpack_from(b'>I', raw, offset+8)[0] + break + offset += 16 # Size of a table record + if os2_table_offset is None: + raise UnsupportedFont('Not a supported font, has no OS/2 table') + + version, = struct.unpack_from(b'>H', raw, os2_table_offset) + + fs_type_offset = os2_table_offset + struct.calcsize(b'>HhHH') + fs_type = struct.unpack_from(b'>H', raw, fs_type_offset)[0] + if fs_type == 0: + return raw + + return raw[:fs_type_offset] + struct.pack(b'>H', 0) + raw[fs_type_offset+2:] + +if __name__ == '__main__': + raw = remove_embed_restriction(open(sys.argv[-1], 'rb').read()) + diff --git a/src/calibre/utils/fonts/win_fonts.py b/src/calibre/utils/fonts/win_fonts.py new file mode 100644 index 0000000000..41e0081627 --- /dev/null +++ b/src/calibre/utils/fonts/win_fonts.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2012, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import os, sys +from itertools import product + +from calibre import prints +from calibre.constants import plugins +from calibre.utils.fonts.utils import (is_truetype_font, + get_font_characteristics) + +class WinFonts(object): + + def __init__(self, winfonts): + self.w = winfonts + + def font_families(self): + names = set() + for font in self.w.enum_font_families(): + if ( + font['is_truetype'] and + # Fonts with names starting with @ are designed for + # vertical text + not font['name'].startswith('@') + ): + names.add(font['name']) + return sorted(names) + + def get_normalized_name(self, is_italic, weight): + if is_italic: + ft = 'bi' if weight == self.w.FW_BOLD else 'italic' + else: + ft = 'bold' if weight == self.w.FW_BOLD else 'normal' + return ft + + def fonts_for_family(self, family, normalize=True): + family = type(u'')(family) + ans = {} + for weight, is_italic in product( (self.w.FW_NORMAL, self.w.FW_BOLD), (False, True) ): + try: + data = self.w.font_data(family, is_italic, weight) + except Exception as e: + prints('Failed to get font data for font: %s [%s] with error: %s'% + (family, self.get_normalized_name(is_italic, weight), e)) + continue + + ok, sig = is_truetype_font(data) + if not ok: + prints('Not a supported font, sfnt_version: %r'%sig) + continue + ext = 'otf' if sig == b'OTTO' else 'ttf' + + try: + weight, is_italic, is_bold, is_regular = get_font_characteristics(data) + except Exception as e: + prints('Failed to get font characteristic for font: %s [%s]' + ' with error: %s'%(family, + self.get_normalized_name(is_italic, weight), e)) + continue + + if normalize: + ft = {(True, True):'bi', (True, False):'italic', (False, + True):'bold', (False, False):'normal'}[(is_italic, + is_bold)] + else: + ft = (1 if is_italic else 0, weight//10) + + ans[ft] = (ext, data) + + return ans + + +def load_winfonts(): + w, err = plugins['winfonts'] + if w is None: + raise RuntimeError('Failed to load the winfonts module: %s'%err) + return WinFonts(w) + +def test_ttf_reading(): + for f in sys.argv[1:]: + raw = open(f).read() + print (os.path.basename(f)) + get_font_characteristics(raw) + print() + +if __name__ == '__main__': + base = os.path.abspath(__file__) + d = os.path.dirname + pluginsd = os.path.join(d(d(d(base))), 'plugins') + if os.path.exists(os.path.join(pluginsd, 'winfonts.pyd')): + sys.path.insert(0, pluginsd) + import winfonts + w = WinFonts(winfonts) + else: + w = load_winfonts() + + print (w.w) + families = w.font_families() + print (families) + + for family in families: + print (family + ':') + for font, data in w.fonts_for_family(family).iteritems(): + print (' ', font, data[0], len(data[1])) + print () + diff --git a/src/calibre/utils/fonts/winfonts.cpp b/src/calibre/utils/fonts/winfonts.cpp index 8bd2cc7c02..0d991c004e 100644 --- a/src/calibre/utils/fonts/winfonts.cpp +++ b/src/calibre/utils/fonts/winfonts.cpp @@ -1,168 +1,202 @@ /* -:mod:`fontconfig` -- Pythonic interface to Windows font api +:mod:`winfont` -- Pythonic interface to Windows font api ============================================================ -.. module:: fontconfig +.. module:: winfonts :platform: All - :synopsis: Pythonic interface to the fontconfig library + :synopsis: Pythonic interface to the windows font routines .. moduleauthor:: Kovid Goyal Copyright 2009 */ +#define _UNICODE #define UNICODE +#define PY_SSIZE_T_CLEAN #include -#include -#include - -using namespace std; - -vector *get_font_data(HDC hdc) { - DWORD sz; - vector *data; - sz = GetFontData(hdc, 0, 0, NULL, 0); - data = new vector(sz); - if (GetFontData(hdc, 0, 0, &((*data)[0]), sz) == GDI_ERROR) { - delete data; data = NULL; - } - return data; +#include +#include +#include +// Utils {{{ +static wchar_t* unicode_to_wchar(PyObject *o) { + wchar_t *buf; + Py_ssize_t len; + if (o == NULL) return NULL; + if (!PyUnicode_Check(o)) {PyErr_Format(PyExc_TypeError, "The python object must be a unicode object"); return NULL;} + len = PyUnicode_GET_SIZE(o); + buf = (wchar_t *)calloc(len+2, sizeof(wchar_t)); + if (buf == NULL) { PyErr_NoMemory(); return NULL; } + len = PyUnicode_AsWideChar((PyUnicodeObject*)o, buf, len); + if (len == -1) { free(buf); PyErr_Format(PyExc_TypeError, "Invalid python unicode object."); return NULL; } + return buf; } -BOOL is_font_embeddable(ENUMLOGFONTEX *lpelfe) { - HDC hdc; - HFONT font; - HFONT old_font = NULL; - UINT sz; - size_t i; - LPOUTLINETEXTMETRICW metrics; - BOOL ans = TRUE; - hdc = GetDC(NULL); - font = CreateFontIndirect(&lpelfe->elfLogFont); - if (font != NULL) { - old_font = SelectObject(hdc, font); - sz = GetOutlineTextMetrics(hdc, 0, NULL); - metrics = new OUTLINETEXTMETRICW[sz]; - if ( GetOutlineTextMetrics(hdc, sz, metrics) != 0) { - for ( i = 0; i < sz; i++) { - if (metrics[i].otmfsType & 0x01) { - wprintf_s(L"Not embeddable: %s\n", lpelfe->elfLogFont.lfFaceName); - ans = FALSE; break; - } - } - } else ans = FALSE; - delete[] metrics; - DeleteObject(font); - SelectObject(hdc, old_font); - } else ans = FALSE; - ReleaseDC(NULL, hdc); +static PyObject* wchar_to_unicode(const wchar_t *o) { + PyObject *ans; + if (o == NULL) return NULL; + ans = PyUnicode_FromWideChar(o, wcslen(o)); + if (ans == NULL) PyErr_NoMemory(); return ans; } -int CALLBACK find_families_callback ( - ENUMLOGFONTEX *lpelfe, /* pointer to logical-font data */ - NEWTEXTMETRICEX *lpntme, /* pointer to physical-font data */ - int FontType, /* type of font */ - LPARAM lParam /* a combo box HWND */ - ) { - size_t i; - LPWSTR tmp; - vector *families = (vector*)lParam; +// }}} - if (FontType & TRUETYPE_FONTTYPE) { - for (i = 0; i < families->size(); i++) { - if (lstrcmp(families->at(i), lpelfe->elfLogFont.lfFaceName) == 0) - return 1; - } - tmp = new WCHAR[LF_FACESIZE]; - swprintf_s(tmp, LF_FACESIZE, L"%s", lpelfe->elfLogFont.lfFaceName); - families->push_back(tmp); - } +// Enumerate font families {{{ +struct EnumData { + HDC hdc; + PyObject *families; +}; + + +static PyObject* logfont_to_dict(const ENUMLOGFONTEX *lf, const TEXTMETRIC *tm, DWORD font_type, HDC hdc) { + PyObject *name, *full_name, *style, *script; + LOGFONT f = lf->elfLogFont; + + name = wchar_to_unicode(f.lfFaceName); + full_name = wchar_to_unicode(lf->elfFullName); + style = wchar_to_unicode(lf->elfStyle); + script = wchar_to_unicode(lf->elfScript); + + return Py_BuildValue("{s:N, s:N, s:N, s:N, s:O, s:O, s:O, s:O, s:l}", + "name", name, + "full_name", full_name, + "style", style, + "script", script, + "is_truetype", (font_type & TRUETYPE_FONTTYPE) ? Py_True : Py_False, + "is_italic", (tm->tmItalic != 0) ? Py_True : Py_False, + "is_underlined", (tm->tmUnderlined != 0) ? Py_True : Py_False, + "is_strikeout", (tm->tmStruckOut != 0) ? Py_True : Py_False, + "weight", tm->tmWeight + ); +} + +static int CALLBACK find_families_callback(const ENUMLOGFONTEX *lpelfe, const TEXTMETRIC *lpntme, DWORD font_type, LPARAM lParam) { + struct EnumData *enum_data = reinterpret_cast(lParam); + PyObject *font = logfont_to_dict(lpelfe, lpntme, font_type, enum_data->hdc); + if (font == NULL) return 0; + PyList_Append(enum_data->families, font); return 1; } - -vector* find_font_families(void) { +static PyObject* enum_font_families(PyObject *self, PyObject *args) { LOGFONTW logfont; HDC hdc; - vector *families; + PyObject *families; + struct EnumData enum_data; - families = new vector(); + families = PyList_New(0); + if (families == NULL) return PyErr_NoMemory(); SecureZeroMemory(&logfont, sizeof(logfont)); logfont.lfCharSet = DEFAULT_CHARSET; - logfont.lfPitchAndFamily = VARIABLE_PITCH | FF_DONTCARE; - StringCchCopyW(logfont.lfFaceName, 2, L"\0"); + logfont.lfFaceName[0] = L'\0'; hdc = GetDC(NULL); - EnumFontFamiliesExW(hdc, &logfont, (FONTENUMPROC)find_families_callback, - (LPARAM)(families), 0); + enum_data.hdc = hdc; + enum_data.families = families; + EnumFontFamiliesExW(hdc, &logfont, (FONTENUMPROC)find_families_callback, + (LPARAM)(&enum_data), 0); ReleaseDC(NULL, hdc); return families; } -inline void free_families_vector(vector *v) { - for (size_t i = 0; i < v->size(); i++) delete[] v->at(i); - delete v; +// }}} + +static PyObject* font_data(PyObject *self, PyObject *args) { + PyObject *ans = NULL, *italic, *pyname; + LOGFONTW lf; + HDC hdc; + LONG weight; + LPWSTR family = NULL; + HGDIOBJ old_font = NULL; + HFONT hf; + DWORD sz; + char *buf; + + SecureZeroMemory(&lf, sizeof(lf)); + + if (!PyArg_ParseTuple(args, "OOl", &pyname, &italic, &weight)) return NULL; + + family = unicode_to_wchar(pyname); + if (family == NULL) { Py_DECREF(ans); return NULL; } + StringCchCopyW(lf.lfFaceName, LF_FACESIZE, family); + free(family); + + lf.lfItalic = (PyObject_IsTrue(italic)) ? 1 : 0; + lf.lfWeight = weight; + lf.lfOutPrecision = OUT_TT_ONLY_PRECIS; + + hdc = GetDC(NULL); + + if ( (hf = CreateFontIndirect(&lf)) != NULL) { + + if ( (old_font = SelectObject(hdc, hf)) != NULL ) { + sz = GetFontData(hdc, 0, 0, NULL, 0); + if (sz != GDI_ERROR) { + buf = (char*)calloc(sz, sizeof(char)); + + if (buf != NULL) { + if (GetFontData(hdc, 0, 0, buf, sz) != GDI_ERROR) { + ans = PyBytes_FromStringAndSize(buf, sz); + if (ans == NULL) PyErr_NoMemory(); + } else PyErr_SetString(PyExc_ValueError, "GDI Error"); + free(buf); + } else PyErr_NoMemory(); + } else PyErr_SetString(PyExc_ValueError, "GDI Error"); + + SelectObject(hdc, old_font); + } else PyErr_SetFromWindowsErr(0); + DeleteObject(hf); + } else PyErr_SetFromWindowsErr(0); + + ReleaseDC(NULL, hdc); + + return ans; } -#ifdef TEST - -int main(int argc, char **argv) { - vector *all_families; - size_t i; - - all_families = find_font_families(); - - for (i = 0; i < all_families->size(); i++) - wprintf_s(L"%s\n", all_families->at(i)); - - free_families_vector(all_families); - - HDC hdc = GetDC(NULL); - HFONT font = CreateFont(72,0,0,0,0,0,0,0,0,0,0,0,0,L"Verdana"); - HFONT old_font = SelectObject(hdc, font); - vector *data = get_font_data(hdc); - DeleteObject(font); - SelectObject(hdc, old_font); - ReleaseDC(NULL, hdc); - if (data != NULL) printf("\nyay: %d\n", data->size()); - delete data; - - return 0; -} -#else - -#define PY_SSIZE_T_CLEAN -#include -# - static -PyMethodDef fontconfig_methods[] = { - {"find_font_families", fontconfig_find_font_families, METH_VARARGS, - "find_font_families(allowed_extensions)\n\n" - "Find all font families on the system for fonts of the specified types. If no " - "types are specified all font families are returned." +PyMethodDef winfonts_methods[] = { + {"enum_font_families", enum_font_families, METH_VARARGS, + "enum_font_families()\n\n" + "Enumerate all regular (not italic/bold/etc. variants) font families on the system. Note there will be multiple entries for every family (corresponding to each charset of the font)." }, + {"font_data", font_data, METH_VARARGS, + "font_data(family_name, italic, weight)\n\n" + "Return the raw font data for the specified font." + }, {NULL, NULL, 0, NULL} }; -extern "C" { PyMODINIT_FUNC -initfontconfig(void) { +initwinfonts(void) { PyObject *m; m = Py_InitModule3( - "fontconfig", fontconfig_methods, - "Find fonts." + "winfonts", winfonts_methods, + "Windows font API" ); if (m == NULL) return; -} + + PyModule_AddIntMacro(m, FW_DONTCARE); + PyModule_AddIntMacro(m, FW_THIN); + PyModule_AddIntMacro(m, FW_EXTRALIGHT); + PyModule_AddIntMacro(m, FW_ULTRALIGHT); + PyModule_AddIntMacro(m, FW_LIGHT); + PyModule_AddIntMacro(m, FW_NORMAL); + PyModule_AddIntMacro(m, FW_REGULAR); + PyModule_AddIntMacro(m, FW_MEDIUM); + PyModule_AddIntMacro(m, FW_SEMIBOLD); + PyModule_AddIntMacro(m, FW_DEMIBOLD); + PyModule_AddIntMacro(m, FW_BOLD); + PyModule_AddIntMacro(m, FW_EXTRABOLD); + PyModule_AddIntMacro(m, FW_ULTRABOLD); + PyModule_AddIntMacro(m, FW_HEAVY); + PyModule_AddIntMacro(m, FW_BLACK); } -#endif