Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewriting add_font() and _putfonts() using Fonttools library #477

Merged
merged 54 commits into from
Sep 7, 2022
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
b20f393
modified add_font
RedShy Jul 28, 2022
af45b27
added parameter ft to makeSubset
RedShy Jul 28, 2022
d5a51c4
using font tools for tables before HMTX
RedShy Jul 28, 2022
8775c5c
refactor getHMTX
RedShy Jul 28, 2022
fddbf3e
managed hmtx and loca tables
RedShy Jul 29, 2022
dca5fc6
Merge branch 'PyFPDF:master' into fonttools_lib
RedShy Aug 1, 2022
ca19056
Merge branch 'fonttools_lib' of https://github.com/RedShy/fpdf2 into …
RedShy Aug 1, 2022
69d1771
fonttools subsetter instead of ttf.makeSubset
RedShy Aug 2, 2022
c7680c4
Merge branch 'PyFPDF:master' into fonttools_lib
RedShy Aug 6, 2022
8063478
options, rewrote codeToGlyph, rem TTFontFile
RedShy Aug 10, 2022
992ec5f
recalcTimestamp and comments
RedShy Aug 21, 2022
60a46a6
removed test add_font_otf
RedShy Aug 21, 2022
5baffe7
new pdfs
RedShy Aug 21, 2022
ddc63aa
Merge branch 'PyFPDF:master' into fonttools_lib
RedShy Aug 21, 2022
65fa718
rewrote test using fonttools
RedShy Aug 22, 2022
a4c0467
added otf font test
RedShy Aug 22, 2022
92b6a95
renamed variables
RedShy Aug 23, 2022
1a623c3
Merge branch 'PyFPDF:master' into fonttools_lib
RedShy Aug 23, 2022
99e09bc
Merge branch 'fonttools_lib' of https://github.com/RedShy/fpdf2 into …
RedShy Aug 23, 2022
2f4f06b
renamed font
RedShy Aug 23, 2022
9b41561
added fonttools to setup.py
RedShy Aug 24, 2022
6cc7f93
added fonttools to readme
RedShy Aug 24, 2022
0ac221c
drop tables
RedShy Aug 25, 2022
caf607d
re-generated pdfs
RedShy Aug 25, 2022
4a04bb1
Merge branch 'PyFPDF:master' into fonttools_lib
RedShy Aug 25, 2022
e6a83d2
Merge branch 'fonttools_lib' of https://github.com/RedShy/fpdf2 into …
RedShy Aug 25, 2022
2832e2f
added font descriptor flags enum
RedShy Aug 25, 2022
adeee41
removed unnecessary code in add_font
RedShy Aug 25, 2022
9ea88f7
Merge branch 'PyFPDF:master' into fonttools_lib
RedShy Aug 26, 2022
6941204
added in changelog
RedShy Aug 26, 2022
5f44a53
removed comment
RedShy Aug 26, 2022
c2064d2
simplified putting font descriptor
RedShy Aug 26, 2022
cdcca37
renamed subset, added comments
RedShy Aug 27, 2022
3c239b4
Merge branch 'PyFPDF:master' into fonttools_lib
RedShy Aug 27, 2022
57c1765
Merge branch 'fonttools_lib' of https://github.com/RedShy/fpdf2 into …
RedShy Aug 27, 2022
8b365c2
simplified _char_widths
RedShy Aug 27, 2022
e4e7652
Merge branch 'PyFPDF:master' into fonttools_lib
RedShy Sep 5, 2022
8a2b771
removed ttfonts.py
RedShy Sep 5, 2022
810af1e
removed .substr()
RedShy Sep 5, 2022
10d283d
removed import substr
RedShy Sep 5, 2022
d643506
removed code not covered by tests
RedShy Sep 5, 2022
5877dc2
generated pdfs
RedShy Sep 5, 2022
ef25097
fixed docstrings
RedShy Sep 5, 2022
f2c77ec
embed pdfs
RedShy Sep 5, 2022
ea1ba21
removed unnecessary code
RedShy Sep 5, 2022
66e66da
fix
RedShy Sep 5, 2022
fe6026e
fix fpdf
RedShy Sep 5, 2022
b05102f
Merge https://github.com/PyFPDF/fpdf2 into fonttools_lib
RedShy Sep 5, 2022
55f1ec8
regenareted pdfs
RedShy Sep 5, 2022
ce23c55
fixed pdfs
RedShy Sep 5, 2022
8fff76e
rename unifontsubset & unicode_font to is_ttf_font
RedShy Sep 7, 2022
7a2d102
fixed renaming
RedShy Sep 7, 2022
bdd5172
remove .char_width()
RedShy Sep 7, 2022
289366a
Merge branch 'PyFPDF:master' into fonttools_lib
RedShy Sep 7, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 128 additions & 35 deletions fpdf/fpdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
from os.path import splitext
from pathlib import Path
from typing import Callable, List, NamedTuple, Optional, Tuple, Union
from fontTools import ttLib
from fontTools import subset as ftsubset
from io import BytesIO

try:
from PIL.Image import Image
Expand Down Expand Up @@ -84,7 +87,6 @@ class Image:
from .syntax import create_list_string as pdf_list
from .syntax import create_stream as pdf_stream
from .syntax import iobj_ref as pdf_ref
from .ttfonts import TTFontFile
from .util import (
enclose_in_parens,
escape_parens,
Expand Down Expand Up @@ -1741,19 +1743,22 @@ def add_font(self, family, style="", fname=None, uni="DEPRECATED"):
"""
if not fname:
raise ValueError('"fname" parameter is required')

ext = splitext(str(fname))[1]
if ext not in (".otf", ".otc", ".ttf", ".ttc"):
raise ValueError(
f"Unsupported font file extension: {ext}."
" add_font() used to accept .pkl file as input, but for security reasons"
" this feature is deprecated since v2.5.1 and has been removed in v2.5.3."
)

if uni != "DEPRECATED":
warnings.warn(
'"uni" parameter is deprecated, unused and will soon be removed',
DeprecationWarning,
stacklevel=2,
)

style = "".join(sorted(style.upper()))
if any(letter not in "BI" for letter in style):
raise ValueError(
Expand All @@ -1765,49 +1770,100 @@ def add_font(self, family, 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

for parent in (".", FPDF_FONT_DIR):
if not parent:
continue

if (Path(parent) / fname).exists():
ttffilename = Path(parent) / fname
break
else:
raise FileNotFoundError(f"TTF Font file not found: {fname}")

# 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 tools
ft = ttLib.TTFont(ttffilename)
Lucas-C marked this conversation as resolved.
Show resolved Hide resolved

scale = 1000 / ft["head"].unitsPerEm
ascent = ft["hhea"].ascent * scale
descent = ft["hhea"].descent * scale
try:
capHeight = ft["OS/2"].sCapHeight * scale
except AttributeError:
capHeight = ascent
bbox = (
f"[{ft['head'].xMin * scale:.0f} {ft['head'].yMin * scale:.0f}"
f" {ft['head'].xMax * scale:.0f} {ft['head'].yMax * scale:.0f}]"
)
stemV = 50 + int(pow((ft["OS/2"].usWeightClass / 65), 2))
italicAngle = ft["post"].italicAngle
underlinePosition = ft["post"].underlinePosition * scale
underlineThickness = ft["post"].underlineThickness * scale

flags = 4
Lucas-C marked this conversation as resolved.
Show resolved Hide resolved
if ft["post"].isFixedPitch:
flags |= 1
if ft["post"].italicAngle != 0:
flags |= 64
if ft["OS/2"].usWeightClass >= 600:
flags |= 262144

aw = ft["hmtx"].metrics[".notdef"][0]
defaultWidth = scale * aw

charWidths = [len(ft.getBestCmap().keys()) - 1]
for char in ft.getBestCmap().keys():
if char in (0, 65535) or char >= 196608:
continue

glyph = ft.getBestCmap()[char]
aw = ft["hmtx"].metrics[glyph][0]

if char >= len(charWidths):
size = (((char + 1) // 1024) + 1) * 1024
delta = size - len(charWidths)
if delta > 0:
charWidths += [defaultWidth] * delta

w = round(scale * aw + 0.001) or 65535 # ROUND_HALF_UP
charWidths[char] = w

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),
"Ascent": round(ascent),
"Descent": round(descent),
"CapHeight": round(capHeight),
"Flags": flags,
"FontBBox": bbox,
"ItalicAngle": int(italicAngle),
"StemV": round(stemV),
"MissingWidth": round(defaultWidth),
}

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

self.font_files[fontkey] = {
"length1": font_dict["originalsize"],
"type": "TTF",
"ttffile": ttffilename,
}

# 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

self.fonts[fontkey] = {
"i": len(self.fonts) + 1,
"type": font_dict["type"],
Expand All @@ -1820,11 +1876,6 @@ def add_font(self, family, style="", fname=None, uni="DEPRECATED"):
"fontkey": fontkey,
"subset": SubsetMap(map(ord, sbarr)),
}
self.font_files[fontkey] = {
"length1": font_dict["originalsize"],
"type": "TTF",
"ttffile": ttffilename,
}

def set_font(self, family=None, style="", size=0):
"""
Expand Down Expand Up @@ -4015,15 +4066,56 @@ def _putfonts(self):
self._out("endobj")
elif my_type == "TTF":
self.fonts[font_name]["n"] = self.n + 1
ttf = TTFontFile()
fontname = f"MPDFAA+{font['name']}"
subset = font["subset"].dict()

# why we delete 0-element?
del subset[0]
ttfontstream = ttf.makeSubset(font["ttffile"], subset)

# ---- FONTTOOLS SUBSETTER ----
# recalcTimestamp=False means that it doesn't modify the "modified" timestamp in head table
Lucas-C marked this conversation as resolved.
Show resolved Hide resolved
# if we leave recalcTimestamp=True the tests will break every time
ft = ttLib.TTFont(file=font["ttffile"], recalcTimestamp=False)

# 1. get all glyphs in PDF
cmap = ft["cmap"].getBestCmap()
glyph_names = [cmap[code] for code in subset if code in cmap]

# 2. make a subset
# notdef_outline=True means that keeps the white box for the .notdef glyph
# recommended_glyphs=True means that adds the .notdef, .null, CR, and space glyphs
options = ftsubset.Options(notdef_outline=True, recommended_glyphs=True)
Lucas-C marked this conversation as resolved.
Show resolved Hide resolved
subsetter = ftsubset.Subsetter(options)
subsetter.populate(glyphs=glyph_names)
subsetter.subset(ft)

# 3. make codeToGlyph
# is a map Character_ID -> Glyph_ID
# it's used for associating glyphs to new codes
# this basically takes the old code of the character
# take the glyph associated with it
# and then associate to the new code the glyph associated with the old code
codeToGlyph = {}
for code, new_code_mapped in subset.items():
if code in cmap:
glyph_name = cmap[code]
codeToGlyph[new_code_mapped] = ft.getGlyphID(glyph_name)
else:
# notdef is associated if no glyph was associated to the old code
# it's not necessary to do this, it seems to be done by default
codeToGlyph[new_code_mapped] = ft.getGlyphID(".notdef")

# check: what is the usage of max_unicode?
max_unicode = max(subset)

# 4. return the ttfile
output = BytesIO()
ft.save(output)

output.seek(0)
ttfontstream = output.read()
ttfontsize = len(ttfontstream)
fontstream = zlib.compress(ttfontstream)
codeToGlyph = ttf.codeToGlyph
# del codeToGlyph[0]

# Type0 Font
# A composite font - a font composed of other fonts,
Expand All @@ -4049,7 +4141,7 @@ def _putfonts(self):
self._out(f"/FontDescriptor {pdf_ref(self.n + 3)}")
if font["desc"].get("MissingWidth"):
self._out(f"/DW {font['desc']['MissingWidth']}")
self._putTTfontwidths(font, ttf.maxUni)
self._putTTfontwidths(font, max_unicode)
self._out(f"/CIDToGIDMap {pdf_ref(self.n + 4)}")
self._out(">>")
self._out("endobj")
Expand Down Expand Up @@ -4138,8 +4230,10 @@ def _putfonts(self):
cidtogidmap[cc * 2] = chr(glyph >> 8)
cidtogidmap[cc * 2 + 1] = chr(glyph & 0xFF)
cidtogidmap = "".join(cidtogidmap)

# manage binary data as latin1 until PEP461-like function is implemented
cidtogidmap = zlib.compress(cidtogidmap.encode("latin1"))

self._newobj()
self._out(f"<</Length {len(cidtogidmap)}")
self._out("/Filter /FlateDecode")
Expand All @@ -4155,7 +4249,6 @@ def _putfonts(self):
self._out(">>")
self._out(pdf_stream(fontstream))
self._out("endobj")
del ttf
else:
# Allow for additional types
mtd = f"_put{my_type.lower()}"
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file modified test/fonts/add_font_unicode.pdf
Binary file not shown.
Binary file modified test/fonts/fonts_emoji_glyph.pdf
Binary file not shown.
Binary file modified test/fonts/fonts_remap_nb.pdf
Binary file not shown.
Binary file modified test/fonts/fonts_two_mappings.pdf
Binary file not shown.
Binary file modified test/fonts/render_en_dash.pdf
Binary file not shown.
12 changes: 6 additions & 6 deletions test/fonts/test_add_font.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,9 @@ def test_render_en_dash(tmp_path): # issue-166
assert_pdf_equal(pdf, HERE / "render_en_dash.pdf", tmp_path)


def test_add_font_otf():
pdf = FPDF()
font_file_path = HERE / "Quicksand-Regular.otf"
with pytest.raises(RuntimeError) as error:
pdf.add_font("Quicksand", fname=font_file_path)
assert str(error.value) == "Postscript outlines are not supported"
# def test_add_font_otf():
# pdf = FPDF()
# font_file_path = HERE / "Quicksand-Regular.otf"
# with pytest.raises(RuntimeError) as error:
# pdf.add_font("Quicksand", fname=font_file_path)
# assert str(error.value) == "Postscript outlines are not supported"
Binary file modified test/fonts/thai_text.pdf
Binary file not shown.
Binary file modified test/html/html_heading_hebrew.pdf
Binary file not shown.
Binary file modified test/html/issue_156.pdf
Binary file not shown.
Binary file modified test/outline/russian_heading.pdf
Binary file not shown.
Binary file modified test/text/cell_curfont_leak.pdf
Binary file not shown.
Binary file modified test/text/cell_markdown_right_aligned.pdf
Binary file not shown.
Binary file modified test/text/cell_markdown_with_ttf_fonts.pdf
Binary file not shown.
Binary file modified test/text/multi_cell_font_leakage.pdf
Binary file not shown.
Binary file modified test/text/multi_cell_font_stretching.pdf
Binary file not shown.
Binary file modified test/text/multi_cell_j_paragraphs.pdf
Binary file not shown.
Binary file modified test/text/multi_cell_markdown_with_ttf_fonts.pdf
Binary file not shown.
Binary file modified test/text/test_multi_cell_justified_with_unicode_font.pdf
Binary file not shown.
Binary file modified test/text/write_font_stretching.pdf
Binary file not shown.