diff --git a/Makefile b/Makefile index cded506..019e04d 100644 --- a/Makefile +++ b/Makefile @@ -61,6 +61,9 @@ ifeq ($(wildcard $(CONFIG_MK)),) no_config_mk := 1 endif +# Skia definitely want c++, and clang++ does not like -std=99 +# Why does this line has no effect? +CC=g++ ifdef no_config_mk exes: @@ -136,8 +139,8 @@ else # # 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) \ - -DHAVE_LIBRSVG + FT_DEMO_CFLAGS := -I$(TOP_DIR_2)/skia/ \ + -DHAVE_SKIA endif FT_INCLUDES := $(OBJ_BUILD) \ @@ -145,10 +148,13 @@ else $(TOP_DIR)/include \ $(SRC_DIR) + # Skia definitely want c++, and clang++ does not like -std=99 + CC=g++ COMPILE = $(CC) $(ANSIFLAGS) \ $(INCLUDES:%=$I%) \ $(CFLAGS) \ - $(FT_DEMO_CFLAGS) + $(FT_DEMO_CFLAGS) -I$(TOP_DIR_2)/skia/ -DHAVE_SKIA + # Enable C99 for gcc to avoid warnings. # Note that clang++ aborts with an error if we use `-std=C99', @@ -186,8 +192,10 @@ else # `FT_DEMO_LDFLAGS` has been set in `unix-cc.mk`, too. override CC = $(CCraw) LINK_CMD = $(LIBTOOL) --mode=link $(CC) \ - $(subst /,$(COMPILER_SEP),$(LDFLAGS)) + -L$(TOP_DIR_2)/skia/out/Release/ -lsvg -lskia -lskshaper -lskunicode -lGL -lfontconfig -lharfbuzz \ + $(subst /,$(COMPILER_SEP),$(LDFLAGS)) LINK_LIBS = $(subst /,$(COMPILER_SEP),$(FTLIB) $(EFENCE)) \ + -L$(TOP_DIR_2)/skia/out/Release/ -lsvg -lskia -lskshaper -lskunicode -lGL -lfontconfig -lharfbuzz \ $(FT_DEMO_LDFLAGS) else LINK_CMD = $(CC) $(subst /,$(COMPILER_SEP),$(LDFLAGS)) @@ -196,10 +204,10 @@ else # all needed libraries manually. LINK_LIBS := $(subst /,$(COMPILER_SEP),$(FTLIB) $(EFENCE)) \ -lm -lrt -lz -lbz2 -lpthread + LINK_LIBS += -L$(TOP_DIR_2)/skia/out/Release/ -lsvg -lskia 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 @@ -469,9 +477,13 @@ else $(OBJ_DIR_2)/rsvg-port.$(SO): $(SRC_DIR)/rsvg-port.c $(SRC_DIR)/rsvg-port.h $(COMPILE) $T$(subst /,$(COMPILER_SEP),$@ $<) + $(OBJ_DIR_2)/skia-port.$(SO): $(SRC_DIR)/skia-port.c $(SRC_DIR)/skia-port.h + $(COMPILE) $T$(subst /,$(COMPILER_SEP),$@ $<) + FTCOMMON_OBJ := $(OBJ_DIR_2)/ftcommon.$(SO) \ $(OBJ_DIR_2)/ftpngout.$(SO) \ - $(OBJ_DIR_2)/rsvg-port.$(SO) + $(OBJ_DIR_2)/rsvg-port.$(SO) \ + $(OBJ_DIR_2)/skia-port.$(SO) #################################################################### diff --git a/README.skia b/README.skia new file mode 100644 index 0000000..35c9185 --- /dev/null +++ b/README.skia @@ -0,0 +1,59 @@ +Using Skia instead of Rsvg for rendering OT-SVG +=============================================== + +Skia has a large code base and a long history, and moving very fast. +There is no official prebuilt binaries AFAIK. The official +build procedure, does a 900MB+ clone, then fetches at least another +1GB of third-party dependencies by clone'ing on the fly. I gave up +after seeing it downloading 2GB+ without doing anything else (yet)! + +So I scouted around for reasonably up-to-date unofficial builds. +This seems to be close enough for this purpose: + + https://github.com/JetBrains/skia-pack/releases/ + +At the time of writing, official skia is at m116, they have +lightly patched m110. Another closely related place, +seems to have unpatched m109: + + https://github.com/HumbleUI/SkiaBuild + +skia-python bundles m87; m98 introduces COLRv1, and m103 introduces +OT-SVG. For this purpose, m103+ is the minimum. + + +PREPARATION: + + mkdir skia/ + pushd skia/ + unzip ""/Skia-m110-d88a7b5-linux-Release-x64.zip + popd + +# This is just mirroring the layout of a submodule-based self-build +# using the official procedure. + + ln -s Release-linux-x64 skia/out/Release + +# Since we set "-Lskia/out/Release/" in the linker, we want to avoid +# picking up those same bundled libraries, but let the linker pick +# up the system versions of these libraries. + + rm skia/out/Release/libharfbuzz.a + rm skia/out/Release/libzlib.a + rm skia/out/Release/libpng.a + rm skia/out/Release/libfreetype2.a + rm skia/out/Release/libjpeg.a + + +BUILDING: + +Run: + CC=c++ ./configure --with-librsvg=no + +in freetype2. Using a C++ compiler is necessary (Skia is heavy-duty +c++ code). Co-existence with librsvg and runtime-switching is +still being looked at. + +Then just build as normal. The main noticeable outcome is that a few +files in "bin/.libs" are 25MB+ (because linking with libskia.a), +instead of a few MB in size. diff --git a/skia/out/Release b/skia/out/Release new file mode 120000 index 0000000..9f40889 --- /dev/null +++ b/skia/out/Release @@ -0,0 +1 @@ +Release-linux-x64 \ No newline at end of file diff --git a/src/ftcommon.c b/src/ftcommon.c index 74553aa..7d8a72e 100644 --- a/src/ftcommon.c +++ b/src/ftcommon.c @@ -29,6 +29,7 @@ #include "strbuf.h" #include "ftcommon.h" #include "rsvg-port.h" +#include "skia-port.h" #include #include @@ -360,7 +361,7 @@ /* The use of an external SVG rendering library is optional. */ (void)FT_Property_Set( handle->library, - "ot-svg", "svg-hooks", &rsvg_hooks ); + "ot-svg", "svg-hooks", &skia_hooks ); error = FTC_Manager_New( handle->library, 0, 0, 0, my_face_requester, 0, &handle->cache_manager ); diff --git a/src/skia-port.c b/src/skia-port.c new file mode 100644 index 0000000..961c705 --- /dev/null +++ b/src/skia-port.c @@ -0,0 +1,356 @@ +/**************************************************************************** + * + * skia-port.c + * + * + * Copyright (C) 2022-2023 by + * Hin-Tak Leung, based on rsvg-port.c + * + * 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. + * + */ + +/* + * Main reference, this landed in Skia m103: + * https://github.com/google/skia/commit/9cbadcd9280dc139af2f4d41d25a6c9a750e0302.patch + + * From 9cbadcd9280dc139af2f4d41d25a6c9a750e0302 Mon Sep 17 00:00:00 2001 + * From: Ben Wagner + * Date: Wed, 20 Apr 2022 17:52:50 -0400 + * Subject: [PATCH] Add optional OT-SVG support to FreeType + + * In particular, + src/ports/SkFontHost_FreeType_common.cpp: + SkScalerContext_FreeType_Base::drawSVGGlyph() + src/ports/SkFontHost_FreeType.cpp: + SkScalerContext_FreeType::generateMetrics() + + Unrelated but good-side reading: + src/ports/SkFontHost_FreeType_common.cpp: + SkScalerContext_FreeType_Base::computeColrV1GlyphBoundingBox() +*/ +#include +#include + +#ifdef HAVE_SKIA + +#include "modules/svg/include/SkSVGDOM.h" +#include "modules/svg/include/SkSVGNode.h" +#include "modules/svg/include/SkSVGRenderContext.h" // SkSVGPresentationContext +#include "include/core/SkBitmap.h" +#include "include/core/SkCanvas.h" +#include "include/core/SkMatrix.h" +#include "include/core/SkStream.h" +#include "include/private/SkFixed.h" // SkFixedToFloat(x) // relocated between m110 and main to "include/private/base/SkFixed.h" +#include +#include + +#include +#include + +#include "skia-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 + skia_port_init( FT_Pointer *state ) + { + /* allocate the memory upon initialization */ + *state = calloc( sizeof( Skia_Port_StateRec ), 1 ); /* XXX error handling */ + // Skia pointers are reference-counted, so + // malloc seems to be buggy and calloc is needed here. + // Unlike rsvg. + + return FT_Err_Ok; + } + + + /* + * Deallocate the state structure. + */ + void + skia_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 + skia_port_render( FT_GlyphSlot slot, + FT_Pointer *_state ) + { + FT_Error error = FT_Err_Ok; + + Skia_Port_State state; + + state = *(Skia_Port_State*)_state; + + /* Create a SkBitmap to store the rendered image. However, */ + /* don't allocate memory; instead use the space already provided in */ + /* `slot->bitmap.buffer`. */ + SkBitmap dstBitmap; + dstBitmap.setInfo(SkImageInfo::Make(slot->bitmap.width, slot->bitmap.rows, + kBGRA_8888_SkColorType, // Not kN32 - FT_Bitmap are platform-neutral + kPremul_SkAlphaType), + slot->bitmap.pitch); + dstBitmap.setPixels(slot->bitmap.buffer); + + SkCanvas canvas(dstBitmap); + + canvas.clear(SK_ColorTRANSPARENT); + + /* Set a translate transform that translates the points in such a way */ + /* that we get a tight rendering with least redundant white spac. */ + canvas.translate( -state->x, -state->y ); + + /* Replay from state->picture. This saves us from parsing the */ + /* document again and redoing what was already done in the preset */ + /* hook. */ + canvas.drawPicture( state->picture ); + + slot->bitmap.pixel_mode = FT_PIXEL_MODE_BGRA; + slot->bitmap.num_grays = 256; + slot->format = FT_GLYPH_FORMAT_BITMAP; + + /* Clean up everything. */ + state->picture = nullptr; + + 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 + skia_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_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; + + SkCanvas *recordingCanvas; + SkMatrix m; + + /* Rendering port's state. */ + Skia_Port_State state; + Skia_Port_StateRec state_dummy; + + /* General variables. */ + double x, y; + double width, height; + + 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 = *(Skia_Port_State*)_state; + else + state = &state_dummy; + + /* Form an `sk_sp` by loading the SVG document. */ + SkMemoryStream svgmem(document->svg_document, + document->svg_document_length, false /*not copying */); + sk_sp svg = SkSVGDOM::MakeFromStream(svgmem); + + //svg->getRoot()->intrinsicSize(); + if (svg->containerSize().isEmpty()) { + SkSize size = SkSize::Make(units_per_EM, units_per_EM); + svg->setContainerSize(size); + } + // Do we care about the viewBox attribute? It is auto I think, anyway. + + /* + * Create a SkPictureRecorder. 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. + */ + SkPictureRecorder recorder; + + SkRect infiniteRect = SkRect::MakeLTRB(-SK_ScalarInfinity, -SK_ScalarInfinity, + SK_ScalarInfinity, SK_ScalarInfinity); + sk_sp bboxh = SkRTreeFactory()(); + + recordingCanvas = recorder.beginRecording(infiniteRect, bboxh); + + /* + * Borrow heavily from: + * src/ports/SkFontHost_FreeType_common.cpp: + * SkScalerContext_FreeType_Base::drawSVGGlyph() + */ + SkASSERT(slot->format == FT_GLYPH_FORMAT_SVG); + + /* + * 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. + */ + FT_Matrix ftMatrix = document->transform; + FT_Vector ftOffset = document->delta; + + /* Skia stores both transformation and translation in one matrix. */ + m.setAll( + SkFixedToFloat(ftMatrix.xx), -SkFixedToFloat(ftMatrix.xy), SkFixedToFloat(ftOffset.x), + -SkFixedToFloat(ftMatrix.yx), SkFixedToFloat(ftMatrix.yy), -SkFixedToFloat(ftOffset.y), + 0 , 0 , 1 ); + + /* Set up a scale transformation to scale up the document to the */ + /* required output size. */ + m.postScale(SkFixedToFloat(document->metrics.x_scale) / 64.0f, + SkFixedToFloat(document->metrics.y_scale) / 64.0f); + + /* Set up a transformation matrix. */ + recordingCanvas->concat(m); + + /* 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 ) + { + SkSVGPresentationContext pctx; + char id[32]; + /* Render only the element with its ID equal to `glyph`. */ + sprintf( id, "glyph%u", slot->glyph_index ); + + /* + * Unlike Rsvg, Skia's renderNode() takes an extra + * SkSVGPresentationContext argument, which sets foreground + * colors, palettes, etc, and does not take NULL to render + * whole. In the case of OT-SVG, there is no extra + * Context, so leaving it as default is fine. + */ + svg->renderNode(recordingCanvas, pctx, id); + } + else + { + /* Render the whole document */ + svg->render(recordingCanvas); + } + + /* Get the bounding box of the drawing. */ + state->picture = recorder.finishRecordingAsPicture(); + SkRect bounds = state->picture->cullRect(); + SkASSERT(bounds.isFinite()); + + width = bounds.width(); + height = bounds.height(); + x = bounds.left(); + y = bounds.top(); + + /* 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; + + slot->bitmap.rows = ceil( height ); + slot->bitmap.width = ceil( width ); + + 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.2f * 64 ); + + /* If a render call is to follow, just destroy the canvas for the */ + /* SkPictureRecorder since no more drawing will be done on it. */ + /* However, keep the Picture itself for use by the render hook. */ + // TRUE defined in /usr/include/glib-2.0/glib/gmacros.h + // TRUE/FALSE not in c++, defined in include/freetype/internal/ftobjs.h (not used) + // but defined indirectly in librsvg. Its header to refer to it (from glib). + if ( !cache ) + { + /* We don't have to do this; just being pedantic */ + state->picture = nullptr; + } + + return error; + } + + + SVG_RendererHooks skia_hooks = { + (SVG_Lib_Init_Func)skia_port_init, + (SVG_Lib_Free_Func)skia_port_free, + (SVG_Lib_Render_Func)skia_port_render, + (SVG_Lib_Preset_Slot_Func)skia_port_preset_slot + }; + +#else /* !HAVE_SKIA */ + + SVG_RendererHooks skia_hooks = { NULL, NULL, NULL, NULL }; + +#endif /* !HAVE_SKIA */ + + +/* End */ diff --git a/src/skia-port.h b/src/skia-port.h new file mode 100644 index 0000000..75c2d5a --- /dev/null +++ b/src/skia-port.h @@ -0,0 +1,71 @@ +/**************************************************************************** + * + * skia-port.h + * + * Skia based hook functions for OT-SVG rendering in FreeType + * (headers). + * + * Copyright (C) 2022-2023 by + * Hin-Tak Leung, based on rsvg-port.h + * + * 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 SKIA_PORT_H +#define SKIA_PORT_H + +#include +#include + +#ifdef HAVE_SKIA + +#include "include/core/SkPictureRecorder.h" +#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 Skia_Port_StateRec_ + { + sk_sp picture; + + double x; + double y; + + } Skia_Port_StateRec; + + typedef struct Skia_Port_StateRec_* Skia_Port_State; + + + FT_Error + skia_port_init( FT_Pointer *state ); + + void + skia_port_free( FT_Pointer *state ); + + FT_Error + skia_port_render( FT_GlyphSlot slot, + FT_Pointer *state ); + + FT_Error + skia_port_preset_slot( FT_GlyphSlot slot, + FT_Bool cache, + FT_Pointer *state ); + +#endif /* HAVE_SKAI */ + + + extern SVG_RendererHooks skia_hooks; + +#endif /* SKIA_PORT_H */ + + +/* End */