From 83ba9b9d9b1e223deb961f89641493292b1f78da Mon Sep 17 00:00:00 2001 From: Moazin Khatti Date: Mon, 11 Oct 2021 01:49:46 +0500 Subject: [PATCH] Prototype SVG support. * src/rsvg-port.c, src/rsvg-port.h: New files, providing SVG rendering hooks using the 'librsvg' library. * src/ftcommon.c: Include `FT_OTSVG_H` and `rsvg-port.h`. (FTDemo_New): Set SVG renderer hooks. (FTDemo_Glyph_To_Bitmap): Also accept SVG glyph format. * Makefile: Update for recent FreeType changes; the library's `configure` script now tests for 'librsvg'. (COMPILE): Updated to use `FT_DEMO_CFLAGS` for normal compilation, and manually adding flags for 'librsvg' otherwise. Update all users. (LINK_LIBS): Updated to use `FT_DEMO_LDFLAGS` for normal compilation, and manually adding flags for 'librsvg' otherwise. (COMMON_OBJ): Updated (rsvg-port): New rule. * meson.build (librsvg_dep): New dependency. (ftcommon_lib): Add `rsvg-port.*` files and update dependencies. --- Makefile | 31 +++- meson.build | 8 +- src/ftcommon.c | 15 +- src/rsvg-port.c | 400 ++++++++++++++++++++++++++++++++++++++++++++++++ src/rsvg-port.h | 63 ++++++++ 5 files changed, 512 insertions(+), 5 deletions(-) create mode 100644 src/rsvg-port.c create mode 100644 src/rsvg-port.h diff --git a/Makefile b/Makefile index 2c9c051..be364ef 100644 --- a/Makefile +++ b/Makefile @@ -125,6 +125,16 @@ else SRC_DIR := $(TOP_DIR_2)/src endif + ifeq ($(PLATFORM),unixdev) + # `FT_DEMO_CFLAGS` comes from FreeType's `configure` script (via + # `builds/unix/unix-cc.mk`), holding additional flags to compile the + # FreeType demo programs. + # + # For the pure `make` call (without using `configure`) we have to add + # all needed cflags manually. + FT_DEMO_CFLAGS := $(shell pkg-config --cflags librsvg-2.0) + endif + FT_INCLUDES := $(OBJ_BUILD) \ $(DEVEL_DIR) \ $(TOP_DIR)/include \ @@ -133,7 +143,8 @@ else COMPILE = $(CC) $(ANSIFLAGS) \ $(INCLUDES:%=$I%) \ $(CPPFLAGS) \ - $(CFLAGS) + $(CFLAGS) \ + $(FT_DEMO_CFLAGS) # Enable C99 for gcc to avoid warnings. # Note that clang++ aborts with an error if we use `-std=C99', @@ -164,17 +175,27 @@ else LINK_ITEMS = $T$(subst /,$(COMPILER_SEP),$@ $<) ifeq ($(PLATFORM),unix) + # `LDFLAGS` comes from the `configure` script (via FreeType's + # `builds/unix/unix-cc.mk`), holding all linker flags necessary to + # link the FreeType library. + # + # `FT_DEMO_LDFLAGS` has been set in `unix-cc.mk`, too. override CC = $(CCraw) LINK_CMD = $(LIBTOOL) --mode=link $(CC) \ $(subst /,$(COMPILER_SEP),$(LDFLAGS)) - LINK_LIBS = $(subst /,$(COMPILER_SEP),$(FTLIB) $(EFENCE)) $(LIB_CLOCK_GETTIME) + LINK_LIBS = $(subst /,$(COMPILER_SEP),$(FTLIB) $(EFENCE)) \ + $(FT_DEMO_LDFLAGS) else LINK_CMD = $(CC) $(subst /,$(COMPILER_SEP),$(LDFLAGS)) ifeq ($(PLATFORM),unixdev) - LINK_LIBS := $(subst /,$(COMPILER_SEP),$(FTLIB) $(EFENCE)) -lm -lrt -lz -lbz2 -lpthread + # For the pure `make` call (without using `configure`) we have to add + # all needed libraries manually. + LINK_LIBS := $(subst /,$(COMPILER_SEP),$(FTLIB) $(EFENCE)) \ + -lm -lrt -lz -lbz2 -lpthread LINK_LIBS += $(shell pkg-config --libs libpng) LINK_LIBS += $(shell pkg-config --libs harfbuzz) LINK_LIBS += $(shell pkg-config --libs libbrotlidec) + LINK_LIBS += $(shell pkg-config --libs librsvg-2.0) else LINK_LIBS = $(subst /,$(COMPILER_SEP),$(FTLIB) $(EFENCE)) endif @@ -349,6 +370,7 @@ else $(OBJ_DIR_2)/mlgetopt.$(SO): $(SRC_DIR)/mlgetopt.c COMMON_OBJ := $(OBJ_DIR_2)/common.$(SO) \ $(OBJ_DIR_2)/strbuf.$(SO) \ + $(OBJ_DIR_2)/rsvg-port.$(SO) \ $(OBJ_DIR_2)/output.$(SO) \ $(OBJ_DIR_2)/md5.$(SO) \ $(OBJ_DIR_2)/mlgetopt.$(SO) @@ -361,6 +383,9 @@ else $(COMPILE) $(GRAPH_INCLUDES:%=$I%) \ $T$(subst /,$(COMPILER_SEP),$@ $<) + $(OBJ_DIR_2)/rsvg-port.$(SO): $(SRC_DIR)/rsvg-port.c $(SRC_DIR)/rsvg-port.h + $(COMPILE) $T$(subst /,$(COMPILER_SEP),$@ $<) + FTCOMMON_OBJ := $(OBJ_DIR_2)/ftcommon.$(SO) \ $(OBJ_DIR_2)/ftpngout.$(SO) diff --git a/meson.build b/meson.build index 61d6aab..9a5b957 100644 --- a/meson.build +++ b/meson.build @@ -30,6 +30,10 @@ libfreetype2_dep = libfreetype2.get_variable('freetype_dep') libpng_dep = dependency('libpng', required: true) +librsvg_dep = dependency('librsvg-2.0', + required: true) + + cc = meson.get_compiler('c') math_dep = cc.find_library('m', @@ -70,8 +74,10 @@ ftcommon_lib = static_library('ftcommon', 'src/ftcommon.c', 'src/ftcommon.h', 'src/ftpngout.c', + 'src/rsvg-port.c', + 'src/rsvg-port.h', ], - dependencies: [libpng_dep, libfreetype2_dep], + dependencies: [libpng_dep, librsvg_dep, libfreetype2_dep], include_directories: graph_include_dir, link_with: [common_lib, graph_lib], ) diff --git a/src/ftcommon.c b/src/ftcommon.c index e9ab93c..725ddb5 100644 --- a/src/ftcommon.c +++ b/src/ftcommon.c @@ -27,6 +27,8 @@ #include FT_BITMAP_H #include FT_FONT_FORMATS_H +#include FT_OTSVG_H + /* error messages */ #undef FTERRORS_H_ @@ -37,6 +39,7 @@ #include "common.h" #include "strbuf.h" #include "ftcommon.h" +#include "rsvg-port.h" #include #include @@ -337,6 +340,12 @@ { FTDemo_Handle* handle; + SVG_RendererHooks hooks = { + (SVG_Lib_Init_Func)rsvg_port_init, + (SVG_Lib_Free_Func)rsvg_port_free, + (SVG_Lib_Render_Func)rsvg_port_render, + (SVG_Lib_Preset_Slot_Func)rsvg_port_preset_slot }; + handle = (FTDemo_Handle *)malloc( sizeof ( FTDemo_Handle ) ); if ( !handle ) @@ -348,6 +357,9 @@ if ( error ) PanicZ( "could not initialize FreeType" ); + /* XXX error handling? */ + FT_Property_Set( handle->library, "ot-svg", "svg_hooks", &hooks ); + error = FTC_Manager_New( handle->library, 0, 0, 0, my_face_requester, 0, &handle->cache_manager ); if ( error ) @@ -1333,7 +1345,8 @@ error = FT_Err_Ok; - if ( glyf->format == FT_GLYPH_FORMAT_OUTLINE ) + if ( glyf->format == FT_GLYPH_FORMAT_OUTLINE || + glyf->format == FT_GLYPH_FORMAT_SVG ) { FT_Render_Mode render_mode; diff --git a/src/rsvg-port.c b/src/rsvg-port.c new file mode 100644 index 0000000..1216630 --- /dev/null +++ b/src/rsvg-port.c @@ -0,0 +1,400 @@ +/**************************************************************************** + * + * rsvg-port.c + * + * Librsvg-based hook functions for OT-SVG rendering in FreeType + * (implementation). + * + * Copyright (C) 2022 by + * David Turner, Robert Wilhelm, Werner Lemberg, and Moazin Khatti. + * + * This file is part of the FreeType project, and may only be used, + * modified, and distributed under the terms of the FreeType project + * license, LICENSE.TXT. By continuing to use, modify, or distribute + * this file you indicate that you have read the license and + * understand and accept it fully. + * + */ + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "rsvg-port.h" + + + /* + * The init hook is called when the first OT-SVG glyph is rendered. All + * we do is to allocate an internal state structure and set the pointer in + * `library->svg_renderer_state`. This state structure becomes very + * useful to cache some of the results obtained by one hook function that + * the other one might use. + */ + FT_Error + rsvg_port_init( FT_Pointer *state ) + { + /* allocate the memory upon initialization */ + *state = malloc( sizeof( Rsvg_Port_StateRec ) ); /* XXX error handling */ + + return FT_Err_Ok; + } + + + /* + * Deallocate the state structure. + */ + void + rsvg_port_free( FT_Pointer *state ) + { + free( *state ); + } + + + /* + * The render hook. The job of this hook is to simply render the glyph in + * the buffer that has been allocated on the FreeType side. Here we + * simply use the recording surface by playing it back against the + * surface. + */ + FT_Error + rsvg_port_render( FT_GlyphSlot slot, + FT_Pointer *_state ) + { + FT_Error error = FT_Err_Ok; + + Rsvg_Port_State state; + cairo_status_t status; + cairo_t *cr; + cairo_surface_t *surface; + + + state = *(Rsvg_Port_State*)_state; + + /* Create an image surface to store the rendered image. However, */ + /* don't allocate memory; instead use the space already provided in */ + /* `slot->bitmap.buffer`. */ + surface = cairo_image_surface_create_for_data( slot->bitmap.buffer, + CAIRO_FORMAT_ARGB32, + (int)slot->bitmap.width, + (int)slot->bitmap.rows, + slot->bitmap.pitch ); + status = cairo_surface_status( surface ); + + if ( status != CAIRO_STATUS_SUCCESS ) + { + if ( status == CAIRO_STATUS_NO_MEMORY ) + return FT_Err_Out_Of_Memory; + else + return FT_Err_Invalid_Outline; + } + + cr = cairo_create( surface ); + status = cairo_status( cr ); + + if ( status != CAIRO_STATUS_SUCCESS ) + { + if ( status == CAIRO_STATUS_NO_MEMORY ) + return FT_Err_Out_Of_Memory; + else + return FT_Err_Invalid_Outline; + } + + /* Set a translate transform that translates the points in such a way */ + /* that we get a tight rendering with least redundant white spac. */ + cairo_translate( cr, -state->x, -state->y ); + + /* Replay from the recorded surface. This saves us from parsing the */ + /* document again and redoing what was already done in the preset */ + /* hook. */ + cairo_set_source_surface( cr, state->rec_surface, 0.0, 0.0 ); + cairo_paint( cr ); + + cairo_surface_flush( surface ); + + slot->bitmap.pixel_mode = FT_PIXEL_MODE_BGRA; + slot->bitmap.num_grays = 256; + slot->format = FT_GLYPH_FORMAT_BITMAP; + + /* Clean up everything. */ + cairo_surface_destroy( surface ); + cairo_destroy( cr ); + cairo_surface_destroy( state->rec_surface ); + + return error; + } + + + /* + * This hook is called at two different locations. Firstly, it is called + * when presetting the glyphslot when `FT_Load_Glyph` is called. + * Secondly, it is called right before the render hook is called. When + * `cache` is false, it is the former, when `cache` is true, it is the + * latter. + * + * The job of this function is to preset the slot setting the width, + * height, pitch, `bitmap.left`, and `bitmap.top`. These are all + * necessary for appropriate memory allocation, as well as ultimately + * compositing the glyph later on by client applications. + */ + FT_Error + rsvg_port_preset_slot( FT_GlyphSlot slot, + FT_Bool cache, + FT_Pointer *_state ) + { + /* FreeType variables. */ + FT_Error error = FT_Err_Ok; + + FT_SVG_Document document = (FT_SVG_Document)slot->other; + FT_Size_Metrics metrics = document->metrics; + + FT_UShort units_per_EM = document->units_per_EM; + FT_UShort end_glyph_id = document->end_glyph_id; + FT_UShort start_glyph_id = document->start_glyph_id; + + /* Librsvg variables. */ + GError *gerror = NULL; + gboolean ret; + + gboolean out_has_width; + gboolean out_has_height; + gboolean out_has_viewbox; + + RsvgHandle *handle; + RsvgLength out_width; + RsvgLength out_height; + RsvgRectangle out_viewbox; + RsvgDimensionData dimension_svg; + + cairo_t *rec_cr; + cairo_matrix_t transform_matrix; + + /* Rendering port's state. */ + Rsvg_Port_State state; + Rsvg_Port_StateRec state_dummy; + + /* General variables. */ + double x, y; + double xx, xy, yx, yy; + double x0, y0; + double width, height; + double x_svg_to_out, y_svg_to_out; + double tmpd; + + float metrics_width, metrics_height; + float horiBearingX, horiBearingY; + float vertBearingX, vertBearingY; + float tmpf; + + + /* If `cache` is `TRUE` we store calculations in the actual port */ + /* state variable, otherwise we just create a dummy variable and */ + /* store there. This saves us from too many 'if' statements. */ + if ( cache ) + state = *(Rsvg_Port_State*)_state; + else + state = &state_dummy; + + /* Form an `RsvgHandle` by loading the SVG document. */ + handle = rsvg_handle_new_from_data( document->svg_document, + document->svg_document_length, + &gerror ); + if ( handle == NULL ) + { + error = FT_Err_Invalid_SVG_Document; + goto CleanLibrsvg; + } + + /* Get attributes like `viewBox` and `width`/`height`. */ + rsvg_handle_get_intrinsic_dimensions( handle, + &out_has_width, + &out_width, + &out_has_height, + &out_height, + &out_has_viewbox, + &out_viewbox ); + + /* + * Figure out the units in the EM square in the SVG document. This is + * specified by the `ViewBox` or the `width`/`height` attributes, if + * present, otherwise it should be assumed that the units in the EM + * square are the same as in the TTF/CFF outlines. + * + * TODO: I'm not sure what the standard says about the situation if + * `ViewBox` as well as `width`/`height` are present; however, I've + * never seen that situation in real fonts. + */ + if ( out_has_viewbox == TRUE ) + { + dimension_svg.width = (int)out_viewbox.width; /* XXX rounding? */ + dimension_svg.height = (int)out_viewbox.height; + } + else if ( out_has_width == TRUE && out_has_height == TRUE ) + { + dimension_svg.width = (int)out_width.length; /* XXX rounding? */ + dimension_svg.height = (int)out_height.length; + } + else + { + /* If neither `ViewBox` nor `width`/`height` are present, the */ + /* `units_per_EM` in SVG coordinates must be the same as */ + /* `units_per_EM` of the TTF/CFF outlines. */ + dimension_svg.width = units_per_EM; + dimension_svg.height = units_per_EM; + } + + /* Scale factors from SVG coordinates to the needed output size. */ + x_svg_to_out = (float)metrics.x_ppem / dimension_svg.width; + y_svg_to_out = (float)metrics.y_ppem / dimension_svg.height; + + /* + * Create a cairo recording surface. This is done for two reasons. + * Firstly, it is required to get the bounding box of the final drawing + * so we can use an appropriate translate transform to get a tight + * rendering. Secondly, if `cache` is true, we can save this surface + * and later replay it against an image surface for the final rendering. + * This saves us from loading and parsing the document again. + */ + state->rec_surface = + cairo_recording_surface_create( CAIRO_CONTENT_COLOR_ALPHA, NULL ); + + rec_cr = cairo_create( state->rec_surface ); + + /* + * We need to take into account any transformations applied. The end + * user who applied the transformation doesn't know the internal details + * of the SVG document. Thus, we expect that the end user should just + * write the transformation as if the glyph is a traditional one. We + * then do some maths on this to get the equivalent transformation in + * SVG coordinates. + */ + xx = (double)document->transform.xx / ( 1 << 16 ); + xy = -(double)document->transform.xy / ( 1 << 16 ); + yx = -(double)document->transform.yx / ( 1 << 16 ); + yy = (double)document->transform.yy / ( 1 << 16 ); + + x0 = (double)document->delta.x / 64 * + dimension_svg.width / metrics.x_ppem; + y0 = -(double)document->delta.y / 64 * + dimension_svg.height / metrics.y_ppem; + + /* Cairo stores both transformation and translation in one matrix. */ + transform_matrix.xx = xx; + transform_matrix.yx = yx; + transform_matrix.xy = xy; + transform_matrix.yy = yy; + transform_matrix.x0 = x0; + transform_matrix.y0 = y0; + + /* Set up a scale transformation to scale up the document to the */ + /* required output size. */ + cairo_scale( rec_cr, x_svg_to_out, y_svg_to_out ); + /* Set up a transformation matrix. */ + cairo_transform( rec_cr, &transform_matrix ); + + /* If the document contains only one glyph, `start_glyph_id` and */ + /* `end_glyph_id` have the same value. Otherwise `end_glyph_id` */ + /* is larger. */ + if ( start_glyph_id == end_glyph_id ) + { + /* Render the whole document to the recording surface. */ + ret = rsvg_handle_render_cairo ( handle, rec_cr ); + if ( ret == FALSE ) + { + error = FT_Err_Invalid_SVG_Document; + goto CleanCairo; + } + } + else if ( start_glyph_id < end_glyph_id ) + { + char str[32] = "#glyph"; + + + /* Render only the element with its ID equal to `glyph`. */ + sprintf( str + 6, "%u", slot->glyph_index ); + ret = rsvg_handle_render_cairo_sub( handle, rec_cr, str ); + if ( ret == FALSE ) + { + error = FT_Err_Invalid_SVG_Document; + goto CleanCairo; + } + } + + /* Get the bounding box of the drawing. */ + cairo_recording_surface_ink_extents( state->rec_surface, &x, &y, + &width, &height ); + + /* We store the bounding box's `x` and `y` values so that the render */ + /* hook can apply a translation to get a tight rendering. */ + state->x = x; + state->y = y; + + /* Preset the values. */ + slot->bitmap_left = (FT_Int) state->x; /* XXX rounding? */ + slot->bitmap_top = (FT_Int)-state->y; + + /* Do conversion in two steps to avoid 'bad function cast' warning. */ + tmpd = ceil( height ); + slot->bitmap.rows = (unsigned int)tmpd; + tmpd = ceil( width ); + slot->bitmap.width = (unsigned int)tmpd; + + slot->bitmap.pitch = (int)slot->bitmap.width * 4; + + slot->bitmap.pixel_mode = FT_PIXEL_MODE_BGRA; + + /* Compute all the bearings and set them correctly. The outline is */ + /* scaled already, we just need to use the bounding box. */ + metrics_width = (float)width; + metrics_height = (float)height; + + horiBearingX = (float) state->x; + horiBearingY = (float)-state->y; + + vertBearingX = slot->metrics.horiBearingX / 64.0f - + slot->metrics.horiAdvance / 64.0f / 2; + vertBearingY = ( slot->metrics.vertAdvance / 64.0f - + slot->metrics.height / 64.0f ) / 2; /* XXX parentheses correct? */ + + /* Do conversion in two steps to avoid 'bad function cast' warning. */ + tmpf = roundf( metrics_width * 64 ); + slot->metrics.width = (FT_Pos)tmpf; + tmpf = roundf( metrics_height * 64 ); + slot->metrics.height = (FT_Pos)tmpf; + + slot->metrics.horiBearingX = (FT_Pos)( horiBearingX * 64 ); /* XXX rounding? */ + slot->metrics.horiBearingY = (FT_Pos)( horiBearingY * 64 ); + slot->metrics.vertBearingX = (FT_Pos)( vertBearingX * 64 ); + slot->metrics.vertBearingY = (FT_Pos)( vertBearingY * 64 ); + + if ( slot->metrics.vertAdvance == 0 ) + slot->metrics.vertAdvance = (FT_Pos)( metrics_height * 1.2 * 64 ); + + /* If a render call is to follow, just destroy the context for the */ + /* recording surface since no more drawing will be done on it. */ + /* However, keep the surface itself for use by the render hook. */ + if ( cache == TRUE ) + { + cairo_destroy( rec_cr ); + goto CleanLibrsvg; + } + + /* Destroy the recording surface as well as the context. */ + CleanCairo: + cairo_surface_destroy( state->rec_surface ); + cairo_destroy( rec_cr ); + + CleanLibrsvg: + /* Destroy the handle. */ + g_object_unref( handle ); + + return error; + } + + +/* End */ diff --git a/src/rsvg-port.h b/src/rsvg-port.h new file mode 100644 index 0000000..87a3885 --- /dev/null +++ b/src/rsvg-port.h @@ -0,0 +1,63 @@ +/**************************************************************************** + * + * rsvg-port.h + * + * Librsvg based hook functions for OT-SVG rendering in FreeType + * (headers). + * + * Copyright (C) 2022 by + * David Turner, Robert Wilhelm, Werner Lemberg, and Moazin Khatti. + * + * This file is part of the FreeType project, and may only be used, + * modified, and distributed under the terms of the FreeType project + * license, LICENSE.TXT. By continuing to use, modify, or distribute + * this file you indicate that you have read the license and + * understand and accept it fully. + * + */ + +#ifndef RSVG_PORT_H +#define RSVG_PORT_H + +#include +#include +#include +#include + + + /* + * Different hook functions can access persisting data by creating a state + * structure and putting its address in `library->svg_renderer_state`. + * Functions can then store and retrieve data from this structure. + */ + typedef struct Rsvg_Port_StateRec_ + { + cairo_surface_t *rec_surface; + + double x; + double y; + + } Rsvg_Port_StateRec; + + typedef struct Rsvg_Port_StateRec_* Rsvg_Port_State; + + + FT_Error + rsvg_port_init( FT_Pointer *state ); + + void + rsvg_port_free( FT_Pointer *state ); + + FT_Error + rsvg_port_render( FT_GlyphSlot slot, + FT_Pointer *state ); + + FT_Error + rsvg_port_preset_slot( FT_GlyphSlot slot, + FT_Bool cache, + FT_Pointer *state ); + +#endif /* RSVG_PORT_H */ + + +/* End */ -- GitLab