Skip to content

Commit

Permalink
Reducing fonttools memory usage by 10MiB by passing lazy=True to TTFo…
Browse files Browse the repository at this point in the history
…nt constructor (#718)
  • Loading branch information
Lucas-C authored Mar 10, 2023
1 parent f8de21c commit 35d8e7a
Show file tree
Hide file tree
Showing 17 changed files with 31 additions and 24 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ This can also be enabled programmatically with `warnings.simplefilter('default',
- hyperlinks were not working on encrypted files - thanks to @andersonhc
- unicode (non limited to ASCII) text can now be provided as metadata [#685](https://github.com/PyFPDF/fpdf2/issues/685)
- all `TitleStyle` constructor parameters are now effectively optional
- memory usage was reduced by 10 MiB in some cases, thanks to a small optimization in using `fonttools`
### Changed
- vector images parsing is now more robust: `fpdf2` can now embed SVG files without `viewPort` or no `height` / `width`
- bitonal images are now encoded using `CCITTFaxDecode`, reducing their size in the PDF document - thanks to @eroux
Expand Down
2 changes: 1 addition & 1 deletion fpdf/fpdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -1798,7 +1798,7 @@ def add_font(self, family=None, style="", fname=None, uni="DEPRECATED"):
if fontkey in self.fonts or fontkey in self.core_fonts:
warnings.warn(f"Core font or font already added '{fontkey}': doing nothing")
return
font = ttLib.TTFont(font_file_path, fontNumber=0)
font = ttLib.TTFont(font_file_path, fontNumber=0, lazy=True)

scale = 1000 / font["head"].unitsPerEm
default_width = round(scale * font["hmtx"].metrics[".notdef"][0])
Expand Down
2 changes: 1 addition & 1 deletion fpdf/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,7 @@ def _add_fonts(self):
# recalcTimestamp=False means that it doesn't modify the "modified" timestamp in head table
# if we leave recalcTimestamp=True the tests will break every time
fonttools_font = ttLib.TTFont(
file=font["ttffile"], recalcTimestamp=False, fontNumber=0
file=font["ttffile"], recalcTimestamp=False, fontNumber=0, lazy=True
)

# 1. get all glyphs in PDF
Expand Down
3 changes: 2 additions & 1 deletion fpdf/svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -635,7 +635,8 @@ def from_file(cls, filename, *args, encoding="utf-8", **kwargs):
def __init__(self, svg_text):
self.cross_references = {}

svg_tree = parse_xml_str(svg_text)
# disabling bandit rule as we use defusedxml:
svg_tree = parse_xml_str(svg_text) # nosec B314

if svg_tree.tag not in xmlns_lookup("svg", "svg"):
raise ValueError(f"root tag must be svg, not {svg_tree.tag}")
Expand Down
1 change: 0 additions & 1 deletion test/end_to_end_legacy/__init__.py

This file was deleted.

5 changes: 0 additions & 5 deletions test/end_to_end_legacy/charmap/__init__.py

This file was deleted.

Binary file added test/fonts/charmap_first_999_chars-NotoSans.pdf
Binary file not shown.
Binary file not shown.
Binary file added test/fonts/charmap_first_999_chars-NotoSerif.pdf
Binary file not shown.
Binary file not shown.
Binary file added test/fonts/charmap_first_999_chars-Waree.pdf
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -11,36 +11,45 @@
"""
from pathlib import Path

from fontTools.ttLib import TTFont
import pytest

import fpdf
from fontTools import ttLib
from fpdf import FPDF
from test.conftest import assert_pdf_equal

HERE = Path(__file__).resolve().parent


@pytest.mark.parametrize(
"font_filename",
["DejaVuSans.ttf", "DroidSansFallback.ttf", "Roboto-Regular.ttf", "cmss12.ttf"],
[
font_file.name
for font_file in HERE.glob("*.*tf")
if not any(
exclude in font_file.stem
for exclude in ("Bold", "Italic", "NotoColorEmoji")
)
],
)
def test_first_999_chars(font_filename, tmp_path):
font_path = HERE / ".." / ".." / "fonts" / font_filename
def test_charmap_first_999_chars(font_filename, tmp_path):
"""
Character Map Test
from PyFPDF version 1.7.2: github.com/reingart/pyfpdf/commit/2eab310cfd866ce24947c3a9d850ebda7c6d515d
"""
font_path = HERE / font_filename
font_name = font_path.stem

pdf = fpdf.FPDF()
pdf = FPDF()
pdf.add_page()
pdf.add_font(font_name, fname=font_path)
pdf.add_font(fname=font_path)
pdf.set_font(font_name, size=10)

font = ttLib.TTFont(font_path)
font = TTFont(font_path, lazy=True)
cmap = font.getBestCmap()

# Create a PDF with the first 999 charters defined in the font:
for counter, character in enumerate(cmap, 0):
# Create a PDF with the first 999 characters defined in the font:
for counter, character in enumerate(list(cmap.keys())[:1000]):
pdf.write(8, f"{counter:03}) {character:03x} - {character:c}", print_sh=True)
pdf.ln()
if counter >= 999:
break

assert_pdf_equal(pdf, HERE / f"charmap_first_999_chars-{font_name}.pdf", tmp_path)
8 changes: 5 additions & 3 deletions test/test_perfs.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
from pathlib import Path

import memunit, pytest
import pytest

from fpdf import FPDF

HERE = Path(__file__).resolve().parent


@pytest.mark.timeout(40)
# ensure memory usage does not get too high - this value depends on Python version:
@memunit.assert_lt_mb(185)
# Note: this does not combine well with memunit.
# memory_profiler.MemTimer does not terminate properly when a signal is raised by pytest-timeout,
# and as a consequence multiprocessing.util._exit_function becomes blocking at the end of Pytest execution,
# (on line "calling join() for process MemTimer-1")
def test_intense_image_rendering():
png_file_paths = []
for png_file_path in (HERE / "image/png_images/").glob("*.png"):
Expand Down

0 comments on commit 35d8e7a

Please sign in to comment.