Skip to content

Commit dc250c9

Browse files
committed
Deprecating font caching mechanism + support for .pkl font files definitions - cf. #345
1 parent 6dedf61 commit dc250c9

17 files changed

+117
-221
lines changed

CHANGELOG.md

+8
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,21 @@ and [PEP 440](https://www.python.org/dev/peps/pep-0440/).
1515

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

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

26+
### Deprecated
27+
- the font caching mechanism, that used the `pickle` module, has been removed, for security reasons,
28+
and because it provided little performance, and only for specific use cases - _cf._ [issue #345](https://github.com/PyFPDF/fpdf2/issues/345).
29+
That means that the `font_cache_dir` optional paramater of `fpdf.FPDF`
30+
and the `uni` optional paramater of `FPDF.add_font` are deprecated.
31+
The `fpdf.fpdf.load_cache` function has also been removed.
32+
2533
## [2.5.0] - 2022-01-22
2634
### Added
2735
Thanks to @torque for contributing this massive new feature:

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
[![Pypi latest version](https://img.shields.io/pypi/v/fpdf2.svg)](https://pypi.python.org/pypi/fpdf2)
33
[![License: LGPL v3](https://img.shields.io/badge/License-LGPL%20v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0)
44
[![codecov](https://codecov.io/gh/PyFPDF/fpdf2/branch/master/graph/badge.svg)](https://codecov.io/gh/PyFPDF/fpdf2)
5+
[![security: bandit](https://img.shields.io/badge/security-bandit-yellow.svg)](https://github.com/PyCQA/bandit)
56
[![Downloads per month](https://pepy.tech/badge/fpdf2/month)](https://pepy.tech/project/fpdf2)
67

78
[![Discussions](https://img.shields.io/github/discussions/PyFPDF/fpdf2)](https://github.com/PyFPDF/fpdf2/discussions)

docs/EmojisSymbolsDingbats.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Here is an example using the [DejaVu](https://dejavu-fonts.github.io) font:
1111
import fpdf
1212

1313
pdf = fpdf.FPDF()
14-
pdf.add_font("DejaVuSans", fname="DejaVuSans.ttf", uni=True)
14+
pdf.add_font("DejaVuSans", fname="DejaVuSans.ttf")
1515
pdf.set_font("DejaVuSans", size=64)
1616
pdf.add_page()
1717
pdf.multi_cell(0, txt="".join([chr(0x1F600 + x) for x in range(68)]))

docs/Unicode.md

+5-5
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ pdf.add_page()
5959
# Add a DejaVu Unicode font (uses UTF-8)
6060
# Supports more than 200 languages. For a coverage status see:
6161
# http://dejavu.svn.sourceforge.net/viewvc/dejavu/trunk/dejavu-fonts/langcover.txt
62-
pdf.add_font('DejaVu', fname='DejaVuSansCondensed.ttf', uni=True)
62+
pdf.add_font('DejaVu', fname='DejaVuSansCondensed.ttf')
6363
pdf.set_font('DejaVu', size=14)
6464

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

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

105105
# Add a Fonts-TLWG (formerly ThaiFonts-Scalable) (uses UTF-8)
106-
pdf.add_font('waree', fname='Waree.ttf', uni=True)
106+
pdf.add_font('waree', fname='Waree.ttf')
107107
pdf.set_font('waree', size=14)
108108
pdf.write(8, u'Thai: สวัสดีชาวโลก')
109109
pdf.ln(20)

fpdf/fpdf.py

+67-126
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
The version number is updated here (above and below in variable).
1515
"""
1616

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

204203

205-
def load_cache(filename: Path):
206-
"""Return unpickled object, or None if cache unavailable"""
207-
if not filename:
208-
return None
209-
try:
210-
return pickle.loads(filename.read_bytes())
211-
# File missing, unsupported pickle, etc
212-
except (OSError, ValueError):
213-
return None
214-
215-
216204
def check_page(fn):
217205
"""Decorator to protect drawing methods"""
218206

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

234222
def __init__(
235-
self, orientation="portrait", unit="mm", format="A4", font_cache_dir=True
223+
self,
224+
orientation="portrait",
225+
unit="mm",
226+
format="A4",
227+
font_cache_dir="DEPRECATED",
236228
):
237229
"""
238230
Args:
@@ -245,11 +237,13 @@ def __init__(
245237
Default to "mm".
246238
format (str): possible values are "a3", "a4", "a5", "letter", "legal" or a tuple
247239
(width, height) expressed in the given unit. Default to "a4".
248-
font_cache_dir (Path or str): directory where pickle files
249-
for TTF font files are kept.
250-
`None` disables font chaching.
251-
The default is `True`, meaning the current folder.
240+
font_cache_dir (Path or str): [**DEPRECATED**] unused
252241
"""
242+
if font_cache_dir != "DEPRECATED":
243+
warnings.warn(
244+
'"font_cache_dir" parameter is deprecated, unused and will soon be removed',
245+
PendingDeprecationWarning,
246+
)
253247
super().__init__()
254248
# Initialization of instance attributes
255249
self.offsets = {} # array of object offsets
@@ -272,7 +266,6 @@ def __init__(
272266

273267
self.ws = 0 # word spacing
274268
self.angle = 0 # used by deprecated method: rotate()
275-
self.font_cache_dir = font_cache_dir
276269
self.xmp_metadata = None
277270
self.image_filter = "AUTO"
278271
self.page_duration = 0 # optional pages display duration, cf. add_page()
@@ -1428,13 +1421,13 @@ def solid_arc(
14281421
style,
14291422
)
14301423

1431-
def add_font(self, family, style="", fname=None, uni=False):
1424+
def add_font(self, family, style="", fname=None, uni="DEPRECATED"):
14321425
"""
1433-
Imports a TrueType, OpenType or Type1 font and makes it available
1426+
Imports a TrueType or OpenType font and makes it available
14341427
for later calls to the `set_font()` method.
14351428
1436-
**Warning:** for Type1 and legacy fonts it is necessary to generate a font definition file first with the `MakeFont` utility.
1437-
This feature is currently deprecated in favour of TrueType Unicode font support
1429+
**Warning:** there is partial support for Type1 and legacy fonts in .pkl font definition files,
1430+
generated by the `MakeFont` utility, but this feature is getting deprecated in favour of TrueType Unicode font support
14381431
(whose fonts are automatically processed with the included `ttfonts.py` module).
14391432
14401433
You will find more information on the "Unicode" documentation page.
@@ -1444,17 +1437,13 @@ def add_font(self, family, style="", fname=None, uni=False):
14441437
style (str): font style. "B" for bold, "I" for italic.
14451438
fname (str): font file name. You can specify a relative or full path.
14461439
If the file is not found, it will be searched in `FPDF_FONT_DIR`.
1447-
uni (bool): if set to `True`, enable TrueType font subset embedding.
1448-
Text will then be treated as `utf8` by default.
1449-
Calling this method with uni=False is discouraged as legacy font support is complex and deprecated.
1450-
1451-
Notes
1452-
-----
1453-
1454-
Due to the fact that font processing can occupy large amount of time, some data is cached.
1455-
Cache files are created in the current folder by default.
1456-
This can be controlled with the `font_cache_dir` paramater of the `FPDF` constructor.
1440+
uni (bool): [**DEPRECATED**] unused
14571441
"""
1442+
if uni != "DEPRECATED":
1443+
warnings.warn(
1444+
'"uni" parameter is deprecated, unused and will soon be removed',
1445+
PendingDeprecationWarning,
1446+
)
14581447
if not fname:
14591448
fname = family.replace(" ", "") + f"{style.lower()}.pkl"
14601449
style = "".join(sorted(style.upper()))
@@ -1468,7 +1457,7 @@ def add_font(self, family, style="", fname=None, uni=False):
14681457
if fontkey in self.fonts or fontkey in self.core_fonts:
14691458
warnings.warn(f"Core font or font already added '{fontkey}': doing nothing")
14701459
return
1471-
if uni:
1460+
if str(fname).endswith(".ttf"):
14721461
for parent in (".", FPDF_FONT_DIR):
14731462
if not parent:
14741463
continue
@@ -1478,59 +1467,41 @@ def add_font(self, family, style="", fname=None, uni=False):
14781467
else:
14791468
raise FileNotFoundError(f"TTF Font file not found: {fname}")
14801469

1481-
if self.font_cache_dir is None:
1482-
cache_dir = unifilename = None
1483-
else:
1484-
cache_dir = (
1485-
Path() if self.font_cache_dir is True else Path(self.font_cache_dir)
1486-
)
1487-
unifilename = cache_dir / f"{ttffilename.stem}.pkl"
1488-
14891470
# include numbers in the subset! (if alias present)
14901471
# ensure that alias is mapped 1-by-1 additionally (must be replaceable)
14911472
sbarr = "\x00 "
14921473
if self.str_alias_nb_pages:
14931474
sbarr += "0123456789"
14941475
sbarr += self.str_alias_nb_pages
14951476

1496-
font_dict = load_cache(unifilename)
1497-
if font_dict is None:
1498-
ttf = TTFontFile()
1499-
ttf.getMetrics(ttffilename)
1500-
desc = {
1501-
"Ascent": round(ttf.ascent),
1502-
"Descent": round(ttf.descent),
1503-
"CapHeight": round(ttf.capHeight),
1504-
"Flags": ttf.flags,
1505-
"FontBBox": (
1506-
f"[{ttf.bbox[0]:.0f} {ttf.bbox[1]:.0f}"
1507-
f" {ttf.bbox[2]:.0f} {ttf.bbox[3]:.0f}]"
1508-
),
1509-
"ItalicAngle": int(ttf.italicAngle),
1510-
"StemV": round(ttf.stemV),
1511-
"MissingWidth": round(ttf.defaultWidth),
1512-
}
1513-
1514-
# Generate metrics .pkl file
1515-
font_dict = {
1516-
"type": "TTF",
1517-
"name": re.sub("[ ()]", "", ttf.fullName),
1518-
"desc": desc,
1519-
"up": round(ttf.underlinePosition),
1520-
"ut": round(ttf.underlineThickness),
1521-
"ttffile": ttffilename,
1522-
"fontkey": fontkey,
1523-
"unifilename": unifilename,
1524-
"originalsize": os.stat(ttffilename).st_size,
1525-
"cw": ttf.charWidths,
1526-
}
1527-
1528-
if unifilename:
1529-
try:
1530-
unifilename.write_bytes(pickle.dumps(font_dict))
1531-
except OSError as e:
1532-
if e.errno != errno.EACCES:
1533-
raise # Not a permission error.
1477+
ttf = TTFontFile()
1478+
ttf.getMetrics(ttffilename)
1479+
desc = {
1480+
"Ascent": round(ttf.ascent),
1481+
"Descent": round(ttf.descent),
1482+
"CapHeight": round(ttf.capHeight),
1483+
"Flags": ttf.flags,
1484+
"FontBBox": (
1485+
f"[{ttf.bbox[0]:.0f} {ttf.bbox[1]:.0f}"
1486+
f" {ttf.bbox[2]:.0f} {ttf.bbox[3]:.0f}]"
1487+
),
1488+
"ItalicAngle": int(ttf.italicAngle),
1489+
"StemV": round(ttf.stemV),
1490+
"MissingWidth": round(ttf.defaultWidth),
1491+
}
1492+
1493+
# Generate metrics .pkl file
1494+
font_dict = {
1495+
"type": "TTF",
1496+
"name": re.sub("[ ()]", "", ttf.fullName),
1497+
"desc": desc,
1498+
"up": round(ttf.underlinePosition),
1499+
"ut": round(ttf.underlineThickness),
1500+
"ttffile": ttffilename,
1501+
"fontkey": fontkey,
1502+
"originalsize": os.stat(ttffilename).st_size,
1503+
"cw": ttf.charWidths,
1504+
}
15341505

15351506
self.fonts[fontkey] = {
15361507
"i": len(self.fonts) + 1,
@@ -1543,29 +1514,21 @@ def add_font(self, family, style="", fname=None, uni=False):
15431514
"ttffile": font_dict["ttffile"],
15441515
"fontkey": fontkey,
15451516
"subset": SubsetMap(map(ord, sbarr)),
1546-
"unifilename": unifilename,
15471517
}
15481518
self.font_files[fontkey] = {
15491519
"length1": font_dict["originalsize"],
15501520
"type": "TTF",
15511521
"ttffile": ttffilename,
15521522
}
1553-
self.font_files[fname] = {"type": "TTF"}
15541523
else:
1555-
if fname.endswith(".ttf"):
1556-
warnings.warn(
1557-
"When providing a TTF font file you must pass uni=True to FPDF.add_font"
1558-
)
1524+
warnings.warn(
1525+
"Support for .pkl font files definition is deprecated, and will be removed from fpdf2 soon."
1526+
" If you require this feature, please report your need on fpdf2 GitHub project.",
1527+
PendingDeprecationWarning,
1528+
)
15591529
font_dict = pickle.loads(Path(fname).read_bytes())
1560-
if font_dict["type"] == "TTF":
1561-
warnings.warn(
1562-
"Pickle was generated from TTF font file, setting uni=True"
1563-
)
1564-
self.add_font(family, style=style, fname=fname, uni=True)
1565-
return
1566-
1567-
self.fonts[fontkey] = {"i": len(self.fonts) + 1}
1568-
self.fonts[fontkey].update(font_dict)
1530+
font_dict["i"] = len(self.fonts) + 1
1531+
self.fonts[fontkey] = font_dict
15691532
diff = font_dict.get("diff")
15701533
if diff:
15711534
# Search existing encodings
@@ -2722,7 +2685,7 @@ def image(
27222685
"""
27232686
if type:
27242687
warnings.warn(
2725-
'"type" is unused and will soon be deprecated',
2688+
'"type" parameter is deprecated, unused and will soon be removed',
27262689
PendingDeprecationWarning,
27272690
)
27282691
if str(name).endswith(".svg"):
@@ -3010,7 +2973,7 @@ def output(self, name="", dest=""):
30102973
"""
30112974
if dest:
30122975
warnings.warn(
3013-
'"dest" is unused and will soon be deprecated',
2976+
'"dest" parameter is deprecated, unused and will soon be removed',
30142977
PendingDeprecationWarning,
30152978
)
30162979
# Finish document if necessary:
@@ -3438,40 +3401,18 @@ def _putfonts(self):
34383401
self.mtd(font)
34393402

34403403
def _putTTfontwidths(self, font, maxUni):
3441-
if font["unifilename"] is None:
3442-
cw127fname = None
3443-
else:
3444-
cw127fname = Path(font["unifilename"]).with_suffix(".cw127.pkl")
3445-
font_dict = load_cache(cw127fname)
3446-
if font_dict:
3447-
rangeid = font_dict["rangeid"]
3448-
range_ = font_dict["range"]
3449-
prevcid = font_dict["prevcid"]
3450-
prevwidth = font_dict["prevwidth"]
3451-
interval = font_dict["interval"]
3452-
range_interval = font_dict["range_interval"]
3453-
startcid = 128
3454-
else:
3455-
rangeid = 0
3456-
range_ = {}
3457-
range_interval = {}
3458-
prevcid = -2
3459-
prevwidth = -1
3460-
interval = False
3461-
startcid = 1
3404+
rangeid = 0
3405+
range_ = {}
3406+
range_interval = {}
3407+
prevcid = -2
3408+
prevwidth = -1
3409+
interval = False
3410+
startcid = 1
34623411
cwlen = maxUni + 1
34633412

34643413
# for each character
34653414
subset = font["subset"].dict()
34663415
for cid in range(startcid, cwlen):
3467-
if cid == 128 and font_dict:
3468-
try:
3469-
with cw127fname.open("wb") as fh:
3470-
pickle.dump(font_dict, fh)
3471-
except OSError as e:
3472-
if e.errno != errno.EACCES:
3473-
raise # Not a permission error.
3474-
34753416
width = _char_width(font, cid)
34763417
if "dw" not in font or (font["dw"] and width != font["dw"]):
34773418
cid_mapped = subset.get(cid)
@@ -4194,4 +4135,4 @@ def _is_xml(img: io.BytesIO):
41944135
sys.modules[__name__].__class__ = WarnOnDeprecatedModuleAttributes
41954136

41964137

4197-
__all__ = ["FPDF", "load_cache", "get_page_format", "TitleStyle", "PAGE_FORMATS"]
4138+
__all__ = ["FPDF", "get_page_format", "TitleStyle", "PAGE_FORMATS"]

0 commit comments

Comments
 (0)