mirror of
https://github.com/Kozea/WeasyPrint.git
synced 2024-09-11 12:46:15 +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
|
/.cache
|
||||||
|
|
||||||
# Tests
|
# Tests
|
||||||
|
/.pytest_cache
|
||||||
/weasyprint/tests/.cache
|
/weasyprint/tests/.cache
|
||||||
/weasyprint/tests/test_draw/results
|
/weasyprint/tests/test_draw/results
|
||||||
|
@ -33,17 +33,37 @@ class FontConfiguration:
|
|||||||
"""Add a font into the application."""
|
"""Add a font into the application."""
|
||||||
|
|
||||||
|
|
||||||
if sys.platform.startswith('win'):
|
fontconfig = None
|
||||||
warnings.warn('@font-face is currently not supported on Windows')
|
pangoft2 = None
|
||||||
elif pango.pango_version() < 13800:
|
|
||||||
|
if pango.pango_version() < 13800:
|
||||||
warnings.warn('@font-face support needs Pango >= 1.38')
|
warnings.warn('@font-face support needs Pango >= 1.38')
|
||||||
else:
|
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('''
|
ffi.cdef('''
|
||||||
// FontConfig
|
// FontConfig
|
||||||
|
|
||||||
typedef int FcBool;
|
typedef int FcBool;
|
||||||
typedef struct _FcConfig FcConfig;
|
typedef struct _FcConfig FcConfig;
|
||||||
typedef struct _FcPattern FcPattern;
|
typedef struct _FcPattern FcPattern;
|
||||||
|
typedef struct _FcStrList FcStrList;
|
||||||
typedef unsigned char FcChar8;
|
typedef unsigned char FcChar8;
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
@ -55,8 +75,20 @@ else:
|
|||||||
FcMatchPattern, FcMatchFont, FcMatchScan
|
FcMatchPattern, FcMatchFont, FcMatchScan
|
||||||
} FcMatchKind;
|
} FcMatchKind;
|
||||||
|
|
||||||
|
|
||||||
|
typedef struct _FcFontSet {
|
||||||
|
int nfont;
|
||||||
|
int sfont;
|
||||||
|
FcPattern **fonts;
|
||||||
|
} FcFontSet;
|
||||||
|
|
||||||
|
typedef enum _FcSetName {
|
||||||
|
FcSetSystem = 0,
|
||||||
|
FcSetApplication = 1
|
||||||
|
} FcSetName;
|
||||||
|
|
||||||
FcConfig * FcInitLoadConfigAndFonts (void);
|
FcConfig * FcInitLoadConfigAndFonts (void);
|
||||||
FcPattern * FcConfigDestroy (FcConfig *config);
|
void FcConfigDestroy (FcConfig *config);
|
||||||
FcBool FcConfigAppFontAddFile (
|
FcBool FcConfigAppFontAddFile (
|
||||||
FcConfig *config, const FcChar8 *file);
|
FcConfig *config, const FcChar8 *file);
|
||||||
FcConfig * FcConfigGetCurrent (void);
|
FcConfig * FcConfigGetCurrent (void);
|
||||||
@ -64,6 +96,10 @@ else:
|
|||||||
FcBool FcConfigParseAndLoad (
|
FcBool FcConfigParseAndLoad (
|
||||||
FcConfig *config, const FcChar8 *file, FcBool complain);
|
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);
|
void FcDefaultSubstitute (FcPattern *pattern);
|
||||||
FcBool FcConfigSubstitute (
|
FcBool FcConfigSubstitute (
|
||||||
FcConfig *config, FcPattern *p, FcMatchKind kind);
|
FcConfig *config, FcPattern *p, FcMatchKind kind);
|
||||||
@ -96,11 +132,6 @@ else:
|
|||||||
cairo_font_type_t fonttype);
|
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 = {
|
FONTCONFIG_WEIGHT_CONSTANTS = {
|
||||||
'normal': 'normal',
|
'normal': 'normal',
|
||||||
'bold': 'bold',
|
'bold': 'bold',
|
||||||
@ -133,6 +164,61 @@ else:
|
|||||||
'ultra-expanded': 'ultraexpanded',
|
'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):
|
class FontConfiguration(FontConfiguration):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""Create a FT2 font configuration.
|
"""Create a FT2 font configuration.
|
||||||
@ -142,26 +228,48 @@ else:
|
|||||||
how-to-use-custom-application-fonts.html
|
how-to-use-custom-application-fonts.html
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
# load the master config file and the fonts
|
||||||
self._fontconfig_config = ffi.gc(
|
self._fontconfig_config = ffi.gc(
|
||||||
fontconfig.FcInitLoadConfigAndFonts(),
|
fontconfig.FcInitLoadConfigAndFonts(),
|
||||||
fontconfig.FcConfigDestroy)
|
fontconfig.FcConfigDestroy)
|
||||||
self.font_map = ffi.gc(
|
# usable config?
|
||||||
pangocairo.pango_cairo_font_map_new_for_font_type(
|
if not _checkfontconfiguration(self._fontconfig_config):
|
||||||
cairo.FONT_TYPE_FT),
|
self.font_map = None
|
||||||
gobject.g_object_unref)
|
else:
|
||||||
pangoft2.pango_fc_font_map_set_config(
|
self.font_map = ffi.gc(
|
||||||
ffi.cast('PangoFcFontMap *', self.font_map),
|
pangocairo.pango_cairo_font_map_new_for_font_type(
|
||||||
self._fontconfig_config)
|
cairo.FONT_TYPE_FT),
|
||||||
# pango_fc_font_map_set_config keeps a reference to config
|
gobject.g_object_unref)
|
||||||
fontconfig.FcConfigDestroy(self._fontconfig_config)
|
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 = []
|
self._filenames = []
|
||||||
|
|
||||||
def add_font_face(self, rule_descriptors, url_fetcher):
|
def add_font_face(self, rule_descriptors, url_fetcher):
|
||||||
|
if self.font_map is None:
|
||||||
|
return
|
||||||
for font_type, url in rule_descriptors['src']:
|
for font_type, url in rule_descriptors['src']:
|
||||||
if url is None:
|
if url is None:
|
||||||
continue
|
continue
|
||||||
if font_type in ('external', 'local'):
|
if font_type in ('external', 'local'):
|
||||||
config = self._fontconfig_config
|
config = self._fontconfig_config
|
||||||
|
# default: use `url_fetcher` to fetch the font
|
||||||
|
fetch_as_url = True
|
||||||
if font_type == 'local':
|
if font_type == 'local':
|
||||||
font_name = url.encode('utf-8')
|
font_name = url.encode('utf-8')
|
||||||
pattern = ffi.gc(
|
pattern = ffi.gc(
|
||||||
@ -193,20 +301,33 @@ else:
|
|||||||
config, pattern, result)
|
config, pattern, result)
|
||||||
fontconfig.FcPatternGetString(
|
fontconfig.FcPatternGetString(
|
||||||
matching_pattern, b'file', 0, filename)
|
matching_pattern, b'file', 0, filename)
|
||||||
url = (
|
# cant use urlopen('file://..') on Windows.
|
||||||
u'file://' +
|
# Fails with
|
||||||
ffi.string(filename[0]).decode('utf-8'))
|
# 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:
|
else:
|
||||||
LOGGER.warning(
|
LOGGER.warning(
|
||||||
'Failed to load local font "%s"',
|
'Failed to load local font "%s"',
|
||||||
font_name.decode('utf-8'))
|
font_name.decode('utf-8'))
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
with fetch(url_fetcher, url) as result:
|
if fetch_as_url:
|
||||||
if 'string' in result:
|
with fetch(url_fetcher, url) as result:
|
||||||
font = result['string']
|
if 'string' in result:
|
||||||
else:
|
font = result['string']
|
||||||
font = result['file_obj'].read()
|
else:
|
||||||
|
font = result['file_obj'].read()
|
||||||
|
else:
|
||||||
|
with open(url, 'rb') as fd:
|
||||||
|
font = fd.read()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
LOGGER.error(
|
LOGGER.error(
|
||||||
'Failed to load font at "%s" (%s)', url, exc)
|
'Failed to load font at "%s" (%s)', url, exc)
|
||||||
@ -222,7 +343,7 @@ else:
|
|||||||
**font_features).items():
|
**font_features).items():
|
||||||
features_string += '<string>%s %s</string>' % (
|
features_string += '<string>%s %s</string>' % (
|
||||||
key, value)
|
key, value)
|
||||||
fd, filename = tempfile.mkstemp()
|
fd, filename = tempfile.mkstemp(dir=self._tempdir)
|
||||||
os.write(fd, font)
|
os.write(fd, font)
|
||||||
os.close(fd)
|
os.close(fd)
|
||||||
self._filenames.append(filename)
|
self._filenames.append(filename)
|
||||||
@ -263,7 +384,7 @@ else:
|
|||||||
FONTCONFIG_STRETCH_CONSTANTS[
|
FONTCONFIG_STRETCH_CONSTANTS[
|
||||||
rule_descriptors.get('font_stretch', 'normal')],
|
rule_descriptors.get('font_stretch', 'normal')],
|
||||||
filename, features_string)
|
filename, features_string)
|
||||||
fd, conf_filename = tempfile.mkstemp()
|
fd, conf_filename = tempfile.mkstemp(dir=self._tempdir)
|
||||||
# TODO: is this encoding OK?
|
# TODO: is this encoding OK?
|
||||||
os.write(fd, xml.encode('utf-8'))
|
os.write(fd, xml.encode('utf-8'))
|
||||||
os.close(fd)
|
os.close(fd)
|
||||||
@ -275,6 +396,9 @@ else:
|
|||||||
if font_added:
|
if font_added:
|
||||||
# TODO: we should mask local fonts with the same name
|
# TODO: we should mask local fonts with the same name
|
||||||
# too as explained in Behdad's blog entry
|
# 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
|
return filename
|
||||||
else:
|
else:
|
||||||
LOGGER.error('Failed to load font at "%s"', url)
|
LOGGER.error('Failed to load font at "%s"', url)
|
||||||
@ -284,5 +408,14 @@ else:
|
|||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
"""Clean a font configuration for a document."""
|
"""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:
|
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')
|
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 = {}
|
PANGO_ATTR_FONT_FEATURES_CACHE = {}
|
||||||
|
|
||||||
|
|
||||||
@ -623,12 +628,9 @@ class Layout(object):
|
|||||||
def __init__(self, context, font_size, style):
|
def __init__(self, context, font_size, style):
|
||||||
self.context = context
|
self.context = context
|
||||||
hinting = context.enable_hinting if context else False
|
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(
|
self.layout = ffi.gc(
|
||||||
pangocairo.pango_cairo_create_layout(ffi.cast(
|
pangocairo.pango_cairo_create_layout(ffi.cast(
|
||||||
'cairo_t *', cairo_dummy_context._pointer)),
|
'cairo_t *', CAIRO_DUMMY_CONTEXT[hinting]._pointer)),
|
||||||
gobject.g_object_unref)
|
gobject.g_object_unref)
|
||||||
pango_context = pango.pango_layout_get_context(self.layout)
|
pango_context = pango.pango_layout_get_context(self.layout)
|
||||||
if context and context.font_config.font_map:
|
if context and context.font_config.font_map:
|
||||||
|
@ -81,13 +81,24 @@ def iri_to_uri(url):
|
|||||||
|
|
||||||
|
|
||||||
def path2url(path):
|
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)
|
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.
|
# Make sure directory names have a trailing slash.
|
||||||
# Otherwise relative URIs are resolved from the parent directory.
|
# Otherwise relative URIs are resolved from the parent directory.
|
||||||
path += os.path.sep
|
path += os.path.sep
|
||||||
|
wants_trailing_slash = True
|
||||||
path = pathname2url(path)
|
path = pathname2url(path)
|
||||||
|
# on Windows pathname2url cuts off trailing slash
|
||||||
|
if wants_trailing_slash and not path.endswith('/'):
|
||||||
|
path += '/'
|
||||||
if path.startswith('///'):
|
if path.startswith('///'):
|
||||||
# On Windows pathname2url(r'C:\foo') is apparently '///C:/foo'
|
# On Windows pathname2url(r'C:\foo') is apparently '///C:/foo'
|
||||||
# That enough slashes already.
|
# That enough slashes already.
|
||||||
|
Loading…
Reference in New Issue
Block a user