#!/usr/bin/env python # -*- coding: utf-8 -*- # # OT-SVG example # # Copyright 2023 Hin-Tak Leung # Distributed under the terms of the new BSD license. # This is largely a python re-write of freetype2-demos:src/rsvg-port.c . # It is designed to be embeddable as a module in another python script . # # To use, rename this to, for example "otsvg.py", then insert # # ''' # from otsvg import hooks # # library = get_handle() # FT_Property_Set( library, b"ot-svg", b"svg-hooks", byref(hooks) # ''' # # in your script, and add "FT_LOAD_COLOR | FT_LOAD_RENDER" # to load_glyph() or load_char() calls. As in "__main__" below, # but also in the longer example of hb-view-ot-svg.py # in https://github.com/HinTak/harfbuzz-python-demos/ . # # Limitation: it is necessary to have "_state" as a module-level global # partially (in svg_init/svg_free, not in svg_render/svg_preset_slot) # to stop python destroying it when execution is in the c-side. # # Note: # Strictly-speaking, cairo.FORMAT_ARGB32 is host-order, # while freetype.FT_PIXEL_MODE_BGRA is small-endian. They are # different on big-endian platforms. The below # works in all circumstances, only because the bitmap is both # generated by cairo at the beginning and also consumed # by cairo in the end. # # The use of "pythonapi.PyMemoryView_FromMemory" is flaky - # specific to CPython 3.3+. # # Uses librsvg 2.52+ only. Or deprecation warnings per run. # # Not using Rsvg.Handle.render_document() . # Rsvg.Handle.render_layer(..., None, ...) does the same work. # # The original C-code does not check # start_glyph_id <= glyph_index <= end_glyph_id.value . import gi gi.require_version('Rsvg', '2.0') from gi.repository import Rsvg as rsvg from freetype import * from cairo import * # cairo.Matrix shadows freetype.Matrix from math import ceil _state = None def svg_init(ctx): global _state _state = {} ctx.contents.value = _state return FT_Err_Ok def svg_free(ctx): global _state _state = None # "None" is strictly speaking a special pyobject, # this line does not do what it should, i.e. setting the # pointer to NULL. ctx.contents = None return # void def svg_render(slot, ctx): state = ctx.contents.value #pythonapi is imported from ctypes pythonapi.PyMemoryView_FromMemory.argtypes = (c_char_p, c_ssize_t, c_int) pythonapi.PyMemoryView_FromMemory.restype = py_object surface = ImageSurface.create_for_data( pythonapi.PyMemoryView_FromMemory(cast(slot.contents.bitmap.buffer, c_char_p), slot.contents.bitmap.rows * slot.contents.bitmap.pitch, 0x200), FORMAT_ARGB32, slot.contents.bitmap.width, slot.contents.bitmap.rows, slot.contents.bitmap.pitch ) cr = Context( surface ) cr.translate( -state['x'], -state['y'] ) cr.set_source_surface( state['rec_surface'] ) # 0,0 is default cr.paint() surface.flush() slot.contents.bitmap.pixel_mode = FT_PIXEL_MODE_BGRA slot.contents.bitmap.num_grays = 256 slot.contents.format = FT_GLYPH_FORMAT_BITMAP state['rec_surface'] = None # Let python destroy the surface return FT_Err_Ok def svg_preset_slot(slot, cached, ctx): state = ctx.contents.value document = ctypes.cast(slot.contents.other, FT_SVG_Document) metrics = SizeMetrics(document.contents.metrics) units_per_EM = FT_UShort(document.contents.units_per_EM) end_glyph_id = FT_UShort(document.contents.end_glyph_id) start_glyph_id = FT_UShort(document.contents.start_glyph_id) dimension_svg = rsvg.DimensionData() handle = rsvg.Handle.new_from_data( ctypes.string_at(document.contents.svg_document, # not terminated size=document.contents.svg_document_length) ) (out_has_width, out_width, out_has_height, out_height, out_has_viewbox, out_viewbox) = handle.get_intrinsic_dimensions() if ( out_has_viewbox == True ): dimension_svg.width = out_viewbox.width dimension_svg.height = out_viewbox.height else: # "out_has_width" and "out_has_height" are True always dimension_svg.width = units_per_EM.value dimension_svg.height = units_per_EM.value if (( out_width.length != 1) or (out_height.length != 1 )): dimension_svg.width = out_width.length dimension_svg.height = out_height.length x_svg_to_out = metrics.x_ppem / dimension_svg.width y_svg_to_out = metrics.y_ppem / dimension_svg.height state['rec_surface'] = RecordingSurface( Content.COLOR_ALPHA, None ) rec_cr = Context( state['rec_surface'] ) xx = document.contents.transform.xx / ( 1 << 16 ) xy = -document.contents.transform.xy / ( 1 << 16 ) yx = -document.contents.transform.yx / ( 1 << 16 ) yy = document.contents.transform.yy / ( 1 << 16 ) x0 = document.contents.delta.x / 64 * dimension_svg.width / metrics.x_ppem y0 = -document.contents.delta.y / 64 * dimension_svg.height / metrics.y_ppem transform_matrix = Matrix(xx, yx, xy, yy, x0, y0) # cairo.Matrix rec_cr.scale( x_svg_to_out, y_svg_to_out ) rec_cr.transform( transform_matrix ) viewport = rsvg.Rectangle() viewport.x = 0 viewport.y = 0 viewport.width = dimension_svg.width viewport.height = dimension_svg.height str = None # render whole document - not using Handle.render_document() if ( start_glyph_id.value < end_glyph_id.value ): str = "#glyph%u" % (slot.contents.glyph_index ) handle.render_layer( rec_cr, str, viewport ) (state['x'], state['y'], width, height) = state['rec_surface'].ink_extents() slot.contents.bitmap_left = int(state['x']) slot.contents.bitmap_top = int(-state['y']) slot.contents.bitmap.rows = ceil( height ) slot.contents.bitmap.width = ceil( width ) slot.contents.bitmap.pitch = slot.contents.bitmap.width * 4 slot.contents.bitmap.pixel_mode = FT_PIXEL_MODE_BGRA metrics_width = width metrics_height = height horiBearingX = state['x'] horiBearingY = -state['y'] vertBearingX = slot.contents.metrics.horiBearingX / 64.0 - slot.contents.metrics.horiAdvance / 64.0 / 2 vertBearingY = ( slot.contents.metrics.vertAdvance / 64.0 - slot.contents.metrics.height / 64.0 ) / 2 slot.contents.metrics.width = int(round( metrics_width * 64 )) slot.contents.metrics.height = int(round( metrics_height * 64 )) slot.contents.metrics.horiBearingX = int( horiBearingX * 64 ) slot.contents.metrics.horiBearingY = int( horiBearingY * 64 ) slot.contents.metrics.vertBearingX = int( vertBearingX * 64 ) slot.contents.metrics.vertBearingY = int( vertBearingY * 64 ) if ( slot.contents.metrics.vertAdvance == 0 ): slot.contents.metrics.vertAdvance = int( metrics_height * 1.2 * 64 ) if ( cached == False ): state['rec_surface'] = None state['x'] = 0 state['y'] = 0 return FT_Err_Ok hooks = SVG_RendererHooks(svg_init=SVG_Lib_Init_Func(svg_init), svg_free=SVG_Lib_Free_Func(svg_free), svg_render=SVG_Lib_Render_Func(svg_render), svg_preset_slot=SVG_Lib_Preset_Slot_Func(svg_preset_slot)) if __name__ == '__main__': import sys execname = sys.argv[0] if len(sys.argv) < 2: print("Example usage: %s TrajanColor-Concept.otf" % execname) exit(1) face = Face(sys.argv[1]) face.set_char_size( 160*64 ) library = get_handle() FT_Property_Set( library, b"ot-svg", b"svg-hooks", byref(hooks) ) # python 3 only syntax face.load_char('A', FT_LOAD_COLOR | FT_LOAD_RENDER ) bitmap = face.glyph.bitmap width = face.glyph.bitmap.width rows = face.glyph.bitmap.rows if ( face.glyph.bitmap.pitch != width * 4 ): raise RuntimeError('pitch != width * 4 for color bitmap: Please report this.') I = ImageSurface.create_for_data( pythonapi.PyMemoryView_FromMemory(cast(bitmap._FT_Bitmap.buffer, c_char_p), bitmap.rows * bitmap.pitch, 0x200), # Read-Write FORMAT_ARGB32, width, rows, bitmap.pitch ) surface = ImageSurface(FORMAT_ARGB32, 2*width, rows) ctx = Context(surface) ctx.set_source_surface(I, 0, 0) ctx.paint() ctx.set_source_surface(I, width/2, 0) ctx.paint() ctx.set_source_surface(I, width , 0) ctx.paint() surface.write_to_png("ot-svg-example.png") from PIL import Image Image.open("ot-svg-example.png").show()