Skip to content

Commit

Permalink
Deprecating font caching mechanism + support for .pkl font files defi…
Browse files Browse the repository at this point in the history
…nitions - cf. #345
  • Loading branch information
Lucas-C committed Mar 4, 2022
1 parent 6dedf61 commit dc250c9
Show file tree
Hide file tree
Showing 17 changed files with 117 additions and 221 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,21 @@ and [PEP 440](https://www.python.org/dev/peps/pep-0440/).

### Changed
- `image()` method now insert `.svg` images as PDF paths
- the [defusedxml](https://pypi.org/project/defusedxml/) package was added as dependency in order to make SVG parsing safer
- log level of `_substitute_page_number()` has been lowered from `INFO` to `DEBUG`

### Fixed
- a bug in `get_string_width()` with unicode fonts and Markdown enabled,
resulting in calls to `cell()` / `multi_cell()` with `align="R"` to display nothing - thanks @mcerveny for the fix!
- a bug with incorrect width calculation of markdown text

### Deprecated
- the font caching mechanism, that used the `pickle` module, has been removed, for security reasons,
and because it provided little performance, and only for specific use cases - _cf._ [issue #345](https://github.com/PyFPDF/fpdf2/issues/345).
That means that the `font_cache_dir` optional paramater of `fpdf.FPDF`
and the `uni` optional paramater of `FPDF.add_font` are deprecated.
The `fpdf.fpdf.load_cache` function has also been removed.

## [2.5.0] - 2022-01-22
### Added
Thanks to @torque for contributing this massive new feature:
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
[![Pypi latest version](https://img.shields.io/pypi/v/fpdf2.svg)](https://pypi.python.org/pypi/fpdf2)
[![License: LGPL v3](https://img.shields.io/badge/License-LGPL%20v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0)
[![codecov](https://codecov.io/gh/PyFPDF/fpdf2/branch/master/graph/badge.svg)](https://codecov.io/gh/PyFPDF/fpdf2)
[![security: bandit](https://img.shields.io/badge/security-bandit-yellow.svg)](https://github.com/PyCQA/bandit)
[![Downloads per month](https://pepy.tech/badge/fpdf2/month)](https://pepy.tech/project/fpdf2)

[![Discussions](https://img.shields.io/github/discussions/PyFPDF/fpdf2)](https://github.com/PyFPDF/fpdf2/discussions)
Expand Down
2 changes: 1 addition & 1 deletion docs/EmojisSymbolsDingbats.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Here is an example using the [DejaVu](https://dejavu-fonts.github.io) font:
import fpdf

pdf = fpdf.FPDF()
pdf.add_font("DejaVuSans", fname="DejaVuSans.ttf", uni=True)
pdf.add_font("DejaVuSans", fname="DejaVuSans.ttf")
pdf.set_font("DejaVuSans", size=64)
pdf.add_page()
pdf.multi_cell(0, txt="".join([chr(0x1F600 + x) for x in range(68)]))
Expand Down
10 changes: 5 additions & 5 deletions docs/Unicode.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ pdf.add_page()
# Add a DejaVu Unicode font (uses UTF-8)
# Supports more than 200 languages. For a coverage status see:
# http://dejavu.svn.sourceforge.net/viewvc/dejavu/trunk/dejavu-fonts/langcover.txt
pdf.add_font('DejaVu', fname='DejaVuSansCondensed.ttf', uni=True)
pdf.add_font('DejaVu', fname='DejaVuSansCondensed.ttf')
pdf.set_font('DejaVu', size=14)

text = u"""
Expand All @@ -81,14 +81,14 @@ for txt in text.split('\n'):
# Supports: Bengali, Devanagari, Gujarati,
# Gurmukhi (including the variants for Punjabi)
# Kannada, Malayalam, Oriya, Tamil, Telugu, Tibetan
pdf.add_font('gargi', fname='gargi.ttf', uni=True)
pdf.add_font('gargi', fname='gargi.ttf')
pdf.set_font('gargi', size=14)
pdf.write(8, u'Hindi: नमस्ते दुनिया')
pdf.ln(20)

# Add a AR PL New Sung Unicode font (uses UTF-8)
# The Open Source Chinese Font (also supports other east Asian languages)
pdf.add_font('fireflysung', fname='fireflysung.ttf', uni=True)
pdf.add_font('fireflysung', fname='fireflysung.ttf')
pdf.set_font('fireflysung', size=14)
pdf.write(8, u'Chinese: 你好世界\n')
pdf.write(8, u'Japanese: こんにちは世界\n')
Expand All @@ -97,13 +97,13 @@ pdf.ln(10)
# Add a Alee Unicode font (uses UTF-8)
# General purpose Hangul truetype fonts that contain Korean syllable
# and Latin9 (iso8859-15) characters.
pdf.add_font('eunjin', fname='Eunjin.ttf', uni=True)
pdf.add_font('eunjin', fname='Eunjin.ttf')
pdf.set_font('eunjin', size=14)
pdf.write(8, u'Korean: 안녕하세요')
pdf.ln(20)

# Add a Fonts-TLWG (formerly ThaiFonts-Scalable) (uses UTF-8)
pdf.add_font('waree', fname='Waree.ttf', uni=True)
pdf.add_font('waree', fname='Waree.ttf')
pdf.set_font('waree', size=14)
pdf.write(8, u'Thai: สวัสดีชาวโลก')
pdf.ln(20)
Expand Down
193 changes: 67 additions & 126 deletions fpdf/fpdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
The version number is updated here (above and below in variable).
"""

import errno
import hashlib
import io
import logging
Expand Down Expand Up @@ -202,17 +201,6 @@ def get_page_format(format, k=None):
raise FPDFPageFormatException(f"Arguments must be numbers: {args}") from e


def load_cache(filename: Path):
"""Return unpickled object, or None if cache unavailable"""
if not filename:
return None
try:
return pickle.loads(filename.read_bytes())
# File missing, unsupported pickle, etc
except (OSError, ValueError):
return None


def check_page(fn):
"""Decorator to protect drawing methods"""

Expand All @@ -232,7 +220,11 @@ class FPDF(GraphicsStateMixin):
MARKDOWN_UNDERLINE_MARKER = "--"

def __init__(
self, orientation="portrait", unit="mm", format="A4", font_cache_dir=True
self,
orientation="portrait",
unit="mm",
format="A4",
font_cache_dir="DEPRECATED",
):
"""
Args:
Expand All @@ -245,11 +237,13 @@ def __init__(
Default to "mm".
format (str): possible values are "a3", "a4", "a5", "letter", "legal" or a tuple
(width, height) expressed in the given unit. Default to "a4".
font_cache_dir (Path or str): directory where pickle files
for TTF font files are kept.
`None` disables font chaching.
The default is `True`, meaning the current folder.
font_cache_dir (Path or str): [**DEPRECATED**] unused
"""
if font_cache_dir != "DEPRECATED":
warnings.warn(
'"font_cache_dir" parameter is deprecated, unused and will soon be removed',
PendingDeprecationWarning,
)
super().__init__()
# Initialization of instance attributes
self.offsets = {} # array of object offsets
Expand All @@ -272,7 +266,6 @@ def __init__(

self.ws = 0 # word spacing
self.angle = 0 # used by deprecated method: rotate()
self.font_cache_dir = font_cache_dir
self.xmp_metadata = None
self.image_filter = "AUTO"
self.page_duration = 0 # optional pages display duration, cf. add_page()
Expand Down Expand Up @@ -1428,13 +1421,13 @@ def solid_arc(
style,
)

def add_font(self, family, style="", fname=None, uni=False):
def add_font(self, family, style="", fname=None, uni="DEPRECATED"):
"""
Imports a TrueType, OpenType or Type1 font and makes it available
Imports a TrueType or OpenType font and makes it available
for later calls to the `set_font()` method.
**Warning:** for Type1 and legacy fonts it is necessary to generate a font definition file first with the `MakeFont` utility.
This feature is currently deprecated in favour of TrueType Unicode font support
**Warning:** there is partial support for Type1 and legacy fonts in .pkl font definition files,
generated by the `MakeFont` utility, but this feature is getting deprecated in favour of TrueType Unicode font support
(whose fonts are automatically processed with the included `ttfonts.py` module).
You will find more information on the "Unicode" documentation page.
Expand All @@ -1444,17 +1437,13 @@ def add_font(self, family, style="", fname=None, uni=False):
style (str): font style. "B" for bold, "I" for italic.
fname (str): font file name. You can specify a relative or full path.
If the file is not found, it will be searched in `FPDF_FONT_DIR`.
uni (bool): if set to `True`, enable TrueType font subset embedding.
Text will then be treated as `utf8` by default.
Calling this method with uni=False is discouraged as legacy font support is complex and deprecated.
Notes
-----
Due to the fact that font processing can occupy large amount of time, some data is cached.
Cache files are created in the current folder by default.
This can be controlled with the `font_cache_dir` paramater of the `FPDF` constructor.
uni (bool): [**DEPRECATED**] unused
"""
if uni != "DEPRECATED":
warnings.warn(
'"uni" parameter is deprecated, unused and will soon be removed',
PendingDeprecationWarning,
)
if not fname:
fname = family.replace(" ", "") + f"{style.lower()}.pkl"
style = "".join(sorted(style.upper()))
Expand All @@ -1468,7 +1457,7 @@ def add_font(self, family, style="", fname=None, uni=False):
if fontkey in self.fonts or fontkey in self.core_fonts:
warnings.warn(f"Core font or font already added '{fontkey}': doing nothing")
return
if uni:
if str(fname).endswith(".ttf"):
for parent in (".", FPDF_FONT_DIR):
if not parent:
continue
Expand All @@ -1478,59 +1467,41 @@ def add_font(self, family, style="", fname=None, uni=False):
else:
raise FileNotFoundError(f"TTF Font file not found: {fname}")

if self.font_cache_dir is None:
cache_dir = unifilename = None
else:
cache_dir = (
Path() if self.font_cache_dir is True else Path(self.font_cache_dir)
)
unifilename = cache_dir / f"{ttffilename.stem}.pkl"

# include numbers in the subset! (if alias present)
# ensure that alias is mapped 1-by-1 additionally (must be replaceable)
sbarr = "\x00 "
if self.str_alias_nb_pages:
sbarr += "0123456789"
sbarr += self.str_alias_nb_pages

font_dict = load_cache(unifilename)
if font_dict is None:
ttf = TTFontFile()
ttf.getMetrics(ttffilename)
desc = {
"Ascent": round(ttf.ascent),
"Descent": round(ttf.descent),
"CapHeight": round(ttf.capHeight),
"Flags": ttf.flags,
"FontBBox": (
f"[{ttf.bbox[0]:.0f} {ttf.bbox[1]:.0f}"
f" {ttf.bbox[2]:.0f} {ttf.bbox[3]:.0f}]"
),
"ItalicAngle": int(ttf.italicAngle),
"StemV": round(ttf.stemV),
"MissingWidth": round(ttf.defaultWidth),
}

# Generate metrics .pkl file
font_dict = {
"type": "TTF",
"name": re.sub("[ ()]", "", ttf.fullName),
"desc": desc,
"up": round(ttf.underlinePosition),
"ut": round(ttf.underlineThickness),
"ttffile": ttffilename,
"fontkey": fontkey,
"unifilename": unifilename,
"originalsize": os.stat(ttffilename).st_size,
"cw": ttf.charWidths,
}

if unifilename:
try:
unifilename.write_bytes(pickle.dumps(font_dict))
except OSError as e:
if e.errno != errno.EACCES:
raise # Not a permission error.
ttf = TTFontFile()
ttf.getMetrics(ttffilename)
desc = {
"Ascent": round(ttf.ascent),
"Descent": round(ttf.descent),
"CapHeight": round(ttf.capHeight),
"Flags": ttf.flags,
"FontBBox": (
f"[{ttf.bbox[0]:.0f} {ttf.bbox[1]:.0f}"
f" {ttf.bbox[2]:.0f} {ttf.bbox[3]:.0f}]"
),
"ItalicAngle": int(ttf.italicAngle),
"StemV": round(ttf.stemV),
"MissingWidth": round(ttf.defaultWidth),
}

# Generate metrics .pkl file
font_dict = {
"type": "TTF",
"name": re.sub("[ ()]", "", ttf.fullName),
"desc": desc,
"up": round(ttf.underlinePosition),
"ut": round(ttf.underlineThickness),
"ttffile": ttffilename,
"fontkey": fontkey,
"originalsize": os.stat(ttffilename).st_size,
"cw": ttf.charWidths,
}

self.fonts[fontkey] = {
"i": len(self.fonts) + 1,
Expand All @@ -1543,29 +1514,21 @@ def add_font(self, family, style="", fname=None, uni=False):
"ttffile": font_dict["ttffile"],
"fontkey": fontkey,
"subset": SubsetMap(map(ord, sbarr)),
"unifilename": unifilename,
}
self.font_files[fontkey] = {
"length1": font_dict["originalsize"],
"type": "TTF",
"ttffile": ttffilename,
}
self.font_files[fname] = {"type": "TTF"}
else:
if fname.endswith(".ttf"):
warnings.warn(
"When providing a TTF font file you must pass uni=True to FPDF.add_font"
)
warnings.warn(
"Support for .pkl font files definition is deprecated, and will be removed from fpdf2 soon."
" If you require this feature, please report your need on fpdf2 GitHub project.",
PendingDeprecationWarning,
)
font_dict = pickle.loads(Path(fname).read_bytes())
if font_dict["type"] == "TTF":
warnings.warn(
"Pickle was generated from TTF font file, setting uni=True"
)
self.add_font(family, style=style, fname=fname, uni=True)
return

self.fonts[fontkey] = {"i": len(self.fonts) + 1}
self.fonts[fontkey].update(font_dict)
font_dict["i"] = len(self.fonts) + 1
self.fonts[fontkey] = font_dict
diff = font_dict.get("diff")
if diff:
# Search existing encodings
Expand Down Expand Up @@ -2722,7 +2685,7 @@ def image(
"""
if type:
warnings.warn(
'"type" is unused and will soon be deprecated',
'"type" parameter is deprecated, unused and will soon be removed',
PendingDeprecationWarning,
)
if str(name).endswith(".svg"):
Expand Down Expand Up @@ -3010,7 +2973,7 @@ def output(self, name="", dest=""):
"""
if dest:
warnings.warn(
'"dest" is unused and will soon be deprecated',
'"dest" parameter is deprecated, unused and will soon be removed',
PendingDeprecationWarning,
)
# Finish document if necessary:
Expand Down Expand Up @@ -3438,40 +3401,18 @@ def _putfonts(self):
self.mtd(font)

def _putTTfontwidths(self, font, maxUni):
if font["unifilename"] is None:
cw127fname = None
else:
cw127fname = Path(font["unifilename"]).with_suffix(".cw127.pkl")
font_dict = load_cache(cw127fname)
if font_dict:
rangeid = font_dict["rangeid"]
range_ = font_dict["range"]
prevcid = font_dict["prevcid"]
prevwidth = font_dict["prevwidth"]
interval = font_dict["interval"]
range_interval = font_dict["range_interval"]
startcid = 128
else:
rangeid = 0
range_ = {}
range_interval = {}
prevcid = -2
prevwidth = -1
interval = False
startcid = 1
rangeid = 0
range_ = {}
range_interval = {}
prevcid = -2
prevwidth = -1
interval = False
startcid = 1
cwlen = maxUni + 1

# for each character
subset = font["subset"].dict()
for cid in range(startcid, cwlen):
if cid == 128 and font_dict:
try:
with cw127fname.open("wb") as fh:
pickle.dump(font_dict, fh)
except OSError as e:
if e.errno != errno.EACCES:
raise # Not a permission error.

width = _char_width(font, cid)
if "dw" not in font or (font["dw"] and width != font["dw"]):
cid_mapped = subset.get(cid)
Expand Down Expand Up @@ -4194,4 +4135,4 @@ def _is_xml(img: io.BytesIO):
sys.modules[__name__].__class__ = WarnOnDeprecatedModuleAttributes


__all__ = ["FPDF", "load_cache", "get_page_format", "TitleStyle", "PAGE_FORMATS"]
__all__ = ["FPDF", "get_page_format", "TitleStyle", "PAGE_FORMATS"]
Loading

0 comments on commit dc250c9

Please sign in to comment.