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 shape(self, text, *, features=None, varLocation=None, direction=None, language=None, script=None): if features is None: features = {} if varLocation is None: varLocation = {} self.font.scale = (self.face.upem, self.face.upem) self.font.set_variations(varLocation) hb.ot_font_set_funcs(self.font) if self._funcs is not None: self.font.funcs = self._funcs buf = hb.Buffer.create() buf.add_str(str(text)) # add_str() does not accept str subclasses buf.guess_segment_properties() if direction is not None: buf.direction = direction if language is not None: buf.language = language if script is not None: buf.script = script hb.shape(self.font, buf, features) glyphOrder = self.glyphOrder infos = [] for info, pos in zip(buf.glyph_infos, buf.glyph_positions): infos.append(GlyphInfo(info.codepoint, glyphOrder[info.codepoint], info.cluster, *pos.position)) return infos
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 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 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 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 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 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 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 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 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 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_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_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 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 __isEmojiSupportedByFont(self, emoji: Emoji) -> bool: # Load font (has to be done for call): face = Face(self.fontdata) font = Font(face) upem = face.upem font.scale = (upem, upem) ot_font_set_funcs(font) # Create text buffer: buf = Buffer() buf.add_str(emoji.emoji) buf.guess_segment_properties() # Shape text: features = {"kern": True, "liga": True} shape(font, buf, features) infos = buf.glyph_infos # Remove all variant selectors: while len(infos) > 0 and infos[-1].codepoint == 3: infos = infos[:-1] # Filter empty: if len(infos) <= 0: return False # Remove uncombined ending with skin tone like "👭🏿": lastCp = infos[-1].codepoint if lastCp == 1076 or lastCp == 1079 or lastCp == 1082 or lastCp == 1085 or lastCp == 1088: return False # If there is a code point 0 => Emoji not fully supported by font: return all(info.codepoint != 0 and info.codepoint != 3 for info in infos)
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 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 _shape_string(self, font, string, ot_features): buf = hb.Buffer.create() buf.add_str(string) buf.guess_segment_properties() try: features = {f: True for f in ot_features} hb.shape(font.hbfont, buf, features) except KeyError: hb.shape(font.hbfont, buf) return buf
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 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_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 emojiSupported(emoji: str, fontdata) -> bool: """ This function checks for support for a given emoji in a font file, particularly the multi-byte ZWJ sequences. Many thanks to StackOverflow user COM8 for this code. https://stackoverflow.com/a/55560968/1174966 """ # Load font (has to be done for call): face = Face(fontdata) font = UFont(face) upem = face.upem font.scale = (upem, upem) ot_font_set_funcs(font) # Create text buffer: buf = Buffer() buf.add_str(emoji) buf.guess_segment_properties() # Shape text: features = {"kern": True, "liga": True} shape(font, buf, features) infos = buf.glyph_infos # Remove all variant selectors: while len(infos) > 0 and infos[-1].codepoint == 3: infos = infos[:-1] # Filter empty: if len(infos) <= 0: return False # Remove uncombined, ending with skin tone like "ðŸ‘ðŸ�¿": lastCp = infos[-1].codepoint print(lastCp) badCp = [1076, 1079, 1082, 1085, 1088] if lastCp in badCp: return False # If there is a code point 0 or 3 => Emoji not fully supported by font: return all(info.codepoint != 0 and info.codepoint != 3 for info in infos)
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 shape(self, text, *, features=None, varLocation=None, direction=None, language=None, script=None): if features is None: features = {} if varLocation is None: varLocation = {} self.font.set_variations(varLocation) if self._funcs is not None: self.font.funcs = self._funcs buf = hb.Buffer.create() buf.add_str(str(text)) # add_str() does not accept str subclasses buf.guess_segment_properties() msgfunc, history = self.buildMessageHistoryFunction(buf) buf.set_message_func(msgfunc) buf.cluster_level = hb.BufferClusterLevel.MONOTONE_CHARACTERS if direction is not None: buf.direction = direction if language is not None: buf.set_language_from_ot_tag(language) if script is not None: buf.set_script_from_ot_tag(script) hb.shape(self.font, buf, features) glyphOrder = self.glyphOrder infos = [] for info, pos in zip(buf.glyph_infos, buf.glyph_positions): infos.append( GlyphInfo(info.codepoint, glyphOrder[info.codepoint], info.cluster, *pos.position)) return infos, history
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