|
| 1 | +import logging |
| 2 | + |
| 3 | +from typing import List, Tuple, TYPE_CHECKING |
| 4 | +from io import BytesIO |
| 5 | +from fontTools.ttLib.tables.BitmapGlyphMetrics import BigGlyphMetrics, SmallGlyphMetrics |
| 6 | + |
| 7 | +from .image_datastructures import RasterImageInfo, VectorImageInfo |
| 8 | + |
| 9 | + |
| 10 | + |
| 11 | +if TYPE_CHECKING: |
| 12 | + from .fpdf import FPDF |
| 13 | + from .fonts import TTFFont |
| 14 | + |
| 15 | + |
| 16 | +LOGGER = logging.getLogger(__name__) |
| 17 | + |
| 18 | + |
| 19 | +class Type3FontGlyph: |
| 20 | + # RAM usage optimization: |
| 21 | + __slots__ = ( |
| 22 | + "obj_id", |
| 23 | + "glyph_id", |
| 24 | + "unicode", |
| 25 | + "glyph_name", |
| 26 | + "glyph_width", |
| 27 | + "glyph", |
| 28 | + "_glyph_bounds", |
| 29 | + ) |
| 30 | + obj_id: int |
| 31 | + glyph_id: int |
| 32 | + unicode: Tuple |
| 33 | + glyph_name: str |
| 34 | + glyph_width: int |
| 35 | + glyph: str |
| 36 | + _glyph_bounds: Tuple[int, int, int, int] |
| 37 | + |
| 38 | + def __init__(self): |
| 39 | + pass |
| 40 | + |
| 41 | + def __hash__(self): |
| 42 | + return self.glyph_id |
| 43 | + |
| 44 | + |
| 45 | +class Type3Font: |
| 46 | + |
| 47 | + def __init__(self, fpdf: "FPDF", base_font: "TTFFont"): |
| 48 | + self.i = 1 |
| 49 | + self.type = "type3" |
| 50 | + self.fpdf = fpdf |
| 51 | + self.base_font = base_font |
| 52 | + self.upem = self.base_font.ttfont["head"].unitsPerEm |
| 53 | + self.scale = 1000 / self.upem |
| 54 | + self.images_used = set() |
| 55 | + self.graphics_style_used = set() |
| 56 | + self.glyphs: List[Type3FontGlyph] = [] |
| 57 | + |
| 58 | + @classmethod |
| 59 | + def get_notdef_glyph(cls, glyph_id) -> Type3FontGlyph: |
| 60 | + notdef = Type3FontGlyph() |
| 61 | + notdef.glyph_id = glyph_id |
| 62 | + notdef.unicode = 0 |
| 63 | + notdef.glyph_name = ".notdef" |
| 64 | + notdef.glyph_width = 0 |
| 65 | + notdef.glyph = "0 0 d0" |
| 66 | + return notdef |
| 67 | + |
| 68 | + def get_space_glyph(self, glyph_id) -> Type3FontGlyph: |
| 69 | + space = Type3FontGlyph() |
| 70 | + space.glyph_id = glyph_id |
| 71 | + space.unicode = 0x20 |
| 72 | + space.glyph_name = "space" |
| 73 | + space.glyph_width = self.base_font.desc.missing_width |
| 74 | + space.glyph = f"{space.glyph_width} 0 d0" |
| 75 | + return space |
| 76 | + |
| 77 | + def load_glyphs(self): |
| 78 | + for glyph, char_id in self.base_font.subset.items(): |
| 79 | + if not self.glyph_exists(glyph.glyph_name): |
| 80 | + # print(f"notdef id {char_id} name {glyph.glyph_name}") |
| 81 | + if char_id == 0x20: |
| 82 | + self.glyphs.append(self.get_space_glyph(char_id)) |
| 83 | + else: |
| 84 | + self.glyphs.append(self.get_notdef_glyph(char_id)) |
| 85 | + continue |
| 86 | + self.add_glyph(glyph.glyph_name, char_id) |
| 87 | + |
| 88 | + def add_glyph(self, glyph_name, char_id): |
| 89 | + g = Type3FontGlyph() |
| 90 | + g.glyph_id = char_id |
| 91 | + g.unicode = char_id |
| 92 | + g.glyph_name = glyph_name |
| 93 | + self.load_glyph_image(g) |
| 94 | + self.glyphs.append(g) |
| 95 | + |
| 96 | + def load_glyph_image(self, glyph: Type3FontGlyph): |
| 97 | + x_min, y_min, x_max, y_max, _, glyph_bitmap = self.read_glyph_data( |
| 98 | + glyph.glyph_name |
| 99 | + ) |
| 100 | + bio = BytesIO(glyph_bitmap) |
| 101 | + bio.seek(0) |
| 102 | + _, img, info = self.fpdf.preload_image(bio, None) |
| 103 | + if isinstance(info, VectorImageInfo): |
| 104 | + w = round( |
| 105 | + self.base_font.ttfont["hmtx"].metrics[glyph.glyph_name][0] * self.scale |
| 106 | + + 0.001 |
| 107 | + ) |
| 108 | + # _, _, path = img.transform_to_rect_viewport(self.fpdf.k, None, None, align_viewbox=False) |
| 109 | + _, _, path = img.transform_to_page_viewport( |
| 110 | + pdf=self.fpdf, align_viewbox=False |
| 111 | + ) |
| 112 | + output_stream = self.fpdf.draw_vector_glyph(path, self) |
| 113 | + glyph.glyph = ( |
| 114 | + f"{w} 0 d0\n" |
| 115 | + "q\n" |
| 116 | + # f"1 0 0 1 {x_min * self.scale} {y_min * self.scale} cm\n" |
| 117 | + f"{output_stream}\n" |
| 118 | + "Q" |
| 119 | + ) |
| 120 | + glyph.glyph_width = x_max |
| 121 | + elif isinstance(info, RasterImageInfo): |
| 122 | + glyph.glyph = ( |
| 123 | + f"{x_max * self.scale} 0 d0\n" |
| 124 | + "q\n" |
| 125 | + f"{x_max * self.scale} 0 0 {(-y_min + y_max) * self.scale} {x_min * self.scale} {y_min * self.scale} cm\n" |
| 126 | + f"/I{info['i']} Do\nQ" |
| 127 | + ) |
| 128 | + glyph.glyph_width = x_max |
| 129 | + self.images_used.add(info["i"]) |
| 130 | + |
| 131 | + def glyph_exists(self, glyph_name: str) -> bool: |
| 132 | + raise NotImplementedError("Method must be implemented on child class") |
| 133 | + |
| 134 | + def read_glyph_data(self, glyph_name): |
| 135 | + raise NotImplementedError("Method must be implemented on child class") |
| 136 | + |
| 137 | + |
| 138 | +class SVGColorFont(Type3Font): |
| 139 | + |
| 140 | + def glyph_exists(self, glyph_name): |
| 141 | + glyph_id = self.base_font.ttfont.getGlyphID(glyph_name) |
| 142 | + return any( |
| 143 | + svg_doc.startGlyphID <= glyph_id <= svg_doc.endGlyphID |
| 144 | + for svg_doc in self.base_font.ttfont["SVG "].docList |
| 145 | + ) |
| 146 | + |
| 147 | + def read_glyph_data(self, glyph_name: str) -> BytesIO: |
| 148 | + glyph_id = self.base_font.ttfont.getGlyphID(glyph_name) |
| 149 | + glyph_svg_data = None |
| 150 | + for svg_doc in self.base_font.ttfont["SVG "].docList: |
| 151 | + if svg_doc.startGlyphID <= glyph_id <= svg_doc.endGlyphID: |
| 152 | + glyph_svg_data = svg_doc.data.encode("utf-8") |
| 153 | + |
| 154 | + x_min, y_min, x_max, y_max = self.get_glyph_bounds(glyph_name) |
| 155 | + x_min = round(x_min) # * self.upem / ppem) |
| 156 | + y_min = round(y_min) # * self.upem / ppem) |
| 157 | + x_max = round(x_max) # * self.upem / ppem) |
| 158 | + y_max = round(y_max) # * self.upem / ppem) |
| 159 | + |
| 160 | + return x_min, y_min, x_max, y_max, x_max, glyph_svg_data |
| 161 | + |
| 162 | + def get_glyph_bounds(self, glyph_name: str) -> Tuple[int, int, int, int]: |
| 163 | + glyph_id = self.base_font.ttfont.getGlyphID(glyph_name) |
| 164 | + x, y, w, h = self.base_font.hbfont.get_glyph_extents(glyph_id) |
| 165 | + # convert from HB's x/y_bearing + extents to xMin, yMin, xMax, yMax |
| 166 | + y += h |
| 167 | + h = -h |
| 168 | + w += x |
| 169 | + h += y |
| 170 | + # print(f"harfbuzz values: {x}, {y}, {w}, {h}") |
| 171 | + return x, y, w, h |
| 172 | + |
| 173 | + |
| 174 | +class CBDTColorFont(Type3Font): |
| 175 | + |
| 176 | + # Only looking at the first strike - Need to look all strikes available on the CBLC table first? |
| 177 | + |
| 178 | + def glyph_exists(self, glyph_name): |
| 179 | + return glyph_name in self.base_font.ttfont["CBDT"].strikeData[0] |
| 180 | + |
| 181 | + def read_glyph_data(self, glyph_name): |
| 182 | + ppem = self.base_font.ttfont["CBLC"].strikes[0].bitmapSizeTable.ppemX |
| 183 | + glyph = self.base_font.ttfont["CBDT"].strikeData[0][glyph_name] |
| 184 | + glyph_bitmap = glyph.data[9:] |
| 185 | + metrics = glyph.metrics |
| 186 | + if isinstance(metrics, SmallGlyphMetrics): |
| 187 | + x_min = round(metrics.BearingX * self.upem / ppem) |
| 188 | + y_min = round((metrics.BearingY - metrics.height) * self.upem / ppem) |
| 189 | + x_max = round(metrics.width * self.upem / ppem) |
| 190 | + y_max = round(metrics.BearingY * self.upem / ppem) |
| 191 | + advance = round(metrics.Advance * self.upem / ppem) |
| 192 | + elif isinstance(metrics, BigGlyphMetrics): |
| 193 | + x_min = round(metrics.horiBearingX * self.upem / ppem) |
| 194 | + y_min = round((metrics.horiBearingY - metrics.height) * self.upem / ppem) |
| 195 | + x_max = round(metrics.width * self.upem / ppem) |
| 196 | + y_max = round(metrics.horiBearingY * self.upem / ppem) |
| 197 | + advance = round(metrics.horiAdvance * self.upem / ppem) |
| 198 | + else: # fallback scenario: use font bounding box |
| 199 | + x_min = self.base_font.ttfont["head"].xMin |
| 200 | + y_min = self.base_font.ttfont["head"].yNin |
| 201 | + x_max = self.base_font.ttfont["head"].xMax |
| 202 | + y_max = self.base_font.ttfont["head"].yMax |
| 203 | + advance = self.base_font.ttfont["hmtx"].metrics[".notdef"][0] |
| 204 | + return x_min, y_min, x_max, y_max, advance, glyph_bitmap |
| 205 | + |
| 206 | + |
| 207 | +class SBIXColorFont(Type3Font): |
| 208 | + |
| 209 | + def glyph_exists(self, glyph_name): |
| 210 | + ppem = list(self.base_font.ttfont["sbix"].strikes.keys())[0] |
| 211 | + return ( |
| 212 | + self.base_font.ttfont["sbix"].strikes[ppem].glyphs.get(glyph_name.upper()) |
| 213 | + ) |
| 214 | + |
| 215 | + def read_glyph_data(self, glyph_name: str) -> BytesIO: |
| 216 | + # how to select the ideal ppm? |
| 217 | + # print(self.base_font.ttfont["sbix"].strikes.keys()) |
| 218 | + ppem = list(self.base_font.ttfont["sbix"].strikes.keys())[0] |
| 219 | + # print(f"ppem {ppem}") |
| 220 | + # print(f'unitsPerEm {self.base_font.ttfont["head"].unitsPerEm}') |
| 221 | + # print( |
| 222 | + # f'xMin {self.base_font.ttfont["head"].xMin} xMax {self.base_font.ttfont["head"].xMax}' |
| 223 | + # ) |
| 224 | + # print( |
| 225 | + # f'yMin {self.base_font.ttfont["head"].yMin} yMax {self.base_font.ttfont["head"].yMax}' |
| 226 | + # ) |
| 227 | + # print(f'glyphDataFormat {self.base_font.ttfont["head"].glyphDataFormat}') |
| 228 | + |
| 229 | + glyph = self.base_font.ttfont["sbix"].strikes[ppem].glyphs.get(glyph_name) |
| 230 | + if not glyph: |
| 231 | + return None |
| 232 | + |
| 233 | + if glyph.graphicType == "dupe": |
| 234 | + return None |
| 235 | + # to do - waiting for an example to test |
| 236 | + # dupe_char = font.getBestCmap()[glyph.imageData] |
| 237 | + # return self.get_color_glyph(dupe_char) |
| 238 | + |
| 239 | + x_min, y_min, x_max, y_max = self.get_glyph_bounds(glyph_name) |
| 240 | + x_min = round(x_min * self.upem / ppem) |
| 241 | + y_min = round(y_min * self.upem / ppem) |
| 242 | + x_max = round(x_max * self.upem / ppem) |
| 243 | + y_max = round(y_max * self.upem / ppem) |
| 244 | + |
| 245 | + # graphic type 'pdf' or 'mask' are not supported |
| 246 | + return x_min, y_min, x_max, y_max, x_max, glyph.imageData |
| 247 | + |
| 248 | + def get_glyph_bounds(self, glyph_name: str) -> Tuple[int, int, int, int]: |
| 249 | + glyph_id = self.base_font.ttfont.getGlyphID(glyph_name) |
| 250 | + x, y, w, h = self.base_font.hbfont.get_glyph_extents(glyph_id) |
| 251 | + # convert from HB's x/y_bearing + extents to xMin, yMin, xMax, yMax |
| 252 | + y += h |
| 253 | + h = -h |
| 254 | + w += x |
| 255 | + h += y |
| 256 | + # print(f"harfbuzz values: {x}, {y}, {w}, {h}") |
| 257 | + return x, y, w, h |
| 258 | + |
| 259 | + |
| 260 | +# pylint: disable=too-many-return-statements |
| 261 | +def get_color_font_object(fpdf: "FPDF", base_font: "TTFFont") -> Type3Font: |
| 262 | + if "CBDT" in base_font.ttfont: |
| 263 | + LOGGER.warning("Font %s is a CBLC+CBDT color font", base_font.name) |
| 264 | + return CBDTColorFont(fpdf, base_font) |
| 265 | + if "EBDT" in base_font.ttfont: |
| 266 | + LOGGER.warning("%s - EBLC+EBDT color font is not supported yet", base_font.name) |
| 267 | + return None |
| 268 | + if "COLR" in base_font.ttfont: |
| 269 | + if base_font.ttfont["COLR"].version == 0: |
| 270 | + LOGGER.warning("Font %s is a COLRv0 color font", base_font.name) |
| 271 | + return None |
| 272 | + LOGGER.warning("Font %s is a COLRv1 color font", base_font.name) |
| 273 | + return None |
| 274 | + if "SVG " in base_font.ttfont: |
| 275 | + LOGGER.warning("Font %s is a SVG color font", base_font.name) |
| 276 | + return SVGColorFont(fpdf, base_font) |
| 277 | + if "sbix" in base_font.ttfont: |
| 278 | + LOGGER.warning("Font %s is a SBIX color font", base_font.name) |
| 279 | + return SBIXColorFont(fpdf, base_font) |
| 280 | + return None |
0 commit comments