def test_rvrn(latest_otf, otf_font, latest_ttf, ttf_font, wght_val): """ Ensure that the 'rvrn' feature is activated/not activated at expected variations. The 'rvrn' feature of this font substitutes a design variation of '$' and '¢' at heavier weights. """ test_str = "$2.00 5¢" for expected, actual in ((latest_otf, otf_font), (latest_ttf, ttf_font)): buf_expected = hb.Buffer() buf_expected.add_str(test_str) buf_expected.guess_segment_properties() buf_actual = hb.Buffer() buf_actual.add_str(test_str) buf_actual.guess_segment_properties() expected.set_variations({"wght": wght_val}) hb.shape(expected, buf_expected, None) infos_expected = buf_expected.glyph_infos actual.set_variations({"wght": wght_val}) hb.shape(actual, buf_actual, None) infos_actual = buf_actual.glyph_infos assert len(infos_actual) == len(infos_expected) for i in range(len(infos_expected)): gn_expected = expected.get_glyph_name(infos_expected[i].codepoint) gn_actual = actual.get_glyph_name(infos_actual[i].codepoint) assert gn_actual == gn_expected
def test_kern_regression(latest_otf, otf_font, latest_ttf, ttf_font, string, use_kerning): for expected, actual in ((latest_otf, otf_font), (latest_ttf, ttf_font)): features = {"kern": use_kerning} buf_expected = hb.Buffer() buf_expected.add_str(string) buf_expected.guess_segment_properties() buf_actual = hb.Buffer() buf_actual.add_str(string) buf_actual.guess_segment_properties() hb.shape(expected, buf_expected, features) infos_expected = buf_expected.glyph_infos positions_expected = buf_expected.glyph_positions hb.shape(actual, buf_actual, features) infos_actual = buf_actual.glyph_infos positions_actual = buf_actual.glyph_positions assert len(infos_expected) == len(infos_actual) for i in range(len(infos_expected)): gn_expected = expected.get_glyph_name(infos_expected[i].codepoint) gn_actual = actual.get_glyph_name(infos_actual[i].codepoint) assert gn_actual == gn_expected pos_expected = positions_expected[i].x_advance pos_actual = positions_actual[i].x_advance assert pos_actual == pos_expected
def pair_kerning(self, left, right): """The kerning between two glyphs (specified by name), in font units.""" if self.face.has_kerning: return (self.face.get_kerning(left, right).x >> 6) * self.scale_factor else: if not self.hbFont: with open(self.filename, "rb") as fontfile: fontdata = fontfile.read() face = hb.Face(fontdata) font = hb.Font(face) scale = face.upem * self.scale_factor font.scale = (scale, scale) self.hbFont = font buf = hb.Buffer() buf.add_str(left+right) buf.guess_segment_properties() hb.shape(self.hbFont, buf, {"kern":True}) pos = buf.glyph_positions[0].x_advance buf = hb.Buffer() buf.add_str(left+right) buf.guess_segment_properties() hb.shape(self.hbFont, buf, {"kern":False}) pos2 = buf.glyph_positions[0].x_advance return pos-pos2
def test_figs_regression(latest_otf, otf_font, latest_ttf, ttf_font, features_on, result_suffix): """Compare figure/digit substitutions against latest release.""" digit_str = "0123456789" features = {feat: True for feat in features_on} for expected, actual in ((latest_otf, otf_font), (latest_ttf, ttf_font)): buf_expected = hb.Buffer() buf_expected.add_str(digit_str) buf_expected.guess_segment_properties() buf_actual = hb.Buffer() buf_actual.add_str(digit_str) buf_actual.guess_segment_properties() hb.shape(expected, buf_expected, features) infos_expected = buf_expected.glyph_infos hb.shape(actual, buf_actual, features) infos_actual = buf_actual.glyph_infos assert len(infos_expected) == len(infos_actual) for i in range(len(infos_expected)): gn_expected = expected.get_glyph_name(infos_expected[i].codepoint) gn_actual = actual.get_glyph_name(infos_actual[i].codepoint) assert gn_actual == gn_expected cl_expected = infos_expected[i].cluster cl_actual = infos_actual[i].cluster assert cl_actual == cl_expected
def test_zero_regression(latest_otf, otf_font, latest_ttf, ttf_font): zero_str = "90125" features = {"zero": True} for expected, actual in ((latest_otf, otf_font), (latest_ttf, ttf_font)): buf_expected = hb.Buffer() buf_expected.add_str(zero_str) buf_expected.guess_segment_properties() buf_actual = hb.Buffer() buf_actual.add_str(zero_str) buf_actual.guess_segment_properties() hb.shape(expected, buf_expected, features) infos_expected = buf_expected.glyph_infos hb.shape(actual, buf_actual, features) infos_actual = buf_actual.glyph_infos assert len(infos_expected) == len(infos_actual) for i in range(len(infos_expected)): gn_expected = expected.get_glyph_name(infos_expected[i].codepoint) gn_actual = actual.get_glyph_name(infos_actual[i].codepoint) assert gn_actual == gn_expected cl_expected = infos_expected[i].cluster cl_actual = infos_actual[i].cluster assert cl_actual == cl_expected
def test_guess_set_segment_properties(self): buf = hb.Buffer() buf.add_str("הארץ") buf.guess_segment_properties() assert buf.direction == "rtl" assert buf.script == "Hebr" # the guessed language seems to be locale specific # assert buf.language == "en-us" assert buf.language buf.direction = "ltr" assert buf.direction == "ltr" buf.script = "Latn" assert buf.script == "Latn" buf.language = "he-il" assert buf.language == "he-il" buf.set_script_from_ot_tag("mym2") assert buf.script == "Mymr" buf.set_language_from_ot_tag("BGR") assert buf.language == "bg"
def test_message_func(self, blankfont): # Glyph IDs 1, 2, 3, 4, 5 map to glyphs a, b, c, d, e. # The calt feature replaces c by a in the context e, d, c', b, a. # The kern feature kerns b, a by +100. string = "edcba" buf = hb.Buffer() buf.add_str(string) buf.guess_segment_properties() messages = [] infos_trace = [] positions_trace = [] def message(msg): messages.append(msg) infos_trace.append(buf.glyph_infos) positions_trace.append(buf.glyph_positions) buf.set_message_func(message) hb.shape(blankfont, buf) gids = [g.codepoint for g in buf.glyph_infos] assert gids == [5, 4, 1, 2, 1] pos = [g.x_advance for g in buf.glyph_positions] assert pos == [0, 0, 0, 100, 0] # messages: start GSUB lookup, end GSUB lookup, start GPOS lookup, end GPOS lookup assert messages == [ 'start lookup 0', 'end lookup 0', 'start lookup 0', 'end lookup 0' ] gids_trace = [[g.codepoint for g in infos] for infos in infos_trace] assert gids_trace == [[5, 4, 3, 2, 1], [5, 4, 1, 2, 1], [5, 4, 1, 2, 1], [5, 4, 1, 2, 1]] advances_trace = [[g.x_advance for g in pos] for pos in positions_trace] assert advances_trace == [[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 100, 0]]
def shape_text_to_glyph_names( self, text: str, features: dict = None, gid_to_name: dict[int, str] = None, ) -> list[str]: buffer = hb.Buffer() # type: ignore buffer.add_str(text) buffer.guess_segment_properties() hb.shape(self.font, buffer, features) # type: ignore names = [] for info, position in zip(buffer.glyph_infos, buffer.glyph_positions): gid = info.codepoint if gid_to_name is None: name = self.font.get_glyph_name(gid) else: name = gid_to_name.get(gid, f"gid{gid}") if name == "space" and position.x_advance == 0: # HarfBuzz pseudo space for invisible glyphs name = "_invisible" names.append(name) return names
def test_gid_and_cluster_no_features(self, blankfont, string, expected): buf = hb.Buffer() buf.add_str(string) buf.guess_segment_properties() hb.shape(blankfont, buf) infos = [(g.codepoint, g.cluster) for g in buf.glyph_infos] assert infos == expected
def shaping_string(fontdata, glyphOrder, text, language=None): face = hb.Face(fontdata) font = hb.Font(face) upem = face.upem font.scale = (upem, upem) hb.ot_font_set_funcs(font) buf = hb.Buffer() buf.add_str(text) buf.guess_segment_properties() if language: buf.language = language features = {"kern": True, "liga": True} hb.shape(font, buf, features) infos = buf.glyph_infos positions = buf.glyph_positions outs = [] for info, pos in zip(buf.glyph_infos, buf.glyph_positions): name = glyphOrder[info.codepoint] if name in ignorables: continue outs.append("%s=%i" % (name, info.cluster)) if pos.position[0] != 0 or pos.position[1] != 0: outs[-1] = outs[-1] + "<%i,%i>" % (pos.position[0], pos.position[1]) return "|".join(outs)
def shape(self, text, onchange=None): """Shapes a text This shapes a piece of text, return a uharfbuzz `Buffer` object. Additionally, if an `onchange` function is provided, this will be called every time the buffer changes *during* shaping, with the following arguments: - ``self``: the vharfbuzz object. - ``stage``: either "GSUB" or "GPOS" - ``lookupid``: the current lookup ID - ``buffer``: a copy of the buffer as a list of lists (glyphname, cluster, position) """ self.prepare_shaper() buf = hb.Buffer() buf.add_str(text) buf.guess_segment_properties() self.stage = "GSUB" if onchange: f = self.make_message_handling_function(buf, onchange) buf.set_message_func(f) hb.shape(self.hbfont, buf, shapers=self.shapers) self.stage = "GPOS" return buf
def test_message_func_return_false(self, blankfont): # Glyph IDs 1, 2, 3, 4, 5 map to glyphs a, b, c, d, e. # The calt feature replaces c by a in the context e, d, c', b, a. # The kern feature kerns b, a by +100. string = "edcba" buf = hb.Buffer() buf.add_str(string) buf.guess_segment_properties() messages = [] infos_trace = [] positions_trace = [] def message(msg): messages.append(msg) infos_trace.append(buf.glyph_infos) positions_trace.append(buf.glyph_positions) return False buf.set_message_func(message) hb.shape(blankfont, buf) gids = [g.codepoint for g in buf.glyph_infos] assert gids == [5, 4, 3, 2, 1] pos = [g.x_advance for g in buf.glyph_positions] assert pos == [0, 0, 0, 0, 0] expected_messages = [ 'start table GSUB', 'start table GPOS', ] assert messages == expected_messages gids_trace = [[g.codepoint for g in infos] for infos in infos_trace] assert gids_trace == [[5, 4, 3, 2, 1], [5, 4, 3, 2, 1]] advances_trace = [[g.x_advance for g in pos] for pos in positions_trace if pos] assert advances_trace == [[0, 0, 0, 0, 0]]
def metrics_from_text( ctx, font_path, text ): import uharfbuzz as HB ### NOTE import after ctx available ### mhbfont = get_mhbfont( ctx, font_path ) bfr = HB.Buffer() bfr.add_str( text ) bfr.guess_segment_properties() features = { 'kern': True, 'liga': True, } HB.shape( mhbfont.font, bfr, features ) infos = bfr.glyph_infos positions = bfr.glyph_positions scale = 1000 / mhbfont.upem R = ctx.AttributeDict() width = 0 R.width = width R.parts = [] for info, position in zip( infos, positions ): part = ctx.AttributeDict() x_advance = position.x_advance * scale part.dx = round( x_advance ) width += x_advance # part.x_offset = position.x_offset * scale # part.y_advance = position.y_advance * scale # part.y_offset = position.y_offset * scale part.fid = 'f123' ### NOTE fake font ID, to be replaced by viable ID of font ### part.gid = info.codepoint R.parts.append( part ) # ctx.log( '^77767^', part ) R.width = round( width ) return R
def shape_a_text(self, text): buf = hb.Buffer() buf.add_str(text) buf.guess_segment_properties() hb.shape(self.hbfont, buf) self.direction = buf.direction return buf
def draw(surface, paths, text, features): bounds = None lines = [] y = 0 for path in paths: font = BlackRendererFont(path) buf = hb.Buffer() buf.add_str(text) buf.guess_segment_properties() hb.shape(font.hbFont, buf, features) line, rect, height = makeLine(buf, font, y) lines.append((font, line, rect, y)) if bounds is None: bounds = rect bounds = unionRect(bounds, rect) y += height with surface.canvas(bounds) as canvas: for font, line, rect, y in lines: with canvas.savedState(): # Center align the line. x = (bounds[2] - rect[2]) / 2 canvas.translate(x, y) for glyph in line: with canvas.savedState(): canvas.translate(glyph.xOffset, glyph.yOffset) font.drawGlyph(glyph.name, canvas) canvas.translate(glyph.xAdvance, glyph.yAdvance)
def test_message_func_crash(self, blankfont): string = "edcba" buf = hb.Buffer() buf.add_str(string) buf.guess_segment_properties() message_collector = MessageCollector() buf.set_message_func(message_collector.message) hb.shape(blankfont, buf)
def test_features_slice(self, blankfont, string, features, expected): buf = hb.Buffer() buf.add_str(string) buf.guess_segment_properties() hb.shape(blankfont, buf, features) glyph_names = [blankfont.glyph_to_string(g.codepoint) for g in buf.glyph_infos] assert glyph_names == expected
def shape(self, text, onchange=None): self.prepare_shaper() buf = hb.Buffer() buf.add_str(text) buf.guess_segment_properties() test = self.make_message_handling_function(buf, onchange) if onchange: buf.set_message_func(test) hb.shape(self.hbfont, buf)
def pair_kerning(font, left, right): """The kerning between two glyphs (specified by name), in font units.""" with open(font, "rb") as fontfile: fontdata = fontfile.read() face = hb.Face(fontdata) font = hb.Font(face) scale = face.upem font.scale = (scale, scale) buf = hb.Buffer() buf.add_str(left + right) buf.guess_segment_properties() hb.shape(font, buf, {"kern": True}) pos = buf.glyph_positions[0].x_advance buf = hb.Buffer() buf.add_str(left + right) buf.guess_segment_properties() hb.shape(font, buf, {"kern": False}) pos2 = buf.glyph_positions[0].x_advance return pos - pos2
def test_shape_set_shaper(self, blankfont): string = "abcde" expected = [] buf = hb.Buffer() buf.add_str(string) buf.guess_segment_properties() hb.shape(blankfont, buf, shapers=["fallback"]) pos = [g.position for g in buf.glyph_positions] expected = [(0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0)] assert pos == expected
def set_text(self, text): newtrace = [] self.clear() buf = hb.Buffer() buf.add_str(text) buf.guess_segment_properties() buf.set_message_func(self.process_msg) self.stack = [QTreeWidgetItem(["GSUB", ""])] self.addTopLevelItem(self.stack[0]) hb.shape(self.font.vharfbuzz.hbfont, buf)
def test_cluster_level_int(self): buf = hb.Buffer() assert buf.cluster_level == 0 buf.cluster_level = 1 assert buf.cluster_level == 1 with pytest.raises(ValueError): # 5 is not a valid BufferClusterLevel buf.cluster_level = 5 assert buf.cluster_level == 1
def shape(self, text, parameters=None, onchange=None): """Shapes a text This shapes a piece of text. Args: text (str): A string of text parameters: A dictionary containing parameters to pass to Harfbuzz. Relevant keys include ``script``, ``direction``, ``language`` (these three are normally guessed from the string contents), ``features``, ``variations`` and ``shaper``. onchange: An optional function with three parameters. See below. Additionally, if an `onchange` function is provided, this will be called every time the buffer changes *during* shaping, with the following arguments: - ``self``: the vharfbuzz object. - ``stage``: either "GSUB" or "GPOS" - ``lookupid``: the current lookup ID - ``buffer``: a copy of the buffer as a list of lists (glyphname, cluster, position) Returns: A uharfbuzz ``hb.Buffer`` object """ if not parameters: parameters = {} self.prepare_shaper() buf = hb.Buffer() buf.add_str(text) buf.guess_segment_properties() if "script" in parameters and parameters["script"]: buf.script = parameters["script"] if "direction" in parameters and parameters["direction"]: buf.direction = parameters["direction"] if "language" in parameters and parameters["language"]: buf.language = parameters["language"] shapers = self.shapers if "shaper" in parameters and parameters["shaper"]: shapers = [parameters["shaper"]] features = parameters.get("features") if "variations" in parameters: self.hbfont.set_variations(parameters["variations"]) self.stage = "GSUB" if onchange: f = self.make_message_handling_function(buf, onchange) buf.set_message_func(f) hb.shape(self.hbfont, buf, features, shapers=shapers) self.stage = "GPOS" return buf
def test_basic_var(latest_otf, otf_font, latest_ttf, ttf_font, axis_dict): """ Check x_advances of each character in string at several variations. """ var_str = "Hello, World! 12345 ÀÉÏøÑ [({})]" for expected, actual in ((latest_otf, otf_font), (latest_ttf, ttf_font)): buf_expected = hb.Buffer() buf_expected.add_str(var_str) buf_expected.guess_segment_properties() buf_actual = hb.Buffer() buf_actual.add_str(var_str) buf_actual.guess_segment_properties() expected.set_variations(axis_dict) hb.shape(expected, buf_expected, None) infos_expected = buf_expected.glyph_infos positions_expected = buf_expected.glyph_positions actual.set_variations(axis_dict) hb.shape(actual, buf_actual, None) infos_actual = buf_actual.glyph_infos positions_actual = buf_actual.glyph_positions assert len(infos_expected) == len(infos_actual) for i in range(len(infos_expected)): gn_expected = expected.get_glyph_name(infos_expected[i].codepoint) gn_actual = actual.get_glyph_name(infos_actual[i].codepoint) assert gn_actual == gn_expected pos_expected = positions_expected[i].x_advance pos_actual = positions_actual[i].x_advance assert pos_actual == pos_expected
def test_glyh_name_no_features(self, blankfont, string, expected): buf = hb.Buffer() buf.add_str(string) buf.guess_segment_properties() hb.shape(blankfont, buf) # font.get_glyph_name() returns None if the font does not contain glyph names # or if the glyph ID does not exist. glyph_names = [blankfont.get_glyph_name(g.codepoint) for g in buf.glyph_infos] assert glyph_names == expected assert blankfont.get_glyph_name(1000) is None # font.glyph_to_string() return "gidN" if the font does not contain glyph names # or if the glyph ID does not exist. glyph_names = [blankfont.glyph_to_string(g.codepoint) for g in buf.glyph_infos] assert glyph_names == expected assert blankfont.glyph_to_string(1000) == 'gid1000'
def test_cluster_level(self): buf = hb.Buffer() assert buf.cluster_level == hb.BufferClusterLevel.DEFAULT buf.cluster_level = hb.BufferClusterLevel.MONOTONE_CHARACTERS assert buf.cluster_level == hb.BufferClusterLevel.MONOTONE_CHARACTERS buf.cluster_level = hb.BufferClusterLevel.MONOTONE_GRAPHEMES assert buf.cluster_level == hb.BufferClusterLevel.MONOTONE_GRAPHEMES buf.cluster_level = hb.BufferClusterLevel.CHARACTERS assert buf.cluster_level == hb.BufferClusterLevel.CHARACTERS buf.cluster_level = hb.BufferClusterLevel.DEFAULT assert buf.cluster_level == hb.BufferClusterLevel.DEFAULT
def test_glyph_h_advance_func(self, blankfont): string = "abcde" expected = [456, 456, 456, 456, 456] buf = hb.Buffer() buf.add_str(string) buf.guess_segment_properties() def h_advance_func(font, gid, data): return 456 funcs = hb.FontFuncs.create() funcs.set_glyph_h_advance_func(h_advance_func, None) blankfont.funcs = funcs hb.shape(blankfont, buf) infos = [pos.x_advance for pos in buf.glyph_positions] assert infos == expected
def test_nominal_glyph_func(self, blankfont): string = "abcde" expected = [97, 98, 99, 100, 101] buf = hb.Buffer() buf.add_str(string) buf.guess_segment_properties() def nominal_glyph_func(font, code_point, data): return code_point funcs = hb.FontFuncs.create() funcs.set_nominal_glyph_func(nominal_glyph_func, None) blankfont.funcs = funcs hb.shape(blankfont, buf) infos = [g.codepoint for g in buf.glyph_infos] assert infos == expected
async def shape(self, text): if not text: return ShapeResult() buffer = hb.Buffer() buffer.add_str(text) font = self.font features = self.features_dict if font.is_vertical: buffer.direction = 'ttb' assert features and features['vert'] else: buffer.direction = 'ltr' assert not features or not features.get('vert') if self.language: buffer.language = f'x-hbot{self.language}' # buffer.set_language_from_ot_tag(self.language) if self.script: buffer.script = self.script # buffer.set_script_from_ot_tag(self.script) logger.debug('%s lang=%s script=%s features=%s', ' '.join(f'U+{ord(ch):04X}' for ch in text), self.language, self.script, features) # logger.debug('lang=%s, script=%s, features=%s', buffer.language, # buffer.script, features) if utils._log_shaper_logs: buffer.set_message_func( lambda message: logger.debug('uharfbuzz: %s', message)) # buffer.cluster_level = hb.BufferClusterLevel.DEFAULT # buffer.guess_segment_properties() hb.shape(font.hbfont, buffer, features, self._shapers) infos = buffer.glyph_infos positions = buffer.glyph_positions assert len(infos) == len(positions) if font.is_vertical: glyphs = (GlyphData(info.codepoint, info.cluster, -pos.y_advance, -pos.y_offset) for info, pos in zip(infos, positions)) else: glyphs = (GlyphData(info.codepoint, info.cluster, pos.x_advance, pos.x_offset) for info, pos in zip(infos, positions)) result = ShapeResult(glyphs) self._log_result(result, text) return result
def shapeHB(text, font_name, font_size, features: Dict[str, bool] = None): font = pdfmetrics.getFont(font_name) if not isinstance(font, TTFont): # TODO make valid for all types of fonts raise RLKerningError("Not a TTF font") fontdata = font.face._ttf_data face = hb.Face(fontdata) font = hb.Font(face) # HB scales to integers in offset and advance so very big scale # will divide by SCALE_MULT to get the actual size in fractional points font.scale = (font_size * SCALE_MULT, font_size * SCALE_MULT) hb.ot_font_set_funcs(font) buf = hb.Buffer() buf.add_str(text) buf.guess_segment_properties() hb.shape(font, buf, features) return buf