Skip to content

Commit fd4f260

Browse files
committed
update tests. add to_unicode
1 parent 4c89e04 commit fd4f260

19 files changed

+137
-29
lines changed

fpdf/drawing.py

+5
Original file line numberDiff line numberDiff line change
@@ -3789,6 +3789,11 @@ def _insert_implicit_close_if_open(self):
37893789
self._close_context = self._graphics_context
37903790
self._closed = True
37913791

3792+
def linear_gradient_fill(self, x1, y1, x2, y2, stops):
3793+
gradient = LinearGradient(fpdf=None, from_x=x1, from_y=y1, to_x=x2, to_ys=y2, colors=[stop[1] for stop in stops])
3794+
gradient.coords = list(map(str, [x1, x2, y1, y2]))
3795+
self.gradient=gradient
3796+
37923797
def render(
37933798
self, gsd_registry, style, last_item, initial_point, debug_stream=None, pfx=None
37943799
):

fpdf/font_type_3.py

+81-23
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from typing import List, Tuple, TYPE_CHECKING
44
from io import BytesIO
55
from fontTools.ttLib.tables.BitmapGlyphMetrics import BigGlyphMetrics, SmallGlyphMetrics
6+
from fontTools.ttLib.tables.C_O_L_R_ import table_C_O_L_R_
7+
from fontTools.ttLib.tables.otTables import Paint, PaintFormat
68

79
from .drawing import DeviceRGB, GraphicsContext, Transform, PathPen, PaintedPath
810

@@ -146,34 +148,56 @@ def load_glyph_image(self, glyph: Type3FontGlyph):
146148

147149
class COLRFont(Type3Font):
148150

151+
def __init__(self, fpdf: "FPDF", base_font: "TTFFont"):
152+
super().__init__(fpdf, base_font)
153+
colr_table: table_C_O_L_R_ = self.base_font.ttfont["COLR"]
154+
self.colrv0_glyphs = []
155+
self.colrv1_glyphs = []
156+
self.version = colr_table.version
157+
if colr_table.version == 0:
158+
self.colrv0_glyphs = colr_table.ColorLayers
159+
else:
160+
self.colrv0_glyphs = colr_table._decompileColorLayersV0(colr_table.table)
161+
self.colrv1_glyphs = {
162+
glyph.BaseGlyph: glyph
163+
for glyph in colr_table.table.BaseGlyphList.BaseGlyphPaintRecord
164+
}
165+
self.palette = None
166+
if "CPAL" in self.base_font.ttfont:
167+
# hardcoding the first palette for now
168+
print(f"This font has {len(self.base_font.ttfont['CPAL'].palettes)} palettes")
169+
palette = self.base_font.ttfont["CPAL"].palettes[0]
170+
self.palette = [
171+
(color.red / 255, color.green / 255, color.blue / 255, color.alpha / 255) for color in palette
172+
]
173+
174+
149175
def glyph_exists(self, glyph_name):
150-
return glyph_name in self.base_font.ttfont["COLR"].ColorLayers
176+
return glyph_name in self.colrv0_glyphs or glyph_name in self.colrv1_glyphs
151177

152178
def load_glyph_image(self, glyph: Type3FontGlyph):
153179
w = round(self.base_font.ttfont["hmtx"].metrics[glyph.glyph_name][0] + 0.001)
154-
glyph_layers = self.base_font.ttfont["COLR"].ColorLayers[glyph.glyph_name]
155-
img = self.draw_glyph_colrv0(glyph_layers)
156-
img.transform = Transform.scaling(self.scale, -self.scale)
180+
if glyph.glyph_name in self.colrv0_glyphs:
181+
glyph_layers = self.base_font.ttfont["COLR"].ColorLayers[glyph.glyph_name]
182+
img = self.draw_glyph_colrv0(glyph_layers)
183+
else:
184+
img = self.draw_glyph_colrv1(glyph.glyph_name)
185+
img.transform = img.transform @ Transform.scaling(self.scale, -self.scale)
157186
output_stream = self.fpdf.draw_vector_glyph(img, self)
158187
glyph.glyph = (
159188
f"{w * self.scale / self.upem} 0 d0\n" "q\n" f"{output_stream}\n" "Q"
160189
)
161190
glyph.glyph_width = w
162191

163-
def get_color(self, color_index, palette=0):
164-
palettes = [
165-
[(c.red / 255, c.green / 255, c.blue / 255, c.alpha / 255) for c in p]
166-
for p in self.base_font.ttfont["CPAL"].palettes
167-
]
168-
r, g, b, a = palettes[palette][color_index]
169-
# a *= alpha
192+
def get_color(self, color_index, alpha=1):
193+
r, g, b, a = self.palette[color_index]
194+
a *= alpha
170195
return DeviceRGB(r, g, b, a)
171196

172197
def draw_glyph_colrv0(self, layers):
173198
gc = GraphicsContext()
199+
gc.transform = Transform.identity()
174200
for layer in layers:
175-
print(layer.__repr__)
176-
print(layer.colorID, layer.name)
177201
path = PaintedPath()
178202
glyph_set = self.base_font.ttfont.getGlyphSet()
179203
pen = PathPen(path, glyphSet=glyph_set)
@@ -183,10 +207,44 @@ def draw_glyph_colrv0(self, layers):
183207
path.style.stroke_color = self.get_color(layer.colorID)
184208
gc.add_item(path)
185209
return gc
186-
# print(path)
187-
# self.base_font.hbfont.draw_glyph_with_pen(gid, path)
188-
# canvas.drawPathSolid(path, self._getColor(layer.colorID, 1))
210+
211+
def draw_glyph_colrv1(self, glyph_name):
212+
gc = GraphicsContext()
213+
gc.transform = Transform.identity()
214+
glyph = self.colrv1_glyphs[glyph_name]
215+
self.draw_colrv1_paint(glyph.Paint, gc)
216+
return gc
189217

218+
def draw_colrv1_paint(self, paint: Paint, gc: GraphicsContext):
219+
print(paint.getFormatName())
220+
if paint.Format == PaintFormat.PaintColrLayers: #1
221+
print("[PaintColrLayers] FirstLayerIndex: ", paint.FirstLayerIndex, " NumLayers: ", paint.NumLayers)
222+
layer_list = self.base_font.ttfont["COLR"].table.LayerList
223+
for layer in range(paint.FirstLayerIndex, paint.FirstLayerIndex + paint.NumLayers):
224+
self.draw_colrv1_paint(layer_list.Paint[layer], gc)
225+
elif paint.Format == PaintFormat.PaintSolid: #2
226+
color = self.get_color(paint.PaletteIndex, paint.Alpha)
227+
path: PaintedPath = gc.path_items[-1]
228+
path.style.fill_color = color
229+
path.style.stroke_color = color
230+
elif paint.Format == PaintFormat.PaintLinearGradient: #4
231+
print("[PaintLinearGradient] ColorLine: ")
232+
for stop in paint.ColorLine.ColorStop:
233+
print("Stop: ", stop.StopOffset, " color: ", stop.PaletteIndex)
234+
print("x0: ", paint.x0, " y0: ", paint.y0, " x1: ", paint.x1, " y1: ", paint.y1)
235+
elif paint.Format == PaintFormat.PaintGlyph: #10
236+
path = PaintedPath()
237+
glyph_set = self.base_font.ttfont.getGlyphSet()
238+
pen = PathPen(path, glyphSet=glyph_set)
239+
glyph = glyph_set[paint.Glyph]
240+
glyph.draw(pen)
241+
gc.add_item(path)
242+
self.draw_colrv1_paint(paint.Paint, gc)
243+
else:
244+
print("Unknown PaintFormat: ", paint.Format)
245+
246+
247+
190248

191249
class CBDTColorFont(Type3Font):
192250

@@ -195,11 +253,11 @@ class CBDTColorFont(Type3Font):
195253
def glyph_exists(self, glyph_name):
196254
return glyph_name in self.base_font.ttfont["CBDT"].strikeData[0]
197255

198-
def load_glyph_image(self, glyph_name):
256+
def load_glyph_image(self, glyph: Type3FontGlyph):
199257
ppem = self.base_font.ttfont["CBLC"].strikes[0].bitmapSizeTable.ppemX
200-
glyph = self.base_font.ttfont["CBDT"].strikeData[0][glyph_name]
201-
glyph_bitmap = glyph.data[9:]
202-
metrics = glyph.metrics
258+
g = self.base_font.ttfont["CBDT"].strikeData[0][glyph.glyph_name]
259+
glyph_bitmap = g.data[9:]
260+
metrics = g.metrics
203261
if isinstance(metrics, SmallGlyphMetrics):
204262
x_min = round(metrics.BearingX * self.upem / ppem)
205263
y_min = round((metrics.BearingY - metrics.height) * self.upem / ppem)
@@ -300,9 +358,9 @@ def get_color_font_object(fpdf: "FPDF", base_font: "TTFFont") -> Type3Font:
300358
if "COLR" in base_font.ttfont:
301359
if base_font.ttfont["COLR"].version == 0:
302360
LOGGER.warning("Font %s is a COLRv0 color font", base_font.name)
303-
return COLRFont(fpdf, base_font)
304-
LOGGER.warning("Font %s is a COLRv1 color font", base_font.name)
305-
return None
361+
else:
362+
LOGGER.warning("Font %s is a COLRv1 color font", base_font.name)
363+
return COLRFont(fpdf, base_font)
306364
if "SVG " in base_font.ttfont:
307365
LOGGER.warning("Font %s is a SVG color font", base_font.name)
308366
return SVGColorFont(fpdf, base_font)

fpdf/fonts.py

+3
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,9 @@ def get_glyph(
622622
if unicode == 0x00:
623623
glyph_id = next(iter(self.font.cmap))
624624
return Glyph(glyph_id, (0x00,), ".notdef", 0)
625+
if unicode == 0x20:
626+
glyph_id = next(iter(self.font.cmap))
627+
return Glyph(glyph_id, (0x20,), "space", self.font.cw[0x20])
625628
return None
626629

627630
def get_all_glyph_names(self):

fpdf/output.py

+42
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ def __init__(self, font3: "Type3Font"):
111111
self.first_char = min(g.unicode for g in font3.glyphs)
112112
self.last_char = max(g.unicode for g in font3.glyphs)
113113
self.resources = None
114+
self.to_unicode = None
114115

115116
@property
116117
def char_procs(self):
@@ -760,7 +761,48 @@ def _add_fonts(self, image_objects_per_index, gfxstate_objs_per_name):
760761
glyph.obj_id = self._add_pdf_obj(
761762
PDFContentStream(contents=glyph.glyph, compress=False), "fonts"
762763
)
764+
bfChar = []
765+
def format_code(unicode):
766+
if unicode > 0xFFFF:
767+
# Calculate surrogate pair
768+
code_high = 0xD800 | (unicode - 0x10000) >> 10
769+
code_low = 0xDC00 | (unicode & 0x3FF)
770+
return f"{code_high:04X}{code_low:04X}"
771+
return f"{unicode:04X}"
772+
773+
for glyph, code_mapped in font.subset.items():
774+
if len(glyph.unicode) == 0:
775+
continue
776+
bfChar.append(
777+
f'<{code_mapped:02X}> <{"".join(format_code(code) for code in glyph.unicode)}>\n'
778+
)
779+
780+
to_unicode_obj = PDFContentStream(
781+
"/CIDInit /ProcSet findresource begin\n"
782+
"12 dict begin\n"
783+
"begincmap\n"
784+
"/CIDSystemInfo\n"
785+
"<</Registry (Adobe)\n"
786+
"/Ordering (UCS)\n"
787+
"/Supplement 0\n"
788+
">> def\n"
789+
"/CMapName /Adobe-Identity-UCS def\n"
790+
"/CMapType 2 def\n"
791+
"1 begincodespacerange\n"
792+
"<00> <FF>\n"
793+
"endcodespacerange\n"
794+
f"{len(bfChar)} beginbfchar\n"
795+
f"{''.join(bfChar)}"
796+
"endbfchar\n"
797+
"endcmap\n"
798+
"CMapName currentdict /CMap defineresource pop\n"
799+
"end\n"
800+
"end"
801+
)
802+
self._add_pdf_obj(to_unicode_obj, "fonts")
803+
763804
t3_font_obj = PDFType3Font(font.color_font)
805+
t3_font_obj.to_unicode = pdf_ref(to_unicode_obj.id)
764806
t3_font_obj.generate_resources(
765807
image_objects_per_index, gfxstate_objs_per_name
766808
)
685 Bytes
Binary file not shown.
791 Bytes
Binary file not shown.
761 Bytes
Binary file not shown.

test/color_font/colrv0-twemoji.pdf

669 Bytes
Binary file not shown.
777 Bytes
Binary file not shown.
64.5 KB
Binary file not shown.

test/color_font/sbix_bungee.pdf

827 Bytes
Binary file not shown.

test/color_font/sbix_compyx.pdf

779 Bytes
Binary file not shown.

test/color_font/svg_bungee.pdf

825 Bytes
Binary file not shown.

test/color_font/svg_gilbert.pdf

674 Bytes
Binary file not shown.
1.34 KB
Binary file not shown.
1.13 KB
Binary file not shown.

test/color_font/test_colr.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def test_twemoji(tmp_path):
2020
pdf.cell(text="Top 10 emojis:", new_x="right", new_y="top")
2121
pdf.set_font("Twemoji", "", 24)
2222
pdf.cell(text=test_text, new_x="lmargin", new_y="next")
23-
assert_pdf_equal(pdf, HERE / "colrv0-twemoji.pdf", tmp_path, generate=True)
23+
assert_pdf_equal(pdf, HERE / "colrv0-twemoji.pdf", tmp_path)
2424

2525

2626
def test_twemoji_shaping(tmp_path):
@@ -38,7 +38,7 @@ def test_twemoji_shaping(tmp_path):
3838
pdf.set_font("Twemoji", "", 24)
3939
pdf.set_text_shaping(True)
4040
pdf.multi_cell(w=pdf.epw, text=combined_emojis, new_x="lmargin", new_y="next")
41-
assert_pdf_equal(pdf, HERE / "colrv0-twemoji_shaping.pdf", tmp_path, generate=True)
41+
assert_pdf_equal(pdf, HERE / "colrv0-twemoji_shaping.pdf", tmp_path)
4242

4343

4444
def test_twemoji_text(tmp_path):
@@ -50,4 +50,4 @@ def test_twemoji_text(tmp_path):
5050
pdf.set_fallback_fonts(["Twemoji"])
5151
pdf.add_page()
5252
pdf.multi_cell(w=pdf.epw, text=text)
53-
assert_pdf_equal(pdf, HERE / "colrv0-twemoji_text.pdf", tmp_path, generate=True)
53+
assert_pdf_equal(pdf, HERE / "colrv0-twemoji_text.pdf", tmp_path)

test/color_font/test_sbix.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def test_sbix_compyx(tmp_path):
2222
pdf.ln()
2323
pdf.multi_cell(w=pdf.epw, text=LOREM_IPSUM.lower(), align="J")
2424

25-
assert_pdf_equal(pdf, HERE / "sbix_compyx.pdf", tmp_path, generate=True)
25+
assert_pdf_equal(pdf, HERE / "sbix_compyx.pdf", tmp_path)
2626

2727

2828
def test_sbix_bungee(tmp_path):
@@ -40,4 +40,4 @@ def test_sbix_bungee(tmp_path):
4040
pdf.ln()
4141
pdf.multi_cell(w=pdf.epw, text=LOREM_IPSUM.upper(), align="J")
4242

43-
assert_pdf_equal(pdf, HERE / "sbix_bungee.pdf", tmp_path, generate=True)
43+
assert_pdf_equal(pdf, HERE / "sbix_bungee.pdf", tmp_path)

test/color_font/test_svg.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def test_svg_bungee(tmp_path):
3939
pdf.ln()
4040
pdf.multi_cell(w=pdf.epw, text=LOREM_IPSUM.upper(), align="J")
4141

42-
assert_pdf_equal(pdf, HERE / "svg_bungee.pdf", tmp_path, generate=True)
42+
assert_pdf_equal(pdf, HERE / "svg_bungee.pdf", tmp_path)
4343

4444

4545
def test_twitter_emoji_shaping(tmp_path):

0 commit comments

Comments
 (0)