mirror of
https://github.com/Kozea/WeasyPrint.git
synced 2024-08-17 00:20:42 +03:00
Merge branch 'master' into test
This commit is contained in:
commit
dafed64645
1
.gitignore
vendored
1
.gitignore
vendored
@ -18,5 +18,6 @@
|
||||
/.cache
|
||||
|
||||
# Tests
|
||||
/.pytest_cache
|
||||
/weasyprint/tests/.cache
|
||||
/weasyprint/tests/test_draw/results
|
||||
|
@ -33,17 +33,37 @@ class FontConfiguration:
|
||||
"""Add a font into the application."""
|
||||
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
warnings.warn('@font-face is currently not supported on Windows')
|
||||
elif pango.pango_version() < 13800:
|
||||
fontconfig = None
|
||||
pangoft2 = None
|
||||
|
||||
if pango.pango_version() < 13800:
|
||||
warnings.warn('@font-face support needs Pango >= 1.38')
|
||||
else:
|
||||
try:
|
||||
fontconfig = dlopen(ffi, 'fontconfig', 'libfontconfig',
|
||||
'libfontconfig-1.dll',
|
||||
'libfontconfig.so.1', 'libfontconfig-1.dylib')
|
||||
pangoft2 = dlopen(ffi, 'pangoft2-1.0', 'libpangoft2-1.0-0',
|
||||
'libpangoft2-1.0.so', 'libpangoft2-1.0.dylib')
|
||||
except Exception as err:
|
||||
# dont alter behavior on other platforms!
|
||||
if not sys.platform.startswith('win'):
|
||||
raise err
|
||||
else:
|
||||
warnings.warn("'@font-face not supported: {0}".format(err))
|
||||
fontconfig = None
|
||||
pangoft2 = None
|
||||
|
||||
|
||||
# if both libraries are present: Use them
|
||||
if fontconfig and pangoft2:
|
||||
ffi.cdef('''
|
||||
// FontConfig
|
||||
|
||||
typedef int FcBool;
|
||||
typedef struct _FcConfig FcConfig;
|
||||
typedef struct _FcPattern FcPattern;
|
||||
typedef struct _FcStrList FcStrList;
|
||||
typedef unsigned char FcChar8;
|
||||
|
||||
typedef enum {
|
||||
@ -55,8 +75,20 @@ else:
|
||||
FcMatchPattern, FcMatchFont, FcMatchScan
|
||||
} FcMatchKind;
|
||||
|
||||
|
||||
typedef struct _FcFontSet {
|
||||
int nfont;
|
||||
int sfont;
|
||||
FcPattern **fonts;
|
||||
} FcFontSet;
|
||||
|
||||
typedef enum _FcSetName {
|
||||
FcSetSystem = 0,
|
||||
FcSetApplication = 1
|
||||
} FcSetName;
|
||||
|
||||
FcConfig * FcInitLoadConfigAndFonts (void);
|
||||
FcPattern * FcConfigDestroy (FcConfig *config);
|
||||
void FcConfigDestroy (FcConfig *config);
|
||||
FcBool FcConfigAppFontAddFile (
|
||||
FcConfig *config, const FcChar8 *file);
|
||||
FcConfig * FcConfigGetCurrent (void);
|
||||
@ -64,6 +96,10 @@ else:
|
||||
FcBool FcConfigParseAndLoad (
|
||||
FcConfig *config, const FcChar8 *file, FcBool complain);
|
||||
|
||||
FcFontSet * FcConfigGetFonts(FcConfig *config, FcSetName set);
|
||||
FcStrList * FcConfigGetConfigFiles(FcConfig *config);
|
||||
FcChar8 * FcStrListNext(FcStrList *list);
|
||||
|
||||
void FcDefaultSubstitute (FcPattern *pattern);
|
||||
FcBool FcConfigSubstitute (
|
||||
FcConfig *config, FcPattern *p, FcMatchKind kind);
|
||||
@ -96,11 +132,6 @@ else:
|
||||
cairo_font_type_t fonttype);
|
||||
''')
|
||||
|
||||
fontconfig = dlopen(ffi, 'fontconfig', 'libfontconfig',
|
||||
'libfontconfig.so.1', 'libfontconfig-1.dylib')
|
||||
pangoft2 = dlopen(ffi, 'pangoft2-1.0', 'libpangoft2-1.0-0',
|
||||
'libpangoft2-1.0.so', 'libpangoft2-1.0.dylib')
|
||||
|
||||
FONTCONFIG_WEIGHT_CONSTANTS = {
|
||||
'normal': 'normal',
|
||||
'bold': 'bold',
|
||||
@ -133,6 +164,61 @@ else:
|
||||
'ultra-expanded': 'ultraexpanded',
|
||||
}
|
||||
|
||||
_warned_once = False
|
||||
|
||||
def _warn_once(msg):
|
||||
"""don't annoy with warnings, one is enough"""
|
||||
global _warned_once
|
||||
if not _warned_once:
|
||||
warnings.warn(msg)
|
||||
_warned_once = True
|
||||
|
||||
def _checkfontconfiguration(font_config):
|
||||
"""
|
||||
Check whether the given font_config has fonts.
|
||||
|
||||
Maybe that never happens on Nix, but the GTK3 Runtime for Windows,
|
||||
https://github.com/tschoonj/
|
||||
GTK-for-Windows-Runtime-Environment-Installer
|
||||
which is recommended at
|
||||
http://weasyprint.readthedocs.io/en/latest/install.html#windows
|
||||
comes without fonts.conf in etc/fonts, giving
|
||||
"Fontconfig error: Cannot load default config file"
|
||||
|
||||
No default config == No fonts.
|
||||
No fonts == expect ugly output.
|
||||
If you happen to have an html without a valid @font-face all
|
||||
letters turn into rectangles.
|
||||
If you happen to have an html with at least one valid @font-face
|
||||
all text is styled with that font.
|
||||
"""
|
||||
# Nobody ever complained about such a situation on Nix...
|
||||
# Since I cannot test this on Linux, and dont know whta happens
|
||||
# without FontConfig, I leave it as it was before an return True
|
||||
if not sys.platform.startswith('win'):
|
||||
return True
|
||||
|
||||
fonts = fontconfig.FcConfigGetFonts(
|
||||
font_config, fontconfig.FcSetSystem)
|
||||
if fonts.nfont > 0:
|
||||
return True
|
||||
# Is the reason a missing default config file?
|
||||
configfiles = fontconfig.FcConfigGetConfigFiles(font_config)
|
||||
file = fontconfig.FcStrListNext(configfiles)
|
||||
if file == ffi.NULL:
|
||||
_warn_once(
|
||||
'@font-face not supported: Cannot load default config file')
|
||||
else:
|
||||
_warn_once('@font-face not supported: no fonts configured')
|
||||
# fall back to defaul @font-face-less behaviour
|
||||
return False
|
||||
# on Windows we could try to add the system fonts like that:
|
||||
# fontdir = os.path.join(os.environ['WINDIR'], 'Fonts')
|
||||
# fontconfig.FcConfigAppFontAddDir(
|
||||
# font_config,
|
||||
# # not shure which encoding fontconfig expects
|
||||
# fontdir.encode('mbcs'))
|
||||
|
||||
class FontConfiguration(FontConfiguration):
|
||||
def __init__(self):
|
||||
"""Create a FT2 font configuration.
|
||||
@ -142,26 +228,48 @@ else:
|
||||
how-to-use-custom-application-fonts.html
|
||||
|
||||
"""
|
||||
# load the master config file and the fonts
|
||||
self._fontconfig_config = ffi.gc(
|
||||
fontconfig.FcInitLoadConfigAndFonts(),
|
||||
fontconfig.FcConfigDestroy)
|
||||
self.font_map = ffi.gc(
|
||||
pangocairo.pango_cairo_font_map_new_for_font_type(
|
||||
cairo.FONT_TYPE_FT),
|
||||
gobject.g_object_unref)
|
||||
pangoft2.pango_fc_font_map_set_config(
|
||||
ffi.cast('PangoFcFontMap *', self.font_map),
|
||||
self._fontconfig_config)
|
||||
# pango_fc_font_map_set_config keeps a reference to config
|
||||
fontconfig.FcConfigDestroy(self._fontconfig_config)
|
||||
# usable config?
|
||||
if not _checkfontconfiguration(self._fontconfig_config):
|
||||
self.font_map = None
|
||||
else:
|
||||
self.font_map = ffi.gc(
|
||||
pangocairo.pango_cairo_font_map_new_for_font_type(
|
||||
cairo.FONT_TYPE_FT),
|
||||
gobject.g_object_unref)
|
||||
pangoft2.pango_fc_font_map_set_config(
|
||||
ffi.cast('PangoFcFontMap *', self.font_map),
|
||||
self._fontconfig_config)
|
||||
# pango_fc_font_map_set_config keeps a reference to config
|
||||
fontconfig.FcConfigDestroy(self._fontconfig_config)
|
||||
# On Windows the font tempfiles cannot be deleted
|
||||
# putting them in a subfolder made my life easier.
|
||||
self._tempdir = None
|
||||
if sys.platform.startswith('win'):
|
||||
self._tempdir = os.path.join(
|
||||
tempfile.gettempdir(), 'weasyprint')
|
||||
try:
|
||||
os.mkdir(self._tempdir)
|
||||
except FileExistsError:
|
||||
pass
|
||||
except Exception:
|
||||
# back to default
|
||||
self._tempdir = None
|
||||
self._filenames = []
|
||||
|
||||
def add_font_face(self, rule_descriptors, url_fetcher):
|
||||
if self.font_map is None:
|
||||
return
|
||||
for font_type, url in rule_descriptors['src']:
|
||||
if url is None:
|
||||
continue
|
||||
if font_type in ('external', 'local'):
|
||||
config = self._fontconfig_config
|
||||
# default: use `url_fetcher` to fetch the font
|
||||
fetch_as_url = True
|
||||
if font_type == 'local':
|
||||
font_name = url.encode('utf-8')
|
||||
pattern = ffi.gc(
|
||||
@ -193,20 +301,33 @@ else:
|
||||
config, pattern, result)
|
||||
fontconfig.FcPatternGetString(
|
||||
matching_pattern, b'file', 0, filename)
|
||||
url = (
|
||||
u'file://' +
|
||||
ffi.string(filename[0]).decode('utf-8'))
|
||||
# cant use urlopen('file://..') on Windows.
|
||||
# Fails with
|
||||
# URLError: <urlopen error file ot on local host>
|
||||
if sys.platform.startswith('win'):
|
||||
fetch_as_url = False
|
||||
url = (
|
||||
ffi.string(filename[0]).decode(
|
||||
sys.getfilesystemencoding()))
|
||||
else:
|
||||
url = (
|
||||
u'file://' +
|
||||
ffi.string(filename[0]).decode('utf-8'))
|
||||
else:
|
||||
LOGGER.warning(
|
||||
'Failed to load local font "%s"',
|
||||
font_name.decode('utf-8'))
|
||||
continue
|
||||
try:
|
||||
with fetch(url_fetcher, url) as result:
|
||||
if 'string' in result:
|
||||
font = result['string']
|
||||
else:
|
||||
font = result['file_obj'].read()
|
||||
if fetch_as_url:
|
||||
with fetch(url_fetcher, url) as result:
|
||||
if 'string' in result:
|
||||
font = result['string']
|
||||
else:
|
||||
font = result['file_obj'].read()
|
||||
else:
|
||||
with open(url, 'rb') as fd:
|
||||
font = fd.read()
|
||||
except Exception as exc:
|
||||
LOGGER.error(
|
||||
'Failed to load font at "%s" (%s)', url, exc)
|
||||
@ -222,7 +343,7 @@ else:
|
||||
**font_features).items():
|
||||
features_string += '<string>%s %s</string>' % (
|
||||
key, value)
|
||||
fd, filename = tempfile.mkstemp()
|
||||
fd, filename = tempfile.mkstemp(dir=self._tempdir)
|
||||
os.write(fd, font)
|
||||
os.close(fd)
|
||||
self._filenames.append(filename)
|
||||
@ -263,7 +384,7 @@ else:
|
||||
FONTCONFIG_STRETCH_CONSTANTS[
|
||||
rule_descriptors.get('font_stretch', 'normal')],
|
||||
filename, features_string)
|
||||
fd, conf_filename = tempfile.mkstemp()
|
||||
fd, conf_filename = tempfile.mkstemp(dir=self._tempdir)
|
||||
# TODO: is this encoding OK?
|
||||
os.write(fd, xml.encode('utf-8'))
|
||||
os.close(fd)
|
||||
@ -275,6 +396,9 @@ else:
|
||||
if font_added:
|
||||
# TODO: we should mask local fonts with the same name
|
||||
# too as explained in Behdad's blog entry
|
||||
# What about pango_fc_font_map_config_changed()
|
||||
# as suggested in Behdad's blog entry?
|
||||
# Though it seems to work without...
|
||||
return filename
|
||||
else:
|
||||
LOGGER.error('Failed to load font at "%s"', url)
|
||||
@ -284,5 +408,14 @@ else:
|
||||
|
||||
def __del__(self):
|
||||
"""Clean a font configuration for a document."""
|
||||
# Can't cleanup the temporary font files on Windows,
|
||||
# library has still open file handles.
|
||||
# On Unix `os.remove()` a file that is in use works fine,
|
||||
# on Windows a PermissionError is raised.
|
||||
# FcConfigAppFontClear() doesn't help
|
||||
# pango_fc_font_map_shutdown() doesn't help
|
||||
for filename in self._filenames:
|
||||
os.remove(filename)
|
||||
try:
|
||||
os.remove(filename)
|
||||
except OSError:
|
||||
continue
|
||||
|
@ -22,6 +22,11 @@ if cairo.cairo_version() <= 11400:
|
||||
warnings.warn('There are known rendering problems with Cairo <= 1.14.0')
|
||||
|
||||
|
||||
CAIRO_DUMMY_CONTEXT = {
|
||||
True: cairo.Context(cairo.ImageSurface(cairo.FORMAT_ARGB32, 1, 1)),
|
||||
False: cairo.Context(cairo.PDFSurface(None, 1, 1))}
|
||||
|
||||
|
||||
PANGO_ATTR_FONT_FEATURES_CACHE = {}
|
||||
|
||||
|
||||
@ -623,12 +628,9 @@ class Layout(object):
|
||||
def __init__(self, context, font_size, style):
|
||||
self.context = context
|
||||
hinting = context.enable_hinting if context else False
|
||||
cairo_dummy_context = (
|
||||
cairo.Context(cairo.ImageSurface(cairo.FORMAT_ARGB32, 1, 1))
|
||||
if hinting else cairo.Context(cairo.PDFSurface(None, 1, 1)))
|
||||
self.layout = ffi.gc(
|
||||
pangocairo.pango_cairo_create_layout(ffi.cast(
|
||||
'cairo_t *', cairo_dummy_context._pointer)),
|
||||
'cairo_t *', CAIRO_DUMMY_CONTEXT[hinting]._pointer)),
|
||||
gobject.g_object_unref)
|
||||
pango_context = pango.pango_layout_get_context(self.layout)
|
||||
if context and context.font_config.font_map:
|
||||
|
@ -81,13 +81,24 @@ def iri_to_uri(url):
|
||||
|
||||
|
||||
def path2url(path):
|
||||
"""Return file URL of `path`"""
|
||||
"""Return file URL of `path`.
|
||||
Accepts 'str' or 'bytes', returns 'str'
|
||||
"""
|
||||
# Ensure 'str'
|
||||
if isinstance(path, bytes):
|
||||
path = path.decode(sys.getfilesystemencoding())
|
||||
# if a trailing path.sep is given -- keep it
|
||||
wants_trailing_slash = path.endswith(os.path.sep) or path.endswith('/')
|
||||
path = os.path.abspath(path)
|
||||
if os.path.isdir(path):
|
||||
if wants_trailing_slash or os.path.isdir(path):
|
||||
# Make sure directory names have a trailing slash.
|
||||
# Otherwise relative URIs are resolved from the parent directory.
|
||||
path += os.path.sep
|
||||
wants_trailing_slash = True
|
||||
path = pathname2url(path)
|
||||
# on Windows pathname2url cuts off trailing slash
|
||||
if wants_trailing_slash and not path.endswith('/'):
|
||||
path += '/'
|
||||
if path.startswith('///'):
|
||||
# On Windows pathname2url(r'C:\foo') is apparently '///C:/foo'
|
||||
# That enough slashes already.
|
||||
|
Loading…
Reference in New Issue
Block a user