From cca66b9ec4e494c1d919bff0f71a820d8afab1fa Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 20:24:48 +0200 Subject: Adding upstream version 1.2.2. Signed-off-by: Daniel Baumann --- src/libnrtype/CMakeLists.txt | 43 + src/libnrtype/FontFactory.cpp | 877 ++++++++++ src/libnrtype/FontFactory.h | 207 +++ src/libnrtype/FontInstance.cpp | 1016 +++++++++++ src/libnrtype/Layout-TNG-Compute.cpp | 2414 ++++++++++++++++++++++++++ src/libnrtype/Layout-TNG-Input.cpp | 241 +++ src/libnrtype/Layout-TNG-OutIter.cpp | 1175 +++++++++++++ src/libnrtype/Layout-TNG-Output.cpp | 910 ++++++++++ src/libnrtype/Layout-TNG-Scanline-Maker.h | 186 ++ src/libnrtype/Layout-TNG-Scanline-Makers.cpp | 196 +++ src/libnrtype/Layout-TNG.cpp | 48 + src/libnrtype/Layout-TNG.h | 1207 +++++++++++++ src/libnrtype/OpenTypeUtil.cpp | 447 +++++ src/libnrtype/OpenTypeUtil.h | 117 ++ src/libnrtype/font-glyph.h | 36 + src/libnrtype/font-instance.h | 158 ++ src/libnrtype/font-lister.cpp | 1271 ++++++++++++++ src/libnrtype/font-lister.h | 364 ++++ src/libnrtype/font-style.h | 44 + 19 files changed, 10957 insertions(+) create mode 100644 src/libnrtype/CMakeLists.txt create mode 100644 src/libnrtype/FontFactory.cpp create mode 100644 src/libnrtype/FontFactory.h create mode 100644 src/libnrtype/FontInstance.cpp create mode 100644 src/libnrtype/Layout-TNG-Compute.cpp create mode 100644 src/libnrtype/Layout-TNG-Input.cpp create mode 100644 src/libnrtype/Layout-TNG-OutIter.cpp create mode 100644 src/libnrtype/Layout-TNG-Output.cpp create mode 100644 src/libnrtype/Layout-TNG-Scanline-Maker.h create mode 100644 src/libnrtype/Layout-TNG-Scanline-Makers.cpp create mode 100644 src/libnrtype/Layout-TNG.cpp create mode 100644 src/libnrtype/Layout-TNG.h create mode 100644 src/libnrtype/OpenTypeUtil.cpp create mode 100644 src/libnrtype/OpenTypeUtil.h create mode 100644 src/libnrtype/font-glyph.h create mode 100644 src/libnrtype/font-instance.h create mode 100644 src/libnrtype/font-lister.cpp create mode 100644 src/libnrtype/font-lister.h create mode 100644 src/libnrtype/font-style.h (limited to 'src/libnrtype') diff --git a/src/libnrtype/CMakeLists.txt b/src/libnrtype/CMakeLists.txt new file mode 100644 index 0000000..4a46bd0 --- /dev/null +++ b/src/libnrtype/CMakeLists.txt @@ -0,0 +1,43 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +set(nrtype_SRC + FontFactory.cpp + FontInstance.cpp + font-lister.cpp + Layout-TNG.cpp + Layout-TNG-Compute.cpp + Layout-TNG-Input.cpp + Layout-TNG-OutIter.cpp + Layout-TNG-Output.cpp + Layout-TNG-Scanline-Makers.cpp + OpenTypeUtil.cpp + + # ------- + # Headers + font-glyph.h + font-instance.h + font-lister.h + font-style.h + FontFactory.h + Layout-TNG-Scanline-Maker.h + Layout-TNG.h + OpenTypeUtil.cpp +) + +add_inkscape_lib(nrtype_LIB "${nrtype_SRC}") + +# we have circular references between nrtype_LIB and inkscape_base! +# this workaround prevents undefined references in nrtype_LIB when building static libraries (likely link order problem) +if(NOT BUILD_SHARED_LIBS) + if( WIN32 ) + target_link_libraries(nrtype_LIB PRIVATE inkscape_base -lGdi32) + else() + target_link_libraries(nrtype_LIB PRIVATE inkscape_base) + endif() +else() + if( WIN32 ) + target_link_libraries(nrtype_LIB PUBLIC 2Geom::2geom -lGdi32) + else() + target_link_libraries(nrtype_LIB PUBLIC 2Geom::2geom) + endif() +endif() diff --git a/src/libnrtype/FontFactory.cpp b/src/libnrtype/FontFactory.cpp new file mode 100644 index 0000000..88c8b55 --- /dev/null +++ b/src/libnrtype/FontFactory.cpp @@ -0,0 +1,877 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: + * fred + * bulia byak + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#ifndef PANGO_ENABLE_ENGINE +#define PANGO_ENABLE_ENGINE +#endif + +#include + +#include + +#include + +#include +#include +#include + +#include "io/sys.h" +#include "io/resource.h" + +#include "libnrtype/FontFactory.h" +#include "libnrtype/font-instance.h" +#include "libnrtype/OpenTypeUtil.h" + +# ifdef _WIN32 + +#include +#include + +#endif + +typedef std::unordered_map FaceMapType; + +// need to avoid using the size field +size_t font_descr_hash::operator()( PangoFontDescription *const &x) const { + int h = 0; + char const *theF = sp_font_description_get_family(x); + h += (theF)?g_str_hash(theF):0; + h *= 1128467; + h += (int)pango_font_description_get_style(x); + h *= 1128467; + h += (int)pango_font_description_get_variant(x); + h *= 1128467; + h += (int)pango_font_description_get_weight(x); + h *= 1128467; + h += (int)pango_font_description_get_stretch(x); + char const *theV = pango_font_description_get_variations(x); + h *= 1128467; + h += (theV)?g_str_hash(theV):0; + return h; +} + +bool font_descr_equal::operator()( PangoFontDescription *const&a, PangoFontDescription *const &b) const { + //if ( pango_font_description_equal(a,b) ) return true; + char const *fa = sp_font_description_get_family(a); + char const *fb = sp_font_description_get_family(b); + if ( ( fa && fb == nullptr ) || ( fb && fa == nullptr ) ) return false; + if ( fa && fb && strcmp(fa,fb) != 0 ) return false; + if ( pango_font_description_get_style(a) != pango_font_description_get_style(b) ) return false; + if ( pango_font_description_get_variant(a) != pango_font_description_get_variant(b) ) return false; + if ( pango_font_description_get_weight(a) != pango_font_description_get_weight(b) ) return false; + if ( pango_font_description_get_stretch(a) != pango_font_description_get_stretch(b) ) return false; + if ( g_strcmp0( pango_font_description_get_variations(a), + pango_font_description_get_variations(b) ) != 0 ) return false; + return true; +} + +// User must free return value. +PangoFontDescription* ink_font_description_from_style(SPStyle const *style) +{ + PangoFontDescription *descr = pango_font_description_new(); + + pango_font_description_set_family(descr, style->font_family.value()); + + // This duplicates Layout::EnumConversionItem... perhaps we can share code? + switch ( style->font_style.computed ) { + case SP_CSS_FONT_STYLE_ITALIC: + pango_font_description_set_style(descr, PANGO_STYLE_ITALIC); + break; + + case SP_CSS_FONT_STYLE_OBLIQUE: + pango_font_description_set_style(descr, PANGO_STYLE_OBLIQUE); + break; + + case SP_CSS_FONT_STYLE_NORMAL: + default: + pango_font_description_set_style(descr, PANGO_STYLE_NORMAL); + break; + } + + switch( style->font_weight.computed ) { + case SP_CSS_FONT_WEIGHT_100: + pango_font_description_set_weight(descr, PANGO_WEIGHT_THIN); + break; + + case SP_CSS_FONT_WEIGHT_200: + pango_font_description_set_weight(descr, PANGO_WEIGHT_ULTRALIGHT); + break; + + case SP_CSS_FONT_WEIGHT_300: + pango_font_description_set_weight(descr, PANGO_WEIGHT_LIGHT); + break; + + case SP_CSS_FONT_WEIGHT_400: + case SP_CSS_FONT_WEIGHT_NORMAL: + pango_font_description_set_weight(descr, PANGO_WEIGHT_NORMAL); + break; + + case SP_CSS_FONT_WEIGHT_500: + pango_font_description_set_weight(descr, PANGO_WEIGHT_MEDIUM); + break; + + case SP_CSS_FONT_WEIGHT_600: + pango_font_description_set_weight(descr, PANGO_WEIGHT_SEMIBOLD); + break; + + case SP_CSS_FONT_WEIGHT_700: + case SP_CSS_FONT_WEIGHT_BOLD: + pango_font_description_set_weight(descr, PANGO_WEIGHT_BOLD); + break; + + case SP_CSS_FONT_WEIGHT_800: + pango_font_description_set_weight(descr, PANGO_WEIGHT_ULTRABOLD); + break; + + case SP_CSS_FONT_WEIGHT_900: + pango_font_description_set_weight(descr, PANGO_WEIGHT_HEAVY); + break; + + case SP_CSS_FONT_WEIGHT_LIGHTER: + case SP_CSS_FONT_WEIGHT_BOLDER: + default: + g_warning("FaceFromStyle: Unrecognized font_weight.computed value"); + pango_font_description_set_weight(descr, PANGO_WEIGHT_NORMAL); + break; + } + // PANGO_WIEGHT_ULTRAHEAVY not used (not CSS2) + + switch (style->font_stretch.computed) { + case SP_CSS_FONT_STRETCH_ULTRA_CONDENSED: + pango_font_description_set_stretch(descr, PANGO_STRETCH_ULTRA_CONDENSED); + break; + + case SP_CSS_FONT_STRETCH_EXTRA_CONDENSED: + pango_font_description_set_stretch(descr, PANGO_STRETCH_EXTRA_CONDENSED); + break; + + case SP_CSS_FONT_STRETCH_CONDENSED: + pango_font_description_set_stretch(descr, PANGO_STRETCH_CONDENSED); + break; + + case SP_CSS_FONT_STRETCH_SEMI_CONDENSED: + pango_font_description_set_stretch(descr, PANGO_STRETCH_SEMI_CONDENSED); + break; + + case SP_CSS_FONT_STRETCH_NORMAL: + pango_font_description_set_stretch(descr, PANGO_STRETCH_NORMAL); + break; + + case SP_CSS_FONT_STRETCH_SEMI_EXPANDED: + pango_font_description_set_stretch(descr, PANGO_STRETCH_SEMI_EXPANDED); + break; + + case SP_CSS_FONT_STRETCH_EXPANDED: + pango_font_description_set_stretch(descr, PANGO_STRETCH_EXPANDED); + break; + + case SP_CSS_FONT_STRETCH_EXTRA_EXPANDED: + pango_font_description_set_stretch(descr, PANGO_STRETCH_EXTRA_EXPANDED); + break; + + case SP_CSS_FONT_STRETCH_ULTRA_EXPANDED: + pango_font_description_set_stretch(descr, PANGO_STRETCH_ULTRA_EXPANDED); + + case SP_CSS_FONT_STRETCH_WIDER: + case SP_CSS_FONT_STRETCH_NARROWER: + default: + g_warning("FaceFromStyle: Unrecognized font_stretch.computed value"); + pango_font_description_set_stretch(descr, PANGO_STRETCH_NORMAL); + break; + } + + switch ( style->font_variant.computed ) { + case SP_CSS_FONT_VARIANT_SMALL_CAPS: + pango_font_description_set_variant(descr, PANGO_VARIANT_SMALL_CAPS); + break; + + case SP_CSS_FONT_VARIANT_NORMAL: + default: + pango_font_description_set_variant(descr, PANGO_VARIANT_NORMAL); + break; + } + + // Check if not empty as Pango will add @ to string even if empty (bug in Pango?). + if (!style->font_variation_settings.axes.empty()) { + pango_font_description_set_variations(descr, style->font_variation_settings.toString().c_str()); + } + + return descr; +} + +/////////////////// helper functions + +static void noop(...) {} +//#define PANGO_DEBUG g_print +#define PANGO_DEBUG noop + + +///////////////////// FontFactory +#ifndef USE_PANGO_WIN32 +// the substitute function to tell fontconfig to enforce outline fonts +static void FactorySubstituteFunc(FcPattern *pattern,gpointer /*data*/) +{ + FcPatternAddBool(pattern, "FC_OUTLINE",FcTrue); + //char *fam = NULL; + //FcPatternGetString(pattern, "FC_FAMILY",0, &fam); + //printf("subst_f on %s\n",fam); +} +#endif + + +font_factory *font_factory::lUsine = nullptr; + +font_factory *font_factory::Default() +{ + if ( lUsine == nullptr ) lUsine = new font_factory; + return lUsine; +} + +font_factory::font_factory() : + nbEnt(0), // Note: this "ents" cache only keeps fonts from being unreffed, does not speed up access + maxEnt(32), + ents(static_cast(g_malloc(maxEnt*sizeof(font_entry)))), +#ifdef USE_PANGO_WIN32 + fontServer(pango_win32_font_map_for_display()), + pangoFontCache(pango_win32_font_map_get_font_cache(fontServer)), + hScreenDC(pango_win32_get_dc()), +#else + fontServer(pango_ft2_font_map_new()), +#endif + fontContext(pango_font_map_create_context(fontServer)), + fontSize(512), + loadedPtr(new FaceMapType()) +{ +#ifndef USE_PANGO_WIN32 + pango_ft2_font_map_set_resolution(PANGO_FT2_FONT_MAP(fontServer), + 72, 72); +#if PANGO_VERSION_CHECK(1,48,0) + pango_fc_font_map_set_default_substitute(PANGO_FC_FONT_MAP(fontServer), +#else + pango_ft2_font_map_set_default_substitute(PANGO_FT2_FONT_MAP(fontServer), +#endif + FactorySubstituteFunc, + this, + nullptr); +#endif + +} + +font_factory::~font_factory() +{ + for (int i = 0;i < nbEnt;i++) ents[i].f->Unref(); + if ( ents ) g_free(ents); + + g_object_unref(fontServer); +#ifdef USE_PANGO_WIN32 + pango_win32_shutdown_display(); +#else + //pango_ft2_shutdown_display(); +#endif + //g_object_unref(fontContext); + + if (loadedPtr) { + FaceMapType* tmp = static_cast(loadedPtr); + delete tmp; + loadedPtr = nullptr; + } +} + + +Glib::ustring font_factory::ConstructFontSpecification(PangoFontDescription *font) +{ + Glib::ustring pangoString; + + g_assert(font); + + if (font) { + // Once the format for the font specification is decided, it must be + // kept.. if it is absolutely necessary to change it, the attribute + // it is written to needs to have a new version so the legacy files + // can be read. + + PangoFontDescription *copy = pango_font_description_copy(font); + + pango_font_description_unset_fields (copy, PANGO_FONT_MASK_SIZE); + char * copyAsString = pango_font_description_to_string(copy); + pangoString = copyAsString; + g_free(copyAsString); + copyAsString = nullptr; + + pango_font_description_free(copy); + + } + + return pangoString; +} + +Glib::ustring font_factory::ConstructFontSpecification(font_instance *font) +{ + Glib::ustring pangoString; + + g_assert(font); + + if (font) { + pangoString = ConstructFontSpecification(font->descr); + } + + return pangoString; +} + +/* + * Wrap calls to pango_font_description_get_family + * and replace some of the pango font names with generic css names + * http://www.w3.org/TR/2008/REC-CSS2-20080411/fonts.html#generic-font-families + * + * This function should be called in place of pango_font_description_get_family() + */ +const char *sp_font_description_get_family(PangoFontDescription const *fontDescr) { + + static std::map fontNameMap; + std::map::iterator it; + + if (fontNameMap.empty()) { + fontNameMap.insert(std::make_pair("Sans", "sans-serif")); + fontNameMap.insert(std::make_pair("Serif", "serif")); + fontNameMap.insert(std::make_pair("Monospace", "monospace")); + } + + const char *pangoFamily = pango_font_description_get_family(fontDescr); + + if (pangoFamily && ((it = fontNameMap.find(pangoFamily)) != fontNameMap.end())) { + return (it->second).c_str(); + } + + return pangoFamily; +} + +Glib::ustring font_factory::GetUIFamilyString(PangoFontDescription const *fontDescr) +{ + Glib::ustring family; + + g_assert(fontDescr); + + if (fontDescr) { + // For now, keep it as family name taken from pango + const char *pangoFamily = sp_font_description_get_family(fontDescr); + + if( pangoFamily ) { + family = pangoFamily; + } + } + + return family; +} + +Glib::ustring font_factory::GetUIStyleString(PangoFontDescription const *fontDescr) +{ + Glib::ustring style; + + g_assert(fontDescr); + + if (fontDescr) { + PangoFontDescription *fontDescrCopy = pango_font_description_copy(fontDescr); + + pango_font_description_unset_fields(fontDescrCopy, PANGO_FONT_MASK_FAMILY); + pango_font_description_unset_fields(fontDescrCopy, PANGO_FONT_MASK_SIZE); + + // For now, keep it as style name taken from pango + char *fontDescrAsString = pango_font_description_to_string(fontDescrCopy); + style = fontDescrAsString; + g_free(fontDescrAsString); + fontDescrAsString = nullptr; + pango_font_description_free(fontDescrCopy); + } + + return style; +} + + +///// + +// Calculate a Style "value" based on CSS values for ordering styles. +static int StyleNameValue( const Glib::ustring &style ) +{ + + PangoFontDescription *pfd = pango_font_description_from_string ( style.c_str() ); + int value = + pango_font_description_get_weight ( pfd ) * 1000000 + + pango_font_description_get_style ( pfd ) * 10000 + + pango_font_description_get_stretch( pfd ) * 100 + + pango_font_description_get_variant( pfd ); + pango_font_description_free ( pfd ); + return value; +} + +// Determines order in which styles are presented (sorted by CSS style values) +//static bool StyleNameCompareInternal(const StyleNames &style1, const StyleNames &style2) +//{ +// return( StyleNameValue( style1.CssName ) < StyleNameValue( style2.CssName ) ); +//} + +static gint StyleNameCompareInternalGlib(gconstpointer a, gconstpointer b) +{ + return( StyleNameValue( ((StyleNames *)a)->CssName ) < + StyleNameValue( ((StyleNames *)b)->CssName ) ? -1 : 1 ); +} + +static bool ustringPairSort(std::pair const& first, std::pair const& second) +{ + // well, this looks weird. + return first.second < second.second; +} + +void font_factory::GetUIFamilies(std::vector& out) +{ + // Gather the family names as listed by Pango + PangoFontFamily** families = nullptr; + int numFamilies = 0; + pango_font_map_list_families(fontServer, &families, &numFamilies); + + std::vector > sorted; + + // not size_t + for (int currentFamily = 0; currentFamily < numFamilies; ++currentFamily) { + const char* displayName = pango_font_family_get_name(families[currentFamily]); + + if (displayName == nullptr || *displayName == '\0') { + std::cerr << "font_factory::GetUIFamilies: Missing displayName! " << std::endl; + continue; + } + if (!g_utf8_validate(displayName, -1, nullptr)) { + // TODO: can can do anything about this or does it always indicate broken fonts that should not be used? + std::cerr << "font_factory::GetUIFamilies: Illegal characters in displayName. "; + std::cerr << "Ignoring font '" << displayName << "'" << std::endl; + continue; + } + sorted.emplace_back(families[currentFamily], displayName); + } + + std::sort(sorted.begin(), sorted.end(), ustringPairSort); + + for (auto & i : sorted) { + out.push_back(i.first); + } +} + +GList* font_factory::GetUIStyles(PangoFontFamily * in) +{ + GList* ret = nullptr; + // Gather the styles for this family + PangoFontFace** faces = nullptr; + int numFaces = 0; + if (in == nullptr) { + std::cerr << "font_factory::GetUIStyles(): PangoFontFamily is NULL" << std::endl; + return ret; + } + + pango_font_family_list_faces(in, &faces, &numFaces); + + for (int currentFace = 0; currentFace < numFaces; currentFace++) { + + // If the face has a name, describe it, and then use the + // description to get the UI family and face strings + const gchar* displayName = pango_font_face_get_face_name(faces[currentFace]); + // std::cout << "Display Name: " << displayName << std::endl; + if (displayName == nullptr || *displayName == '\0') { + std::cerr << "font_factory::GetUIStyles: Missing displayName! " << std::endl; + continue; + } + + PangoFontDescription *faceDescr = pango_font_face_describe(faces[currentFace]); + if (faceDescr) { + Glib::ustring familyUIName = GetUIFamilyString(faceDescr); + Glib::ustring styleUIName = GetUIStyleString(faceDescr); + // std::cout << " " << familyUIName << " styleUIName: " << styleUIName << " displayName: " << displayName << std::endl; + + // Disable synthesized (faux) font faces except for CSS generic faces + if (pango_font_face_is_synthesized(faces[currentFace]) ) { + if (familyUIName.compare( "sans-serif" ) != 0 && + familyUIName.compare( "serif" ) != 0 && + familyUIName.compare( "monospace" ) != 0 && + familyUIName.compare( "fantasy" ) != 0 && + familyUIName.compare( "cursive" ) != 0 ) { + continue; + } + } + + // Pango breaks the 1 to 1 mapping between Pango weights and CSS weights by + // adding Semi-Light (as of 1.36.7), Book (as of 1.24), and Ultra-Heavy (as of + // 1.24). We need to map these weights to CSS weights. Book and Ultra-Heavy + // are rarely used. Semi-Light (350) is problematic as it is halfway between + // Light (300) and Normal (400) and if care is not taken it is converted to + // Normal, rather than Light. + // + // Note: The ultimate solution to handling various weight in the same + // font family is to support the @font rules from CSS. + // + // Additional notes, helpful for debugging: + // Pango's FC backend: + // Weights defined in fontconfig/fontconfig.h + // String equivalents in src/fcfreetype.c + // Weight set from os2->usWeightClass + // Use Fontforge: Element->Font Info...->OS/2->Misc->Weight Class to check font weight + size_t f = styleUIName.find( "Book" ); + if( f != Glib::ustring::npos ) { + styleUIName.replace( f, 4, "Normal" ); + } + f = styleUIName.find( "Semi-Light" ); + if( f != Glib::ustring::npos ) { + styleUIName.replace( f, 10, "Light" ); + } + f = styleUIName.find( "Ultra-Heavy" ); + if( f != Glib::ustring::npos ) { + styleUIName.replace( f, 11, "Heavy" ); + } + + bool exists = false; + for(GList *temp = ret; temp; temp = temp->next) { + if( ((StyleNames*)temp->data)->CssName.compare( styleUIName ) == 0 ) { + exists = true; + std::cerr << "Warning: Font face with same CSS values already added: " + << familyUIName.raw() << " " << styleUIName.raw() + << " (" << ((StyleNames*)temp->data)->DisplayName.raw() + << ", " << displayName << ")" << std::endl; + break; + } + } + + if (!exists && !familyUIName.empty() && !styleUIName.empty()) { + // Add the style information + ret = g_list_append(ret, new StyleNames(styleUIName, displayName)); + } + } + pango_font_description_free(faceDescr); + } + g_free(faces); + + // Sort the style lists + ret = g_list_sort( ret, StyleNameCompareInternalGlib ); + return ret; +} + + +font_instance* font_factory::FaceFromStyle(SPStyle const *style) +{ + font_instance *font = nullptr; + + g_assert(style); + + if (style) { + + // First try to use the font specification if it is set + char const *val; + if (style->font_specification.set + && (val = style->font_specification.value()) + && val[0]) { + + font = FaceFromFontSpecification(val); + } + + // If that failed, try using the CSS information in the style + if (!font) { + PangoFontDescription* temp_descr = + ink_font_description_from_style(style); + font = Face(temp_descr); + pango_font_description_free(temp_descr); + } + } + + return font; +} + +font_instance *font_factory::FaceFromDescr(char const *family, char const *style) +{ + PangoFontDescription *temp_descr = pango_font_description_from_string(style); + pango_font_description_set_family(temp_descr,family); + font_instance *res = Face(temp_descr); + pango_font_description_free(temp_descr); + return res; +} + +font_instance* font_factory::FaceFromPangoString(char const *pangoString) +{ + font_instance *fontInstance = nullptr; + + g_assert(pangoString); + + if (pangoString) { + + // Create a font description from the string - this may fail or + // produce unexpected results if the string does not have a good format + PangoFontDescription *descr = pango_font_description_from_string(pangoString); + + if (descr) { + if (sp_font_description_get_family(descr) != nullptr) { + fontInstance = Face(descr); + } + pango_font_description_free(descr); + } + } + + return fontInstance; +} + +font_instance* font_factory::FaceFromFontSpecification(char const *fontSpecification) +{ + font_instance *font = nullptr; + + g_assert(fontSpecification); + + if (fontSpecification) { + // How the string is used to reconstruct a font depends on how it + // was constructed in ConstructFontSpecification. As it stands, + // the font specification is a pango-created string + font = FaceFromPangoString(fontSpecification); + } + + return font; +} + +font_instance *font_factory::Face(PangoFontDescription *descr, bool canFail) +{ +#ifdef USE_PANGO_WIN32 + // damn Pango fudges the size, so we need to unfudge. See source of pango_win32_font_map_init() + pango_font_description_set_size(descr, (int) (fontSize*PANGO_SCALE*72/GetDeviceCaps(pango_win32_get_dc(),LOGPIXELSY))); // mandatory huge size (hinting workaround) +#else + pango_font_description_set_size(descr, (int) (fontSize*PANGO_SCALE)); // mandatory huge size (hinting workaround) +#endif + + font_instance *res = nullptr; + + FaceMapType& loadedFaces = *static_cast(loadedPtr); + if ( loadedFaces.find(descr) == loadedFaces.end() ) { + // not yet loaded + PangoFont *nFace = nullptr; + + // workaround for bug #1025565. + // fonts without families blow up Pango. + if (sp_font_description_get_family(descr) != nullptr) { + nFace = pango_font_map_load_font(fontServer,fontContext,descr); + } + else { + g_warning("%s", _("Ignoring font without family that will crash Pango")); + } + + if ( nFace ) { + // duplicate FcPattern, the hard way + res = new font_instance(); + // store the descr of the font we asked for, since this is the key where we intend + // to put the font_instance at in the unordered_map. the descr of the returned + // pangofont may differ from what was asked, so we don't know (at this + // point) whether loadedFaces[that_descr] is free or not (and overwriting + // an entry will bring deallocation problems) + res->descr = pango_font_description_copy(descr); + res->parent = this; + res->InstallFace(nFace); + if ( res->pFont == nullptr ) { + // failed to install face -> bitmap font + // printf("face failed\n"); + res->parent = nullptr; + delete res; + res = nullptr; + if ( canFail ) { + char *tc = pango_font_description_to_string(descr); + PANGO_DEBUG("falling back from %s to 'sans-serif' because InstallFace failed\n",tc); + g_free(tc); + pango_font_description_set_family(descr,"sans-serif"); + res = Face(descr,false); + } + } else { + loadedFaces[res->descr]=res; + res->Ref(); + AddInCache(res); + } + } else { + // no match + if ( canFail ) { + PANGO_DEBUG("falling back to 'sans-serif'\n"); + PangoFontDescription *new_descr = pango_font_description_new(); + pango_font_description_set_family(new_descr, "sans-serif"); + res = Face(new_descr, false); + pango_font_description_free(new_descr); + } else { + g_critical("Could not load any face for font '%s'.", pango_font_description_to_string(descr)); + } + } + + } else { + // already here + res = loadedFaces[descr]; + res->Ref(); + AddInCache(res); + } + if (res) { + res->InitTheFace(); + } + return res; +} + +// Not used, need to add variations if ever used. +// font_instance *font_factory::Face(char const *family, int variant, int style, int weight, int stretch, int /*size*/, int /*spacing*/) +// { +// // std::cout << "font_factory::Face(family, variant, style, weight, stretch,)" << std::endl; +// PangoFontDescription *temp_descr = pango_font_description_new(); +// pango_font_description_set_family(temp_descr,family); +// pango_font_description_set_weight(temp_descr,(PangoWeight)weight); +// pango_font_description_set_stretch(temp_descr,(PangoStretch)stretch); +// pango_font_description_set_style(temp_descr,(PangoStyle)style); +// pango_font_description_set_variant(temp_descr,(PangoVariant)variant); +// font_instance *res = Face(temp_descr); +// pango_font_description_free(temp_descr); +// return res; +// } + +void font_factory::UnrefFace(font_instance *who) +{ + if ( who ) { + FaceMapType& loadedFaces = *static_cast(loadedPtr); + + if ( loadedFaces.find(who->descr) == loadedFaces.end() ) { + // not found + char *tc = pango_font_description_to_string(who->descr); + g_warning("unrefFace %p=%s: failed\n",who,tc); + g_free(tc); + } else { + loadedFaces.erase(loadedFaces.find(who->descr)); + // printf("unrefFace %p: success\n",who); + } + } +} + +void font_factory::AddInCache(font_instance *who) +{ + if ( who == nullptr ) return; + for (int i = 0;i < nbEnt;i++) ents[i].age *= 0.9; + for (int i = 0;i < nbEnt;i++) { + if ( ents[i].f == who ) { + // printf("present\n"); + ents[i].age += 1.0; + return; + } + } + if ( nbEnt > maxEnt ) { + printf("cache sur-plein?\n"); + return; + } + who->Ref(); + if ( nbEnt == maxEnt ) { // cache is filled, unref the oldest-accessed font in it + int bi = 0; + double ba = ents[bi].age; + for (int i = 1;i < nbEnt;i++) { + if ( ents[i].age < ba ) { + bi = i; + ba = ents[bi].age; + } + } + ents[bi].f->Unref(); + ents[bi]=ents[--nbEnt]; + } + ents[nbEnt].f = who; + ents[nbEnt].age = 1.0; + nbEnt++; +} + +# ifdef _WIN32 +void font_factory::AddFontFilesWin32(char const * directory_path ) +{ + std::vector allowed_ext = {"ttf","otf" }; + std::vector files = {}; + Inkscape::IO::Resource::get_filenames_from_path(files,directory_path, allowed_ext,(std::vector) {}); + for (auto file : files) { + int result = AddFontResourceExA(file.c_str(),FR_PRIVATE,0); + if (result != 0 ) { + g_info("Font File: %s added sucessfully.",file.c_str()); + } else { + g_warning("Font File: %s wasn't added sucessfully",file.c_str()); + } + } +} +# endif + +void font_factory::AddFontsDir(char const *utf8dir) +{ +#ifdef USE_PANGO_WIN32 + g_info("Adding additional font directories only supported for fontconfig backend."); +#else + if (!Inkscape::IO::file_test(utf8dir, G_FILE_TEST_IS_DIR)) { + g_info("Fonts dir '%s' does not exist and will be ignored.", utf8dir); + return; + } + + gchar *dir; +# ifdef _WIN32 + AddFontFilesWin32(utf8dir); + dir = g_win32_locale_filename_from_utf8(utf8dir); +# else + dir = g_filename_from_utf8(utf8dir, -1, nullptr, nullptr, nullptr); +# endif + + FcConfig *conf = nullptr; + conf = pango_fc_font_map_get_config(PANGO_FC_FONT_MAP(fontServer)); + FcBool res = FcConfigAppFontAddDir(conf, (FcChar8 const *)dir); + if (res == FcTrue) { + g_info("Fonts dir '%s' added successfully.", utf8dir); + pango_fc_font_map_config_changed(PANGO_FC_FONT_MAP(fontServer)); + } else { + g_warning("Could not add fonts dir '%s'.", utf8dir); + } + + g_free(dir); +#endif +} + +void font_factory::AddFontFile(char const *utf8file) +{ +#ifdef USE_PANGO_WIN32 + g_info("Adding additional font only supported for fontconfig backend."); +#else + if (!Inkscape::IO::file_test(utf8file, G_FILE_TEST_IS_REGULAR)) { + g_warning("Font file '%s' does not exist and will be ignored.", utf8file); + return; + } + + gchar *file; +# ifdef _WIN32 + file = g_win32_locale_filename_from_utf8(utf8file); +# else + file = g_filename_from_utf8(utf8file, -1, nullptr, nullptr, nullptr); +# endif + + FcConfig *conf = nullptr; + conf = pango_fc_font_map_get_config(PANGO_FC_FONT_MAP(fontServer)); + FcBool res = FcConfigAppFontAddFile(conf, (FcChar8 const *)file); + if (res == FcTrue) { + g_info("Font file '%s' added successfully.", utf8file); + pango_fc_font_map_config_changed(PANGO_FC_FONT_MAP(fontServer)); + } else { + g_warning("Could not add font file '%s'.", utf8file); + } + + g_free(file); +#endif +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/libnrtype/FontFactory.h b/src/libnrtype/FontFactory.h new file mode 100644 index 0000000..e3bf1d2 --- /dev/null +++ b/src/libnrtype/FontFactory.h @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* + * FontFactory.h + * testICU + * + */ + +#ifndef my_font_factory +#define my_font_factory + +#include +#include +#include + +#ifdef _WIN32 +//#define USE_PANGO_WIN32 // disable for Bug 165665 +#endif + +#include +#include "style.h" + +/* Freetype */ +#ifdef USE_PANGO_WIN32 +#include +#else +#include +#include +#include FT_FREETYPE_H +#endif + + +class font_instance; + +namespace Glib +{ + class ustring; +} + +// the font_factory keeps a hashmap of all the loaded font_instances, and uses the PangoFontDescription +// as index (nota: since pango already does that, using the PangoFont could work too) +struct font_descr_hash : public std::unary_function { + size_t operator()(PangoFontDescription *const &x) const; +}; +struct font_descr_equal : public std::binary_function { + bool operator()(PangoFontDescription *const &a, PangoFontDescription *const &b) const; +}; + +// Constructs a PangoFontDescription from SPStyle. Font size is not included. +// User must free return value. +PangoFontDescription* ink_font_description_from_style(SPStyle const *style); + +// Wraps calls to pango_font_description_get_family with some name substitution +const char *sp_font_description_get_family(PangoFontDescription const *fontDescr); + +// Class for style strings: both CSS and as suggested by font. +class StyleNames { + +public: + StyleNames() = default;; + StyleNames( Glib::ustring name ) : + CssName( name ), DisplayName( name ) {}; + StyleNames( Glib::ustring cssname, Glib::ustring displayname ) : + CssName(std::move( cssname )), DisplayName(std::move( displayname )) {}; + +public: + Glib::ustring CssName; // Style as Pango/CSS would write it. + Glib::ustring DisplayName; // Style as Font designer named it. +}; + +// Map type for gathering UI family and style names +// typedef std::map > FamilyToStylesMap; + +class font_factory { +public: + static font_factory *lUsine; /**< The default font_factory; i cannot think of why we would + * need more than one. + * + * ("l'usine" is french for "the factory".) + */ + + /** A little cache for fonts, so that you don't loose your time looking up fonts in the font list + * each font in the cache is refcounted once (and deref'd when removed from the cache). */ + struct font_entry { + font_instance *f; + double age; + }; + int nbEnt; ///< Number of entries. + int maxEnt; ///< Cache size. + font_entry *ents; + + // Pango data. Backend-specific structures are cast to these opaque types. + PangoFontMap *fontServer; + PangoContext *fontContext; +#ifdef USE_PANGO_WIN32 + PangoWin32FontCache *pangoFontCache; + HDC hScreenDC; +#endif + double fontSize; /**< The huge fontsize used as workaround for hinting. + * Different between freetype and win32. */ + + font_factory(); + virtual ~font_factory(); + + /// Returns the default font_factory. + static font_factory* Default(); + + /// Constructs a pango string for use with the fontStringMap (see below) + Glib::ustring ConstructFontSpecification(PangoFontDescription *font); + Glib::ustring ConstructFontSpecification(font_instance *font); + + /// Returns strings to be used in the UI for family and face (or "style" as the column is labeled) + Glib::ustring GetUIFamilyString(PangoFontDescription const *fontDescr); + Glib::ustring GetUIStyleString(PangoFontDescription const *fontDescr); + + // Helpfully inserts all font families into the provided vector + void GetUIFamilies(std::vector& out); + // Retrieves style information about a family in a newly allocated GList. + GList* GetUIStyles(PangoFontFamily * in); + + /// Retrieve a font_instance from a style object, first trying to use the font-specification, the CSS information + font_instance* FaceFromStyle(SPStyle const *style); + + // Various functions to get a font_instance from different descriptions. + font_instance* FaceFromDescr(char const *family, char const *style); + font_instance* FaceFromUIStrings(char const *uiFamily, char const *uiStyle); + font_instance* FaceFromPangoString(char const *pangoString); + font_instance* FaceFromFontSpecification(char const *fontSpecification); + font_instance* Face(PangoFontDescription *descr, bool canFail=true); + font_instance* Face(char const *family, + int variant=PANGO_VARIANT_NORMAL, int style=PANGO_STYLE_NORMAL, + int weight=PANGO_WEIGHT_NORMAL, int stretch=PANGO_STRETCH_NORMAL, + int size=10, int spacing=0); + + /// Semi-private: tells the font_factory that the font_instance 'who' has died and should be removed from loadedFaces + void UnrefFace(font_instance* who); + + // internal + void AddInCache(font_instance *who); + +# ifdef _WIN32 + void AddFontFilesWin32(char const *directory_path); +# endif + + /// Add a directory from which to include additional fonts + void AddFontsDir(char const *utf8dir); + + /// Add a an additional font. + void AddFontFile(char const *utf8file); + +private: + void* loadedPtr; + + + // The following two commented out maps were an attempt to allow Inkscape to use font faces + // that could not be distinguished by CSS values alone. In practice, they never were that + // useful as PangoFontDescription, which is used throughout our code, cannot distinguish + // between faces anymore than raw CSS values (with the exception of two additional weight + // values). + // + // During various works, for example to handle font-family lists and fonts that are not + // installed on the system, the code has become less reliant on these maps. And in the work to + // catch style information to speed up start up times, the maps were not being filled. + // I've removed all code that used these maps as of Oct 2014 in the experimental branch. + // The commented out maps are left here as a reminder of the path that was attempted. + // + // One possible method to keep track of font faces would be to use the 'display name', keeping + // pointers to the appropriate PangoFontFace. The font_factory loadedFaces map indexing would + // have to be changed to incorporate 'display name' (InkscapeFontDescription?). + + + // These two maps are used for translating between what's in the UI and a pango + // font description. This is necessary because Pango cannot always + // reproduce these structures from the names it gave us in the first place. + + // Key: A string produced by font_factory::ConstructFontSpecification + // Value: The associated PangoFontDescription + // typedef std::map PangoStringToDescrMap; + // PangoStringToDescrMap fontInstanceMap; + + // Key: Family name in UI + Style name in UI + // Value: The associated string that should be produced with font_factory::ConstructFontSpecification + // typedef std::map UIStringToPangoStringMap; + // UIStringToPangoStringMap fontStringMap; +}; + + +#endif /* my_font_factory */ + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/libnrtype/FontInstance.cpp b/src/libnrtype/FontInstance.cpp new file mode 100644 index 0000000..e17d593 --- /dev/null +++ b/src/libnrtype/FontInstance.cpp @@ -0,0 +1,1016 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: + * fred + * bulia byak + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#ifndef PANGO_ENABLE_ENGINE +#define PANGO_ENABLE_ENGINE +#endif + +#include +#include FT_OUTLINE_H +#include FT_BBOX_H +#include FT_TRUETYPE_TAGS_H +#include FT_TRUETYPE_TABLES_H +#include FT_GLYPH_H +#include FT_MULTIPLE_MASTERS_H + +#include +#include +#include + +#include + +#include <2geom/pathvector.h> +#include <2geom/path-sink.h> +#include "libnrtype/font-glyph.h" +#include "libnrtype/font-instance.h" + +#include "display/cairo-utils.h" // Inkscape::Pixbuf + +#ifndef USE_PANGO_WIN32 +/* + * Outline extraction + */ + +struct FT2GeomData { + FT2GeomData(Geom::PathBuilder &b, double s) + : builder(b) + , last(0, 0) + , scale(s) + {} + Geom::PathBuilder &builder; + Geom::Point last; + double scale; +}; + +// outline as returned by freetype +static int ft2_move_to(FT_Vector const *to, void * i_user) +{ + FT2GeomData *user = (FT2GeomData*)i_user; + Geom::Point p(to->x, to->y); + // printf("m t=%f %f\n",p[0],p[1]); + user->builder.moveTo(p * user->scale); + user->last = p; + return 0; +} + +static int ft2_line_to(FT_Vector const *to, void *i_user) +{ + FT2GeomData *user = (FT2GeomData*)i_user; + Geom::Point p(to->x, to->y); + // printf("l t=%f %f\n",p[0],p[1]); + user->builder.lineTo(p * user->scale); + user->last = p; + return 0; +} + +static int ft2_conic_to(FT_Vector const *control, FT_Vector const *to, void *i_user) +{ + FT2GeomData *user = (FT2GeomData*)i_user; + Geom::Point p(to->x, to->y), c(control->x, control->y); + user->builder.quadTo(c * user->scale, p * user->scale); + // printf("b c=%f %f t=%f %f\n",c[0],c[1],p[0],p[1]); + user->last = p; + return 0; +} + +static int ft2_cubic_to(FT_Vector const *control1, FT_Vector const *control2, FT_Vector const *to, void *i_user) +{ + FT2GeomData *user = (FT2GeomData*)i_user; + Geom::Point p(to->x, to->y); + Geom::Point c1(control1->x, control1->y); + Geom::Point c2(control2->x, control2->y); + // printf("c c1=%f %f c2=%f %f t=%f %f\n",c1[0],c1[1],c2[0],c2[1],p[0],p[1]); + //user->theP->CubicTo(p,3*(c1-user->last),3*(p-c2)); + user->builder.curveTo(c1 * user->scale, c2 * user->scale, p * user->scale); + user->last = p; + return 0; +} +#endif + +/* *** END #if HACK *** */ + +/* + * + */ + +font_instance::font_instance() +{ + //printf("font instance born\n"); + _ascent = _ascent_max = 0.8; + _descent = _descent_max = 0.2; + _xheight = 0.5; + + // Default baseline values, alphabetic is reference + _baselines[ SP_CSS_BASELINE_AUTO ] = 0.0; + _baselines[ SP_CSS_BASELINE_ALPHABETIC ] = 0.0; + _baselines[ SP_CSS_BASELINE_IDEOGRAPHIC ] = -_descent; + _baselines[ SP_CSS_BASELINE_HANGING ] = 0.8 * _ascent; + _baselines[ SP_CSS_BASELINE_MATHEMATICAL ] = 0.8 * _xheight; + _baselines[ SP_CSS_BASELINE_CENTRAL ] = 0.5 - _descent; + _baselines[ SP_CSS_BASELINE_MIDDLE ] = 0.5 * _xheight; + _baselines[ SP_CSS_BASELINE_TEXT_BEFORE_EDGE ] = _ascent; + _baselines[ SP_CSS_BASELINE_TEXT_AFTER_EDGE ] = -_descent; +} + +font_instance::~font_instance() +{ + if ( parent ) { + parent->UnrefFace(this); + parent = nullptr; + } + + //printf("font instance death\n"); + if ( pFont ) { + FreeTheFace(); + g_object_unref(pFont); + pFont = nullptr; + } + + if ( descr ) { + pango_font_description_free(descr); + descr = nullptr; + } + + // if ( theFace ) FT_Done_Face(theFace); // owned by pFont. don't touch + theFace = nullptr; + + for (int i=0;ipangoFontCache,lf); + g_free(lf); + + XFORM identity = {1.0, 0.0, 0.0, 1.0, 0.0, 0.0}; + SetWorldTransform(parent->hScreenDC, &identity); + SetGraphicsMode(parent->hScreenDC, GM_COMPATIBLE); + SelectObject(parent->hScreenDC,theFace); + +#else + + hb_font_t* hb_font = pango_font_get_hb_font(pFont); // Pango owns hb_font. + +#if HB_VERSION_ATLEAST(2,6,5) + // hb_font is immutable, yet we need to act on it (with set_funcs) to extract the freetype face + hb_font_copy = hb_font_create_sub_font(hb_font); + + hb_ft_font_set_funcs(hb_font_copy); + theFace = hb_ft_font_lock_face(hb_font_copy); +#else + theFace = pango_fc_font_lock_face(PANGO_FC_FONT(pFont)); +#endif + + if ( theFace ) { + FT_Select_Charmap(theFace, ft_encoding_unicode); + FT_Select_Charmap(theFace, ft_encoding_symbol); + } + + if (!hb_font) { + std::cerr << "font_instance::InitTheFace: Failed to get hb_font!" << std::endl; + } else { + if (loadgsub) { + readOpenTypeGsubTable (hb_font, openTypeTables); + fulloaded = true; + } + readOpenTypeSVGTable (hb_font, openTypeSVGGlyphs); + } + readOpenTypeFvarAxes( theFace, openTypeVarAxes ); + + if (openTypeSVGGlyphs.size() > 0 ) { + fontHasSVG = true; + } + +#if FREETYPE_MAJOR == 2 && FREETYPE_MINOR >= 8 // 2.8 does not seem to work even though it has some support. + + // 'font-variation-settings' support. + // The font returned from pango_fc_font_lock_face does not include variation settings. We must set them. + + // We need to: + // Extract axes with values from Pango font description. + // Replace default axis values with extracted values. + + char const *var = pango_font_description_get_variations( descr ); + if (var) { + Glib::ustring variations(var); + + FT_MM_Var* mmvar = nullptr; + FT_Multi_Master mmtype; + if (FT_HAS_MULTIPLE_MASTERS( theFace ) && // Font has variables + FT_Get_MM_Var(theFace, &mmvar) == 0 && // We found the data + FT_Get_Multi_Master(theFace, &mmtype) !=0) { // It's not an Adobe MM font + + // std::cout << " Multiple Masters: variables: " << mmvar->num_axis + // << " named styles: " << mmvar->num_namedstyles << std::endl; + + // Get the required values from Pango Font Description + // Need to check format of values from Pango, for the moment accept any format. + Glib::RefPtr regex = Glib::Regex::create("(\\w{4})=([-+]?\\d*\\.?\\d+([eE][-+]?\\d+)?)"); + Glib::MatchInfo matchInfo; + + const FT_UInt num_axis = openTypeVarAxes.size(); + FT_Fixed w[num_axis]; + for (int i = 0; i < num_axis; ++i) w[i] = 0; + + std::vector tokens = Glib::Regex::split_simple(",", variations); + for (auto token: tokens) { + + regex->match(token, matchInfo); + if (matchInfo.matches()) { + + float value = std::stod(matchInfo.fetch(2).raw()); // Should clamp value + + // Translate the "named" axes. + Glib::ustring name = matchInfo.fetch(1); + if (name == "wdth") name = "Width" ; // 'font-stretch' + if (name == "wght") name = "Weight" ; // 'font-weight' + if (name == "opsz") name = "OpticalSize" ; // 'font-optical-sizing' (indirectly) + if (name == "slnt") name = "Slant" ; // 'font-style' + if (name == "ital") name = "Italic" ; // 'font-style' + + auto it = openTypeVarAxes.find(name); + if (it != openTypeVarAxes.end()) { + it->second.set_val = value; + w[it->second.index] = value * 65536; + } + } + } + + // Set design coordinates + FT_Error err; + err = FT_Set_Var_Design_Coordinates (theFace, num_axis, w); + if (err) { + std::cerr << "font_instance::InitTheFace(): Error in call to FT_Set_Var_Design_Coordinates(): " << err << std::endl; + } + + // FT_Done_MM_Var(mmlib, mmvar); + } + } + +#endif // FreeType +#endif // !USE_PANGO_WIN32 + + FindFontMetrics(); + } + +#ifdef USE_PANGO_WIN32 + // Someone (probably pango or cairo) sets the world transform during initialization and does not reset it. + // Work around this by explicitly setting it again (even if the font is already initialized) + XFORM identity = {1.0, 0.0, 0.0, 1.0, 0.0, 0.0}; + SetWorldTransform(parent->hScreenDC, &identity); + SetGraphicsMode(parent->hScreenDC, GM_COMPATIBLE); + SelectObject(parent->hScreenDC,theFace); +#endif + +} + +void font_instance::FreeTheFace() +{ +#ifdef USE_PANGO_WIN32 + SelectObject(parent->hScreenDC,GetStockObject(SYSTEM_FONT)); + pango_win32_font_cache_unload(parent->pangoFontCache,theFace); +#else + +#if HB_VERSION_ATLEAST(2,6,5) + hb_ft_font_unlock_face(hb_font_copy); + hb_font_destroy(hb_font_copy); +#else + pango_fc_font_unlock_face(PANGO_FC_FONT(pFont)); +#endif +#endif + theFace=nullptr; +} + +void font_instance::InstallFace(PangoFont* iFace) +{ + if ( !iFace ) { + return; + } + pFont=iFace; + iFace = nullptr; + + InitTheFace(); + + if ( pFont && IsOutlineFont() == false ) { + FreeTheFace(); + if ( pFont ) { + g_object_unref(pFont); + } + pFont=nullptr; + } +} + +bool font_instance::IsOutlineFont() +{ + if ( pFont == nullptr ) { + return false; + } + InitTheFace(); +#ifdef USE_PANGO_WIN32 + TEXTMETRIC tm; + return GetTextMetrics(parent->hScreenDC,&tm) && tm.tmPitchAndFamily&(TMPF_TRUETYPE|TMPF_DEVICE); +#else + return FT_IS_SCALABLE(theFace); +#endif +} + +int font_instance::MapUnicodeChar(gunichar c) +{ + int res = 0; + if ( pFont ) { +#ifdef USE_PANGO_WIN32 + res = pango_win32_font_get_glyph_index(pFont, c); +#else + if (!theFace) { + std::cerr << "Face not properly initialized (should not happen)" << std::endl; + } + if ( c > 0xf0000 ) { + res = CLAMP(c, 0xf0000, 0x1fffff) - 0xf0000; + } else { + res = FT_Get_Char_Index(theFace, c); + } +#endif + } + return res; +} + + +#ifdef USE_PANGO_WIN32 +static inline Geom::Point pointfx_to_nrpoint(const POINTFX &p, double scale) +{ + return Geom::Point(*(long*)&p.x / 65536.0 * scale, + *(long*)&p.y / 65536.0 * scale); +} +#endif + +void font_instance::LoadGlyph(int glyph_id) +{ + if ( pFont == nullptr ) { + return; + } + InitTheFace(); +#ifndef USE_PANGO_WIN32 + if ( !FT_IS_SCALABLE(theFace) ) { + return; // bitmap font + } +#endif + + if ( id_to_no.find(glyph_id) == id_to_no.end() ) { + Geom::PathBuilder path_builder; + + if ( nbGlyph >= maxGlyph ) { + maxGlyph=2*nbGlyph+1; + glyphs=(font_glyph*)realloc(glyphs,maxGlyph*sizeof(font_glyph)); + } + font_glyph n_g; + n_g.pathvector=nullptr; + n_g.bbox[0]=n_g.bbox[1]=n_g.bbox[2]=n_g.bbox[3]=0; + n_g.h_advance = 0; + n_g.v_advance = 0; + n_g.h_width = 0; + n_g.v_width = 0; + bool doAdd=false; + +#ifdef USE_PANGO_WIN32 + +#ifndef GGO_UNHINTED // For compatibility with old SDKs. +#define GGO_UNHINTED 0x0100 +#endif + + MAT2 identity = {{0,1},{0,0},{0,0},{0,1}}; + OUTLINETEXTMETRIC otm; + GetOutlineTextMetrics(parent->hScreenDC, sizeof(otm), &otm); + GLYPHMETRICS metrics; + DWORD bufferSize=GetGlyphOutline (parent->hScreenDC, glyph_id, GGO_GLYPH_INDEX | GGO_NATIVE | GGO_UNHINTED, &metrics, 0, NULL, &identity); + double scale=1.0/parent->fontSize; + n_g.h_advance = metrics.gmCellIncX * scale; + n_g.v_advance = otm.otmTextMetrics.tmHeight * scale; + n_g.h_width = metrics.gmBlackBoxX * scale; + n_g.v_width = metrics.gmBlackBoxY * scale; + if ( bufferSize == GDI_ERROR) { + // shit happened + } else if ( bufferSize == 0) { + // character has no visual representation, but is valid (eg whitespace) + doAdd=true; + } else { + char *buffer = new char[bufferSize]; + if ( GetGlyphOutline (parent->hScreenDC, glyph_id, GGO_GLYPH_INDEX | GGO_NATIVE | GGO_UNHINTED, &metrics, bufferSize, buffer, &identity) <= 0 ) { + // shit happened + } else { + // Platform SDK is rubbish, read KB87115 instead + DWORD polyOffset=0; + while ( polyOffset < bufferSize ) { + TTPOLYGONHEADER const *polyHeader=(TTPOLYGONHEADER const *)(buffer+polyOffset); + if (polyOffset+polyHeader->cb > bufferSize) break; + + if (polyHeader->dwType == TT_POLYGON_TYPE) { + path_builder.moveTo(pointfx_to_nrpoint(polyHeader->pfxStart, scale)); + DWORD curveOffset=polyOffset+sizeof(TTPOLYGONHEADER); + + while ( curveOffset < polyOffset+polyHeader->cb ) { + TTPOLYCURVE const *polyCurve=(TTPOLYCURVE const *)(buffer+curveOffset); + POINTFX const *p=polyCurve->apfx; + POINTFX const *endp=p+polyCurve->cpfx; + + switch (polyCurve->wType) { + case TT_PRIM_LINE: + while ( p != endp ) + path_builder.lineTo(pointfx_to_nrpoint(*p++, scale)); + break; + + case TT_PRIM_QSPLINE: + { + g_assert(polyCurve->cpfx >= 2); + + // The list of points specifies one or more control points and ends with the end point. + // The intermediate points (on the curve) are the points between the control points. + Geom::Point this_control = pointfx_to_nrpoint(*p++, scale); + while ( p+1 != endp ) { // Process all "midpoints" (all points except the last) + Geom::Point new_control = pointfx_to_nrpoint(*p++, scale); + path_builder.quadTo(this_control, (new_control+this_control)/2); + this_control = new_control; + } + Geom::Point end = pointfx_to_nrpoint(*p++, scale); + path_builder.quadTo(this_control, end); + } + break; + + case 3: // TT_PRIM_CSPLINE + g_assert(polyCurve->cpfx % 3 == 0); + while ( p != endp ) { + path_builder.curveTo(pointfx_to_nrpoint(p[0], scale), + pointfx_to_nrpoint(p[1], scale), + pointfx_to_nrpoint(p[2], scale)); + p += 3; + } + break; + } + curveOffset += sizeof(TTPOLYCURVE)+sizeof(POINTFX)*(polyCurve->cpfx-1); + } + } + polyOffset += polyHeader->cb; + } + doAdd=true; + } + delete [] buffer; + } +#else + if (FT_Load_Glyph (theFace, glyph_id, FT_LOAD_NO_SCALE | FT_LOAD_NO_HINTING | FT_LOAD_NO_BITMAP)) { + // shit happened + } else { + if ( FT_HAS_HORIZONTAL(theFace) ) { + n_g.h_advance=((double)theFace->glyph->metrics.horiAdvance)/((double)theFace->units_per_EM); + n_g.h_width=((double)theFace->glyph->metrics.width)/((double)theFace->units_per_EM); + } else { + n_g.h_width=n_g.h_advance=((double)(theFace->bbox.xMax-theFace->bbox.xMin))/((double)theFace->units_per_EM); + } + if ( FT_HAS_VERTICAL(theFace) ) { + n_g.v_advance=((double)theFace->glyph->metrics.vertAdvance)/((double)theFace->units_per_EM); + n_g.v_width=((double)theFace->glyph->metrics.height)/((double)theFace->units_per_EM); + } else { + // CSS3 Writing modes dictates that if vertical font metrics are missing we must + // synthisize them. No method is specified. The SVG 1.1 spec suggests using the em + // height (which is not theFace->height as that includes leading). The em height + // is ascender + descender (descender positive). Note: The "Requirements for + // Japanese Text Layout" W3C document says that Japanese kanji should be "set + // solid" which implies that vertical (and horizontal) advance should be 1em. + n_g.v_width=n_g.v_advance= 1.0; + } + if ( theFace->glyph->format == ft_glyph_format_outline ) { + FT_Outline_Funcs ft2_outline_funcs = { + ft2_move_to, + ft2_line_to, + ft2_conic_to, + ft2_cubic_to, + 0, 0 + }; + FT2GeomData user(path_builder, 1.0/((double)theFace->units_per_EM)); + FT_Outline_Decompose (&theFace->glyph->outline, &ft2_outline_funcs, &user); + } + doAdd=true; + } +#endif + path_builder.flush(); + + if ( doAdd ) { + Geom::PathVector pv = path_builder.peek(); + // close all paths + for (auto & i : pv) { + i.close(); + } + if ( !pv.empty() ) { + n_g.pathvector = new Geom::PathVector(pv); + Geom::OptRect bounds = bounds_exact(*n_g.pathvector); + if (bounds) { + n_g.bbox[0] = bounds->left(); + n_g.bbox[1] = bounds->top(); + n_g.bbox[2] = bounds->right(); + n_g.bbox[3] = bounds->bottom(); + } + } + glyphs[nbGlyph]=n_g; + id_to_no[glyph_id]=nbGlyph; + nbGlyph++; + } + } else { + } +} + +bool font_instance::FontMetrics(double &ascent,double &descent,double &xheight) +{ + if ( pFont == nullptr ) { + return false; + } + InitTheFace(); + if ( theFace == nullptr ) { + return false; + } + + ascent = _ascent; + descent = _descent; + xheight = _xheight; + + return true; +} + +bool font_instance::FontDecoration( double &underline_position, double &underline_thickness, + double &linethrough_position, double &linethrough_thickness) +{ + if ( pFont == nullptr ) { + return false; + } + InitTheFace(); + if ( theFace == nullptr ) { + return false; + } +#ifdef USE_PANGO_WIN32 + OUTLINETEXTMETRIC otm; + if ( !GetOutlineTextMetrics(parent->hScreenDC,sizeof(otm),&otm) ) { + return false; + } + double scale=1.0/parent->fontSize; + underline_position = fabs(otm.otmsUnderscorePosition *scale); + underline_thickness = fabs(otm.otmsUnderscoreSize *scale); + linethrough_position = fabs(otm.otmsStrikeoutPosition *scale); + linethrough_thickness = fabs(otm.otmsStrikeoutSize *scale); +#else + if ( theFace->units_per_EM == 0 ) { + return false; // bitmap font + } + underline_position = fabs(((double)theFace->underline_position )/((double)theFace->units_per_EM)); + underline_thickness = fabs(((double)theFace->underline_thickness)/((double)theFace->units_per_EM)); + // there is no specific linethrough information, mock it up from other font fields + linethrough_position = fabs(((double)theFace->ascender / 3.0 )/((double)theFace->units_per_EM)); + linethrough_thickness = fabs(((double)theFace->underline_thickness)/((double)theFace->units_per_EM)); +#endif + return true; +} + + +bool font_instance::FontSlope(double &run, double &rise) +{ + run = 0.0; + rise = 1.0; + + if ( pFont == nullptr ) { + return false; + } + InitTheFace(); + if ( theFace == nullptr ) { + return false; + } + +#ifdef USE_PANGO_WIN32 + OUTLINETEXTMETRIC otm; + if ( !GetOutlineTextMetrics(parent->hScreenDC,sizeof(otm),&otm) ) return false; + run=otm.otmsCharSlopeRun; + rise=otm.otmsCharSlopeRise; +#else + if ( !FT_IS_SCALABLE(theFace) ) { + return false; // bitmap font + } + + TT_HoriHeader *hhea = (TT_HoriHeader*)FT_Get_Sfnt_Table(theFace, ft_sfnt_hhea); + if (hhea == nullptr) { + return false; + } + run = hhea->caret_Slope_Run; + rise = hhea->caret_Slope_Rise; +#endif + return true; +} + +Geom::OptRect font_instance::BBox(int glyph_id) +{ + int no = -1; + if ( id_to_no.find(glyph_id) == id_to_no.end() ) { + LoadGlyph(glyph_id); + if ( id_to_no.find(glyph_id) == id_to_no.end() ) { + // didn't load + } else { + no = id_to_no[glyph_id]; + } + } else { + no = id_to_no[glyph_id]; + } + if ( no < 0 ) { + return Geom::OptRect(); + } else { + Geom::Point rmin(glyphs[no].bbox[0],glyphs[no].bbox[1]); + Geom::Point rmax(glyphs[no].bbox[2],glyphs[no].bbox[3]); + return Geom::Rect(rmin, rmax); + } +} + +Geom::PathVector* font_instance::PathVector(int glyph_id) +{ + int no = -1; + if ( id_to_no.find(glyph_id) == id_to_no.end() ) { + LoadGlyph(glyph_id); + if ( id_to_no.find(glyph_id) == id_to_no.end() ) { + // didn't load + } else { + no = id_to_no[glyph_id]; + } + } else { + no = id_to_no[glyph_id]; + } + if ( no < 0 ) return nullptr; + return glyphs[no].pathvector; +} + +Inkscape::Pixbuf* font_instance::PixBuf(int glyph_id) +{ + Inkscape::Pixbuf* pixbuf = nullptr; + + auto glyph_iter = openTypeSVGGlyphs.find(glyph_id); + if (glyph_iter != openTypeSVGGlyphs.end()) { + + // Glyphs are layed out in the +x, -y quadrant (assuming viewBox origin is 0,0). + // We need to shift the viewBox by the height inorder to generate pixbuf! + // To do: glyphs must draw overflow so we actually need larger pixbuf! + // To do: cache pixbuf. + // To do: Error handling. + + pixbuf = glyph_iter->second.pixbuf; + if (!pixbuf) { + Glib::ustring svg = glyph_iter->second.svg; + + // std::cout << svg << std::endl; + + // Create new viewbox which determines pixbuf size. + Glib::ustring viewbox("viewBox=\"0 "); + viewbox += std::to_string(-_design_units); + viewbox += " "; + viewbox += std::to_string(_design_units); + viewbox += " "; + viewbox += std::to_string(_design_units*2); + viewbox += "\""; + + // Search for existing viewbox + Glib::RefPtr regex = + Glib::Regex::create("viewBox=\"\\s*(\\d*\\.?\\d+)\\s*,?\\s*(\\d*\\.?\\d+)\\s*,?\\s*(\\d+\\.?\\d+)\\s*,?\\s*(\\d+\\.?\\d+)\\s*\""); + Glib::MatchInfo matchInfo; + regex->match(svg, matchInfo); + + if (matchInfo.matches()) { + // We have viewBox! We must transform so viewBox corresponds to design units. + + // Replace viewbox + svg = regex->replace_literal(svg, 0, viewbox, static_cast(0)); + + // Insert group with required transform to map glyph to new viewbox. + double x = std::stod(matchInfo.fetch(1).raw()); + double y = std::stod(matchInfo.fetch(2).raw()); + double w = std::stod(matchInfo.fetch(3).raw()); + double h = std::stod(matchInfo.fetch(4).raw()); + // std::cout << " x: " << x + // << " y: " << y + // << " w: " << w + // << " h: " << h << std::endl; + + if (w <= 0.0 or h <= 0.0) { + std::cerr << "font_instance::PixBuf: Invalid glyph width or height!" << std::endl; + } else { + + double xscale = _design_units/w; + double yscale = _design_units/h; + double xtrans = _design_units/w * x; + double ytrans = _design_units/h * y; + + if (xscale != 1.0 || yscale != 1.0) { + Glib::ustring group = ""; + + // Insert start group tag after initial + Glib::RefPtr regex = Glib::Regex::create("<\\s*svg.*?>"); + regex->match(svg, matchInfo); + if (matchInfo.matches()) { + int start = -1; + int end = -1; + matchInfo.fetch_pos(0, start, end); + svg.insert(end, group); + } else { + std::cerr << "font_instance::PixBuf: Could not find tag!" << std::endl; + } + + // Insert end group tag before final (To do: make sure it is final ) + regex = Glib::Regex::create("<\\s*\\/\\s*svg.*?>"); + regex->match(svg, matchInfo); + if (matchInfo.matches()) { + int start = -1; + int end = -1; + matchInfo.fetch_pos(0, start, end); + svg.insert(start, ""); + } else { + std::cerr << "font_instance::PixBuf: Could not find tag!" << std::endl; + } + } + } + + } else { + // No viewBox! We insert one. (To do: Look at 'width' and 'height' to see if we must scale.) + Glib::RefPtr regex = Glib::Regex::create("<\\s*svg"); + viewbox.insert(0, "replace_literal(svg, 0, viewbox, static_cast(0)); + } + + // std::cout << svg << std::endl; + + // Finally create pixbuf! + pixbuf = Inkscape::Pixbuf::create_from_buffer(svg.raw()); + + // And cache it. + glyph_iter->second.pixbuf = pixbuf; + } + } + + return pixbuf; +} + +double font_instance::Advance(int glyph_id, bool vertical) +{ + int no = -1; + if ( id_to_no.find(glyph_id) == id_to_no.end() ) { + LoadGlyph(glyph_id); + if ( id_to_no.find(glyph_id) == id_to_no.end() ) { + // didn't load + } else { + no=id_to_no[glyph_id]; + } + } else { + no = id_to_no[glyph_id]; + } + if ( no >= 0 ) { + if ( vertical ) { + return glyphs[no].v_advance; + } else { + return glyphs[no].h_advance; + } + } + return 0; +} + +// Internal function to find baselines +void font_instance::FindFontMetrics() { + + // CSS2 recommends using the OS/2 values sTypoAscender and sTypoDescender for the Typographic + // ascender and descender values: + // http://www.w3.org/TR/CSS2/visudet.html#sTypoAscender + // On Windows, the typographic ascender and descender are taken from the otmMacAscent and + // otmMacDescent values: + // http://microsoft.public.win32.programmer.gdi.narkive.com/LV6k4BDh/msdn-documentation-outlinetextmetrics-clarification + // The otmAscent and otmDescent values are the maximum ascent and maximum descent of all the + // glyphs in a font. + if ( theFace ) { + +#ifdef USE_PANGO_WIN32 + OUTLINETEXTMETRIC otm; + if ( GetOutlineTextMetrics(parent->hScreenDC,sizeof(otm),&otm) ) { + double scale=1.0/parent->fontSize; + _ascent = fabs(otm.otmMacAscent * scale); + _descent = fabs(otm.otmMacDescent * scale); + _xheight = fabs(otm.otmsXHeight * scale); + _ascent_max = fabs(otm.otmAscent * scale); + _descent_max = fabs(otm.otmDescent * scale); + _design_units = parent->fontSize; + + // In CSS em size is ascent + descent... which should be 1. If not, + // adjust so it is. + double em = _ascent + _descent; + if( em > 0 ) { + _ascent /= em; + _descent /= em; + } + + // May not be necessary but if OS/2 table missing or not version 2 or higher, + // xheight might be zero. + if( _xheight == 0.0 ) { + _xheight = 0.5; + } + + // Baselines defined relative to alphabetic. + _baselines[ SP_CSS_BASELINE_IDEOGRAPHIC ] = -_descent; // Recommendation + _baselines[ SP_CSS_BASELINE_HANGING ] = 0.8 * _ascent; // Guess + _baselines[ SP_CSS_BASELINE_MATHEMATICAL ] = 0.8 * _xheight; // Guess + _baselines[ SP_CSS_BASELINE_CENTRAL ] = 0.5 - _descent; // Definition + _baselines[ SP_CSS_BASELINE_MIDDLE ] = 0.5 * _xheight; // Definition + _baselines[ SP_CSS_BASELINE_TEXT_BEFORE_EDGE ] = _ascent; // Definition + _baselines[ SP_CSS_BASELINE_TEXT_AFTER_EDGE ] = -_descent; // Definition + + + MAT2 identity = {{0,1},{0,0},{0,0},{0,1}}; + GLYPHMETRICS metrics; + int retval; + + // Better math baseline: + // Try center of minus sign + retval = GetGlyphOutline (parent->hScreenDC, 0x2212, GGO_NATIVE | GGO_UNHINTED, &metrics, 0, NULL, &identity); + // If no minus sign, try hyphen + if( retval <= 0 ) + retval = GetGlyphOutline (parent->hScreenDC, '-', GGO_NATIVE | GGO_UNHINTED, &metrics, 0, NULL, &identity); + + if( retval > 0 ) { + double math = (metrics.gmptGlyphOrigin.y + 0.5 * metrics.gmBlackBoxY) * scale; + _baselines[ SP_CSS_BASELINE_MATHEMATICAL ] = math; + } + + // Find hanging baseline... assume it is at top of 'म'. + retval = GetGlyphOutline (parent->hScreenDC, 0x092E, GGO_NATIVE | GGO_UNHINTED, &metrics, 0, NULL, &identity); + if( retval > 0 ) { + double hanging = metrics.gmptGlyphOrigin.y * scale; + _baselines[ SP_CSS_BASELINE_MATHEMATICAL ] = hanging; + } + } + +#else + + if ( theFace->units_per_EM != 0 ) { // If zero then it's a bitmap font. + + TT_OS2* os2 = (TT_OS2*)FT_Get_Sfnt_Table( theFace, ft_sfnt_os2 ); + if( os2 ) { + _ascent = fabs(((double)os2->sTypoAscender) / ((double)theFace->units_per_EM)); + _descent = fabs(((double)os2->sTypoDescender)/ ((double)theFace->units_per_EM)); + } else { + _ascent = fabs(((double)theFace->ascender) / ((double)theFace->units_per_EM)); + _descent = fabs(((double)theFace->descender) / ((double)theFace->units_per_EM)); + } + _ascent_max = fabs(((double)theFace->ascender) / ((double)theFace->units_per_EM)); + _descent_max = fabs(((double)theFace->descender) / ((double)theFace->units_per_EM)); + _design_units = theFace->units_per_EM; + + // In CSS em size is ascent + descent... which should be 1. If not, + // adjust so it is. + double em = _ascent + _descent; + if( em > 0 ) { + _ascent /= em; + _descent /= em; + } + + // x-height + if( os2 && os2->version >= 0x0002 && os2->version != 0xffffu ) { + // Only os/2 version 2 and above have sxHeight, 0xffff marks "old Mac fonts" without table + _xheight = fabs(((double)os2->sxHeight) / ((double)theFace->units_per_EM)); + } else { + // Measure 'x' height in font. Recommended option by XSL standard if no sxHeight. + FT_UInt index = FT_Get_Char_Index( theFace, 'x' ); + if( index != 0 ) { + FT_Load_Glyph( theFace, index, FT_LOAD_NO_SCALE ); + _xheight = (fabs)(((double)theFace->glyph->metrics.height/(double)theFace->units_per_EM)); + } else { + // No 'x' in font! + _xheight = 0.5; + } + } + + // Baselines defined relative to alphabetic. + _baselines[ SP_CSS_BASELINE_IDEOGRAPHIC ] = -_descent; // Recommendation + _baselines[ SP_CSS_BASELINE_HANGING ] = 0.8 * _ascent; // Guess + _baselines[ SP_CSS_BASELINE_MATHEMATICAL ] = 0.8 * _xheight; // Guess + _baselines[ SP_CSS_BASELINE_CENTRAL ] = 0.5 - _descent; // Definition + _baselines[ SP_CSS_BASELINE_MIDDLE ] = 0.5 * _xheight; // Definition + _baselines[ SP_CSS_BASELINE_TEXT_BEFORE_EDGE ] = _ascent; // Definition + _baselines[ SP_CSS_BASELINE_TEXT_AFTER_EDGE ] = -_descent; // Definition + + // Better math baseline: + // Try center of minus sign + FT_UInt index = FT_Get_Char_Index( theFace, 0x2212 ); //'−' + // If no minus sign, try hyphen + if( index == 0 ) + index = FT_Get_Char_Index( theFace, '-' ); + + if( index != 0 ) { + FT_Load_Glyph( theFace, index, FT_LOAD_NO_SCALE ); + FT_Glyph aglyph; + FT_Get_Glyph( theFace->glyph, &aglyph ); + FT_BBox acbox; + FT_Glyph_Get_CBox( aglyph, FT_GLYPH_BBOX_UNSCALED, &acbox ); + double math = (acbox.yMin + acbox.yMax)/2.0/(double)theFace->units_per_EM; + _baselines[ SP_CSS_BASELINE_MATHEMATICAL ] = math; + // std::cout << "Math baseline: - bbox: y_min: " << acbox.yMin + // << " y_max: " << acbox.yMax + // << " math: " << math << std::endl; + FT_Done_Glyph(aglyph); + } + + // Find hanging baseline... assume it is at top of 'म'. + index = FT_Get_Char_Index( theFace, 0x092E ); // 'म' + if( index != 0 ) { + FT_Load_Glyph( theFace, index, FT_LOAD_NO_SCALE ); + FT_Glyph aglyph; + FT_Get_Glyph( theFace->glyph, &aglyph ); + FT_BBox acbox; + FT_Glyph_Get_CBox( aglyph, FT_GLYPH_BBOX_UNSCALED, &acbox ); + double hanging = (double)acbox.yMax/(double)theFace->units_per_EM; + _baselines[ SP_CSS_BASELINE_HANGING ] = hanging; + // std::cout << "Hanging baseline: प: " << hanging << std::endl; + FT_Done_Glyph(aglyph); + } + } +#endif + // const gchar *family = pango_font_description_get_family(descr); + // std::cout << "Font: " << (family?family:"null") << std::endl; + // std::cout << " ascent: " << _ascent << std::endl; + // std::cout << " descent: " << _descent << std::endl; + // std::cout << " x-height: " << _xheight << std::endl; + // std::cout << " max ascent: " << _ascent_max << std::endl; + // std::cout << " max descent: " << _descent_max << std::endl; + // std::cout << " Baselines:" << std::endl; + // std::cout << " alphabetic: " << _baselines[ SP_CSS_BASELINE_ALPHABETIC ] << std::endl; + // std::cout << " ideographic: " << _baselines[ SP_CSS_BASELINE_IDEOGRAPHIC ] << std::endl; + // std::cout << " hanging: " << _baselines[ SP_CSS_BASELINE_HANGING ] << std::endl; + // std::cout << " math: " << _baselines[ SP_CSS_BASELINE_MATHEMATICAL ] << std::endl; + // std::cout << " central: " << _baselines[ SP_CSS_BASELINE_CENTRAL ] << std::endl; + // std::cout << " middle: " << _baselines[ SP_CSS_BASELINE_MIDDLE ] << std::endl; + // std::cout << " text_before: " << _baselines[ SP_CSS_BASELINE_TEXT_BEFORE_EDGE ] << std::endl; + // std::cout << " text_after: " << _baselines[ SP_CSS_BASELINE_TEXT_AFTER_EDGE ] << std::endl; + } +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: + */ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/libnrtype/Layout-TNG-Compute.cpp b/src/libnrtype/Layout-TNG-Compute.cpp new file mode 100644 index 0000000..aa7992c --- /dev/null +++ b/src/libnrtype/Layout-TNG-Compute.cpp @@ -0,0 +1,2414 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::Text::Layout::Calculator - text layout engine meaty bits + * + * Authors: + * Richard Hughes + * + * Copyright (C) 2005 Richard Hughes + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include + +#include "Layout-TNG.h" +#include "style.h" +#include "font-instance.h" +#include "svg/svg-length.h" +#include "object/sp-object.h" +#include "Layout-TNG-Scanline-Maker.h" +#include +#include "livarot/Shape.h" + +namespace Inkscape { +namespace Text { + +//#define DEBUG_LAYOUT_TNG_COMPUTE +//#define DEBUG_GLYPH + +//#define IFTRACE(_code) _code +#define IFTRACE(_code) + +#define TRACE(_args) IFTRACE(g_print _args) + +/** \brief private to Layout. Does the real work of text flowing. + +This class does a standard greedy paragraph wrapping algorithm. + +Very high-level overview: + +
+foreach(paragraph) {
+  call pango_itemize() (_buildPangoItemizationForPara())
+  break into spans, without dealing with wrapping (_buildSpansForPara())
+  foreach(line in flow shape) {
+    foreach(chunk in flow shape) {   (in _buildChunksInScanRun())
+      // this inner loop in _measureUnbrokenSpan()
+      if the line height changed discard the line and start again
+      keep adding characters until we run out of space in the chunk, then back up to the last word boundary
+      (do sensible things if there is no previous word break)
+    }
+    push all the glyphs, chars, spans, chunks and line to output (not completely trivial because we must draw rtl in character order) (in _outputLine())
+  }
+  push the paragraph (in calculate())
+}
+
+ +...and all of that needs to work vertically too, and with all the little details that make life annoying +*/ +class Layout::Calculator +{ + class SpanPosition; + friend class SpanPosition; + Layout &_flow; + ScanlineMaker *_scanline_maker; + unsigned _current_shape_index; /// index into Layout::_input_wrap_shapes + PangoContext *_pango_context; + Direction _block_progression; + + /** + * For y= attributes in tspan elements et al, we do the adjustment by moving each + * glyph individually by this number. The spec means that this is maintained across + * paragraphs. + * + * To do non-flow text layout, only the first "y" attribute is normally used. If there is only one + * "y" attribute in a other than the first , it is ignored. This allows Inkscape to + * insert a new line anywhere. On output, the Inkscape determined "y" is written out so other SVG + * viewers know where to place the . + */ + double _y_offset; + + /** to stop pango from hinting its output, the font factory creates all fonts very large. + All numbers returned from pango have to be divided by this number \em and divided by + PANGO_SCALE. See font_factory::font_factory(). */ + double _font_factory_size_multiplier; + + /** Temporary storage associated with each item in Layout::_input_stream. */ + struct InputItemInfo { + bool in_sub_flow; + Layout *sub_flow; // this is only set for the first input item in a sub-flow + + InputItemInfo() : in_sub_flow(false), sub_flow(nullptr) {} + + /* fixme: I don't like the fact that InputItemInfo etc. use the default copy constructor and + * operator= (and thus don't involve incrementing reference counts), yet they provide a free method + * that does delete or Unref. + * + * I suggest using the garbage collector to manage deletion. + */ + void free() + { + if (sub_flow) { + delete sub_flow; + sub_flow = nullptr; + } + } + }; + + /** Temporary storage associated with each item returned by the call to + pango_itemize(). */ + struct PangoItemInfo { + PangoItem *item; + font_instance *font; + + PangoItemInfo() : item(nullptr), font(nullptr) {} + + /* fixme: I don't like the fact that InputItemInfo etc. use the default copy constructor and + * operator= (and thus don't involve incrementing reference counts), yet they provide a free method + * that does delete or Unref. + * + * I suggest using the garbage collector to manage deletion. + */ + void free() + { + if (item) { + pango_item_free(item); + item = nullptr; + } + if (font) { + font->Unref(); + font = nullptr; + } + } + }; + + + /** These spans have approximately the same definition as that used for + * Layout::Span (constant font, direction, etc), except that they are from + * before we have located the line breaks, so bear no relation to chunks. + * They are guaranteed to be in at most one PangoItem (spans with no text in + * them will not have an associated PangoItem), exactly one input object and + * will only have one change of x, y, dx, dy or rotate attribute, which will + * be at the beginning. An UnbrokenSpan can cross a chunk boundary, c.f. + * BrokenSpan. + */ + struct UnbrokenSpan { + PangoGlyphString *glyph_string; + int pango_item_index; /// index into _para.pango_items, or -1 if this is style only + unsigned input_index; /// index into Layout::_input_stream + Glib::ustring::const_iterator input_stream_first_character; + double font_size; + FontMetrics line_height; /// This is not the CSS line-height attribute! + double line_height_multiplier; /// calculated from the font-height css property + double baseline_shift; /// calculated from the baseline-shift css property + SPCSSTextOrientation text_orientation; + unsigned text_bytes; + unsigned char_index_in_para; /// the index of the first character in this span in the paragraph, for looking up char_attributes + SVGLength x, y, dx, dy, rotate; // these are reoriented copies of the attributes. We change span when we encounter one. + + UnbrokenSpan() : glyph_string(nullptr) {} + void free() + { + if (glyph_string) + pango_glyph_string_free(glyph_string); + glyph_string = nullptr; + } + }; + + + /** Used to provide storage for anything that applies to the current + paragraph only. Since we're only processing one paragraph at a time, + there's only one instantiation of this struct, on the stack of + calculate(). */ + struct ParagraphInfo { + Glib::ustring text; + unsigned first_input_index; ///< Index into Layout::_input_stream. + Direction direction; + Alignment alignment; + std::vector input_items; + std::vector pango_items; + std::vector char_attributes; ///< For every character in the paragraph. + std::vector unbroken_spans; + + template static void free_sequence(T &seq) + { + for (typename T::iterator it(seq.begin()); it != seq.end(); ++it) { + it->free(); + } + seq.clear(); + } + + void free() + { + text = ""; + free_sequence(input_items); + free_sequence(pango_items); + free_sequence(unbroken_spans); + } + }; + + + /** + * A useful little iterator for moving char-by-char across spans. + */ + struct UnbrokenSpanPosition { + std::vector::iterator iter_span; + unsigned char_byte; + unsigned char_index; + + void increment(); ///< Step forward by one character. + + inline bool operator== (UnbrokenSpanPosition const &other) const + {return char_byte == other.char_byte && iter_span == other.iter_span;} + inline bool operator!= (UnbrokenSpanPosition const &other) const + {return char_byte != other.char_byte || iter_span != other.iter_span;} + }; + + /** + * The line breaking algorithm will convert each UnbrokenSpan into one + * or more of these. A BrokenSpan will never cross a chunk boundary, + * c.f. UnbrokenSpan. + */ + struct BrokenSpan { + UnbrokenSpanPosition start; + UnbrokenSpanPosition end; // the end of this will always be the same as the start of the next + unsigned start_glyph_index; + unsigned end_glyph_index; + double width; + unsigned whitespace_count; + bool ends_with_whitespace; + double each_whitespace_width; + double letter_spacing; // Save so we can subtract from width at end of line (for center justification) + double word_spacing; + void setZero(); + }; + + /** The definition of a chunk used here is the same as that used in Layout: + A collection of contiguous broken spans on the same line. (One chunk per line + unless shape splits line into several sections... then one chunk per section. */ + struct ChunkInfo { + std::vector broken_spans; + double scanrun_width; + double text_width; ///< Total width used by the text (excluding justification). + double x; + int whitespace_count; + }; + + void _buildPangoItemizationForPara(ParagraphInfo *para) const; + static double _computeFontLineHeight( SPStyle const *style ); // Returns line_height_multiplier + unsigned _buildSpansForPara(ParagraphInfo *para) const; + bool _goToNextWrapShape(); + void _createFirstScanlineMaker(); + + bool _findChunksForLine(ParagraphInfo const ¶, + UnbrokenSpanPosition *start_span_pos, + std::vector *chunk_info, + FontMetrics *line_box_height, + FontMetrics const *strut_height); + + bool _buildChunksInScanRun(ParagraphInfo const ¶, + UnbrokenSpanPosition const &start_span_pos, + ScanlineMaker::ScanRun const &scan_run, + std::vector *chunk_info, + FontMetrics *line_height) const; + + bool _measureUnbrokenSpan(ParagraphInfo const ¶, + BrokenSpan *span, + BrokenSpan *last_break_span, + BrokenSpan *last_emergency_break_span, + double maximum_width) const; + + double _getChunkLeftWithAlignment(ParagraphInfo const ¶, + std::vector::const_iterator it_chunk, + double *add_to_each_whitespace) const; + + void _outputLine(ParagraphInfo const ¶, + FontMetrics const &line_height, + std::vector const &chunk_info, + bool hidden); + + static inline PangoLogAttr const &_charAttributes(ParagraphInfo const ¶, + UnbrokenSpanPosition const &span_pos) + { + return para.char_attributes[span_pos.iter_span->char_index_in_para + span_pos.char_index]; + } + +#ifdef DEBUG_LAYOUT_TNG_COMPUTE + static void dumpPangoItemsOut(ParagraphInfo *para); + static void dumpUnbrokenSpans(ParagraphInfo *para); +#endif //DEBUG_LAYOUT_TNG_COMPUTE + +public: + Calculator(Layout *text_flow) + : _flow(*text_flow) {} + + bool calculate(); +}; + + +/** + * Computes the width of a single UnbrokenSpan (pointed to by span->start.iter_span) + * and outputs its vital statistics into the other fields of \a span. + * Measuring will stop if maximum_width is reached and in that case the + * function will return false. In other cases where a line break must be + * done immediately the function will also return false. On return + * \a last_break_span will contain the vital statistics for the span only + * up to the last line breaking change. If there are no line breaking + * characters in the span then \a last_break_span will not be altered. + * Similarly, \a last_emergency_break_span will contain the vital + * statistics for the span up to the last inter-character boundary, + * or will be unaltered if there is none. + * + * An unbroken span corresponds to at most one PangoItem + */ +bool Layout::Calculator::_measureUnbrokenSpan(ParagraphInfo const ¶, + BrokenSpan *span, + BrokenSpan *last_break_span, + BrokenSpan *last_emergency_break_span, + double maximum_width) const +{ + TRACE((" start _measureUnbrokenSpan %g\n", maximum_width)); + span->setZero(); + + if (span->start.iter_span->dx._set && span->start.char_byte == 0){ + if(para.direction == RIGHT_TO_LEFT){ + span->width -= span->start.iter_span->dx.computed; + } else { + span->width += span->start.iter_span->dx.computed; + } + } + + if (span->start.iter_span->pango_item_index == -1) { + // if this is a style-only span there's no text in it + // so we don't need to do very much at all + span->end.iter_span++; + return true; + } + + if (_flow._input_stream[span->start.iter_span->input_index]->Type() == CONTROL_CODE) { + + InputStreamControlCode const *control_code = static_cast(_flow._input_stream[span->start.iter_span->input_index]); + + if (control_code->code == SHAPE_BREAK || control_code->code == PARAGRAPH_BREAK) { + *last_emergency_break_span = *last_break_span = *span; + return false; + } + + if (control_code->code == ARBITRARY_GAP) { // Not used! + if (span->width + control_code->width > maximum_width) + return false; + TRACE((" fitted control code, width = %f\n", control_code->width)); + span->width += control_code->width; + span->end.increment(); + } + return true; + } + + if (_flow._input_stream[span->start.iter_span->input_index]->Type() != TEXT_SOURCE) + return true; // never happens + + InputStreamTextSource const *text_source = static_cast(_flow._input_stream[span->start.iter_span->input_index]); + + if (_directions_are_orthogonal(_block_progression, text_source->styleGetBlockProgression())) { + // TODO: block-progression altered in the middle + // Measure the precomputed flow from para.input_items + span->end.iter_span++; // for now, skip to the next span + return true; + } + + // a normal span going with a normal block-progression + double font_size_multiplier = span->start.iter_span->font_size / (PANGO_SCALE * _font_factory_size_multiplier); + double soft_hyphen_glyph_width = 0.0; + bool soft_hyphen_in_word = false; + bool is_soft_hyphen = false; + IFTRACE(int char_count = 0); + + // if we're not at the start of the span we need to pre-init glyph_index + span->start_glyph_index = 0; + while (span->start_glyph_index < (unsigned)span->start.iter_span->glyph_string->num_glyphs + && span->start.iter_span->glyph_string->log_clusters[span->start_glyph_index] < (int)span->start.char_byte) + span->start_glyph_index++; + span->end_glyph_index = span->start_glyph_index; + + // go char-by-char summing the width, while keeping track of the previous break point + do { + PangoLogAttr const &char_attributes = _charAttributes(para, span->end); + + // guint32 c = *Glib::ustring::const_iterator(span->end.iter_span->input_stream_first_character.base() + span->end.char_byte); + // std::cout << " char_byte: " << span->end.char_byte + // << " char_index: " << span->end.char_index + // << " c: " << c << " " << char(c==10 ? '␤' : c) + // << " line: " << std::boolalpha << char_attributes.is_line_break + // << " mandatory: " << std::boolalpha << char_attributes.is_mandatory_break // Note, break is before character! + // << " char: " << std::boolalpha << char_attributes.is_char_break + // << std::endl; + + if (char_attributes.is_mandatory_break && span->end != span->start) { + TRACE((" is_mandatory_break ************\n")); + *last_emergency_break_span = *last_break_span = *span; + TRACE((" span %ld end of para; width = %f chars = %d\n", span->start.iter_span - para.unbroken_spans.begin(), span->width, char_count)); + return false; + } + + if (char_attributes.is_line_break) { + TRACE((" is_line_break ************\n")); + // a suitable position to break at, record where we are + *last_emergency_break_span = *last_break_span = *span; + if (soft_hyphen_in_word) { + // if there was a previous soft hyphen we're not going to need it any more so we can remove it + span->width -= soft_hyphen_glyph_width; + if (!is_soft_hyphen) + soft_hyphen_in_word = false; + } + } else if (char_attributes.is_char_break) { + *last_emergency_break_span = *span; + } + // todo: break between chars if necessary (ie no word breaks present) when doing rectangular flowing + + // sum the glyph widths, letter spacing, word spacing, and textLength adjustment to get the character width + double char_width = 0.0; + while (span->end_glyph_index < (unsigned)span->end.iter_span->glyph_string->num_glyphs + && span->end.iter_span->glyph_string->log_clusters[span->end_glyph_index] <= (int)span->end.char_byte) { + + PangoGlyphInfo *info = &(span->end.iter_span->glyph_string->glyphs[span->end_glyph_index]); + double glyph_width = font_size_multiplier * info->geometry.width; + + // Advance does not include kerning but Pango gives wrong advances for vertical text + // with upright orientation (pre 1.44.0). + font_instance *font = para.pango_items[span->end.iter_span->pango_item_index].font; + double font_size = span->start.iter_span->font_size; + //double glyph_h_advance = font_size * font->Advance(info->glyph, false); + double glyph_v_advance = font_size * font->Advance(info->glyph, true ); + + if (_block_progression == LEFT_TO_RIGHT || _block_progression == RIGHT_TO_LEFT) { + // Vertical text + + if( text_source->style->text_orientation.computed == SP_CSS_TEXT_ORIENTATION_SIDEWAYS || + (text_source->style->text_orientation.computed == SP_CSS_TEXT_ORIENTATION_MIXED && + para.pango_items[span->end.iter_span->pango_item_index].item->analysis.gravity == PANGO_GRAVITY_SOUTH) ) { + // Sideways orientation + char_width += glyph_width; + } else { + // Upright orientation + guint32 c = *Glib::ustring::const_iterator(span->end.iter_span->input_stream_first_character.base() + span->end.char_byte); + if (g_unichar_type (c) != G_UNICODE_NON_SPACING_MARK) { + // Non-spacing marks should not contribute to width. Fonts may not report the correct advance, especially if the 'vmtx' table is missing. + if (pango_version_check(1,44,0) != nullptr) { + // Pango >= 1.44.0 + char_width += glyph_width; + } else { + // Pango < 1.44.0 glyph_width returned is horizontal width, not vertical. + char_width += glyph_v_advance; + } + } + } + } else { + // Horizontal text + char_width += glyph_width; + } + span->end_glyph_index++; + } + + if (char_attributes.is_cursor_position) + char_width += text_source->style->letter_spacing.computed * _flow.getTextLengthMultiplierDue(); + if (char_attributes.is_white) + char_width += text_source->style->word_spacing.computed * _flow.getTextLengthMultiplierDue(); + char_width += _flow.getTextLengthIncrementDue(); + span->width += char_width; + IFTRACE(char_count++); + + if (char_attributes.is_white) { + span->whitespace_count++; + span->each_whitespace_width = char_width; + } + span->ends_with_whitespace = char_attributes.is_white; + + is_soft_hyphen = (UNICODE_SOFT_HYPHEN == *Glib::ustring::const_iterator(span->end.iter_span->input_stream_first_character.base() + span->end.char_byte)); + if (is_soft_hyphen) + soft_hyphen_glyph_width = char_width; + + // Go to next character (resets end.char_byte to zero if at end) + span->end.increment(); + + // Width should not include letter_spacing (or word_spacing) after last letter at end of line. + // word_spacing is attached to white space that is already removed from line end (?) + double test_width = span->width - text_source->style->letter_spacing.computed; + + // Save letter_spacing and word_spacing for subtraction later if span is last span in line. + span->letter_spacing = text_source->style->letter_spacing.computed; + span->word_spacing = text_source->style->word_spacing.computed; + + if (test_width > maximum_width && !char_attributes.is_white) { // whitespaces don't matter, we can put as many as we want at eol + TRACE((" span %ld exceeded scanrun; width = %f chars = %d\n", span->start.iter_span - para.unbroken_spans.begin(), span->width, char_count)); + return false; + } + + } while (span->end.char_byte != 0); // while we haven't wrapped to the next span + + TRACE((" fitted span %ld width = %f chars = %d\n", span->start.iter_span - para.unbroken_spans.begin(), span->width, char_count)); + TRACE((" end _measureUnbrokenSpan %g\n", maximum_width)); + return true; +} + +/* *********************************************************************************************************/ +// Per-line functions (output) + +/** Uses the paragraph alignment and the chunk information to work out + * where the actual left of the final chunk must be. Also sets + * \a add_to_each_whitespace to be the amount of x to add at each + * whitespace character to make full justification work. + */ +double Layout::Calculator::_getChunkLeftWithAlignment(ParagraphInfo const ¶, + std::vector::const_iterator it_chunk, + double *add_to_each_whitespace) const +{ + *add_to_each_whitespace = 0.0; + if (_flow._input_wrap_shapes.empty()) { + switch (para.alignment) { + case FULL: + case LEFT: + default: + return it_chunk->x; + case RIGHT: + return it_chunk->x - it_chunk->text_width; + case CENTER: + return it_chunk->x - it_chunk->text_width/ 2; + } + } + + switch (para.alignment) { + case FULL: + // Don't justify the last line chunk in the span + if (!it_chunk->broken_spans.empty() && it_chunk->broken_spans.back().end.iter_span != para.unbroken_spans.end()) { + + // Don't justify a single word or a line that ends with a manual line break. + PangoLogAttr const &char_attributes = _charAttributes(para, it_chunk->broken_spans.back().end); + if (it_chunk->whitespace_count && !char_attributes.is_mandatory_break) { + + // Set the amount of extra space between each word to a fraction + // of the remaining line space to justify this line. + *add_to_each_whitespace = (it_chunk->scanrun_width - it_chunk->text_width) / it_chunk->whitespace_count; + } + } + return it_chunk->x; + case LEFT: + default: + return it_chunk->x; + case RIGHT: + return it_chunk->x + it_chunk->scanrun_width - it_chunk->text_width; + case CENTER: + return it_chunk->x + (it_chunk->scanrun_width - it_chunk->text_width) / 2; + } +} + +/** + * Once we've got here we have finished making changes to the line + * and are ready to output the final result to #_flow. + * This method takes its input parameters and does that. + */ +void Layout::Calculator::_outputLine(ParagraphInfo const ¶, + FontMetrics const &line_height, + std::vector const &chunk_info, + bool hidden) +{ + TRACE((" Start _outputLine: ascent %f, descent %f, top of box %f\n", line_height.ascent, line_height.descent, _scanline_maker->yCoordinate() )); + if (chunk_info.empty()) { + TRACE((" line too short to fit anything on it, go to next\n")); + return; + } + + // we've finished fiddling about with ascents and descents: create the output + TRACE((" found line fit; creating output\n")); + Layout::Line new_line; + new_line.in_paragraph = _flow._paragraphs.size() - 1; + new_line.baseline_y = _scanline_maker->yCoordinate(); + new_line.hidden = hidden; + + // The y coordinate is at the beginning edge of the line box (top for horizontal text, left + // edge for vertical lr text, right edge for vertical rl text. We align, by default to the + // alphabetic baseline for horizontal text and the central baseline for vertical text. + if( _block_progression == RIGHT_TO_LEFT ) { + // Vertical text, use em box center as baseline + new_line.baseline_y -= 0.5 * line_height.emSize(); + } else if ( _block_progression == LEFT_TO_RIGHT ) { + // Vertical text, use em box center as baseline + new_line.baseline_y += 0.5 * line_height.emSize(); + } else { + new_line.baseline_y += line_height.getTypoAscent(); + } + + + TRACE((" initial new_line.baseline_y: %f\n", new_line.baseline_y )); + + new_line.in_shape = _current_shape_index; + _flow._lines.push_back(new_line); + + for (std::vector::const_iterator it_chunk = chunk_info.begin() ; it_chunk != chunk_info.end() ; it_chunk++) { + double add_to_each_whitespace; + // add the chunk to the list + Layout::Chunk new_chunk; + new_chunk.in_line = _flow._lines.size() - 1; + + TRACE((" New chunk: in_line: %d\n", new_chunk.in_line)); + if (hidden) { + new_chunk.left_x = it_chunk->x; // Don't align. We'll place below last shape. + } else { + new_chunk.left_x = _getChunkLeftWithAlignment(para, it_chunk, &add_to_each_whitespace); + } + + // we may also have y move orders to deal with here (dx, dy and rotate are done per span) + + // Comment updated: 23 July 2010: + // We must handle two cases: + // + // 1. Inkscape SVG where the first line is placed by the read-in "y" value and the + // rest are determined by 'font-size' and 'line-height' (and not by any + // y-kerning). s in this case are marked by sodipodi:role="line". This + // allows new lines to be inserted in the middle of a object. On output, + // new "y" values are calculated for each that represents a new line. Line + // spacing is already handled by the calling routine. + // + // 2. Plain SVG where each or is placed by its own "x" and "y" values. + // Note that in this case Inkscape treats each object with any included + // s as a single line of text. This can be confusing in the code below. + + if (!it_chunk->broken_spans.empty() // Not empty paragraph + && it_chunk->broken_spans.front().start.char_byte == 0 ) { // Beginning of unbroken span + + // If empty or new line (sodipode:role="line") + if( _flow._characters.empty() || + _flow._characters.back().chunk(&_flow).in_line != _flow._lines.size() - 1) { + + // This is the Inkscape SVG case. + // + // If "y" attribute is set, use it (initial "y" attributes in + // other than the first have already been stripped for + // marked with role="line", see sp-text.cpp: SPText::_buildLayoutInput). + // NOTE: for vertical text, "y" is the user-space "x" value. + if( it_chunk->broken_spans.front().start.iter_span->y._set ) { + + // Use set "y" attribute for baseline + new_line.baseline_y = it_chunk->broken_spans.front().start.iter_span->y.computed; + + TRACE((" chunk new_line.baseline_y: %f\n", new_line.baseline_y )); + + // Save baseline + _flow._lines.back().baseline_y = new_line.baseline_y; + + // Calculate new top of box... given specified baseline. + double top_of_line_box = new_line.baseline_y; + if( _block_progression == RIGHT_TO_LEFT ) { + // Vertical text, use em box center as baseline + top_of_line_box += 0.5 * line_height.emSize(); + } else if (_block_progression == LEFT_TO_RIGHT ) { + // Vertical text, use em box center as baseline + top_of_line_box -= 0.5 * line_height.emSize(); + } else { + top_of_line_box -= line_height.getTypoAscent(); + } + TRACE((" y attribute set, next line top_of_line_box: %f\n", top_of_line_box )); + // Set the initial y coordinate of the for this line (see above). + _scanline_maker->setNewYCoordinate(top_of_line_box); + } + + // Reset relative y_offset ("dy" attribute is relative but should be reset at + // the beginning of each line since each line will have a new "y" written out.) + _y_offset = 0.0; + + } else { + + // This is the plain SVG case + // + // "x" and "y" are used to place text, simulating lines as necessary + if( it_chunk->broken_spans.front().start.iter_span->y._set ) { + _y_offset = it_chunk->broken_spans.front().start.iter_span->y.computed - new_line.baseline_y; + } + } + } + _flow._chunks.push_back(new_chunk); + + double current_x; + double direction_sign; + Direction previous_direction = para.direction; + double counter_directional_width_remaining = 0.0; + float glyph_rotate = 0.0; + if (para.direction == LEFT_TO_RIGHT) { + direction_sign = +1.0; + current_x = 0.0; + } else { + direction_sign = -1.0; + if (para.alignment == FULL && !_flow._input_wrap_shapes.empty()){ + current_x = it_chunk->scanrun_width; + } + else { + current_x = it_chunk->text_width; + } + } + + // Loop over broken spans; a broken span is part of no more than one PangoItem. + for (std::vector::const_iterator it_span = it_chunk->broken_spans.begin() ; it_span != it_chunk->broken_spans.end() ; it_span++) { + + // begin adding spans to the list + UnbrokenSpan const &unbroken_span = *it_span->start.iter_span; + double x_in_span_last = 0.0; // set at the END when a new cluster starts + double x_in_span = 0.0; // set from the preceding at the START when a new cluster starts. + + // for (int i = 0; i < unbroken_span.glyph_string->num_glyphs; ++i) { + // std::cout << "Unbroken span: " << unbroken_span.glyph_string->glyphs[i].glyph << std::endl; + // } + + if (it_span->start.char_byte == 0) { + // Start of an unbroken span, we might have dx, dy or rotate still to process + // (x and y are done per chunk) + if (unbroken_span.dx._set) current_x += unbroken_span.dx.computed; + if (unbroken_span.dy._set) _y_offset += unbroken_span.dy.computed; + if (unbroken_span.rotate._set) glyph_rotate = unbroken_span.rotate.computed * (M_PI/180); + } + + if (_flow._input_stream[unbroken_span.input_index]->Type() == TEXT_SOURCE + && unbroken_span.pango_item_index == -1) { + // style only, nothing to output + continue; + } + + Layout::Span new_span; + + new_span.in_chunk = _flow._chunks.size() - 1; + new_span.line_height = unbroken_span.line_height; + new_span.in_input_stream_item = unbroken_span.input_index; + new_span.baseline_shift = 0.0; + new_span.block_progression = _block_progression; + new_span.text_orientation = unbroken_span.text_orientation; + if ((_flow._input_stream[unbroken_span.input_index]->Type() == TEXT_SOURCE) && (new_span.font = para.pango_items[unbroken_span.pango_item_index].font)) + { + new_span.font->Ref(); + new_span.font_size = unbroken_span.font_size; + new_span.direction = para.pango_items[unbroken_span.pango_item_index].item->analysis.level & 1 ? RIGHT_TO_LEFT : LEFT_TO_RIGHT; + new_span.input_stream_first_character = Glib::ustring::const_iterator(unbroken_span.input_stream_first_character.base() + it_span->start.char_byte); + } else { // a control code + new_span.font = nullptr; + new_span.font_size = new_span.line_height.emSize(); + new_span.direction = para.direction; + } + + if (new_span.direction == para.direction) { + current_x -= counter_directional_width_remaining; + counter_directional_width_remaining = 0.0; + } else if (new_span.direction != previous_direction) { + // measure width of spans we need to switch round + counter_directional_width_remaining = 0.0; + std::vector::const_iterator it_following_span; + for (it_following_span = it_span ; it_following_span != it_chunk->broken_spans.end() ; it_following_span++) { + if (it_following_span->start.iter_span->pango_item_index == -1) break; + Layout::Direction following_span_progression = static_cast(_flow._input_stream[it_following_span->start.iter_span->input_index])->styleGetBlockProgression(); + if (!Layout::_directions_are_orthogonal(following_span_progression, _block_progression)) { + if (new_span.direction != (para.pango_items[it_following_span->start.iter_span->pango_item_index].item->analysis.level & 1 ? RIGHT_TO_LEFT : LEFT_TO_RIGHT)) break; + } + counter_directional_width_remaining += direction_sign * (it_following_span->width + it_following_span->whitespace_count * add_to_each_whitespace); + } + current_x += counter_directional_width_remaining; + counter_directional_width_remaining = 0.0; // we want to go increasingly negative + } + new_span.x_start = current_x; + new_span.y_offset = _y_offset; // Offset from baseline due to 'y' and 'dy' attributes (used to simulate multiline text). + + if (_flow._input_stream[unbroken_span.input_index]->Type() == TEXT_SOURCE) { + // the span is set up, push the glyphs and chars + + InputStreamTextSource const *text_source = static_cast(_flow._input_stream[unbroken_span.input_index]); + Glib::ustring::const_iterator iter_source_text = Glib::ustring::const_iterator(unbroken_span.input_stream_first_character.base() + it_span->start.char_byte) ; + unsigned char_index_in_unbroken_span = it_span->start.char_index; + double font_size_multiplier = new_span.font_size / (PANGO_SCALE * _font_factory_size_multiplier); + int log_cluster_size_glyphs = 0; // Number of glyphs in this log_cluster + int log_cluster_size_chars = 0; // Number of characters in this log_cluster + unsigned end_byte = 0; + + // Get some pointers (constant for an unbroken span). + font_instance *font = para.pango_items[unbroken_span.pango_item_index].font; + PangoItem *pango_item = para.pango_items[unbroken_span.pango_item_index].item; + + // Loop over glyphs in span + double x_offset_cluster = 0.0; // Handle wrong glyph positioning post-1.44 Pango. + double x_offset_center = 0.0; // Handle wrong glyph positioning in pre-1.44 Pango. + double x_offset_advance = 0.0; // Handle wrong advance in pre-1.44 Pango. + +#ifdef DEBUG_GLYPH + std::cerr << "\nGlyphs in span: x_start: " << new_span.x_start << " y_offset: " << new_span.y_offset + << " PangoItem flags: " << (int)pango_item->analysis.flags << " Gravity: " << (int)pango_item->analysis.gravity << std::endl; + std::cerr << " Unicode Glyph h_advance v_advance width cluster orientation new_glyph delta" << std::endl; + std::cerr << " (hex) No. start x y x y" << std::endl; + std::cerr << " -------------------------------------------------------------------------------------------------" << std::endl; +#endif + + for (unsigned glyph_index = it_span->start_glyph_index ; glyph_index < it_span->end_glyph_index ; glyph_index++) { + + unsigned char_byte = iter_source_text.base() - unbroken_span.input_stream_first_character.base(); + bool newcluster = false; + if (unbroken_span.glyph_string->glyphs[glyph_index].attr.is_cluster_start) { + newcluster = true; + x_in_span = x_in_span_last; + } + + if (unbroken_span.glyph_string->log_clusters[glyph_index] < (int)unbroken_span.text_bytes + && *iter_source_text == UNICODE_SOFT_HYPHEN + && glyph_index + 1 != it_span->end_glyph_index) { + // if we're looking at a soft hyphen and it's not the last glyph in the + // chunk we don't draw the glyph but we still need to add to _characters + Layout::Character new_character; + new_character.the_char = *iter_source_text; + new_character.in_span = _flow._spans.size(); // the span hasn't been added yet, so no -1 + new_character.char_attributes = para.char_attributes[unbroken_span.char_index_in_para + char_index_in_unbroken_span]; + new_character.in_glyph = -1; + _flow._characters.push_back(new_character); + iter_source_text++; + char_index_in_unbroken_span++; + while (glyph_index < (unsigned)unbroken_span.glyph_string->num_glyphs + && unbroken_span.glyph_string->log_clusters[glyph_index] == (int)char_byte) + glyph_index++; + glyph_index--; + continue; + } + + // create the Layout::Glyph + PangoGlyphInfo *unbroken_span_glyph_info = &unbroken_span.glyph_string->glyphs[glyph_index]; + double glyph_width = font_size_multiplier * unbroken_span_glyph_info->geometry.width; + + Layout::Glyph new_glyph; + new_glyph.glyph = unbroken_span_glyph_info->glyph; + new_glyph.in_character = _flow._characters.size(); + new_glyph.rotation = glyph_rotate; + new_glyph.orientation = ORIENTATION_UPRIGHT; // Only effects vertical text + new_glyph.hidden = hidden; // SVG 2 overflow + + // Advance does not include kerning but Pango <= 1.43 gives wrong advances for verical upright text. + double glyph_h_advance = new_span.font_size * font->Advance(new_glyph.glyph, false); + double glyph_v_advance = new_span.font_size * font->Advance(new_glyph.glyph, true ); + +#ifdef DEBUG_GLYPH + + bool is_cluster_start = unbroken_span_glyph_info->attr.is_cluster_start; + std::cerr << " " << std::hex << std::setw(6) << *iter_source_text << std::dec + << " " << std::setw(6) << new_glyph.glyph + << std::fixed << std::showpoint << std::setprecision(2) + << " " << std::setw(6) << glyph_h_advance + << " " << std::setw(6) << glyph_v_advance + << " " << std::setw(6) << glyph_width + << " " << std::setw(6) << std::boolalpha << is_cluster_start; // << std::endl; +#endif + + // We may have scaled font size to fit textLength; now, if + // @lengthAdjust=spacingAndGlyphs, this scaling must be only horizontal, + // not vertical, so we unscale it back vertically during output + if (_flow.lengthAdjust == Inkscape::Text::Layout::LENGTHADJUST_SPACINGANDGLYPHS) + new_glyph.vertical_scale = 1.0 / _flow.getTextLengthMultiplierDue(); + else + new_glyph.vertical_scale = 1.0; + + // Position glyph -------------------- + new_glyph.x = current_x; + new_glyph.y =_y_offset; + new_glyph.advance = glyph_width; + + if (*iter_source_text == '\n') { + // Line feeds should take zero space but they are given 'space' width. + new_glyph.advance = 0.0; + } + + // y-coordinate is flipped between vertical and horizontal text... + // delta_y is common offset but applied with opposite sign + double delta_x = unbroken_span_glyph_info->geometry.x_offset * font_size_multiplier; + double delta_y = unbroken_span_glyph_info->geometry.y_offset * font_size_multiplier - unbroken_span.baseline_shift; + SPCSSBaseline dominant_baseline = _flow._blockBaseline(); + + if (_block_progression == LEFT_TO_RIGHT || _block_progression == RIGHT_TO_LEFT) { + // Vertical text + + // Default dominant baseline is determined by overall block (i.e. ) 'text-orientation' value. + if( _flow._blockTextOrientation() != SP_CSS_TEXT_ORIENTATION_SIDEWAYS ) { + if( dominant_baseline == SP_CSS_BASELINE_AUTO ) dominant_baseline = SP_CSS_BASELINE_CENTRAL; + } else { + if( dominant_baseline == SP_CSS_BASELINE_AUTO ) dominant_baseline = SP_CSS_BASELINE_ALPHABETIC; + } + + // TODO: Should also check 'glyph_orientation_vertical' if 'text-orientation' is unset... + if( new_span.text_orientation == SP_CSS_TEXT_ORIENTATION_SIDEWAYS || + (new_span.text_orientation == SP_CSS_TEXT_ORIENTATION_MIXED && pango_item->analysis.gravity == PANGO_GRAVITY_SOUTH) ) { + + // Sideways orientation (Latin characters, CJK punctuation), 90deg rotation done at output stage. + +#ifdef DEBUG_GLYPH + std::cerr << " Sideways" + << " " << std::setw(6) << new_glyph.x + << " " << std::setw(6) << new_glyph.y + << " " << std::setw(6) << delta_x + << " " << std::setw(6) << delta_y + << std::endl; +#endif + + new_glyph.orientation = ORIENTATION_SIDEWAYS; + + new_glyph.x += delta_x; + new_glyph.y -= delta_y; + + // Multiplying by font-size could cause slight differences in + // positioning for different baselines if the font size varies within a + // line of text (e.g. sub-scripts and super-scripts). + new_glyph.y -= new_span.font_size * font->GetBaselines()[ dominant_baseline ]; + + } else { + // Upright orientation + + auto hb_font = pango_font_get_hb_font(font->pFont); + +#ifdef DEBUG_GLYPH + std::cerr << " Upright" + << " " << std::setw(6) << new_glyph.x + << " " << std::setw(6) << new_glyph.y + << " " << std::setw(6) << delta_x + << " " << std::setw(6) << delta_y; + char glyph_name[32]; + hb_font_get_glyph_name(hb_font, new_glyph.glyph, glyph_name, sizeof (glyph_name)); + std::cerr << " " << (glyph_name ? glyph_name : ""); + std::cerr << std::endl; +#endif + + if (pango_version_check(1,44,0) != nullptr) { + // Pango < 1.44.0 (pre HarfBuzz) + new_glyph.x += delta_x; + new_glyph.y -= delta_y; + + double shift = 0; + double scale_factor = PANGO_SCALE * _font_factory_size_multiplier; + if (!FT_HAS_VERTICAL(font->theFace)) { + + // If there are no vertical metrics, glyphs are vertically + // centered before base anchor to mark anchor distance is + // calculated by shaper. We must undo this! + PangoRectangle ink_rect; + PangoRectangle logical_rect; + pango_font_get_glyph_extents (font->pFont, + new_glyph.glyph, + &ink_rect, + &logical_rect); + + // Shift required to move centered glyph back to proper position + // relative to baseline. + shift = + font->GetTypoAscent() + + ink_rect.y / scale_factor + // negative + (ink_rect.height / scale_factor / 2.0) - + 0.5; + } + + // Advance is wrong (horizontal width used instead of vertical)... + if (g_unichar_type(*iter_source_text) != G_UNICODE_NON_SPACING_MARK) { + + x_offset_advance = new_glyph.advance - glyph_v_advance; + new_glyph.advance = glyph_v_advance; + + x_offset_center = shift; + } else { + // Is non-spacing mark! + if (!FT_HAS_VERTICAL(font->theFace)) { + + // If font lacks vertical metrics, all glyphs have em-box advance + // but non-spacing marks should have zero advance. + new_glyph.advance = 0; + + // Correct for base anchor to mark anchor shift. + new_glyph.x += (x_offset_center - shift) * new_span.font_size; + } + + // Correct for advance error. + new_glyph.x += x_offset_advance; + } + + // Need to shift by horizontal to vertical origin (as we don't load glyph + // with vertical metrics). + new_glyph.x += font->GetTypoAscent() * new_span.font_size; + new_glyph.y -= glyph_h_advance/2.0; + + } else if (pango_version_check(1,48,1) != nullptr) { + // 1.44.0 <= Pango < 1.48.1 (minus sign error, mismatch between Cairo/Harfbuzz glyph + // placement) + new_glyph.x += (glyph_width - delta_x); + new_glyph.y -= delta_y; + } else if (pango_version_check(1,48,4) != nullptr) { + // 1.48.1 <= Pango < 1.48.4 (minus sign fix, partial fix for Cairo/Harfbuzz mismatch, + // but bad mark positioning) + new_glyph.x += delta_x; + new_glyph.y -= delta_y; + + // Need to shift by horizontal to vertical origin. Recall Pango lays out vertical text + // as horizontal text then rotates by 90 degress so y_origin -> x, x_origin -> -y. + hb_position_t x_origin = 0.0; + hb_position_t y_origin = 0.0; + hb_font_get_glyph_v_origin(hb_font, new_glyph.glyph, &x_origin, &y_origin); + new_glyph.x += y_origin * font_size_multiplier; + new_glyph.y -= x_origin * font_size_multiplier; + } else { + // 1.48.4 <= Pango (good mark positioning) + new_glyph.x += delta_x; + new_glyph.y -= delta_y; + } + + // If a font has no vertical metrics, HarfBuzz using OpenType functions + // (which Pango uses by default from 1.44.0) to position glyphs so that + // the top of their "ink rectangle" is at the top of the "em-box". This + // section of code moves each cluster (base glyph with marks) down to + // match fonts with vertical metrics. + hb_font_extents_t hb_font_extents_not_used; + if (!hb_font_get_v_extents(hb_font, &hb_font_extents_not_used)) { + // Font does not have vertical metrics! + + if (g_unichar_type(*iter_source_text) != + G_UNICODE_NON_SPACING_MARK) { // Probably should include other marks! + hb_glyph_extents_t glyph_extents; + if (hb_font_get_glyph_extents(hb_font, new_glyph.glyph, &glyph_extents)) { + + // double baseline_adjust = + // font_instance->get_baseline(BASELINE_TEXT_BEFORE_EDGE) - + // font_instance->get_baseline(BASELINE_ALPHABETIC); + // std::cout << "baseline_adjust: " << baseline_adjust << std::endl; + double baseline_adjust = new_span.line_height.ascent / new_span.font_size; + int hb_x_scale = 0; + int hb_y_scale = 0; + hb_font_get_scale(hb_font, &hb_x_scale, &hb_y_scale); + x_offset_cluster = + ((glyph_extents.y_bearing / (double)hb_y_scale) - baseline_adjust) * + new_span.font_size; + } else { + x_offset_cluster = 0.0; // Failed to find extents. + } + } else { + // Is non-spacing mark! + + // Many fonts report a non-zero vertical advance for marks, especially if the 'vmtx' + // table is missing. + new_glyph.advance = 0; + } + + new_glyph.x -= x_offset_cluster; + } + + } + } else { + // Horizontal text + +#ifdef DEBUG_GLYPH + std::cerr << " Horizontal" + << " " << std::setw(6) << new_glyph.x + << " " << std::setw(6) << new_glyph.y + << " " << std::setw(6) << delta_x + << " " << std::setw(6) << delta_y + << std::endl; +#endif + + if( dominant_baseline == SP_CSS_BASELINE_AUTO ) dominant_baseline = SP_CSS_BASELINE_ALPHABETIC; + + new_glyph.x += delta_x; + new_glyph.y += delta_y; + + new_glyph.y += new_span.font_size * font->GetBaselines()[ dominant_baseline ]; + } + + // Correct for right to left text + if (new_span.direction == RIGHT_TO_LEFT) { + + // The following commented out code is from 2005. Subtracting cluster width gives wrong placement if more + // than one glyph has a horizontal advance. See GitHub issue 469. I leave the old code here in case switching to + // subtracting only the glyph width causes unforseen bugs. + + // // pango wanted to give us glyphs in visual order but we refused, so we need to work + // // out where the cluster start is ourselves + + // // Add up widths of remaining glyphs in span. + // double cluster_width = 0.0; + // std::cout << " glyph_index: " << glyph_index << " end_glyph_index: " << it_span->end_glyph_index << std::endl; + // for (unsigned rtl_index = glyph_index; rtl_index < it_span->end_glyph_index ; rtl_index++) { + // if (unbroken_span.glyph_string->glyphs[rtl_index].attr.is_cluster_start && rtl_index != glyph_index) { + // break; + // } + // cluster_width += font_size_multiplier * unbroken_span.glyph_string->glyphs[rtl_index].geometry.width; + // } + // new_glyph.x -= cluster_width; + + new_glyph.x -= font_size_multiplier * unbroken_span.glyph_string->glyphs[glyph_index].geometry.width; + } + + // Store glyph data + _flow._glyphs.push_back(new_glyph); + + // Create the Layout::Character(s) + if (newcluster) { + newcluster = false; + + // Figure out how many glyphs are in the log_cluster. + log_cluster_size_glyphs = 0; + for (; log_cluster_size_glyphs + glyph_index < it_span->end_glyph_index; log_cluster_size_glyphs++){ + if(unbroken_span.glyph_string->log_clusters[glyph_index ] != + unbroken_span.glyph_string->log_clusters[glyph_index + log_cluster_size_glyphs]) break; + } + + // Find where the text ends for this log_cluster. + end_byte = it_span->start.iter_span->text_bytes; // Upper limit + for(int next_glyph_index = glyph_index+1; next_glyph_index < unbroken_span.glyph_string->num_glyphs; next_glyph_index++){ + if(unbroken_span.glyph_string->glyphs[next_glyph_index].attr.is_cluster_start){ + end_byte = unbroken_span.glyph_string->log_clusters[next_glyph_index]; + break; + } + } + + // Figure out how many characters are in the log_cluster. + log_cluster_size_chars = 0; + Glib::ustring::const_iterator lclist = iter_source_text; + unsigned lcb = char_byte; + while (lcb < end_byte){ + log_cluster_size_chars++; + lclist++; + lcb = lclist.base() - unbroken_span.input_stream_first_character.base(); + } + } + + double advance_width = new_glyph.advance; + while (char_byte < end_byte) { + + /* Hack to survive ligatures: in log_cluster keep the number of available chars >= number of glyphs remaining. + When there are no ligatures these two sizes are always the same. + */ + if (log_cluster_size_chars < log_cluster_size_glyphs) { + log_cluster_size_glyphs--; + break; + } + + // Store character info + Layout::Character new_character; + new_character.the_char = *iter_source_text; + new_character.in_span = _flow._spans.size(); + new_character.x = x_in_span; + new_character.char_attributes = para.char_attributes[unbroken_span.char_index_in_para + char_index_in_unbroken_span]; + new_character.in_glyph = (hidden ? -1 : _flow._glyphs.size() - 1); + _flow._characters.push_back(new_character); + + // Letter/word spacing and justification + if (new_character.char_attributes.is_white) + advance_width += text_source->style->word_spacing.computed * _flow.getTextLengthMultiplierDue() + add_to_each_whitespace; // justification + if (new_character.char_attributes.is_cursor_position) + advance_width += text_source->style->letter_spacing.computed * _flow.getTextLengthMultiplierDue(); + advance_width += _flow.getTextLengthIncrementDue(); + + // Update counters + iter_source_text++; + char_index_in_unbroken_span++; + char_byte = iter_source_text.base() - unbroken_span.input_stream_first_character.base(); + log_cluster_size_chars--; + } + + // Update x position variables + advance_width *= direction_sign; + if (new_span.direction != para.direction) { + counter_directional_width_remaining -= advance_width; + current_x -= advance_width; + x_in_span_last -= advance_width; + } else { + current_x += advance_width; + x_in_span_last += advance_width; + } + } // Loop over glyphs in span + + } else if (_flow._input_stream[unbroken_span.input_index]->Type() == CONTROL_CODE) { + current_x += static_cast(_flow._input_stream[unbroken_span.input_index])->width; + } + + new_span.x_end = new_span.x_start + x_in_span_last; + _flow._spans.push_back(new_span); + previous_direction = new_span.direction; + } + // end adding spans to the list, on to the next chunk... + } + TRACE((" End _outputLine\n")); +} + +/** + * Initialises the ScanlineMaker for the first shape in the flow, + * or the infinite version if we're not doing wrapping. + */ +void Layout::Calculator::_createFirstScanlineMaker() +{ + _current_shape_index = 0; + InputStreamTextSource const *text_source = static_cast(_flow._input_stream.front()); + if (_flow._input_wrap_shapes.empty()) { + // create the special no-wrapping infinite scanline maker + double initial_x = 0, initial_y = 0; + if (!text_source->x.empty()) { + initial_x = text_source->x.front().computed; + } + if (!text_source->y.empty()) { + initial_y = text_source->y.front().computed; + } + _scanline_maker = new InfiniteScanlineMaker(initial_x, initial_y, _block_progression); + TRACE((" wrapping disabled\n")); + } + else { + _scanline_maker = new ShapeScanlineMaker(_flow._input_wrap_shapes[_current_shape_index].shape, _block_progression); + TRACE((" begin wrap shape 0\n")); + + // 'inline-size' uses an infinitely high (wide) shape. We must set initial y. (We only need to do it here as there is only one shape.) + if (_flow.wrap_mode == WRAP_INLINE_SIZE) { + _block_progression = _flow._blockProgression(); + if( _block_progression == RIGHT_TO_LEFT || + _block_progression == LEFT_TO_RIGHT ) { + // Vertical text, CJK + if (!text_source->x.empty()) { + double initial_x = text_source->x.front().computed; + _scanline_maker->setNewYCoordinate(initial_x); + } else { + std::cerr << "Layout::Calculator::_createFirstScanlineMaker: no x value with 'inline-size'!" << std::endl; + _scanline_maker->setNewYCoordinate(0); + } + } else { + // Horizontal text + if (!text_source->y.empty()) { + double initial_y = text_source->y.front().computed; + _scanline_maker->setNewYCoordinate(initial_y); + } else { + std::cerr << "Layout::Calculator::_createFirstScanlineMaker: no y value with 'inline-size'!" << std::endl; + _scanline_maker->setNewYCoordinate(0); + } + } + } + } +} + +void Layout::Calculator::UnbrokenSpanPosition::increment() +{ + gchar const *text_base = &*iter_span->input_stream_first_character.base(); + char_byte = g_utf8_next_char(text_base + char_byte) - text_base; + char_index++; + if (char_byte == iter_span->text_bytes) { + iter_span++; + char_index = char_byte = 0; + } +} + +void Layout::Calculator::BrokenSpan::setZero() +{ + end = start; + width = 0.0; + whitespace_count = 0; + end_glyph_index = start_glyph_index = 0; + ends_with_whitespace = false; + each_whitespace_width = 0.0; + letter_spacing = 0.0; + word_spacing = 0.0; +} + +///** +// * For sections of text with a block-progression different to the rest +// * of the flow, the best thing to do is to detect them in advance and +// * create child TextFlow objects with just the rotated text. In the +// * parent we then effectively use ARBITRARY_GAP fields during the +// * flowing (because we don't allow wrapping when the block-progression +// * changes) and copy the actual text in during the output phase. +// * +// * NB: this code not enabled yet. +// */ +//void Layout::Calculator::_initialiseInputItems(ParagraphInfo *para) const +//{ +// Direction prev_block_progression = _block_progression; +// int run_start_input_index = para->first_input_index; +// +// para->free_sequence(para->input_items); +// for(int input_index = para->first_input_index ; input_index < (int)_flow._input_stream.size() ; input_index++) { +// InputItemInfo input_item; +// +// input_item.in_sub_flow = false; +// input_item.sub_flow = NULL; +// if (_flow._input_stream[input_index]->Type() == CONTROL_CODE) { +// Layout::InputStreamControlCode const *control_code = static_cast(_flow._input_stream[input_index]); +// if ( control_code->code == SHAPE_BREAK +// || control_code->code == PARAGRAPH_BREAK) +// break; // stop at the end of the paragraph +// // all other control codes we'll pick up later +// +// } else if (_flow._input_stream[input_index]->Type() == TEXT_SOURCE) { +// Layout::InputStreamTextSource *text_source = static_cast(_flow._input_stream[input_index]); +// Direction this_block_progression = text_source->styleGetBlockProgression(); +// if (this_block_progression != prev_block_progression) { +// if (prev_block_progression != _block_progression) { +// // need to back up so that control codes belong outside the block-progression change +// int run_end_input_index = input_index - 1; +// while (run_end_input_index > run_start_input_index +// && _flow._input_stream[run_end_input_index]->Type() != TEXT_SOURCE) +// run_end_input_index--; +// // now create the sub-flow +// input_item.sub_flow = new Layout; +// for (int sub_input_index = run_start_input_index ; sub_input_index <= run_end_input_index ; sub_input_index++) { +// input_item.in_sub_flow = true; +// if (_flow._input_stream[sub_input_index]->Type() == CONTROL_CODE) { +// Layout::InputStreamControlCode const *control_code = static_cast(_flow._input_stream[sub_input_index]); +// input_item.sub_flow->appendControlCode(control_code->code, control_code->source, control_code->width, control_code->ascent, control_code->descent); +// } else if (_flow._input_stream[sub_input_index]->Type() == TEXT_SOURCE) { +// Layout::InputStreamTextSource *text_source = static_cast(_flow._input_stream[sub_input_index]); +// input_item.sub_flow->appendText(*text_source->text, text_source->style, text_source->source, NULL, 0, text_source->text_begin, text_source->text_end); +// Layout::InputStreamTextSource *sub_flow_text_source = static_cast(input_item.sub_flow->_input_stream.back()); +// sub_flow_text_source->x = text_source->x; // this is easier than going via optionalattrs for the appendText() call +// sub_flow_text_source->y = text_source->y; // should these actually be allowed anyway? You'll almost never get the results you expect +// sub_flow_text_source->dx = text_source->dx; // (not that it's very clear what you should expect, anyway) +// sub_flow_text_source->dy = text_source->dy; +// sub_flow_text_source->rotate = text_source->rotate; +// } +// } +// input_item.sub_flow->calculateFlow(); +// } +// run_start_input_index = input_index; +// } +// prev_block_progression = this_block_progression; +// } +// para->input_items.push_back(input_item); +// } +//} + +/** + * Take all the text from \a _para.first_input_index to the end of the + * paragraph and stitch it together so that pango_itemize() can be called on + * the whole thing. + * + * Input: para.first_input_index. + * Output: para.direction, para.pango_items, para.char_attributes. + * Returns: the number of spans created by pango_itemize + */ +void Layout::Calculator::_buildPangoItemizationForPara(ParagraphInfo *para) const +{ + TRACE(("pango version string: %s\n", pango_version_string() )); + TRACE((" ... compiled for font features\n")); + + TRACE(("itemizing para, first input %d\n", para->first_input_index)); + + PangoAttrList *attributes_list = pango_attr_list_new(); + for (unsigned input_index = para->first_input_index ; input_index < _flow._input_stream.size() ; input_index++) { + if (_flow._input_stream[input_index]->Type() == CONTROL_CODE) { + Layout::InputStreamControlCode const *control_code = static_cast(_flow._input_stream[input_index]); + if ( control_code->code == SHAPE_BREAK + || control_code->code == PARAGRAPH_BREAK) + break; // stop at the end of the paragraph + // all other control codes we'll pick up later + + } else if (_flow._input_stream[input_index]->Type() == TEXT_SOURCE) { + Layout::InputStreamTextSource *text_source = static_cast(_flow._input_stream[input_index]); + + // create the font_instance + font_instance *font = text_source->styleGetFontInstance(); + if (font == nullptr) + continue; // bad news: we'll have to ignore all this text because we know of no font to render it + + PangoAttribute *attribute_font_description = pango_attr_font_desc_new(font->descr); + attribute_font_description->start_index = para->text.bytes(); + + PangoAttribute *attribute_font_features = + pango_attr_font_features_new( text_source->style->getFontFeatureString().c_str()); + attribute_font_features->start_index = para->text.bytes(); + para->text.append(&*text_source->text_begin.base(), text_source->text_length); // build the combined text + + attribute_font_description->end_index = para->text.bytes(); + pango_attr_list_insert(attributes_list, attribute_font_description); + + attribute_font_features->end_index = para->text.bytes(); + pango_attr_list_insert(attributes_list, attribute_font_features); + + // Set language + SPObject * object = text_source->source; + if (!object->lang.empty()) { + PangoLanguage* language = pango_language_from_string(object->lang.c_str()); + PangoAttribute *attribute_language = pango_attr_language_new( language ); + pango_attr_list_insert(attributes_list, attribute_language); + } + + // ownership of attribute is assumed by the list + font->Unref(); + } + } + + TRACE(("whole para: \"%s\"\n", para->text.data())); +// TRACE(("%d input sources used\n", input_index - para->first_input_index)); + + // Pango Itemize + GList *pango_items_glist = nullptr; + para->direction = LEFT_TO_RIGHT; // CSS default + if (_flow._input_stream[para->first_input_index]->Type() == TEXT_SOURCE) { + Layout::InputStreamTextSource const *text_source = static_cast(_flow._input_stream[para->first_input_index]); + + para->direction = (text_source->style->direction.computed == SP_CSS_DIRECTION_LTR) ? LEFT_TO_RIGHT : RIGHT_TO_LEFT; + PangoDirection pango_direction = (text_source->style->direction.computed == SP_CSS_DIRECTION_LTR) ? PANGO_DIRECTION_LTR : PANGO_DIRECTION_RTL; + pango_items_glist = pango_itemize_with_base_dir(_pango_context, pango_direction, para->text.data(), 0, para->text.bytes(), attributes_list, nullptr); + } + + if( pango_items_glist == nullptr ) { + // Type wasn't TEXT_SOURCE or direction was not set. + pango_items_glist = pango_itemize(_pango_context, para->text.data(), 0, para->text.bytes(), attributes_list, nullptr); + } + + pango_attr_list_unref(attributes_list); + + // convert the GList to our vector<> and make the font_instance for each PangoItem at the same time + para->pango_items.reserve(g_list_length(pango_items_glist)); + TRACE(("para itemizes to %d sections\n", g_list_length(pango_items_glist))); + for (GList *current_pango_item = pango_items_glist ; current_pango_item != nullptr ; current_pango_item = current_pango_item->next) { + PangoItemInfo new_item; + new_item.item = (PangoItem*)current_pango_item->data; + PangoFontDescription *font_description = pango_font_describe(new_item.item->analysis.font); + new_item.font = (font_factory::Default())->Face(font_description); + pango_font_description_free(font_description); // Face() makes a copy + para->pango_items.push_back(new_item); + } + g_list_free(pango_items_glist); + + // and get the character attributes on everything + para->char_attributes.resize(para->text.length() + 1); + pango_get_log_attrs(para->text.data(), para->text.bytes(), -1, nullptr, &*para->char_attributes.begin(), para->char_attributes.size()); + + // Fix for Pango 1.49 which changes the end of a paragraph to a mandatory break. + // This breaks Inkscape's multiline text (i.e. sodipodi:role line). + para->char_attributes[para->text.length()].is_mandatory_break = 0; + + TRACE(("end para itemize, direction = %d\n", para->direction)); +} + +/** + * Finds the value of line_height_multiplier given the 'line-height' property. The result of + * multiplying \a l by \a line_height_multiplier is the inline box height as specified in css2 + * section 10.8. http://www.w3.org/TR/CSS2/visudet.html#line-height + * + * The 'computed' value of 'line-height' does not have a consistent meaning. We need to find the + * 'used' value and divide that by the font size. + */ +double Layout::Calculator::_computeFontLineHeight( SPStyle const *style ) +{ + // This is a bit backwards... we should be returning the absolute height + // but as the code expects line_height_multiplier we return that. + if (style->line_height.normal) { + return (LINE_HEIGHT_NORMAL); + } else if (style->line_height.unit == SP_CSS_UNIT_NONE) { + // Special case per CSS, computed value is multiplier + return style->line_height.computed; + } else { + // Normal case, computed value is absolute height. Turn it into multiplier. + return style->line_height.computed / style->font_size.computed; + } +} + +bool compareGlyphWidth(const PangoGlyphInfo &a, const PangoGlyphInfo &b) +{ + bool retval = false; + if ( b.geometry.width == 0 && (a.geometry.width > 0))retval = true; + return (retval); +} + + +/** + * Split the paragraph into spans. Also call pango_shape() on them. + * + * Input: para->first_input_index, para->pango_items + * Output: para->spans + * Returns: the index of the beginning of the following paragraph in _flow._input_stream + */ +unsigned Layout::Calculator::_buildSpansForPara(ParagraphInfo *para) const +{ + unsigned pango_item_index = 0; + unsigned char_index_in_para = 0; + unsigned byte_index_in_para = 0; + unsigned input_index; + unsigned para_text_index = 0; + + TRACE(("build spans\n")); + para->free_sequence(para->unbroken_spans); + + for(input_index = para->first_input_index ; input_index < _flow._input_stream.size() ; input_index++) { + if (_flow._input_stream[input_index]->Type() == CONTROL_CODE) { + Layout::InputStreamControlCode const *control_code = static_cast(_flow._input_stream[input_index]); + + if ( control_code->code == SHAPE_BREAK + || control_code->code == PARAGRAPH_BREAK) { + + // Add span to be used to calculate line spacing of blank lines. + UnbrokenSpan new_span; + new_span.pango_item_index = -1; + new_span.input_index = input_index; + + // No pango object, so find font and line height ourselves. + SPObject * object = control_code->source; + if (object) { + SPStyle * style = object->style; + if (style) { + new_span.font_size = style->font_size.computed * _flow.getTextLengthMultiplierDue(); + font_factory * factory = font_factory::Default(); + font_instance * font = factory->FaceFromStyle( style ); + new_span.line_height_multiplier = _computeFontLineHeight( object->style ); + new_span.line_height.set( font ); + new_span.line_height *= new_span.font_size; + } + } + new_span.text_bytes = 0; + new_span.char_index_in_para = char_index_in_para; + para->unbroken_spans.push_back(new_span); + TRACE(("add empty span for break %lu\n", para->unbroken_spans.size() - 1)); + break; // stop at the end of the paragraph + + } else if (control_code->code == ARBITRARY_GAP) { // Not used! + + UnbrokenSpan new_span; + new_span.pango_item_index = -1; + new_span.input_index = input_index; + new_span.line_height.ascent = control_code->ascent * _flow.getTextLengthMultiplierDue(); + new_span.line_height.descent = control_code->descent * _flow.getTextLengthMultiplierDue(); + new_span.text_bytes = 0; + new_span.char_index_in_para = char_index_in_para; + para->unbroken_spans.push_back(new_span); + TRACE(("add gap span %lu\n", para->unbroken_spans.size() - 1)); + } + } else if (_flow._input_stream[input_index]->Type() == TEXT_SOURCE && pango_item_index < para->pango_items.size()) { + Layout::InputStreamTextSource const *text_source = static_cast(_flow._input_stream[input_index]); + unsigned char_index_in_source = 0; + unsigned span_start_byte_in_source = 0; + + // we'll need to make several spans from each text source, based on the rules described about the UnbrokenSpan definition + for ( ; ; ) { + /* we need to change spans at every change of PangoItem, source stream change, + or change in one of the attributes altering position/rotation. */ + + unsigned const pango_item_bytes = ( pango_item_index >= para->pango_items.size() + ? 0 + : ( para->pango_items[pango_item_index].item->offset + + para->pango_items[pango_item_index].item->length + - byte_index_in_para ) ); + unsigned const text_source_bytes = ( text_source->text_end.base() + - text_source->text_begin.base() + - span_start_byte_in_source ); + TRACE(("New Unbroken Span\n")); + UnbrokenSpan new_span; + new_span.text_bytes = std::min(text_source_bytes, pango_item_bytes); + new_span.input_stream_first_character = Glib::ustring::const_iterator(text_source->text_begin.base() + span_start_byte_in_source); + new_span.char_index_in_para = char_index_in_para + char_index_in_source; + new_span.input_index = input_index; + + // cut at attribute changes as well + new_span.x._set = false; + new_span.y._set = false; + new_span.dx._set = false; + new_span.dy._set = false; + new_span.rotate._set = false; + if (_block_progression == TOP_TO_BOTTOM || _block_progression == BOTTOM_TO_TOP) { + // Horizontal text + if (text_source->x.size() > char_index_in_source) new_span.x = text_source->x[char_index_in_source]; + if (text_source->y.size() > char_index_in_source) new_span.y = text_source->y[char_index_in_source]; + if (text_source->dx.size() > char_index_in_source) new_span.dx = text_source->dx[char_index_in_source].computed * _flow.getTextLengthMultiplierDue(); + if (text_source->dy.size() > char_index_in_source) new_span.dy = text_source->dy[char_index_in_source].computed * _flow.getTextLengthMultiplierDue(); + } else { + // Vertical text + if (text_source->x.size() > char_index_in_source) new_span.y = text_source->x[char_index_in_source]; + if (text_source->y.size() > char_index_in_source) new_span.x = text_source->y[char_index_in_source]; + if (text_source->dx.size() > char_index_in_source) new_span.dy = text_source->dx[char_index_in_source].computed * _flow.getTextLengthMultiplierDue(); + if (text_source->dy.size() > char_index_in_source) new_span.dx = text_source->dy[char_index_in_source].computed * _flow.getTextLengthMultiplierDue(); + } + if (text_source->rotate.size() > char_index_in_source) new_span.rotate = text_source->rotate[char_index_in_source]; + else if (char_index_in_source == 0) new_span.rotate = 0.f; + if (input_index == 0 && para->unbroken_spans.empty() && !new_span.y._set && _flow._input_wrap_shapes.empty()) { + // if we don't set an explicit y some of the automatic wrapping code takes over and moves the text vertically + // so that the top of the letters is at zero, not the baseline + new_span.y = 0.0; + } + Glib::ustring::const_iterator iter_text = new_span.input_stream_first_character; + iter_text++; + for (unsigned i = char_index_in_source + 1 ; ; i++, iter_text++) { + if (iter_text >= text_source->text_end) break; + if (iter_text.base() - new_span.input_stream_first_character.base() >= (int)new_span.text_bytes) break; + if ( i >= text_source->x.size() && i >= text_source->y.size() + && i >= text_source->dx.size() && i >= text_source->dy.size() + && i >= text_source->rotate.size()) break; + if ( (text_source->x.size() > i && text_source->x[i]._set) + || (text_source->y.size() > i && text_source->y[i]._set) + || (text_source->dx.size() > i && text_source->dx[i]._set && text_source->dx[i].computed != 0.0) + || (text_source->dy.size() > i && text_source->dy[i]._set && text_source->dy[i].computed != 0.0) + || (text_source->rotate.size() > i && text_source->rotate[i]._set + && (i == 0 || text_source->rotate[i].computed != text_source->rotate[i - 1].computed))) { + new_span.text_bytes = iter_text.base() - new_span.input_stream_first_character.base(); + break; + } + } + + // now we know the length, do some final calculations and add the UnbrokenSpan to the list + new_span.font_size = text_source->style->font_size.computed * _flow.getTextLengthMultiplierDue(); + if (new_span.text_bytes) { + new_span.glyph_string = pango_glyph_string_new(); + /* Some assertions intended to help diagnose bug #1277746. */ + g_assert( 0 < new_span.text_bytes ); + g_assert( span_start_byte_in_source < text_source->text->bytes() ); + g_assert( span_start_byte_in_source + new_span.text_bytes <= text_source->text->bytes() ); + g_assert( memchr(text_source->text->data() + span_start_byte_in_source, '\0', static_cast(new_span.text_bytes)) + == nullptr ); + + /* Notes as of 4/29/13. Pango_shape is not generating English language ligatures, but it is generating + them for Hebrew (and probably other similar languages). In the case observed 3 unicode characters (a base + and 2 Mark, nonspacings) are merged into two glyphs (the base + first Mn, the 2nd Mn). All of these map + from glyph to first character of the log_cluster range. This destroys the 1:1 correspondence between + characters and glyphs. A big chunk of the conditional code which immediately follows this call + is there to clean up the resulting mess. + */ + + // Assumption: old and new arguments are the same. + auto gold = std::string_view(text_source->text->data() + span_start_byte_in_source, new_span.text_bytes); + auto gnew = std::string_view(para->text.data() + para_text_index, new_span.text_bytes); + assert (gold == gnew); + + // Convert characters to glyphs + pango_shape_full(para->text.data() + para_text_index, + new_span.text_bytes, + para->text.data(), + -1, + ¶->pango_items[pango_item_index].item->analysis, + new_span.glyph_string); + + if (para->pango_items[pango_item_index].item->analysis.level & 1) { + // Right to left text (Arabic, Hebrew, etc.) + + // pango_shape() will reorder glyphs in rtl sections into visual order + // (start offsets in accending order) which messes us up because the svg + // spec requires us to draw glyphs in logical order so let's reverse the + // glyphstring. + + const unsigned nglyphs = new_span.glyph_string->num_glyphs; + std::vector infos(nglyphs); + std::vector clusters(nglyphs); + + for (int i = 0; i < nglyphs; ++i) { + std::copy(&new_span.glyph_string->glyphs[i], &new_span.glyph_string->glyphs[i+1], infos.end() - i - 1); + std::copy(&new_span.glyph_string->log_clusters[i], &new_span.glyph_string->log_clusters[i+1], clusters.end() - i - 1); + } + + std::copy(infos.begin(), infos.end(), new_span.glyph_string->glyphs); + std::copy(clusters.begin(), clusters.end(), new_span.glyph_string->log_clusters); + + // We've messed up the flag that tells a glyph it is first in a cluster. + for (int i = 0; i < nglyphs; ++i) { + + // Set flag for start of cluster, we skip all other glyphs in cluster below. + new_span.glyph_string->glyphs[i].attr.is_cluster_start = 1; + + // Find index of first glyph in next cluster + int j = i + 1; + while( (j < nglyphs) && + (new_span.glyph_string->log_clusters[j] == new_span.glyph_string->log_clusters[i]) + ) { + new_span.glyph_string->glyphs[j].attr.is_cluster_start = 0; // Zero + j++; + } + + // Move on to next cluster. + i = j; + } + + } // End right to left text. + + // The following sorting doesn't seem to be necessary, and causes + // https://gitlab.com/inkscape/inkscape/-/issues/394 ... + + /* + CAREFUL, within a log_cluster the order of glyphs may not map 1:1, or + even in the same order, to the original unicode characters!!! Among + other things, diacritical mark glyphs can end up sequentially in front of the base + character glyph. That makes determining kerning, even approximately, difficult + later on. + + To resolve this to the extent possible sort the glyphs within the same + log_cluster into descending order by width in a special manner before copying. Diacritical marks + and similar have zero width and the glyph they modify has nonzero width. The order + of the zero width ones does not matter. A logical cluster is sorted into sequential order + [base] [zw_modifier1] [zw_modifier2] + where all the modifiers have zero width and the base does not. This works for languages like Hebrew. + + Pango also creates log clusters for languages like Telugu having many glyphs with nonzero widths. + Since these are nonzero, their order is not modified. + + If some language mixes these modes, having a log cluster having something like + [base1] [zw_modifier1] [base2] [zw_modifier2] + the result will be incorrect: + base1] [base2] [zw_modifier1] [zw_modifier2] + + If ligatures other than with Mark, nonspacing are ever implemented in Pango this will screw up, for instance + changing "fi" to "if". + */ + + // If it is necessary to move zero width glyphs.. then it applies to both right-to-left and left-to-right text. + // const unsigned nglyphs = new_span.glyph_string->num_glyphs; + // for (int i = 0; i < nglyphs; ++i) { + + // // Zero flag for start of cluster, we zero the rest below, and then reset it after sorting. + // new_span.glyph_string->glyphs[i].attr.is_cluster_start = 0; + + // // Find index of first glyph in next cluster + // int j = i + 1; + // while( (j < nglyphs) && + // (new_span.glyph_string->log_clusters[j] == new_span.glyph_string->log_clusters[i]) + // ) { + // new_span.glyph_string->glyphs[j].attr.is_cluster_start = 0; // Zero + // j++; + // } + + // if (j - i) { + // // More than one glyph in cluster -> sort. + // std::sort(&(new_span.glyph_string->glyphs[i]), &(new_span.glyph_string->glyphs[j]), compareGlyphWidth); + // } + + // // Now we're sorted, set flag for start of cluster. + // new_span.glyph_string->glyphs[i].attr.is_cluster_start = 1; + + // // Move on to next cluster. + // i = j; + // } + /* glyphs[].x_offset values are probably out of order within any log_clusters, apparently harmless */ + + + new_span.pango_item_index = pango_item_index; + new_span.line_height_multiplier = _computeFontLineHeight( text_source->style ); + new_span.line_height.set( para->pango_items[pango_item_index].font ); + new_span.line_height *= new_span.font_size; + + // At some point we may want to calculate baseline_shift here (to take advantage + // of otm features like superscript baseline), but for now we use style baseline_shift. + new_span.baseline_shift = text_source->style->baseline_shift.computed; + new_span.text_orientation = (SPCSSTextOrientation)text_source->style->text_orientation.computed; + + // TODO: metrics for vertical text + TRACE(("add text span %lu \"%s\"\n", para->unbroken_spans.size(), text_source->text->raw().substr(span_start_byte_in_source, new_span.text_bytes).c_str())); + TRACE((" %d glyphs\n", new_span.glyph_string->num_glyphs)); + } else { + // if there's no text we still need to initialise the styles + new_span.pango_item_index = -1; + font_instance *font = text_source->styleGetFontInstance(); + if (font) { + new_span.line_height_multiplier = _computeFontLineHeight( text_source->style ); + new_span.line_height.set( font ); + new_span.line_height *= new_span.font_size; + font->Unref(); + } else { + new_span.line_height *= 0.0; // Set all to zero + new_span.line_height_multiplier = LINE_HEIGHT_NORMAL; + } + TRACE(("add style init span %lu\n", para->unbroken_spans.size())); + } + para->unbroken_spans.push_back(new_span); + + // calculations for moving to the next UnbrokenSpan + byte_index_in_para += new_span.text_bytes; + para_text_index += new_span.text_bytes; + char_index_in_source += g_utf8_strlen(&*new_span.input_stream_first_character.base(), new_span.text_bytes); + + if (new_span.text_bytes >= pango_item_bytes) { // end of pango item + pango_item_index++; + if (pango_item_index == para->pango_items.size()) break; // end of paragraph + } + if (new_span.text_bytes == text_source_bytes) + break; // end of source + // else attribute changed + span_start_byte_in_source += new_span.text_bytes; + } + char_index_in_para += char_index_in_source; // This seems wrong. Probably should be inside loop. + } + } + TRACE(("end build spans\n")); + return input_index; +} + +/** + * Moves onto next shape with a new scanline_maker. + * If there is no next shape, creates an infinite scanline maker to stash remaining text. + * Returns false if an infinite scanline maker is created. + */ +bool Layout::Calculator::_goToNextWrapShape() +{ + if (_flow._input_wrap_shapes.size() == 0) { + // Shouldn't happen. + std::cerr << "Layout::Calculator::_goToNextWrapShape() called for text without shapes!" << std::endl; + return false; + } + + if (_current_shape_index >= _flow._input_wrap_shapes.size()) { + // Shouldn't happen. + std::cerr << "Layout::Calculator::_goToNextWrapShape(): shape index too large!" << std::endl; + } + + _current_shape_index++; + + delete _scanline_maker; + _scanline_maker = nullptr; + + if (_current_shape_index < _flow._input_wrap_shapes.size()) { + _scanline_maker = new ShapeScanlineMaker(_flow._input_wrap_shapes[_current_shape_index].shape, _block_progression); + TRACE(("begin wrap shape %u\n", _current_shape_index)); + return true; + } else { + // Out of shapes, create infinite scanline maker to stash overflow. + + // First find a suitable position for overflow text. (index - 1 exists since we just incremented index) + double x = _flow._input_wrap_shapes[_current_shape_index - 1].shape->leftX; + double y = _flow._input_wrap_shapes[_current_shape_index - 1].shape->bottomY; + + _scanline_maker = new InfiniteScanlineMaker(x, y, _block_progression); + TRACE(("out of wrap shapes, stash leftover\n")); + return false; + } + + // Shouldn't reach +} + +/** + * Given \a para filled in and \a start_span_pos set, keeps trying to + * find somewhere it can fit the next line of text. The process of finding + * the text that fits will involve creating one or more entries in + * \a chunk_info describing the bounds of the fitted text and several + * bits of information that will prove useful when we come to output the + * line to #_flow. Returns with \a start_span_pos set to the end of the + * text that was fitted, \a chunk_info completely filled out and + * \a line_box_height set with the largest ascent and the largest + * descent (individually per CSS) on the line. The line_box_height + * can never be smaller than the line_box_strut (which is determined + * by the block level value of line_height). The return + * value is false only if we've run out of shapes to wrap inside (and + * hence stashed overflow). + */ +bool Layout::Calculator::_findChunksForLine(ParagraphInfo const ¶, + UnbrokenSpanPosition *start_span_pos, + std::vector *chunk_info, + FontMetrics *line_box_height, + FontMetrics const *strut_height) +{ + TRACE((" begin _findChunksForLine: chunks: %lu, em size: %f\n", chunk_info->size(), line_box_height->emSize() )); + + // CSS 2.1 dictates that the minimum line height (i.e. the strut height) + // is found from the block element. + *line_box_height = *strut_height; + TRACE((" initial line_box_height (em size): %f\n", line_box_height->emSize() )); + + bool truncated = false; + + UnbrokenSpanPosition span_pos; + for( ; ; ) { + // Get regions where one can place one line of text (can be more than one, if filling a + // donut for example). + std::vector scan_runs; + scan_runs = _scanline_maker->makeScanline(*line_box_height); // 1 scan run with "InfiniteScanlineMaker" + + // If scan_runs is empty, we must have reached the bottom of a shape. Go to next shape. + while (scan_runs.empty()) { + // Reset for new shape. + *line_box_height = *strut_height; + + // Only used by ShapeScanlineMaker + if (!_goToNextWrapShape()) { + truncated = true; + } + + // If we've run out of shapes, this will be the infinite line scanline maker with one scan_run). + scan_runs = _scanline_maker->makeScanline(*line_box_height); + } + + + TRACE((" finding line fit y=%f, %lu scan runs\n", scan_runs.front().y, scan_runs.size())); + chunk_info->clear(); + chunk_info->reserve(scan_runs.size()); + if (para.direction == RIGHT_TO_LEFT) std::reverse(scan_runs.begin(), scan_runs.end()); + unsigned scan_run_index; + span_pos = *start_span_pos; + for (scan_run_index = 0 ; scan_run_index < scan_runs.size() ; scan_run_index++) { + // Returns false if some text in line requires a taller line_box_height. + // (We try again with a larger line_box_height.) + if (!_buildChunksInScanRun(para, span_pos, scan_runs[scan_run_index], chunk_info, line_box_height)) { + break; + } + + if (!chunk_info->empty() && !chunk_info->back().broken_spans.empty()) { + span_pos = chunk_info->back().broken_spans.back().end; + } + } + + if (scan_run_index == scan_runs.size()) break; // ie when buildChunksInScanRun() succeeded + + } // End for loop + + *start_span_pos = span_pos; + TRACE((" final line_box_height: %f\n", line_box_height->emSize() )); + TRACE((" end _findChunksForLine: chunks: %lu, truncated: %s\n", chunk_info->size(), truncated ? "true" : "false")); + return !truncated; +} + +/** + * Given a scan run and a first character, append one or more chunks to + * the \a chunk_info vector that describe all the spans and other detail + * necessary to output the greatest amount of text that will fit on this scan + * line (greedy line breaking algorithm). Each chunk contains one or more + * BrokenSpan structures that link back to UnbrokenSpan structures that link + * to the text itself. Normally there will be either one or zero (if the + * scanrun is too short to fit any text) chunk added to \a chunk_info by + * each call to this method, but we will add more than one if an x or y + * attribute has been set on a tspan. \a line_height must be set on input, + * and if it needs to be made larger and the #_scanline_maker can't do + * an in-situ resize then it will be set to the required value and the + * method will return false. + */ +bool Layout::Calculator::_buildChunksInScanRun(ParagraphInfo const ¶, + UnbrokenSpanPosition const &start_span_pos, + ScanlineMaker::ScanRun const &scan_run, + std::vector *chunk_info, + FontMetrics *line_height) const +{ + TRACE((" begin _buildChunksInScanRun: chunks: %lu, em size: %f\n", chunk_info->size(), line_height->emSize() )); + + FontMetrics line_height_saved = *line_height; // Store for recalculating line height if chunks are backed out + + ChunkInfo new_chunk; + new_chunk.text_width = 0.0; + new_chunk.whitespace_count = 0; + new_chunk.scanrun_width = scan_run.width(); + new_chunk.x = scan_run.x_start; + + // we haven't done anything yet so the last valid break position is the beginning + BrokenSpan last_span_at_break, last_span_at_emergency_break; + last_span_at_break.start = start_span_pos; + last_span_at_break.setZero(); + last_span_at_emergency_break.start = start_span_pos; + last_span_at_emergency_break.setZero(); + + TRACE((" trying chunk from %f to %g\n", scan_run.x_start, scan_run.x_end)); + BrokenSpan new_span; + new_span.end = start_span_pos; + while (new_span.end.iter_span != para.unbroken_spans.end()) { // this loops once for each UnbrokenSpan + new_span.start = new_span.end; + + // force a chunk change at x or y attribute change + if ((new_span.start.iter_span->x._set || new_span.start.iter_span->y._set) && new_span.start.char_byte == 0) { + + if (new_span.start.iter_span != start_span_pos.iter_span) + chunk_info->push_back(new_chunk); + + new_chunk.x += new_chunk.text_width; + new_chunk.text_width = 0.0; + new_chunk.whitespace_count = 0; + new_chunk.broken_spans.clear(); + if (new_span.start.iter_span->x._set) new_chunk.x = new_span.start.iter_span->x.computed; + // y doesn't need to be done until output time + } + + // see if this span is too tall to fit on the current line + FontMetrics new_span_height = new_span.start.iter_span->line_height; + new_span_height.computeEffective( new_span.start.iter_span->line_height_multiplier ); + + /* floating point 80-bit/64-bit rounding problems require epsilon. See + discussion http://inkscape.gristle.org/2005-03-16.txt around 22:00 */ + if ( new_span_height.ascent > line_height->ascent + std::numeric_limits::epsilon() || + new_span_height.descent > line_height->descent + std::numeric_limits::epsilon() ) { + // Take larger of each of the two ascents and two descents per CSS + line_height->max(new_span_height); + + // Currently always true for flowed text and false for Inkscape multiline text. + if (!_scanline_maker->canExtendCurrentScanline(*line_height)) { + return false; + } + } + + bool span_fitted = _measureUnbrokenSpan(para, &new_span, &last_span_at_break, &last_span_at_emergency_break, new_chunk.scanrun_width - new_chunk.text_width); + + new_chunk.text_width += new_span.width; + new_chunk.whitespace_count += new_span.whitespace_count; + new_chunk.broken_spans.push_back(new_span); // if !span_fitted we'll correct ourselves below + + if (!span_fitted) break; + + if (new_span.end.iter_span == para.unbroken_spans.end()) { + last_span_at_break = new_span; + break; + } + + PangoLogAttr const &char_attributes = _charAttributes(para, new_span.end); + if (char_attributes.is_mandatory_break) { + last_span_at_break = new_span; + break; + } + } + + TRACE((" chunk complete, used %f width (%d whitespaces, %lu brokenspans)\n", new_chunk.text_width, new_chunk.whitespace_count, new_chunk.broken_spans.size())); + chunk_info->push_back(new_chunk); + + if (scan_run.width() >= 4.0 * line_height->emSize() && last_span_at_break.end == start_span_pos) { + /* **non-SVG spec bit**: See bug #1191102 + If the user types a very long line with no spaces, the way the spec + is written at the moment means that when the length of the text + exceeds the available width of all remaining areas, the text is + completely hidden. This condition alters that behaviour so that if + the length of the line is greater than four times the line-height + and there are no spaces, it'll be emergency-wrapped at the last + character. One could read the SVG Tiny 1.2 draft as permitting this + sort of behaviour, but it's still a bit dodgy. The hard-coding of + 4x is not nice, either. */ + last_span_at_break = last_span_at_emergency_break; + } + + if (!chunk_info->back().broken_spans.empty() && last_span_at_break.end != chunk_info->back().broken_spans.back().end) { + // need to back out spans until we come to the one with the last break in it + while (!chunk_info->empty() && last_span_at_break.start.iter_span != chunk_info->back().broken_spans.back().start.iter_span) { + chunk_info->back().text_width -= chunk_info->back().broken_spans.back().width; + chunk_info->back().whitespace_count -= chunk_info->back().broken_spans.back().whitespace_count; + chunk_info->back().broken_spans.pop_back(); + if (chunk_info->back().broken_spans.empty()) + chunk_info->pop_back(); + } + if (!chunk_info->empty()) { + chunk_info->back().text_width -= chunk_info->back().broken_spans.back().width; + chunk_info->back().whitespace_count -= chunk_info->back().broken_spans.back().whitespace_count; + if (last_span_at_break.start == last_span_at_break.end) { + chunk_info->back().broken_spans.pop_back(); // last break was at an existing boundary + if (chunk_info->back().broken_spans.empty()) + chunk_info->pop_back(); + } else { + chunk_info->back().broken_spans.back() = last_span_at_break; + chunk_info->back().text_width += last_span_at_break.width; + chunk_info->back().whitespace_count += last_span_at_break.whitespace_count; + } + TRACE((" correction: fitted span %lu width = %f\n", last_span_at_break.start.iter_span - para.unbroken_spans.begin(), last_span_at_break.width)); + } + } + + // Recalculate line_box_height after backing out chunks + *line_height = line_height_saved; + for (const auto & it_chunk : *chunk_info) { + for (const auto & broken_span : it_chunk.broken_spans) { + FontMetrics span_height = broken_span.start.iter_span->line_height; + TRACE((" brokenspan line_height: %f\n", span_height.emSize() )); + span_height.computeEffective( broken_span.start.iter_span->line_height_multiplier ); + line_height->max( span_height ); + } + } + TRACE((" line_box_height: %f\n", line_height->emSize())); + + if (!chunk_info->empty() && !chunk_info->back().broken_spans.empty() && chunk_info->back().broken_spans.back().ends_with_whitespace) { + // for justification we need to discard space occupied by the single whitespace at the end of the chunk + TRACE((" backing out whitespace\n")); + chunk_info->back().broken_spans.back().ends_with_whitespace = false; + chunk_info->back().broken_spans.back().width -= chunk_info->back().broken_spans.back().each_whitespace_width; + chunk_info->back().broken_spans.back().whitespace_count--; + chunk_info->back().text_width -= chunk_info->back().broken_spans.back().each_whitespace_width; + chunk_info->back().whitespace_count--; + } + + if (!chunk_info->empty() && !chunk_info->back().broken_spans.empty() ) { + // for justification we need to discard line-spacing and word-spacing at end of the chunk + chunk_info->back().broken_spans.back().width -= chunk_info->back().broken_spans.back().letter_spacing; + chunk_info->back().text_width -= chunk_info->back().broken_spans.back().letter_spacing; + TRACE((" width after subtracting last letter_spacing: %f\n", chunk_info->back().broken_spans.back().width)); + } + + TRACE((" end _buildChunksInScanRun: chunks: %lu\n", chunk_info->size())); + return true; +} + +#ifdef DEBUG_LAYOUT_TNG_COMPUTE +/** + * For debugging, not called in distributed code + * + * Input: para->first_input_index, para->pango_items + */ +void Layout::Calculator::dumpPangoItemsOut(ParagraphInfo *para){ + std::cerr << "Pango items: " << para->pango_items.size() << std::endl; + font_factory * factory = font_factory::Default(); + for(unsigned pidx = 0 ; pidx < para->pango_items.size(); pidx++){ + std::cerr + << "idx: " << pidx + << " offset: " + << para->pango_items[pidx].item->offset + << " length: " + << para->pango_items[pidx].item->length + << " font: " + << factory->ConstructFontSpecification( para->pango_items[pidx].font ) + << std::endl; + } +} + +/** + * For debugging, not called in distributed code + * + * Input: para->first_input_index, para->pango_items + */ +void Layout::Calculator::dumpUnbrokenSpans(ParagraphInfo *para){ + std::cerr << "Unbroken Spans: " << para->unbroken_spans.size() << std::endl; + for(unsigned uidx = 0 ; uidx < para->unbroken_spans.size(); uidx++){ + std::cerr + << "idx: " << uidx + << " pango_item_index: " << para->unbroken_spans[uidx].pango_item_index + << " input_index: " << para->unbroken_spans[uidx].input_index + << " char_index_in_para: " << para->unbroken_spans[uidx].char_index_in_para + << " text_bytes: " << para->unbroken_spans[uidx].text_bytes + << std::endl; + } +} +#endif //DEBUG_LAYOUT_TNG_COMPUTE + +/** The management function to start the whole thing off. */ +bool Layout::Calculator::calculate() +{ + if (_flow._input_stream.empty()) + return false; + /** + * hm, why do we want assert (crash) the application, now do simply return false + * \todo check if this is the correct behaviour + * g_assert(_flow._input_stream.front()->Type() == TEXT_SOURCE); + */ + if (_flow._input_stream.front()->Type() != TEXT_SOURCE) + { + g_warning("flow text is not of type TEXT_SOURCE. Abort."); + return false; + } + TRACE(("begin calculate()\n")); + + _flow._clearOutputObjects(); + + _pango_context = (font_factory::Default())->fontContext; + + _font_factory_size_multiplier = (font_factory::Default())->fontSize; + + _block_progression = _flow._blockProgression(); + if( _block_progression == RIGHT_TO_LEFT || _block_progression == LEFT_TO_RIGHT ) { + // Vertical text, CJK + switch (_flow._blockTextOrientation()) { + case SP_CSS_TEXT_ORIENTATION_MIXED: + pango_context_set_base_gravity(_pango_context, PANGO_GRAVITY_EAST); + pango_context_set_gravity_hint(_pango_context, PANGO_GRAVITY_HINT_NATURAL); + break; + case SP_CSS_TEXT_ORIENTATION_UPRIGHT: + pango_context_set_base_gravity(_pango_context, PANGO_GRAVITY_EAST); + pango_context_set_gravity_hint(_pango_context, PANGO_GRAVITY_HINT_STRONG); + break; + case SP_CSS_TEXT_ORIENTATION_SIDEWAYS: + pango_context_set_base_gravity(_pango_context, PANGO_GRAVITY_SOUTH); + pango_context_set_gravity_hint(_pango_context, PANGO_GRAVITY_HINT_STRONG); + break; + default: + std::cerr << "Layout::Calculator: Unhandled text orientation!" << std::endl; + } + } else { + // Horizontal text + pango_context_set_base_gravity(_pango_context, PANGO_GRAVITY_AUTO); + pango_context_set_gravity_hint(_pango_context, PANGO_GRAVITY_HINT_NATURAL); + } + + // Minimum line box height determined by block container. + FontMetrics strut_height = _flow.strut; + _y_offset = 0.0; + _createFirstScanlineMaker(); + + ParagraphInfo para; + FontMetrics line_box_height; // Current value of line box height for line. + bool keep_going = true; // Set false if we ran out of space and had to stash overflow. + for(para.first_input_index = 0 ; para.first_input_index < _flow._input_stream.size() ; ) { + + // jump to the next wrap shape if this is a SHAPE_BREAK control code + if (_flow._input_stream[para.first_input_index]->Type() == CONTROL_CODE) { + InputStreamControlCode const *control_code = static_cast(_flow._input_stream[para.first_input_index]); + if (control_code->code == SHAPE_BREAK) { + TRACE(("shape break control code\n")); + if (!_goToNextWrapShape()) { + std::cerr << "Layout::Calculator::calculate: Found SHAPE_BREAK but out of shapes!" << std::endl; + } + continue; // Go to next paragraph (paragraph only contained control code). + } + } + + // Break things up into little pango units with unique direction, gravity, etc. + _buildPangoItemizationForPara(¶); + + // Do shaping (convert characters to glyphs) + unsigned para_end_input_index = _buildSpansForPara(¶); + + if (_flow._input_stream[para.first_input_index]->Type() == TEXT_SOURCE) + para.alignment = static_cast(_flow._input_stream[para.first_input_index])->styleGetAlignment(para.direction, !_flow._input_wrap_shapes.empty()); + else + para.alignment = para.direction == LEFT_TO_RIGHT ? LEFT : RIGHT; + + TRACE(("para prepared, adding as #%lu\n", _flow._paragraphs.size())); + Layout::Paragraph new_paragraph; + new_paragraph.base_direction = para.direction; + new_paragraph.alignment = para.alignment; + _flow._paragraphs.push_back(new_paragraph); + + // start scanning lines + UnbrokenSpanPosition span_pos; + span_pos.iter_span = para.unbroken_spans.begin(); + span_pos.char_byte = 0; + span_pos.char_index = 0; + + do { // Until end of paragraph + TRACE(("begin line\n")); + + std::vector line_chunk_info; + + // Fill line. + // If we've run out of space, we've put the remaining text in a single line and + // returned false. If we ran out of space on previous paragraph, we continue with + // single-line scan-line maker. + bool flowed =_findChunksForLine(para, &span_pos, &line_chunk_info, &line_box_height, &strut_height ); + if (!flowed) { + keep_going = false; + } + + if (line_box_height.emSize() < 0.001 && line_chunk_info.empty()) { + // We need to avoid an infinite (or semi-infinite) loop. + std::cerr << "Layout::Calculator::calculate: No room for text and line advance is very small" << std::endl; + return false; // For the moment + } + + + // For Inkscape multi-line text (using role="line") we run into a problem if the first + // line is empty - namely, there is no character to attach a 'y' attribute value. The + // result is that the code that takes a baseline position (e.g. 'y') and finds the top + // of the layout box is bypassed resulting in wrongly placed text (we layout the text + // relative to the top of the box as this is required for text-in-a-shape). We don't + // know how to find the top of the box from the 'y' position until we have found the + // line height parameters for the given line (after calling _findChunksForLine() just + // above). + if (para.first_input_index == 0 && (_flow.wrap_mode == WRAP_NONE)) { + + // Calculate new top of box... given specified baseline. + double top_of_line_box = _scanline_maker->yCoordinate(); // Set in constructor. + if( _block_progression == RIGHT_TO_LEFT ) { + // Vertical text, use em box center as baseline + top_of_line_box += 0.5 * line_box_height.emSize(); + } else if (_block_progression == LEFT_TO_RIGHT ) { + // Vertical text, use em box center as baseline + top_of_line_box -= 0.5 * line_box_height.emSize(); + } else { + top_of_line_box -= line_box_height.getTypoAscent(); + } + TRACE((" y attribute set, next line top_of_line_box: %f\n", top_of_line_box )); + // Set the initial y coordinate of the for this line (see above). + _scanline_maker->setNewYCoordinate(top_of_line_box); + } + + // !keep_going --> truncated --> hidden + _outputLine(para, line_box_height, line_chunk_info, !keep_going); + + _scanline_maker->setLineHeight( line_box_height ); + _scanline_maker->completeLine(); // Increments y by line height + TRACE(("end line\n")); + } while (span_pos.iter_span != para.unbroken_spans.end()); + + TRACE(("para %lu end\n\n", _flow._paragraphs.size() - 1)); + if (keep_going) { + // We have more to do, setup next section. + bool is_empty_para = _flow._characters.empty() || _flow._characters.back().line(&_flow).in_paragraph != _flow._paragraphs.size() - 1; + if ((is_empty_para && para_end_input_index + 1 >= _flow._input_stream.size()) + || para_end_input_index + 1 < _flow._input_stream.size()) { + // we need a span just for the para if it's either an empty last para or a break in the middle + Layout::Span new_span; + if (_flow._spans.empty()) { + new_span.font = nullptr; + new_span.font_size = line_box_height.emSize(); + new_span.line_height = line_box_height; + new_span.x_end = 0.0; + } else { + new_span = _flow._spans.back(); + if (_flow._chunks[new_span.in_chunk].in_line != _flow._lines.size() - 1) + new_span.x_end = 0.0; + } + new_span.in_chunk = _flow._chunks.size() - 1; + if (new_span.font) + new_span.font->Ref(); + new_span.x_start = new_span.x_end; + new_span.baseline_shift = 0.0; + new_span.direction = para.direction; + new_span.block_progression = _block_progression; + if (para_end_input_index == _flow._input_stream.size()) + new_span.in_input_stream_item = _flow._input_stream.size() - 1; + else + new_span.in_input_stream_item = para_end_input_index; + _flow._spans.push_back(new_span); + } + if (para_end_input_index + 1 < _flow._input_stream.size()) { + // we've got to add an invisible character between paragraphs so that we can position iterators + // (and hence cursors) both before and after the paragraph break + Layout::Character new_character; + new_character.the_char = '@'; + new_character.in_span = _flow._spans.size() - 1; + new_character.char_attributes.is_line_break = 1; + new_character.char_attributes.is_mandatory_break = 1; + new_character.char_attributes.is_char_break = 1; + new_character.char_attributes.is_white = 1; + new_character.char_attributes.is_cursor_position = 1; + new_character.char_attributes.is_word_start = 0; + new_character.char_attributes.is_word_end = 1; + new_character.char_attributes.is_sentence_start = 0; + new_character.char_attributes.is_sentence_end = 1; + new_character.char_attributes.is_sentence_boundary = 1; + new_character.char_attributes.backspace_deletes_character = 1; + new_character.x = _flow._spans.back().x_end - _flow._spans.back().x_start; + new_character.in_glyph = -1; + _flow._characters.push_back(new_character); + } + } + // dumpPangoItemsOut(¶); + // dumpUnbrokenSpans(¶); + + para.free(); + para.first_input_index = para_end_input_index + 1; + } // Loop over paras + + para.free(); + if (_scanline_maker) { + delete _scanline_maker; + } + + _flow._input_truncated = !keep_going; + + if (_flow.textLength._set) { + // Calculate the adjustment needed to meet the textLength + double actual_length = _flow.getActualLength(); + double difference = _flow.textLength.computed - actual_length; + _flow.textLengthMultiplier = (actual_length + difference) / actual_length; + _flow.textLengthIncrement = difference / (_flow._characters.size() == 1? 1 : _flow._characters.size() - 1); + } + + return true; +} + +void Layout::_calculateCursorShapeForEmpty() +{ + _empty_cursor_shape.position = Geom::Point(0, 0); + _empty_cursor_shape.height = 0.0; + _empty_cursor_shape.rotation = 0.0; + if (_input_stream.empty() || _input_stream.front()->Type() != TEXT_SOURCE) + return; + + InputStreamTextSource const *text_source = static_cast(_input_stream.front()); + + font_instance *font = text_source->styleGetFontInstance(); + double font_size = text_source->style->font_size.computed; + double caret_slope_run = 0.0, caret_slope_rise = 1.0; + FontMetrics line_height; + if (font) { + const_cast(font)->FontSlope(caret_slope_run, caret_slope_rise); + font->FontMetrics(line_height.ascent, line_height.descent, line_height.xheight); + line_height *= font_size; + font->Unref(); + } + + double caret_slope = atan2(caret_slope_run, caret_slope_rise); + _empty_cursor_shape.height = font_size / cos(caret_slope); + _empty_cursor_shape.rotation = caret_slope; + + if (_input_wrap_shapes.empty()) { + _empty_cursor_shape.position = Geom::Point(text_source->x.empty() || !text_source->x.front()._set ? 0.0 : text_source->x.front().computed, + text_source->y.empty() || !text_source->y.front()._set ? 0.0 : text_source->y.front().computed); + } else if (wrap_mode == WRAP_INLINE_SIZE) { + // 'inline-size' has a wrap shape of an "infinite" rectangle, we need the place where the text should begin. + double x = 0; + double y = 0; + if (!text_source->x.empty()) + x = text_source->x.front().computed; + if (!text_source->y.empty()) + y = text_source->y.front().computed; + _empty_cursor_shape.position = Geom::Point(x, y); + } else { + Direction block_progression = text_source->styleGetBlockProgression(); + ShapeScanlineMaker scanline_maker(_input_wrap_shapes.front().shape, block_progression); + std::vector scan_runs = scanline_maker.makeScanline(line_height); + if (!scan_runs.empty()) { + if (block_progression == LEFT_TO_RIGHT || block_progression == RIGHT_TO_LEFT) { + // Vertical text + _empty_cursor_shape.position = Geom::Point(scan_runs.front().y + font_size, scan_runs.front().x_start); + } else { + // Horizontal text + _empty_cursor_shape.position = Geom::Point(scan_runs.front().x_start, scan_runs.front().y + font_size); + } + } + } +} + +bool Layout::calculateFlow() +{ + TRACE(("begin calculateFlow()\n")); + Layout::Calculator calc = Calculator(this); + bool result = calc.calculate(); + + if (textLengthIncrement != 0) { + TRACE(("Recalculating layout the second time to fit textLength!\n")); + result = calc.calculate(); + } + + if (_characters.empty()) { + _calculateCursorShapeForEmpty(); + } + return result; +} + +}//namespace Text +}//namespace Inkscape + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/libnrtype/Layout-TNG-Input.cpp b/src/libnrtype/Layout-TNG-Input.cpp new file mode 100644 index 0000000..cc7d02a --- /dev/null +++ b/src/libnrtype/Layout-TNG-Input.cpp @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::Text::Layout - text layout engine input functions + * + * Authors: + * Richard Hughes + * + * Copyright (C) 2005 Richard Hughes + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#ifndef PANGO_ENABLE_ENGINE +#define PANGO_ENABLE_ENGINE +#endif + +#include "Layout-TNG.h" +#include "style.h" +#include "svg/svg-length.h" +#include "FontFactory.h" + + +namespace Inkscape { +namespace Text { + +void Layout::_clearInputObjects() +{ + for(auto & it : _input_stream) { + delete it; + } + + _input_stream.clear(); + _input_wrap_shapes.clear(); +} + +// this function does nothing more than store all its parameters for future reference +void Layout::appendText(Glib::ustring const &text, + SPStyle *style, + SPObject *source, + OptionalTextTagAttrs const *optional_attributes, + unsigned optional_attributes_offset, + Glib::ustring::const_iterator text_begin, + Glib::ustring::const_iterator text_end) +{ + if (style == nullptr) return; + + InputStreamTextSource *new_source = new InputStreamTextSource; + + new_source->source = source; + new_source->text = &text; + new_source->text_begin = text_begin; + new_source->text_end = text_end; + new_source->style = style; + sp_style_ref(style); + + new_source->text_length = 0; + for ( ; text_begin != text_end && text_begin != text.end() ; ++text_begin) + new_source->text_length++; // save this because calculating the length of a UTF-8 string is expensive + + if (optional_attributes) { + // we need to fill in x and y even if the text is empty so that empty paragraphs can be positioned correctly + _copyInputVector(optional_attributes->x, optional_attributes_offset, &new_source->x, std::max(1, new_source->text_length)); + _copyInputVector(optional_attributes->y, optional_attributes_offset, &new_source->y, std::max(1, new_source->text_length)); + _copyInputVector(optional_attributes->dx, optional_attributes_offset, &new_source->dx, new_source->text_length); + _copyInputVector(optional_attributes->dy, optional_attributes_offset, &new_source->dy, new_source->text_length); + _copyInputVector(optional_attributes->rotate, optional_attributes_offset, &new_source->rotate, new_source->text_length); + if (!optional_attributes->rotate.empty() && optional_attributes_offset >= optional_attributes->rotate.size()) { + SVGLength last_rotate; + last_rotate = 0.f; + for (auto it : optional_attributes->rotate) + if (it._set) + last_rotate = it; + new_source->rotate.resize(1, last_rotate); + } + new_source->textLength._set = optional_attributes->textLength._set; + new_source->textLength.value = optional_attributes->textLength.value; + new_source->textLength.computed = optional_attributes->textLength.computed; + new_source->textLength.unit = optional_attributes->textLength.unit; + new_source->lengthAdjust = optional_attributes->lengthAdjust; + } + + _input_stream.push_back(new_source); +} + +void Layout::_copyInputVector(std::vector const &input_vector, unsigned input_offset, std::vector *output_vector, size_t max_length) +{ + output_vector->clear(); + if (input_offset >= input_vector.size()) return; + output_vector->reserve(std::min(max_length, input_vector.size() - input_offset)); + while (input_offset < input_vector.size() && max_length != 0) { + if (!input_vector[input_offset]._set) + break; + output_vector->push_back(input_vector[input_offset]); + input_offset++; + max_length--; + } +} + +// just save what we've been given, really +void Layout::appendControlCode(TextControlCode code, SPObject *source, double width, double ascent, double descent) +{ + InputStreamControlCode *new_code = new InputStreamControlCode; + + new_code->source = source; + new_code->code = code; + new_code->width = width; + new_code->ascent = ascent; + new_code->descent = descent; + + _input_stream.push_back(new_code); +} + +// more saving of the parameters +void Layout::appendWrapShape(Shape const *shape, DisplayAlign display_align) +{ + _input_wrap_shapes.emplace_back(); + _input_wrap_shapes.back().shape = shape; + _input_wrap_shapes.back().display_align = display_align; +} + +Layout::Direction Layout::InputStreamTextSource::styleGetBlockProgression() const +{ + switch( style->writing_mode.computed ) { + case SP_CSS_WRITING_MODE_LR_TB: + case SP_CSS_WRITING_MODE_RL_TB: + return TOP_TO_BOTTOM; + + case SP_CSS_WRITING_MODE_TB_RL: + return RIGHT_TO_LEFT; + + case SP_CSS_WRITING_MODE_TB_LR: + return LEFT_TO_RIGHT; + + default: + std::cerr << "Layout::InputTextStream::styleGetBlockProgression: invalid writing mode." << std::endl; + } + return TOP_TO_BOTTOM; +} + +SPCSSTextOrientation Layout::InputStreamTextSource::styleGetTextOrientation() const +{ + return ((SPCSSTextOrientation)style->text_orientation.computed); +} + +SPCSSBaseline Layout::InputStreamTextSource::styleGetDominantBaseline() const +{ + return ((SPCSSBaseline)style->dominant_baseline.computed); +} + +static Layout::Alignment text_anchor_to_alignment(unsigned anchor, Layout::Direction para_direction) +{ + switch (anchor) { + default: + case SP_CSS_TEXT_ANCHOR_START: return para_direction == Layout::LEFT_TO_RIGHT ? Layout::LEFT : Layout::RIGHT; + case SP_CSS_TEXT_ANCHOR_MIDDLE: return Layout::CENTER; + case SP_CSS_TEXT_ANCHOR_END: return para_direction == Layout::LEFT_TO_RIGHT ? Layout::RIGHT : Layout::LEFT; + } +} + +Layout::Alignment Layout::InputStreamTextSource::styleGetAlignment(Layout::Direction para_direction, bool try_text_align) const +{ + if (!try_text_align) + return text_anchor_to_alignment(style->text_anchor.computed, para_direction); + + // there's no way to tell the difference between text-anchor set higher up the cascade to the default and + // text-anchor never set anywhere in the cascade, so in order to detect which of text-anchor or text-align + // to use we'll have to run up the style tree ourselves. + SPStyle const *this_style = style; + + for ( ; ; ) { + // If both text-align and text-anchor are set at the same level, text-align takes + // precedence because it is the most expressive. + if (this_style->text_align.set) { + switch (style->text_align.computed) { + default: + case SP_CSS_TEXT_ALIGN_START: return para_direction == LEFT_TO_RIGHT ? LEFT : RIGHT; + case SP_CSS_TEXT_ALIGN_END: return para_direction == LEFT_TO_RIGHT ? RIGHT : LEFT; + case SP_CSS_TEXT_ALIGN_LEFT: return LEFT; + case SP_CSS_TEXT_ALIGN_RIGHT: return RIGHT; + case SP_CSS_TEXT_ALIGN_CENTER: return CENTER; + case SP_CSS_TEXT_ALIGN_JUSTIFY: return FULL; + } + } + if (this_style->text_anchor.set) + return text_anchor_to_alignment(this_style->text_anchor.computed, para_direction); + if (this_style->object == nullptr || this_style->object->parent == nullptr) break; + this_style = this_style->object->parent->style; + if (this_style == nullptr) break; + } + return para_direction == LEFT_TO_RIGHT ? LEFT : RIGHT; +} + +font_instance *Layout::InputStreamTextSource::styleGetFontInstance() const +{ + PangoFontDescription *descr = styleGetFontDescription(); + if (descr == nullptr) return nullptr; + font_instance *res = (font_factory::Default())->Face(descr); + pango_font_description_free(descr); + return res; +} + +PangoFontDescription *Layout::InputStreamTextSource::styleGetFontDescription() const +{ + // This use to be done by code here but it duplicated more complete code in FontFactory.cpp. + PangoFontDescription *descr = ink_font_description_from_style( style ); + + // Font size not yet set +#ifdef USE_PANGO_WIN32 + + // Damn Pango fudges the size, so we need to unfudge. See source of pango_win32_font_map_init() + pango_font_description_set_size(descr, + (int) ((font_factory::Default())->fontSize*PANGO_SCALE*72 / GetDeviceCaps(pango_win32_get_dc(),LOGPIXELSY)) + ); + + // We unset stretch on Win32, because pango-win32 has no concept of it + // (Windows doesn't really provide any useful field it could use). + // If we did set stretch, then any text with a font-stretch attribute would + // end up falling back to a default. + pango_font_description_unset_fields(descr, PANGO_FONT_MASK_STRETCH); + +#else + + // mandatory huge size (hinting workaround) + pango_font_description_set_size(descr, (int) ((font_factory::Default())->fontSize*PANGO_SCALE)); + +#endif + + return descr; +} + +Layout::InputStreamTextSource::~InputStreamTextSource() +{ + sp_style_unref(style); +} + +}//namespace Text +}//namespace Inkscape diff --git a/src/libnrtype/Layout-TNG-OutIter.cpp b/src/libnrtype/Layout-TNG-OutIter.cpp new file mode 100644 index 0000000..dd2123e --- /dev/null +++ b/src/libnrtype/Layout-TNG-OutIter.cpp @@ -0,0 +1,1175 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::Text::Layout - text layout engine output functions using iterators + * + * Authors: + * Richard Hughes + * + * Copyright (C) 2005 Richard Hughes + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "Layout-TNG.h" +#include "livarot/Path.h" +#include "font-instance.h" +#include "svg/svg-length.h" +#include <2geom/transforms.h> +#include <2geom/line.h> +#include "style.h" + +namespace Inkscape { +namespace Text { + +// Comment 18 Sept 2019: +// Cursor code might be simpler if Character was turned into a proper +// class and kept track of its absolute position and extent. This would +// make handling multi-line text (including multi-line text using +// 'white-space:pre') easier. This would also avoid problems where +// 'dx','dy' moved the character a long distance from its nominal +// position. + +Layout::iterator Layout::_cursorXOnLineToIterator(unsigned line_index, double local_x, double local_y) const +{ + unsigned char_index = _lineToCharacter(line_index); + int best_char_index = -1; + double best_difference = DBL_MAX; + + if (char_index == _characters.size()) return end(); + for ( ; char_index < _characters.size() ; char_index++) { + if (_characters[char_index].chunk(this).in_line != line_index) break; + //if (_characters[char_index].char_attributes.is_mandatory_break) break; + if (!_characters[char_index].char_attributes.is_cursor_position) continue; + + double delta_x = + _characters[char_index].x + + _characters[char_index].span(this).x_start + + _characters[char_index].chunk(this).left_x - + local_x; + + double delta_y = + _characters[char_index].span(this).y_offset + + _characters[char_index].line(this).baseline_y - + local_y; + + double this_difference = std::sqrt(delta_x*delta_x + delta_y*delta_y); + + if (this_difference < best_difference) { + best_difference = this_difference; + best_char_index = char_index; + } + } + + // also try the very end of a para (not lines though because the space wraps) + if (char_index == _characters.size() || _characters[char_index].char_attributes.is_mandatory_break) { + + double delta_x = 0.0; + double delta_y = 0.0; + + if (char_index == 0) { + delta_x = _spans.front().x_end + _chunks.front().left_x - local_x; + delta_y = _spans.front().y_offset + _spans.front().line(this).baseline_y - local_y; + } else { + delta_x = _characters[char_index - 1].span(this).x_end + _characters[char_index - 1].chunk(this).left_x - local_x; + delta_y = _characters[char_index - 1].span(this).y_offset + _characters[char_index - 1].line(this).baseline_y - local_y; + } + + double this_difference = std::sqrt(delta_x*delta_x + delta_y*delta_y); + + if (this_difference < best_difference) { + best_char_index = char_index; + best_difference = this_difference; + } + } + + + if (best_char_index == -1) { + best_char_index = char_index; + } + + if (best_char_index == _characters.size()) { + return end(); + } + + return iterator(this, best_char_index); +} + +double Layout::_getChunkWidth(unsigned chunk_index) const +{ + double chunk_width = 0.0; + unsigned span_index; + if (chunk_index) { + span_index = _lineToSpan(_chunks[chunk_index].in_line); + for ( ; span_index < _spans.size() && _spans[span_index].in_chunk < chunk_index ; span_index++){}; + } else { + span_index = 0; + } + + for ( ; span_index < _spans.size() && _spans[span_index].in_chunk == chunk_index ; span_index++) { + chunk_width = std::max(chunk_width, (double)std::max(_spans[span_index].x_start, _spans[span_index].x_end)); + } + + return chunk_width; +} + +/* getting the cursor position for a mouse click is not as simple as it might +seem. The two major problems are flows set up in multiple columns and large +dy adjustments such that text does not belong to the line it appears to. In +the worst case it's possible to have two characters on top of each other, in +which case the one we pick is arbitrary. + +This is a 3-stage (2 pass) algorithm: +1) search all the spans to see if the point is contained in one, if so take + that. Note that this will collect all clicks from the current UI because + of how the hit detection of nrarena objects works. +2) if that fails, run through all the chunks finding a best guess of the one + the user wanted. This is the one whose y coordinate is nearest, or if + there's a tie, the x. +3) search in that chunk using x-coordinate only to find the position. +*/ +Layout::iterator Layout::getNearestCursorPositionTo(double x, double y) const +{ + if (_lines.empty()) return begin(); + double local_x = x; + double local_y = y; + + if (_path_fitted) { + Path::cut_position position = const_cast(_path_fitted)->PointToCurvilignPosition(Geom::Point(x, y)); + local_x = const_cast(_path_fitted)->PositionToLength(position.piece, position.t); + return _cursorXOnLineToIterator(0, local_x + _chunks.front().left_x); + } + + if (_directions_are_orthogonal(_blockProgression(), TOP_TO_BOTTOM)) { + local_x = y; + local_y = x; + } + + // stage 1: + for (const auto & _span : _spans) { + double span_left, span_right; + if (_span.x_start < _span.x_end) { + span_left = _span.x_start; + span_right = _span.x_end; + } else { + span_left = _span.x_end; + span_right = _span.x_start; + } + + double y_line = _span.line(this).baseline_y + _span.baseline_shift + _span.y_offset; + if ( local_x >= _chunks[_span.in_chunk].left_x + span_left + && local_x <= _chunks[_span.in_chunk].left_x + span_right + && local_y >= y_line - _span.line_height.ascent + && local_y <= y_line + _span.line_height.descent) { + return _cursorXOnLineToIterator(_chunks[_span.in_chunk].in_line, local_x, local_y); + } + } + + // stage 2: + unsigned span_index = 0; + unsigned chunk_index; + int best_chunk_index = -1; + double best_y_range = DBL_MAX; + double best_x_range = DBL_MAX; + for (chunk_index = 0 ; chunk_index < _chunks.size() ; chunk_index++) { + FontMetrics line_height; + line_height *= 0.0; // Set all metrics to zero. + double chunk_width = 0.0; + for ( ; span_index < _spans.size() && _spans[span_index].in_chunk == chunk_index ; span_index++) { + line_height.max(_spans[span_index].line_height); + chunk_width = std::max(chunk_width, (double)std::max(_spans[span_index].x_start, _spans[span_index].x_end)); + } + double this_y_range; + if (local_y < _lines[_chunks[chunk_index].in_line].baseline_y - line_height.ascent) + this_y_range = _lines[_chunks[chunk_index].in_line].baseline_y - line_height.ascent - local_y; + else if (local_y > _lines[_chunks[chunk_index].in_line].baseline_y + line_height.descent) + this_y_range = local_y - (_lines[_chunks[chunk_index].in_line].baseline_y + line_height.descent); + else + this_y_range = 0.0; + if (this_y_range <= best_y_range) { + if (this_y_range < best_y_range) best_x_range = DBL_MAX; + double this_x_range; + if (local_x < _chunks[chunk_index].left_x) + this_x_range = _chunks[chunk_index].left_x - local_y; + else if (local_x > _chunks[chunk_index].left_x + chunk_width) + this_x_range = local_x - (_chunks[chunk_index].left_x + chunk_width); + else + this_x_range = 0.0; + if (this_x_range < best_x_range) { + best_y_range = this_y_range; + best_x_range = this_x_range; + best_chunk_index = chunk_index; + } + } + } + + // stage 3: + if (best_chunk_index == -1) return begin(); // never happens + return _cursorXOnLineToIterator(_chunks[best_chunk_index].in_line, local_x, local_y); +} + +Layout::iterator Layout::getLetterAt(double x, double y) const +{ + Geom::Point point(x, y); + + double rotation; + for (iterator it = begin() ; it != end() ; it.nextCharacter()) { + Geom::Rect box = characterBoundingBox(it, &rotation); + // todo: rotation + if (box.contains(point)) return it; + } + return end(); +} + +Layout::iterator Layout::sourceToIterator(SPObject *source /*, Glib::ustring::const_iterator text_iterator*/) const +{ + unsigned source_index; + if (_characters.empty()) return end(); + for (source_index = 0 ; source_index < _input_stream.size() ; source_index++) + if (_input_stream[source_index]->source == source) break; + if (source_index == _input_stream.size()) return end(); + + unsigned char_index = _sourceToCharacter(source_index); + + // Fix a bug when hidding content in flow box element + if (char_index >= _characters.size()) + return end(); + + if (_input_stream[source_index]->Type() != TEXT_SOURCE) + return iterator(this, char_index); + + return iterator(this, char_index); + /* This code was never used, the text_iterator argument was "NULL" in all calling code + InputStreamTextSource const *text_source = static_cast(_input_stream[source_index]); + + if (text_iterator <= text_source->text_begin) return iterator(this, char_index); + if (text_iterator >= text_source->text_end) { + if (source_index == _input_stream.size() - 1) return end(); + return iterator(this, _sourceToCharacter(source_index + 1)); + } + Glib::ustring::const_iterator iter_text = text_source->text_begin; + for ( ; char_index < _characters.size() ; char_index++) { + if (iter_text == text_iterator) + return iterator(this, char_index); + iter_text++; + } + return end(); // never happens + */ +} + +Geom::OptRect Layout::glyphBoundingBox(iterator const &it, double *rotation) const +{ + if (rotation) *rotation = _glyphs[it._glyph_index].rotation; + return _glyphs[it._glyph_index].span(this).font->BBox(_glyphs[it._glyph_index].glyph); +} + +Geom::Point Layout::characterAnchorPoint(iterator const &it) const +{ + if (_characters.empty()) + return _empty_cursor_shape.position; + + Geom::Point res; + if (it._char_index == _characters.size()) { + res = Geom::Point(_chunks.back().left_x + _spans.back().x_end, _lines.back().baseline_y + _spans.back().baseline_shift); + } else { + res = Geom::Point(_characters[it._char_index].chunk(this).left_x + + _spans[_characters[it._char_index].in_span].x_start + + _characters[it._char_index].x, + _characters[it._char_index].line(this).baseline_y + + _characters[it._char_index].span(this).baseline_shift); + } + if (_directions_are_orthogonal(_blockProgression(), TOP_TO_BOTTOM)) { + std::swap(res[Geom::X], res[Geom::Y]); + } + return res; +} + +std::optional Layout::baselineAnchorPoint() const +{ + iterator pos = this->begin(); + Geom::Point left_pt = this->characterAnchorPoint(pos); + pos.thisEndOfLine(); + Geom::Point right_pt = this->characterAnchorPoint(pos); + + switch (this->paragraphAlignment(pos)) { + case LEFT: + case FULL: + return left_pt; + break; + case CENTER: + return (left_pt + right_pt)/2; // middle point + break; + case RIGHT: + return right_pt; + break; + default: + return std::optional(); + break; + } +} + +Geom::Path Layout::baseline() const +{ + iterator pos = this->begin(); + Geom::Point left_pt = this->characterAnchorPoint(pos); + pos.thisEndOfLine(); + Geom::Point right_pt = this->characterAnchorPoint(pos); + + Geom::Path baseline; + baseline.start(left_pt); + baseline.appendNew(right_pt); + + return baseline; +} + + +Geom::Point Layout::chunkAnchorPoint(iterator const &it) const +{ + unsigned chunk_index; + + if (_chunks.empty()) + return Geom::Point(0.0, 0.0); + + if (_characters.empty()) + chunk_index = 0; + else if (it._char_index == _characters.size()) + chunk_index = _chunks.size() - 1; + else chunk_index = _characters[it._char_index].span(this).in_chunk; + + Alignment alignment = _paragraphs[_lines[_chunks[chunk_index].in_line].in_paragraph].alignment; + double x = _chunks[chunk_index].left_x; + double y = _lines[_chunks[chunk_index].in_line].baseline_y; + double chunk_width = _getChunkWidth(chunk_index); + if (alignment == RIGHT) { + x += chunk_width; + } else if (alignment == CENTER) { + x += chunk_width * 0.5; + } + + if (_directions_are_orthogonal(_blockProgression(), TOP_TO_BOTTOM)) { + return Geom::Point(y, x); + } else { + return Geom::Point(x, y); + } +} + +Geom::Rect Layout::characterBoundingBox(iterator const &it, double *rotation) const +{ + Geom::Point top_left, bottom_right; + unsigned char_index = it._char_index; + + if (_path_fitted) { + double cluster_half_width = 0.0; + for (int glyph_index = _characters[char_index].in_glyph ; _glyphs.size() != glyph_index ; glyph_index++) { + if (_glyphs[glyph_index].in_character != char_index) break; + cluster_half_width += _glyphs[glyph_index].advance; + } + cluster_half_width *= 0.5; + + double midpoint_offset = _characters[char_index].span(this).x_start + _characters[char_index].x + cluster_half_width; + int unused = 0; + Path::cut_position *midpoint_otp = const_cast(_path_fitted)->CurvilignToPosition(1, &midpoint_offset, unused); + if (midpoint_offset >= 0.0 && midpoint_otp != nullptr && midpoint_otp[0].piece >= 0) { + Geom::Point midpoint; + Geom::Point tangent; + Span const &span = _characters[char_index].span(this); + + const_cast(_path_fitted)->PointAndTangentAt(midpoint_otp[0].piece, midpoint_otp[0].t, midpoint, tangent); + top_left[Geom::X] = midpoint[Geom::X] - cluster_half_width; + top_left[Geom::Y] = midpoint[Geom::Y] - span.line_height.ascent; + bottom_right[Geom::X] = midpoint[Geom::X] + cluster_half_width; + bottom_right[Geom::Y] = midpoint[Geom::Y] + span.line_height.descent; + Geom::Point normal = tangent.cw(); + top_left += span.baseline_shift * normal; + bottom_right += span.baseline_shift * normal; + if (rotation) + *rotation = atan2(tangent[1], tangent[0]); + } + g_free(midpoint_otp); + } else { + if (it._char_index == _characters.size()) { + top_left[Geom::X] = bottom_right[Geom::X] = _chunks.back().left_x + _spans.back().x_end; + char_index--; + } else { + double span_x = _spans[_characters[it._char_index].in_span].x_start + _characters[it._char_index].chunk(this).left_x; + top_left[Geom::X] = span_x + _characters[it._char_index].x; + if (it._char_index + 1 == _characters.size() || _characters[it._char_index + 1].in_span != _characters[it._char_index].in_span) + bottom_right[Geom::X] = _spans[_characters[it._char_index].in_span].x_end + _characters[it._char_index].chunk(this).left_x; + else + bottom_right[Geom::X] = span_x + _characters[it._char_index + 1].x; + } + + double baseline_y = _characters[char_index].line(this).baseline_y + _characters[char_index].span(this).baseline_shift; + if (_directions_are_orthogonal(_blockProgression(), TOP_TO_BOTTOM)) { + double span_height = _spans[_characters[char_index].in_span].line_height.emSize(); + top_left[Geom::Y] = top_left[Geom::X]; + top_left[Geom::X] = baseline_y - span_height * 0.5; + bottom_right[Geom::Y] = bottom_right[Geom::X]; + bottom_right[Geom::X] = baseline_y + span_height * 0.5; + } else { + top_left[Geom::Y] = baseline_y - _spans[_characters[char_index].in_span].line_height.ascent; + bottom_right[Geom::Y] = baseline_y + _spans[_characters[char_index].in_span].line_height.descent; + } + + if (rotation) { + if (it._glyph_index == -1) + *rotation = 0.0; + else if (it._glyph_index == (int)_glyphs.size()) + *rotation = _glyphs.back().rotation; + else + *rotation = _glyphs[it._glyph_index].rotation; + } + } + + return Geom::Rect(top_left, bottom_right); +} + +std::vector Layout::createSelectionShape(iterator const &it_start, iterator const &it_end, Geom::Affine const &transform) const +{ + std::vector quads; + unsigned char_index; + unsigned end_char_index; + + if (it_start._char_index < it_end._char_index) { + char_index = it_start._char_index; + end_char_index = it_end._char_index; + } else { + char_index = it_end._char_index; + end_char_index = it_start._char_index; + } + for ( ; char_index < end_char_index ; ) { + if (_characters[char_index].in_glyph == -1) { + char_index++; + continue; + } + double char_rotation = _glyphs[_characters[char_index].in_glyph].rotation; + unsigned span_index = _characters[char_index].in_span; + + Geom::Point top_left, bottom_right; + if (_path_fitted || char_rotation != 0.0) { + Geom::Rect box = characterBoundingBox(iterator(this, char_index), &char_rotation); + top_left = box.min(); + bottom_right = box.max(); + char_index++; + } else { // for straight text we can be faster by combining all the character boxes in a span into one box + double span_x = _spans[span_index].x_start + _spans[span_index].chunk(this).left_x; + top_left[Geom::X] = span_x + _characters[char_index].x; + while (char_index < end_char_index && _characters[char_index].in_span == span_index) + char_index++; + if (char_index == _characters.size() || _characters[char_index].in_span != span_index) + bottom_right[Geom::X] = _spans[span_index].x_end + _spans[span_index].chunk(this).left_x; + else + bottom_right[Geom::X] = span_x + _characters[char_index].x; + + double baseline_y = _spans[span_index].line(this).baseline_y + _spans[span_index].baseline_shift; + double vertical_scale = _glyphs.back().vertical_scale; + double offset_y = _spans[span_index].y_offset; + + if (_directions_are_orthogonal(_blockProgression(), TOP_TO_BOTTOM)) { + double span_height = vertical_scale * _spans[span_index].line_height.emSize(); + top_left[Geom::Y] = top_left[Geom::X]; + top_left[Geom::X] = offset_y + baseline_y - span_height * 0.5; + bottom_right[Geom::Y] = bottom_right[Geom::X]; + bottom_right[Geom::X] = offset_y + baseline_y + span_height * 0.5; + } else { + top_left[Geom::Y] = offset_y + baseline_y - vertical_scale * _spans[span_index].line_height.ascent; + bottom_right[Geom::Y] = offset_y + baseline_y + vertical_scale * _spans[span_index].line_height.descent; + } + } + + Geom::Rect char_box(top_left, bottom_right); + if (char_box.dimensions()[Geom::X] == 0.0 || char_box.dimensions()[Geom::Y] == 0.0) + continue; + Geom::Point center_of_rotation((top_left[Geom::X] + bottom_right[Geom::X]) * 0.5, + top_left[Geom::Y] + _spans[span_index].line_height.ascent); + Geom::Affine total_transform = Geom::Translate(-center_of_rotation) * Geom::Rotate(char_rotation) * Geom::Translate(center_of_rotation) * transform; + for(int i = 0; i < 4; i ++) + quads.push_back(char_box.corner(i) * total_transform); + } + return quads; +} + +void Layout::queryCursorShape(iterator const &it, Geom::Point &position, double &height, double &rotation) const +{ + if (_characters.empty()) { + position = _empty_cursor_shape.position; + height = _empty_cursor_shape.height; + rotation = _empty_cursor_shape.rotation; + } else { + // we want to cursor to be positioned where the left edge of a character that is about to be typed will be. + // this means x & rotation are the current values but y & height belong to the previous character. + // this isn't quite right because dx attributes will be moved along, but it's good enough + Span const *span; + bool vertical_text = _directions_are_orthogonal(_blockProgression(), TOP_TO_BOTTOM); + if (_path_fitted) { + // text on a path + double x; + if (it._char_index >= _characters.size()) { + span = &_spans.back(); + x = span->x_end + _chunks.back().left_x - _chunks[0].left_x; + } else { + span = &_spans[_characters[it._char_index].in_span]; + x = _chunks[span->in_chunk].left_x + span->x_start + _characters[it._char_index].x - _chunks[0].left_x; + if (vertical_text) + x -= span->line_height.descent; + if (it._char_index != 0) + span = &_spans[_characters[it._char_index - 1].in_span]; + } + double path_length = const_cast(_path_fitted)->Length(); + double x_on_path = x; + if (x_on_path < 0.0) x_on_path = 0.0; + + int unused = 0; + // as far as I know these functions are const, they're just not marked as such + Path::cut_position *path_parameter_list = const_cast(_path_fitted)->CurvilignToPosition(1, &x_on_path, unused); + Path::cut_position path_parameter; + if (path_parameter_list != nullptr && path_parameter_list[0].piece >= 0) + path_parameter = path_parameter_list[0]; + else { + path_parameter.piece = _path_fitted->descr_cmd.size() - 1; + path_parameter.t = 0.9999; // 1.0 will get the wrong tangent + } + g_free(path_parameter_list); + + Geom::Point point; + Geom::Point tangent; + const_cast(_path_fitted)->PointAndTangentAt(path_parameter.piece, path_parameter.t, point, tangent); + if (x < 0.0) + point += x * tangent; + if (x > path_length ) + point += (x - path_length) * tangent; + if (vertical_text) { + rotation = atan2(-tangent[Geom::X], tangent[Geom::Y]); + position[Geom::X] = point[Geom::Y] - tangent[Geom::X] * span->baseline_shift; + position[Geom::Y] = point[Geom::X] + tangent[Geom::Y] * span->baseline_shift; + } else { + rotation = atan2(tangent); + position[Geom::X] = point[Geom::X] - tangent[Geom::Y] * span->baseline_shift; + position[Geom::Y] = point[Geom::Y] + tangent[Geom::X] * span->baseline_shift; + } + + } else { + // text is not on a path + + bool last_char_is_newline = false; + if (it._char_index >= _characters.size()) { + span = &_spans.back(); + position[Geom::X] = _chunks[span->in_chunk].left_x + span->x_end; + rotation = _glyphs.empty() ? 0.0 : _glyphs.back().rotation; + + // Check if last character is new line. + if (_characters.back().the_char == '\n') { + last_char_is_newline = true; + position[Geom::X] = chunkAnchorPoint(it)[vertical_text ? Geom::Y : Geom::X]; + } + } else { + span = &_spans[_characters[it._char_index].in_span]; + position[Geom::X] = _chunks[span->in_chunk].left_x + span->x_start + _characters[it._char_index].x; + if (it._glyph_index == -1) { + rotation = 0.0; + } else if(it._glyph_index == 0) { + rotation = _glyphs.empty() ? 0.0 : _glyphs[0].rotation; + } else{ + rotation = _glyphs[it._glyph_index - 1].rotation; + } + // the first char in a line wants to have the y of the new line, so in that case we don't switch to the previous span + if (it._char_index != 0 && _characters[it._char_index - 1].chunk(this).in_line == _chunks[span->in_chunk].in_line) + span = &_spans[_characters[it._char_index - 1].in_span]; + } + position[Geom::Y] = span->line(this).baseline_y + span->baseline_shift + span->y_offset; + + if (last_char_is_newline) { + // Move cursor to empty new line. + double vertical_scale = _glyphs.empty() ? 1.0 : _glyphs.back().vertical_scale; + if (vertical_text) { + // Vertical text + position[Geom::Y] -= vertical_scale * span->line_height.emSize(); + } else { + position[Geom::Y] += vertical_scale * span->line_height.emSize(); + } + } + } + + // up to now *position is the baseline point, not the final point which will be the bottom of the descent + double vertical_scale = _glyphs.empty() ? 1.0 : _glyphs.back().vertical_scale; + + if (vertical_text) { + // Vertical text + height = vertical_scale * span->line_height.emSize(); + rotation += M_PI / 2; + std::swap(position[Geom::X], position[Geom::Y]); + position[Geom::X] -= vertical_scale * sin(rotation) * height * 0.5; + position[Geom::Y] += vertical_scale * cos(rotation) * height * 0.5; + } else { + // Horizontal text + double caret_slope_run = 0.0, caret_slope_rise = 1.0; + if (span->font) + const_cast(span->font)->FontSlope(caret_slope_run, caret_slope_rise); + double caret_slope = atan2(caret_slope_run, caret_slope_rise); + height = vertical_scale * (span->line_height.emSize()) / cos(caret_slope); + rotation += caret_slope; + position[Geom::X] -= sin(rotation) * vertical_scale * span->line_height.descent; + position[Geom::Y] += cos(rotation) * vertical_scale * span->line_height.descent; + } + } +} + +bool Layout::isHidden(iterator const &it) const +{ + return _characters[it._char_index].line(this).hidden; +} + + +void Layout::getSourceOfCharacter(iterator const &it, SPObject **source, Glib::ustring::iterator *text_iterator) const +{ + if (it._char_index >= _characters.size()) { + *source = nullptr; + return; + } + InputStreamItem *stream_item = _input_stream[_spans[_characters[it._char_index].in_span].in_input_stream_item]; + *source = stream_item->source; + if (text_iterator && stream_item->Type() == TEXT_SOURCE) { + InputStreamTextSource *text_source = dynamic_cast(stream_item); + + // In order to return a non-const iterator in text_iterator, do the const_cast here. + // Note that, although ugly, it is safe because we do not write to *iterator anywhere. + Glib::ustring::iterator text_iter = const_cast(text_source->text)->begin(); + + unsigned char_index = it._char_index; + unsigned original_input_source_index = _spans[_characters[char_index].in_span].in_input_stream_item; + // confusing algorithm because the iterator goes forwards while the index goes backwards. + // It's just that it's faster doing it that way + while (char_index && _spans[_characters[char_index - 1].in_span].in_input_stream_item == original_input_source_index) { + ++text_iter; + char_index--; + } + + if (text_iterator) { + *text_iterator = text_iter; + } + } +} + +void Layout::simulateLayoutUsingKerning(iterator const &from, iterator const &to, OptionalTextTagAttrs *result) const +{ + SVGLength zero_length; + zero_length = 0.0; + + result->x.clear(); + result->y.clear(); + result->dx.clear(); + result->dy.clear(); + result->rotate.clear(); + if (to._char_index <= from._char_index) + return; + result->dx.reserve(to._char_index - from._char_index); + result->dy.reserve(to._char_index - from._char_index); + result->rotate.reserve(to._char_index - from._char_index); + for (unsigned char_index = from._char_index ; char_index < to._char_index ; char_index++) { + if (!_characters[char_index].char_attributes.is_char_break) + continue; + if (char_index == 0) + continue; + if (_characters[char_index].chunk(this).in_line != _characters[char_index - 1].chunk(this).in_line) + continue; + + unsigned prev_cluster_char_index; + for (prev_cluster_char_index = char_index - 1 ; + prev_cluster_char_index != 0 && !_characters[prev_cluster_char_index].char_attributes.is_cursor_position ; + prev_cluster_char_index--){}; + if (_characters[char_index].span(this).in_chunk == _characters[char_index - 1].span(this).in_chunk) { + // dx is zero for the first char in a chunk + // this algorithm works by comparing the summed widths of the glyphs with the observed + // difference in x coordinates of characters, and subtracting the two to produce the x kerning. + double glyphs_width = 0.0; + if (_characters[prev_cluster_char_index].in_glyph != -1) + for (int glyph_index = _characters[prev_cluster_char_index].in_glyph ; glyph_index < _characters[char_index].in_glyph ; glyph_index++) + glyphs_width += _glyphs[glyph_index].advance; + if (_characters[char_index].span(this).direction == RIGHT_TO_LEFT) + glyphs_width = -glyphs_width; + + double dx = (_characters[char_index].x + _characters[char_index].span(this).x_start + - _characters[prev_cluster_char_index].x - _characters[prev_cluster_char_index].span(this).x_start) + - glyphs_width; + + + InputStreamItem *input_item = _input_stream[_characters[char_index].span(this).in_input_stream_item]; + if (input_item->Type() == TEXT_SOURCE) { + SPStyle const *style = static_cast(input_item)->style; + if (_characters[char_index].char_attributes.is_white) + dx -= style->word_spacing.computed * getTextLengthMultiplierDue(); + if (_characters[char_index].char_attributes.is_cursor_position) + dx -= style->letter_spacing.computed * getTextLengthMultiplierDue(); + dx -= getTextLengthIncrementDue(); + } + + if (fabs(dx) > 0.0001) { + result->dx.resize(char_index - from._char_index + 1, zero_length); + result->dx.back() = dx; + } + } + double dy = _characters[char_index].span(this).baseline_shift - _characters[prev_cluster_char_index].span(this).baseline_shift; + if (fabs(dy) > 0.0001) { + result->dy.resize(char_index - from._char_index + 1, zero_length); + result->dy.back() = dy; + } + if (_characters[char_index].in_glyph != -1 && _glyphs[_characters[char_index].in_glyph].rotation != 0.0) { + result->rotate.resize(char_index - from._char_index + 1, zero_length); + result->rotate.back() = _glyphs[_characters[char_index].in_glyph].rotation; + } + } +} + +#define PREV_START_OF_ITEM(this_func) \ + { \ + _cursor_moving_vertically = false; \ + if (_char_index == 0) return false; \ + _char_index--; \ + return this_func(); \ + } +// end of macro + +#define THIS_START_OF_ITEM(item_getter) \ + { \ + _cursor_moving_vertically = false; \ + if (_char_index == 0) return false; \ + unsigned original_item; \ + if (_char_index == _parent_layout->_characters.size()) { \ + _char_index--; \ + original_item = item_getter; \ + } else { \ + original_item = item_getter; \ + _char_index--; \ + } \ + while (item_getter == original_item) { \ + if (_char_index == 0) { \ + _glyph_index = _parent_layout->_characters[_char_index].in_glyph; \ + return true; \ + } \ + _char_index--; \ + } \ + _char_index++; \ + _glyph_index = _parent_layout->_characters[_char_index].in_glyph; \ + return true; \ + } +// end of macro + +#define NEXT_START_OF_ITEM(item_getter) \ + { \ + _cursor_moving_vertically = false; \ + if (_char_index == _parent_layout->_characters.size()) return false; \ + unsigned original_item = item_getter; \ + for( ; ; ) { \ + _char_index++; \ + if (_char_index == _parent_layout->_characters.size()) { \ + _glyph_index = _parent_layout->_glyphs.size(); \ + return false; \ + } \ + if (item_getter != original_item) break; \ + } \ + _glyph_index = _parent_layout->_characters[_char_index].in_glyph; \ + return true; \ + } +// end of macro + +bool Layout::iterator::prevStartOfSpan() + PREV_START_OF_ITEM(thisStartOfSpan); + +bool Layout::iterator::thisStartOfSpan() + THIS_START_OF_ITEM(_parent_layout->_characters[_char_index].in_span); + +bool Layout::iterator::nextStartOfSpan() + NEXT_START_OF_ITEM(_parent_layout->_characters[_char_index].in_span); + + +bool Layout::iterator::prevStartOfChunk() + PREV_START_OF_ITEM(thisStartOfChunk); + +bool Layout::iterator::thisStartOfChunk() + THIS_START_OF_ITEM(_parent_layout->_characters[_char_index].span(_parent_layout).in_chunk); + +bool Layout::iterator::nextStartOfChunk() + NEXT_START_OF_ITEM(_parent_layout->_characters[_char_index].span(_parent_layout).in_chunk); + + +bool Layout::iterator::prevStartOfLine() + PREV_START_OF_ITEM(thisStartOfLine); + +bool Layout::iterator::thisStartOfLine() + THIS_START_OF_ITEM(_parent_layout->_characters[_char_index].chunk(_parent_layout).in_line); + +bool Layout::iterator::nextStartOfLine() + NEXT_START_OF_ITEM(_parent_layout->_characters[_char_index].chunk(_parent_layout).in_line); + + +bool Layout::iterator::prevStartOfShape() + PREV_START_OF_ITEM(thisStartOfShape); + +bool Layout::iterator::thisStartOfShape() + THIS_START_OF_ITEM(_parent_layout->_characters[_char_index].line(_parent_layout).in_shape); + +bool Layout::iterator::nextStartOfShape() + NEXT_START_OF_ITEM(_parent_layout->_characters[_char_index].line(_parent_layout).in_shape); + + +bool Layout::iterator::prevStartOfParagraph() + PREV_START_OF_ITEM(thisStartOfParagraph); + +bool Layout::iterator::thisStartOfParagraph() + THIS_START_OF_ITEM(_parent_layout->_characters[_char_index].line(_parent_layout).in_paragraph); + +bool Layout::iterator::nextStartOfParagraph() + NEXT_START_OF_ITEM(_parent_layout->_characters[_char_index].line(_parent_layout).in_paragraph); + + +bool Layout::iterator::prevStartOfSource() + PREV_START_OF_ITEM(thisStartOfSource); + +bool Layout::iterator::thisStartOfSource() + THIS_START_OF_ITEM(_parent_layout->_characters[_char_index].span(_parent_layout).in_input_stream_item); + +bool Layout::iterator::nextStartOfSource() + NEXT_START_OF_ITEM(_parent_layout->_characters[_char_index].span(_parent_layout).in_input_stream_item); + + +bool Layout::iterator::thisEndOfLine() +{ + if (_char_index == _parent_layout->_characters.size()) return false; + if (nextStartOfLine()) + { + if (_char_index && _parent_layout->_characters[_char_index - 1].char_attributes.is_white) + return prevCursorPosition(); + return true; + } + if (_char_index && _parent_layout->_characters[_char_index - 1].chunk(_parent_layout).in_line != _parent_layout->_lines.size() - 1) + return prevCursorPosition(); // for when the last paragraph is empty + return false; +} + +void Layout::iterator::beginCursorUpDown() +{ + if (_char_index == _parent_layout->_characters.size()) + _x_coordinate = _parent_layout->_chunks.back().left_x + _parent_layout->_spans.back().x_end; + else + _x_coordinate = _parent_layout->_characters[_char_index].x + _parent_layout->_characters[_char_index].span(_parent_layout).x_start + _parent_layout->_characters[_char_index].chunk(_parent_layout).left_x; + _cursor_moving_vertically = true; +} + +bool Layout::iterator::nextLineCursor(int n) +{ + if (!_cursor_moving_vertically) + beginCursorUpDown(); + if (_char_index == _parent_layout->_characters.size()) + return false; + unsigned line_index = _parent_layout->_characters[_char_index].chunk(_parent_layout).in_line; + if (line_index == _parent_layout->_lines.size() - 1) + return false; // nowhere to go + else + n = MIN (n, static_cast(_parent_layout->_lines.size() - 1 - line_index)); + if (_parent_layout->_lines[line_index + n].in_shape != _parent_layout->_lines[line_index].in_shape) { + // switching between shapes: adjust the stored x to compensate + _x_coordinate += _parent_layout->_chunks[_parent_layout->_spans[_parent_layout->_lineToSpan(line_index + n)].in_chunk].left_x + - _parent_layout->_chunks[_parent_layout->_spans[_parent_layout->_lineToSpan(line_index)].in_chunk].left_x; + } + _char_index = _parent_layout->_cursorXOnLineToIterator(line_index + n, _x_coordinate)._char_index; + if (_char_index == _parent_layout->_characters.size()) + _glyph_index = _parent_layout->_glyphs.size(); + else + _glyph_index = _parent_layout->_characters[_char_index].in_glyph; + return true; +} + +bool Layout::iterator::prevLineCursor(int n) +{ + if (!_cursor_moving_vertically) + beginCursorUpDown(); + int line_index; + if (_char_index == _parent_layout->_characters.size()) + line_index = _parent_layout->_lines.size() - 1; + else + line_index = _parent_layout->_characters[_char_index].chunk(_parent_layout).in_line; + if (line_index <= 0) + return false; // nowhere to go + else + n = MIN (n, static_cast(line_index)); + if (_parent_layout->_lines[line_index - n].in_shape != _parent_layout->_lines[line_index].in_shape) { + // switching between shapes: adjust the stored x to compensate + _x_coordinate += _parent_layout->_chunks[_parent_layout->_spans[_parent_layout->_lineToSpan(line_index - n)].in_chunk].left_x + - _parent_layout->_chunks[_parent_layout->_spans[_parent_layout->_lineToSpan(line_index)].in_chunk].left_x; + } + _char_index = _parent_layout->_cursorXOnLineToIterator(line_index - n, _x_coordinate)._char_index; + _glyph_index = _parent_layout->_characters[_char_index].in_glyph; + return true; +} + +#define NEXT_WITH_ATTRIBUTE_SET(attr) \ + { \ + _cursor_moving_vertically = false; \ + for ( ; ; ) { \ + if (_char_index + 1 >= _parent_layout->_characters.size()) { \ + _char_index = _parent_layout->_characters.size(); \ + _glyph_index = _parent_layout->_glyphs.size(); \ + return false; \ + } \ + _char_index++; \ + if (_parent_layout->_characters[_char_index].char_attributes.attr) break; \ + } \ + _glyph_index = _parent_layout->_characters[_char_index].in_glyph; \ + return true; \ + } +// end of macro + +#define PREV_WITH_ATTRIBUTE_SET(attr) \ + { \ + _cursor_moving_vertically = false; \ + for ( ; ; ) { \ + if (_char_index == 0) { \ + _glyph_index = 0; \ + return false; \ + } \ + _char_index--; \ + if (_parent_layout->_characters[_char_index].char_attributes.attr) break; \ + } \ + _glyph_index = _parent_layout->_characters[_char_index].in_glyph; \ + return true; \ + } +// end of macro + +bool Layout::iterator::nextCursorPosition() + NEXT_WITH_ATTRIBUTE_SET(is_cursor_position); + +bool Layout::iterator::prevCursorPosition() + PREV_WITH_ATTRIBUTE_SET(is_cursor_position); + +bool Layout::iterator::nextStartOfWord() + NEXT_WITH_ATTRIBUTE_SET(is_word_start); + +bool Layout::iterator::prevStartOfWord() + PREV_WITH_ATTRIBUTE_SET(is_word_start); + +bool Layout::iterator::nextEndOfWord() + NEXT_WITH_ATTRIBUTE_SET(is_word_end); + +bool Layout::iterator::prevEndOfWord() + PREV_WITH_ATTRIBUTE_SET(is_word_end); + +bool Layout::iterator::nextStartOfSentence() + NEXT_WITH_ATTRIBUTE_SET(is_sentence_start); + +bool Layout::iterator::prevStartOfSentence() + PREV_WITH_ATTRIBUTE_SET(is_sentence_start); + +bool Layout::iterator::nextEndOfSentence() + NEXT_WITH_ATTRIBUTE_SET(is_sentence_end); + +bool Layout::iterator::prevEndOfSentence() + PREV_WITH_ATTRIBUTE_SET(is_sentence_end); + +bool Layout::iterator::_cursorLeftOrRightLocalX(Direction direction) +{ + // the only reason this function is so complicated is to enable visual cursor + // movement moving in to or out of counterdirectional runs + if (_parent_layout->_characters.empty()) return false; + unsigned old_span_index; + Direction old_span_direction; + if (_char_index == _parent_layout->_characters.size()) + old_span_index = _parent_layout->_spans.size() - 1; + else + old_span_index = _parent_layout->_characters[_char_index].in_span; + old_span_direction = _parent_layout->_spans[old_span_index].direction; + Direction para_direction = _parent_layout->_spans[old_span_index].paragraph(_parent_layout).base_direction; + + int scan_direction; + unsigned old_char_index = _char_index; + if (old_span_direction != para_direction + && ((_char_index == 0 && direction == para_direction) + || (_char_index == _parent_layout->_characters.size() && direction != para_direction))) { + // the end of the text is actually in the middle because of reordering. Do cleverness + scan_direction = direction == para_direction ? +1 : -1; + } else { + if (direction == old_span_direction) { + if (!nextCursorPosition()) return false; + } else { + if (!prevCursorPosition()) return false; + } + + unsigned new_span_index = _parent_layout->_characters[_char_index].in_span; + if (new_span_index == old_span_index) return true; + if (old_span_direction != _parent_layout->_spans[new_span_index].direction) { + // we must jump to the other end of a counterdirectional run + scan_direction = direction == para_direction ? +1 : -1; + } else if (_parent_layout->_spans[old_span_index].in_chunk != _parent_layout->_spans[new_span_index].in_chunk) { + // we might have to do a weird jump when we would have crossed a chunk/line break + if (_parent_layout->_spans[old_span_index].line(_parent_layout).in_paragraph != _parent_layout->_spans[new_span_index].line(_parent_layout).in_paragraph) + return true; + if (old_span_direction == para_direction) + return true; + scan_direction = direction == para_direction ? +1 : -1; + } else + return true; // same direction, same chunk: no cleverness required + } + + unsigned new_span_index = old_span_index; + for ( ; ; ) { + if (scan_direction > 0) { + if (new_span_index == _parent_layout->_spans.size() - 1) { + if (_parent_layout->_spans[new_span_index].direction == old_span_direction) { + _char_index = old_char_index; + return false; // the visual end is in the logical middle + } + break; + } + new_span_index++; + } else { + if (new_span_index == 0) { + if (_parent_layout->_spans[new_span_index].direction == old_span_direction) { + _char_index = old_char_index; + return false; // the visual end is in the logical middle + } + break; + } + new_span_index--; + } + if (_parent_layout->_spans[new_span_index].direction == para_direction) { + if (para_direction == old_span_direction) + new_span_index -= scan_direction; + break; + } + if (_parent_layout->_spans[new_span_index].in_chunk != _parent_layout->_spans[old_span_index].in_chunk) { + if (_parent_layout->_spans[old_span_index].line(_parent_layout).in_paragraph == _parent_layout->_spans[new_span_index].line(_parent_layout).in_paragraph + && para_direction == old_span_direction) + new_span_index -= scan_direction; + break; + } + } + + // found the correct span, now find the correct character + if (_parent_layout->_spans[old_span_index].line(_parent_layout).in_paragraph != _parent_layout->_spans[new_span_index].line(_parent_layout).in_paragraph) { + if (new_span_index > old_span_index) + _char_index = _parent_layout->_spanToCharacter(new_span_index); + else + _char_index = _parent_layout->_spanToCharacter(new_span_index + 1) - 1; + } else { + if (_parent_layout->_spans[new_span_index].direction != direction) { + if (new_span_index >= _parent_layout->_spans.size() - 1) + _char_index = _parent_layout->_characters.size(); + else + _char_index = _parent_layout->_spanToCharacter(new_span_index + 1) - 1; + } else + _char_index = _parent_layout->_spanToCharacter(new_span_index); + } + if (_char_index == _parent_layout->_characters.size()) { + _glyph_index = _parent_layout->_glyphs.size(); + return false; + } + _glyph_index = _parent_layout->_characters[_char_index].in_glyph; + return _char_index != 0; +} + +bool Layout::iterator::_cursorLeftOrRightLocalXByWord(Direction direction) +{ + bool r; + while ((r = _cursorLeftOrRightLocalX(direction)) + && !_parent_layout->_characters[_char_index].char_attributes.is_word_start){}; + return r; +} + +bool Layout::iterator::cursorUp(int n) +{ + Direction block_progression = _parent_layout->_blockProgression(); + if(block_progression == TOP_TO_BOTTOM) + return prevLineCursor(n); + else if(block_progression == BOTTOM_TO_TOP) + return nextLineCursor(n); + else + return _cursorLeftOrRightLocalX(RIGHT_TO_LEFT); +} + +bool Layout::iterator::cursorDown(int n) +{ + Direction block_progression = _parent_layout->_blockProgression(); + if(block_progression == TOP_TO_BOTTOM) + return nextLineCursor(n); + else if(block_progression == BOTTOM_TO_TOP) + return prevLineCursor(n); + else + return _cursorLeftOrRightLocalX(LEFT_TO_RIGHT); +} + +bool Layout::iterator::cursorLeft() +{ + Direction block_progression = _parent_layout->_blockProgression(); + if(block_progression == LEFT_TO_RIGHT) + return prevLineCursor(); + else if(block_progression == RIGHT_TO_LEFT) + return nextLineCursor(); + else + return _cursorLeftOrRightLocalX(RIGHT_TO_LEFT); +} + +bool Layout::iterator::cursorRight() +{ + Direction block_progression = _parent_layout->_blockProgression(); + if(block_progression == LEFT_TO_RIGHT) + return nextLineCursor(); + else if(block_progression == RIGHT_TO_LEFT) + return prevLineCursor(); + else + return _cursorLeftOrRightLocalX(LEFT_TO_RIGHT); +} + +bool Layout::iterator::cursorUpWithControl() +{ + Direction block_progression = _parent_layout->_blockProgression(); + if(block_progression == TOP_TO_BOTTOM) + return prevStartOfParagraph(); + else if(block_progression == BOTTOM_TO_TOP) + return nextStartOfParagraph(); + else + return _cursorLeftOrRightLocalXByWord(RIGHT_TO_LEFT); +} + +bool Layout::iterator::cursorDownWithControl() +{ + Direction block_progression = _parent_layout->_blockProgression(); + if(block_progression == TOP_TO_BOTTOM) + return nextStartOfParagraph(); + else if(block_progression == BOTTOM_TO_TOP) + return prevStartOfParagraph(); + else + return _cursorLeftOrRightLocalXByWord(LEFT_TO_RIGHT); +} + +bool Layout::iterator::cursorLeftWithControl() +{ + Direction block_progression = _parent_layout->_blockProgression(); + if(block_progression == LEFT_TO_RIGHT) + return prevStartOfParagraph(); + else if(block_progression == RIGHT_TO_LEFT) + return nextStartOfParagraph(); + else + return _cursorLeftOrRightLocalXByWord(RIGHT_TO_LEFT); +} + +bool Layout::iterator::cursorRightWithControl() +{ + Direction block_progression = _parent_layout->_blockProgression(); + if(block_progression == LEFT_TO_RIGHT) + return nextStartOfParagraph(); + else if(block_progression == RIGHT_TO_LEFT) + return prevStartOfParagraph(); + else + return _cursorLeftOrRightLocalXByWord(LEFT_TO_RIGHT); +} + +}//namespace Text +}//namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/libnrtype/Layout-TNG-Output.cpp b/src/libnrtype/Layout-TNG-Output.cpp new file mode 100644 index 0000000..66a1516 --- /dev/null +++ b/src/libnrtype/Layout-TNG-Output.cpp @@ -0,0 +1,910 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::Text::Layout - text layout engine output functions + * + * Authors: + * Richard Hughes + * + * Copyright (C) 2005 Richard Hughes + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include "Layout-TNG.h" +#include "display/drawing-text.h" +#include "style.h" +#include "print.h" +#include "extension/print.h" +#include "livarot/Path.h" +#include "font-instance.h" +#include "svg/svg-length.h" +#include "extension/internal/cairo-render-context.h" +#include "display/curve.h" +#include <2geom/pathvector.h> +#include <3rdparty/libuemf/symbol_convert.h> + + +using Inkscape::Extension::Internal::CairoRenderContext; +using Inkscape::Extension::Internal::CairoGlyphInfo; + +namespace Inkscape { +namespace Text { + +/* + dx array (character widths) and + ky (vertical kerning for entire span) + rtl (+1 for LTR, -1 RTL) + + are smuggled through to the EMF (ignored by others) as: + textN w1 w2 w3 ...wNy1 y2 y3 .. yN + The ndx, widths, y kern, and rtl are all 7 characters wide. ndx and rtl are ints, the widths and ky are + formatted as ' 6f'. +*/ +char *smuggle_adxkyrtl_in(const char *string, int ndx, float *adx, float ky, float rtl){ + int slen = strlen(string); + /* holds: string + fake terminator (one \0) + Number of widths (ndx) + series of widths (ndx entries) + fake terminator (one \0) + y kern value (one float) + rtl value (one float) + real terminator (two \0) + */ + int newsize=slen + 1 + 7 + 7*ndx + 1 + 7 + 7 + 2; + newsize = 8*((7 + newsize)/8); // suppress valgrind messages if it is a multiple of 8 bytes??? + char *smuggle=(char *)malloc(newsize); + strcpy(smuggle,string); // text to pass, includes the first fake terminator + char *cptr = smuggle + slen + 1; // immediately after the first fake terminator + sprintf(cptr,"%07d",ndx); // number of widths to pass + cptr+=7; // advance over ndx + for(int i=0; iUnref(); + _spans.clear(); + _characters.clear(); + _glyphs.clear(); + _path_fitted = nullptr; +} + +void Layout::FontMetrics::set(font_instance *font) +{ + if( font != nullptr ) { + ascent = font->GetTypoAscent(); + descent = font->GetTypoDescent(); + xheight = font->GetXHeight(); + ascent_max = font->GetMaxAscent(); + descent_max = font->GetMaxDescent(); + } +} + +void Layout::FontMetrics::max(FontMetrics const &other) +{ + if (other.ascent > ascent ) ascent = other.ascent; + if (other.descent > descent ) descent = other.descent; + if( other.xheight > xheight ) xheight = other.xheight; + if( other.ascent_max > ascent_max ) ascent_max = other.ascent_max; + if( other.descent_max > descent_max ) descent_max = other.descent_max; +} + +void Layout::FontMetrics::computeEffective( const double &line_height_multiplier ) { + double half_leading = 0.5 * (line_height_multiplier - 1.0) * emSize(); + ascent += half_leading; + descent += half_leading; +} + +void Layout::_getGlyphTransformMatrix(int glyph_index, Geom::Affine *matrix) const +{ + Span const &span = _glyphs[glyph_index].span(this); + double rotation = _glyphs[glyph_index].rotation; + if ( (span.block_progression == LEFT_TO_RIGHT || span.block_progression == RIGHT_TO_LEFT) && + _glyphs[glyph_index].orientation == ORIENTATION_SIDEWAYS ) { + // Vertical sideways text + rotation += M_PI/2.0; + } + double sin_rotation = sin(rotation); + double cos_rotation = cos(rotation); + (*matrix)[0] = span.font_size * cos_rotation; + (*matrix)[1] = span.font_size * sin_rotation; + (*matrix)[2] = span.font_size * sin_rotation; + (*matrix)[3] = -span.font_size * cos_rotation * (_glyphs[glyph_index].vertical_scale); // unscale vertically so the specified text height is preserved if lengthAdjust=spacingAndGlyphs + if (span.block_progression == LEFT_TO_RIGHT || span.block_progression == RIGHT_TO_LEFT) { + // Vertical text + // This effectively swaps x for y which changes handedness of coordinate system. This is a bit strange + // and not what one would expect but the compute code already reverses y so OK. + (*matrix)[4] = _lines[_chunks[span.in_chunk].in_line].baseline_y + _glyphs[glyph_index].y; + (*matrix)[5] = _chunks[span.in_chunk].left_x + _glyphs[glyph_index].x; + } else { + // Horizontal text + (*matrix)[4] = _chunks[span.in_chunk].left_x + _glyphs[glyph_index].x; + (*matrix)[5] = _lines[_chunks[span.in_chunk].in_line].baseline_y + _glyphs[glyph_index].y; + } +} + +void Layout::show(DrawingGroup *in_arena, Geom::OptRect const &paintbox) const +{ + int glyph_index = 0; + double phase0 = 0.0; + for (unsigned span_index = 0 ; span_index < _spans.size() ; span_index++) { + if (_input_stream[_spans[span_index].in_input_stream_item]->Type() != TEXT_SOURCE) continue; + + if (_spans[span_index].line(this).hidden) continue; // Line corresponds to text overflow. Don't show! + + InputStreamTextSource const *text_source = static_cast(_input_stream[_spans[span_index].in_input_stream_item]); + + text_source->style->text_decoration_data.tspan_width = _spans[span_index].width(); + text_source->style->text_decoration_data.ascender = _spans[span_index].line_height.getTypoAscent(); + text_source->style->text_decoration_data.descender = _spans[span_index].line_height.getTypoDescent(); + + if(!span_index || + (_chunks[_spans[span_index].in_chunk].in_line != _chunks[_spans[span_index-1].in_chunk].in_line)){ + text_source->style->text_decoration_data.tspan_line_start = true; + } + else { + text_source->style->text_decoration_data.tspan_line_start = false; + } + if((span_index == _spans.size() -1) || + (_chunks[_spans[span_index].in_chunk].in_line != _chunks[_spans[span_index+1].in_chunk].in_line)){ + text_source->style->text_decoration_data.tspan_line_end = true; + } + else { + text_source->style->text_decoration_data.tspan_line_end = false; + } + if(_spans[span_index].font){ + double underline_thickness, underline_position, line_through_thickness,line_through_position; + _spans[span_index].font->FontDecoration(underline_position, underline_thickness, line_through_position, line_through_thickness); + text_source->style->text_decoration_data.underline_thickness = underline_thickness; + text_source->style->text_decoration_data.underline_position = underline_position; + text_source->style->text_decoration_data.line_through_thickness = line_through_thickness; + text_source->style->text_decoration_data.line_through_position = line_through_position; + } + else { // can this case ever occur? + text_source->style->text_decoration_data.underline_thickness = + text_source->style->text_decoration_data.underline_position = + text_source->style->text_decoration_data.line_through_thickness = + text_source->style->text_decoration_data.line_through_position = 0.0; + } + + DrawingText *nr_text = new DrawingText(in_arena->drawing()); + + bool first_line_glyph = true; + while (glyph_index < (int)_glyphs.size() && _characters[_glyphs[glyph_index].in_character].in_span == span_index) { + if (_characters[_glyphs[glyph_index].in_character].in_glyph != -1) { + Geom::Affine glyph_matrix; + _getGlyphTransformMatrix(glyph_index, &glyph_matrix); + if(first_line_glyph && text_source->style->text_decoration_data.tspan_line_start){ + first_line_glyph = false; + phase0 = glyph_matrix.translation()[Geom::X]; + } + // Save the starting coordinates for the line - these are needed for figuring out + // dot/dash/wave phase. + // Use maximum ascent and descent to ensure glyphs that extend outside the embox + // are fully drawn. + (void) nr_text->addComponent(_spans[span_index].font, _glyphs[glyph_index].glyph, glyph_matrix, + _glyphs[glyph_index].advance, + _spans[span_index].line_height.getMaxAscent(), + _spans[span_index].line_height.getMaxDescent(), + glyph_matrix.translation()[Geom::X] - phase0 + ); + } + glyph_index++; + } + nr_text->setStyle(text_source->style); + nr_text->setItemBounds(paintbox); + // Text spans must be painted in the right order (see inkscape/685) + in_arena->appendChild(nr_text); + // Set item bounds without filter enlargement + in_arena->setItemBounds(paintbox); + } +} + +Geom::OptRect Layout::bounds(Geom::Affine const &transform, bool with_stroke, int start, int length) const +{ + Geom::OptRect bbox; + for (unsigned glyph_index = 0 ; glyph_index < _glyphs.size() ; glyph_index++) { + if (_glyphs[glyph_index].hidden) continue; // To do: This and the next line should represent the same thing, use on or the other. + if (_characters[_glyphs[glyph_index].in_character].in_glyph == -1) continue; + if (start != -1 && (int) _glyphs[glyph_index].in_character < start) continue; + if (length != -1) { + if (start == -1) + start = 0; + if ((int) _glyphs[glyph_index].in_character > start + length) continue; + } + // this could be faster + Geom::Affine glyph_matrix; + _getGlyphTransformMatrix(glyph_index, &glyph_matrix); + Geom::Affine total_transform = glyph_matrix; + total_transform *= transform; + if(_glyphs[glyph_index].span(this).font) { + Geom::OptRect glyph_rect = _glyphs[glyph_index].span(this).font->BBox(_glyphs[glyph_index].glyph); + if (glyph_rect) { + auto glyph_box = *glyph_rect * total_transform; + // FIXME: Expand rectangle by half stroke width, this doesn't include meters + // and so is not the most ideal calculation, we could use the glyph Path here. + if (with_stroke) { + Span const &span = _spans[_characters[_glyphs[glyph_index].in_character].in_span]; + auto text_source = static_cast(_input_stream[span.in_input_stream_item]); + if (!text_source->style->stroke.isNone()) { + double scale = transform.descrim(); + glyph_box.expandBy(0.5 * text_source->style->stroke_width.computed * scale); + } + } + bbox.unionWith(glyph_box); + } + } + } + return bbox; +} + +/* This version is much simpler than the old one +*/ +void Layout::print(SPPrintContext *ctx, + Geom::OptRect const &pbox, Geom::OptRect const &dbox, Geom::OptRect const &bbox, + Geom::Affine const &ctm) const +{ +bool text_to_path = ctx->module->textToPath(); +#define MAX_DX 2048 +float hold_dx[MAX_DX]; // For smuggling dx values (character widths) into print functions, unlikely any simple text output will be longer than this. + +Geom::Affine glyph_matrix; + + if (_input_stream.empty()) return; + if (!_glyphs.size()) return; // yes, this can happen. + if (text_to_path || _path_fitted) { + for (unsigned glyph_index = 0 ; glyph_index < _glyphs.size() ; glyph_index++) { + if (_characters[_glyphs[glyph_index].in_character].in_glyph == -1)continue; //invisible glyphs + Span const &span = _spans[_characters[_glyphs[glyph_index].in_character].in_span]; + Geom::PathVector const * pv = span.font->PathVector(_glyphs[glyph_index].glyph); + InputStreamTextSource const *text_source = static_cast(_input_stream[span.in_input_stream_item]); + if (pv) { + _getGlyphTransformMatrix(glyph_index, &glyph_matrix); + Geom::PathVector temp_pv = (*pv) * glyph_matrix; + if (!text_source->style->fill.isNone()) + ctx->fill(temp_pv, ctm, text_source->style, pbox, dbox, bbox); + if (!text_source->style->stroke.isNone()) + ctx->stroke(temp_pv, ctm, text_source->style, pbox, dbox, bbox); + } + } + } + else { + /* index by characters, referencing glyphs and spans only as needed */ + double char_x; + int doUTN = CanUTN(); // Unicode to Nonunicode translation enabled if true + Direction block_progression = _blockProgression(); + int oldtarget = 0; + int ndx = 0; + double rtl = 1.0; // 1 L->R, -1 R->L, constant across a span. 1.0 for t->b b->t??? + + for (unsigned char_index = 0 ; char_index < _characters.size() ; ) { + Glib::ustring text_string; // accumulate text for record in this + Geom::Point g_pos(0,0); // all strings are output at (0,0) because we do the translation using the matrix + int glyph_index = _characters[char_index].in_glyph; + if(glyph_index == -1){ // if the character maps to an invisible glyph we cannot know its geometry, so skip it and move on + char_index++; + continue; + } + float ky = _glyphs[glyph_index].y; // For smuggling y kern value for span // same value for all positions in a span + unsigned span_index = _characters[char_index].in_span; + Span const &span = _spans[span_index]; + char_x = 0.0; + Glib::ustring::const_iterator text_iter = span.input_stream_first_character; + InputStreamTextSource const *text_source = static_cast(_input_stream[span.in_input_stream_item]); + glyph_matrix = Geom::Scale(1.0, -1.0) * (Geom::Affine)Geom::Rotate(_glyphs[glyph_index].rotation); + if (block_progression == LEFT_TO_RIGHT || block_progression == RIGHT_TO_LEFT) { + glyph_matrix[4] = span.line(this).baseline_y + span.baseline_shift; + // since we're outputting character codes, not glyphs, we want the character x + glyph_matrix[5] = span.chunk(this).left_x + span.x_start + _characters[_glyphs[glyph_index].in_character].x; + } else { + glyph_matrix[4] = span.chunk(this).left_x + span.x_start + _characters[_glyphs[glyph_index].in_character].x; + glyph_matrix[5] = span.line(this).baseline_y + span.baseline_shift; + } + switch(span.direction){ + case Layout::TOP_TO_BOTTOM: + case Layout::BOTTOM_TO_TOP: + case Layout::LEFT_TO_RIGHT: rtl = 1.0; break; + case Layout::RIGHT_TO_LEFT: rtl = -1.0; break; + } + if(doUTN){ + oldtarget=SingleUnicodeToNon(*text_iter); // this should only ever be with a 1:1 glyph:character situation + } + + // accumulate a record to write + + unsigned lc_index = char_index; + unsigned hold_iisi = _spans[span_index].in_input_stream_item; + int newtarget = 0; + while(true){ + glyph_index = _characters[lc_index].in_glyph; + if(glyph_index == -1){ // end of a line within a paragraph, for instance + lc_index++; + break; + } + + // always append if here + text_string += *text_iter; + + // figure out char widths, used by EMF, not currently used elsewhere + double cwidth; + if(lc_index == _glyphs[glyph_index].in_character){ // Glyph width is used only for the first character, these may be 0 + cwidth = rtl * _glyphs[glyph_index].advance; // advance might be zero + } + else { + cwidth = 0; + } + char_x += cwidth; +/* +std:: cout << "DEBUG Layout::print in while " +<< " char_index " << char_index +<< " lc_index " << lc_index +<< " character " << std::hex << (int) *text_iter << std::dec +<< " glyph_index " << glyph_index +<< " glyph_xy " << _glyphs[glyph_index].x << " , " << _glyphs[glyph_index].y +<< " span_index " << span_index +<< " hold_iisi " << hold_iisi +<< std::endl; //DEBUG +*/ + if(ndx < MAX_DX){ + hold_dx[ndx++] = fabs(cwidth); + } + else { // silently truncate any text line silly enough to be longer than MAX_DX + lc_index = _characters.size(); + break; + } + + + // conditions that prevent this character from joining the record + lc_index++; + if(lc_index >= _characters.size()) break; // nothing more to process, so it must be the end of the record + ++text_iter; + if(doUTN)newtarget=SingleUnicodeToNon(*text_iter); // this should only ever be with a 1:1 glyph:character situation + if(newtarget != oldtarget)break; // change in unicode to nonunicode translation status + // MUST exit on any major span change, but not on some little events, like a font substitution event irrelevant for the file save + unsigned next_span_index = _characters[lc_index].in_span; + if(span_index != next_span_index){ + /* on major changes break out of loop. + 1st case usually indicates an entire input line has been processed (out of several in a paragraph) + 2nd case usually indicates that a format change within a line (font/size/color/etc) is present. + */ +/* +std:: cout << "DEBUG Layout::print in while --- " +<< " char_index " << char_index +<< " lc_index " << lc_index +<< " cwidth " << cwidth +<< " _char.x (next) " << (lc_index < _characters.size() ? _characters[lc_index].x : -1) +<< " char_x (end this)" << char_x +<< " diff " << fabs(char_x - _characters[lc_index].x) +<< " oldy " << ky +<< " nexty " << _glyphs[_characters[lc_index].in_glyph].y +<< std::endl; //DEBUG +*/ + if(hold_iisi != _spans[next_span_index].in_input_stream_item)break; // major change, font, size, color, etc, must exit + if(fabs(char_x - _spans[next_span_index].x_start) >= 1e-4)break; // xkerning change + if(ky != _glyphs[_characters[lc_index].in_glyph].y)break; // ykerning change + /* + None of the above? Then this is a minor "pangito", update span_index and keep going. + The font used by the display may have failed over, but print does not care and can continue to use + whatever was specified in the XML. + */ + span_index = next_span_index; + text_iter = _spans[span_index].input_stream_first_character; + } + + } + // write it + ctx->bind(glyph_matrix, 1.0); + + // the dx array is smuggled through to the EMF driver (ignored by others) as: + // textw1 w2 w3 ...wn + // where the widths are floats 7 characters wide, including the space + + char *smuggle_string=smuggle_adxkyrtl_in(text_string.c_str(),ndx, &hold_dx[0], ky, rtl); + ctx->text(smuggle_string, g_pos, text_source->style); + free(smuggle_string); + ctx->release(); + ndx = 0; + char_index = lc_index; + } + } +} + + +void Layout::showGlyphs(CairoRenderContext *ctx) const +{ + if (_input_stream.empty()) return; + + bool clip_mode = false;//(ctx->getRenderMode() == CairoRenderContext::RENDER_MODE_CLIP); + std::vector glyphtext; + + for (unsigned glyph_index = 0 ; glyph_index < _glyphs.size() ; ) { + if (_characters[_glyphs[glyph_index].in_character].in_glyph == -1) { + // invisible glyphs + unsigned same_character = _glyphs[glyph_index].in_character; + while (_glyphs[glyph_index].in_character == same_character) { + glyph_index++; + if (glyph_index == _glyphs.size()) + return; + } + continue; + } + Span const &span = _spans[_characters[_glyphs[glyph_index].in_character].in_span]; + InputStreamTextSource const *text_source = static_cast(_input_stream[span.in_input_stream_item]); + + Geom::Affine glyph_matrix; + _getGlyphTransformMatrix(glyph_index, &glyph_matrix); + if (clip_mode) { + Geom::PathVector const *pathv = span.font->PathVector(_glyphs[glyph_index].glyph); + if (pathv) { + Geom::PathVector pathv_trans = (*pathv) * glyph_matrix; + SPStyle const *style = text_source->style; + ctx->renderPathVector(pathv_trans, style, Geom::OptRect()); + } + glyph_index++; + continue; + } + + Geom::Affine font_matrix = glyph_matrix; + font_matrix[4] = 0; + font_matrix[5] = 0; + + Glib::ustring::const_iterator span_iter = span.input_stream_first_character; + unsigned char_index = _glyphs[glyph_index].in_character; + unsigned original_span = _characters[char_index].in_span; + while (char_index && _characters[char_index - 1].in_span == original_span) { + char_index--; + ++span_iter; + } + + // try to output as many characters as possible in one go + Glib::ustring span_string; + unsigned this_span_index = _characters[_glyphs[glyph_index].in_character].in_span; + unsigned int first_index = glyph_index; + glyphtext.clear(); + do { + span_string += *span_iter; + ++span_iter; + + unsigned same_character = _glyphs[glyph_index].in_character; + while (glyph_index < _glyphs.size() && _glyphs[glyph_index].in_character == same_character) { + if (glyph_index != first_index) + _getGlyphTransformMatrix(glyph_index, &glyph_matrix); + + CairoGlyphInfo info; + info.index = _glyphs[glyph_index].glyph; + // this is the translation for x,y-offset + info.x = glyph_matrix[4]; + info.y = glyph_matrix[5]; + + glyphtext.push_back(info); + + glyph_index++; + } + } while (glyph_index < _glyphs.size() + && _path_fitted == nullptr + && (font_matrix * glyph_matrix.inverse()).isIdentity() + && _characters[_glyphs[glyph_index].in_character].in_span == this_span_index); + + // remove vertical flip + Geom::Affine flip_matrix; + flip_matrix.setIdentity(); + flip_matrix[3] = -1.0; + font_matrix = flip_matrix * font_matrix; + + SPStyle const *style = text_source->style; + float opacity = SP_SCALE24_TO_FLOAT(style->opacity.value); + + if (opacity != 1.0) { + ctx->pushState(); + ctx->setStateForStyle(style); + ctx->pushLayer(); + } + if (glyph_index - first_index > 0) + ctx->renderGlyphtext(span.font->pFont, font_matrix, glyphtext, style); + if (opacity != 1.0) { + ctx->popLayer(); + ctx->popState(); + } + } +} + +#if DEBUG_TEXTLAYOUT_DUMPASTEXT +// these functions are for dumpAsText() only. No need to translate +static char const *direction_to_text(Layout::Direction d) +{ + switch (d) { + case Layout::LEFT_TO_RIGHT: return "ltr"; + case Layout::RIGHT_TO_LEFT: return "rtl"; + case Layout::TOP_TO_BOTTOM: return "ttb"; + case Layout::BOTTOM_TO_TOP: return "btt"; + } + return "???"; +} + +static char const *style_to_text(PangoStyle s) +{ + switch (s) { + case PANGO_STYLE_NORMAL: return "upright"; + case PANGO_STYLE_ITALIC: return "italic"; + case PANGO_STYLE_OBLIQUE: return "oblique"; + } + return "???"; +} + +static std::string weight_to_text(PangoWeight w) +{ + switch (w) { + case PANGO_WEIGHT_THIN : return "thin"; + case PANGO_WEIGHT_ULTRALIGHT: return "ultralight"; + case PANGO_WEIGHT_LIGHT : return "light"; + case PANGO_WEIGHT_SEMILIGHT : return "semilight"; + case PANGO_WEIGHT_BOOK : return "book"; + case PANGO_WEIGHT_NORMAL : return "normalweight"; + case PANGO_WEIGHT_MEDIUM : return "medium"; + case PANGO_WEIGHT_SEMIBOLD : return "semibold"; + case PANGO_WEIGHT_BOLD : return "bold"; + case PANGO_WEIGHT_ULTRABOLD : return "ultrabold"; + case PANGO_WEIGHT_HEAVY : return "heavy"; + case PANGO_WEIGHT_ULTRAHEAVY: return "ultraheavy"; + } + return std::to_string(w); +} +#endif //DEBUG_TEXTLAYOUT_DUMPASTEXT + +Glib::ustring Layout::getFontFamily(unsigned span_index) const +{ + if (span_index >= _spans.size()) + return ""; + + if (_spans[span_index].font) { + return sp_font_description_get_family(_spans[span_index].font->descr); + } + + return ""; +} + +#if DEBUG_TEXTLAYOUT_DUMPASTEXT +Glib::ustring Layout::dumpAsText() const +{ + Glib::ustring result; + Glib::ustring::const_iterator icc; + char line[256]; + + result = Glib::ustring::compose("spans %1\nchars %2\nglyphs %3\n", _spans.size(), _characters.size(), _glyphs.size()); + if(_characters.size() > 1){ + unsigned lastspan=5000; + for(unsigned j = 0; j < _characters.size() ; j++){ + if(lastspan != _characters[j].in_span){ + lastspan = _characters[j].in_span; + icc = _spans[lastspan].input_stream_first_character; + } + snprintf(line, sizeof(line), "char %4u: '%c' 0x%4.4x x=%8.4f glyph=%3d span=%3d\n", j, *icc, *icc, _characters[j].x, _characters[j].in_glyph, _characters[j].in_span); + result += line; + ++icc; + } + } + if(_glyphs.size()){ + for(unsigned j = 0; j < _glyphs.size() ; j++){ + snprintf(line, sizeof(line), "glyph %4u: %4d (%8.4f,%8.4f) rot=%8.4f cx=%8.4f char=%4d\n", + j, _glyphs[j].glyph, _glyphs[j].x, _glyphs[j].y, _glyphs[j].rotation, _glyphs[j].width, _glyphs[j].in_character); + result += line; + } + } + + for (unsigned span_index = 0 ; span_index < _spans.size() ; span_index++) { + result += Glib::ustring::compose("==== span %1 \n", span_index) + + Glib::ustring::compose(" in para %1 (direction=%2)\n", _lines[_chunks[_spans[span_index].in_chunk].in_line].in_paragraph, + direction_to_text(_paragraphs[_lines[_chunks[_spans[span_index].in_chunk].in_line].in_paragraph].base_direction)) + + Glib::ustring::compose(" in source %1 (type=%2, cookie=%3)\n", _spans[span_index].in_input_stream_item, + _input_stream[_spans[span_index].in_input_stream_item]->Type(), + _input_stream[_spans[span_index].in_input_stream_item]->source) + + Glib::ustring::compose(" in line %1 (baseline=%2, shape=%3)\n", _chunks[_spans[span_index].in_chunk].in_line, + _lines[_chunks[_spans[span_index].in_chunk].in_line].baseline_y, + _lines[_chunks[_spans[span_index].in_chunk].in_line].in_shape) + + Glib::ustring::compose(" in chunk %1 (x=%2, baselineshift=%3)\n", _spans[span_index].in_chunk, _chunks[_spans[span_index].in_chunk].left_x, _spans[span_index].baseline_shift); + + if (_spans[span_index].font) { + const char* variations = pango_font_description_get_variations(_spans[span_index].font->descr); + result += Glib::ustring::compose( + " font '%1' %2 %3 %4 %5\n", + sp_font_description_get_family(_spans[span_index].font->descr), + _spans[span_index].font_size, + style_to_text( pango_font_description_get_style(_spans[span_index].font->descr) ), + weight_to_text( pango_font_description_get_weight(_spans[span_index].font->descr) ), + (variations?variations:"") + ); + } + result += Glib::ustring::compose(" x_start = %1, x_end = %2\n", _spans[span_index].x_start, _spans[span_index].x_end) + + Glib::ustring::compose(" line height: ascent %1, descent %2\n", _spans[span_index].line_height.ascent, _spans[span_index].line_height.descent) + + Glib::ustring::compose(" direction %1, block-progression %2\n", direction_to_text(_spans[span_index].direction), direction_to_text(_spans[span_index].block_progression)) + + " ** characters:\n"; + Glib::ustring::const_iterator iter_char = _spans[span_index].input_stream_first_character; + // very inefficient code. what the hell, it's only debug stuff. + for (unsigned char_index = 0 ; char_index < _characters.size() ; char_index++) { + union {const PangoLogAttr* pattr; const unsigned* uattr;} u; + u.pattr = &_characters[char_index].char_attributes; + if (_characters[char_index].in_span != span_index) continue; + if (_input_stream[_spans[span_index].in_input_stream_item]->Type() != TEXT_SOURCE) { + snprintf(line, sizeof(line), " %u: control x=%f flags=%03x glyph=%d\n", char_index, _characters[char_index].x, *u.uattr, _characters[char_index].in_glyph); + } else { // some text has empty tspans, iter_char cannot be dereferenced + snprintf(line, sizeof(line), " %u: '%c' 0x%4.4x x=%f flags=%03x glyph=%d\n", char_index, *iter_char, *iter_char, _characters[char_index].x, *u.uattr, _characters[char_index].in_glyph); + ++iter_char; + } + result += line; + } + result += " ** glyphs:\n"; + for (unsigned glyph_index = 0 ; glyph_index < _glyphs.size() ; glyph_index++) { + if (_characters[_glyphs[glyph_index].in_character].in_span != span_index) continue; + snprintf(line, sizeof(line), " %u: %d (%f,%f) rot=%f cx=%f char=%d\n", glyph_index, _glyphs[glyph_index].glyph, _glyphs[glyph_index].x, _glyphs[glyph_index].y, _glyphs[glyph_index].rotation, _glyphs[glyph_index].width, _glyphs[glyph_index].in_character); + result += line; + } + result += "\n"; + } + result += "EOT\n"; + return result; +} +#endif //DEBUG_TEXTLAYOUT_DUMPASTEXT + +void Layout::fitToPathAlign(SVGLength const &startOffset, Path const &path) +{ + double offset = 0.0; + + if (startOffset._set) { + if (startOffset.unit == SVGLength::PERCENT) + offset = startOffset.computed * const_cast(path).Length(); + else + offset = startOffset.computed; + } + + Alignment alignment = _paragraphs.empty() ? LEFT : _paragraphs.front().alignment; + switch (alignment) { + case CENTER: + offset -= _getChunkWidth(0) * 0.5; + break; + case RIGHT: + offset -= _getChunkWidth(0); + break; + default: + break; + } + + if (_characters.empty()) { + int unused = 0; + Path::cut_position *point_otp = const_cast(path).CurvilignToPosition(1, &offset, unused); + if (offset >= 0.0 && point_otp != nullptr && point_otp[0].piece >= 0) { + Geom::Point point; + Geom::Point tangent; + const_cast(path).PointAndTangentAt(point_otp[0].piece, point_otp[0].t, point, tangent); + _empty_cursor_shape.position = point; + if (_directions_are_orthogonal(_blockProgression(), TOP_TO_BOTTOM)) { + _empty_cursor_shape.rotation = atan2(-tangent[Geom::X], tangent[Geom::Y]); + } else { + _empty_cursor_shape.rotation = atan2(tangent[Geom::Y], tangent[Geom::X]); + } + } + } + + for (unsigned char_index = 0 ; char_index < _characters.size() ; ) { + Span const &span = _characters[char_index].span(this); + + size_t next_cluster_char_index = 0; // TODO refactor to not bump via for loops + for (next_cluster_char_index = char_index + 1 ; next_cluster_char_index < _characters.size() ; next_cluster_char_index++) { + if (_characters[next_cluster_char_index].in_glyph != -1 && _characters[next_cluster_char_index].char_attributes.is_cursor_position) + { + break; + } + } + + size_t next_cluster_glyph_index = 0; + if (next_cluster_char_index == _characters.size()) { + next_cluster_glyph_index = _glyphs.size(); + } else { + next_cluster_glyph_index = _characters[next_cluster_char_index].in_glyph; + } + + double start_offset = offset + span.x_start + _characters[char_index].x; + double cluster_width = 0.0; + size_t const current_cluster_glyph_index = _characters[char_index].in_glyph; + for (size_t glyph_index = current_cluster_glyph_index ; glyph_index < next_cluster_glyph_index ; glyph_index++) + { + cluster_width += _glyphs[glyph_index].advance; + } + // TODO block progression? + if (span.direction == RIGHT_TO_LEFT) + { + start_offset -= cluster_width; + } + double end_offset = start_offset + cluster_width; + + int unused = 0; + double midpoint_offset = (start_offset + end_offset) * 0.5; + // as far as I know these functions are const, they're just not marked as such + Path::cut_position *midpoint_otp = const_cast(path).CurvilignToPosition(1, &midpoint_offset, unused); + if (midpoint_offset >= 0.0 && midpoint_otp != nullptr && midpoint_otp[0].piece >= 0) { + Geom::Point midpoint; + Geom::Point tangent; + const_cast(path).PointAndTangentAt(midpoint_otp[0].piece, midpoint_otp[0].t, midpoint, tangent); + + if (start_offset >= 0.0 && end_offset >= 0.0) { + Path::cut_position *start_otp = const_cast(path).CurvilignToPosition(1, &start_offset, unused); + if (start_otp != nullptr && start_otp[0].piece >= 0) { + Path::cut_position *end_otp = const_cast(path).CurvilignToPosition(1, &end_offset, unused); + if (end_otp != nullptr && end_otp[0].piece >= 0) { + bool on_same_subpath = true; + for (const auto & pt : path.pts) { + if (pt.piece <= start_otp[0].piece) continue; + if (pt.piece >= end_otp[0].piece) break; + if (pt.isMoveTo == polyline_moveto) { + on_same_subpath = false; + break; + } + } + if (on_same_subpath) { + // both points were on the same subpath (without this test the angle is very weird) + Geom::Point startpoint, endpoint; + const_cast(path).PointAt(start_otp[0].piece, start_otp[0].t, startpoint); + const_cast(path).PointAt(end_otp[0].piece, end_otp[0].t, endpoint); + if (endpoint != startpoint) { + tangent = endpoint - startpoint; + tangent.normalize(); + } + } + g_free(end_otp); + } + g_free(start_otp); + } + } + + if (_directions_are_orthogonal(_blockProgression(), TOP_TO_BOTTOM)) { + double rotation = atan2(-tangent[Geom::X], tangent[Geom::Y]); + for (size_t glyph_index = current_cluster_glyph_index; glyph_index < next_cluster_glyph_index ; glyph_index++) { + _glyphs[glyph_index].x = midpoint[Geom::Y] - tangent[Geom::X] * _glyphs[glyph_index].y - span.chunk(this).left_x; + _glyphs[glyph_index].y = midpoint[Geom::X] + tangent[Geom::Y] * _glyphs[glyph_index].y - _lines.front().baseline_y; + _glyphs[glyph_index].rotation += rotation; + } + } else { + double rotation = atan2(tangent[Geom::Y], tangent[Geom::X]); + for (size_t glyph_index = current_cluster_glyph_index; glyph_index < next_cluster_glyph_index ; glyph_index++) { + double tangent_shift = -cluster_width * 0.5 + _glyphs[glyph_index].x - (_characters[char_index].x + span.x_start); + if (span.direction == RIGHT_TO_LEFT) + { + tangent_shift += cluster_width; + } + _glyphs[glyph_index].x = midpoint[Geom::X] + tangent[Geom::X] * tangent_shift - tangent[Geom::Y] * _glyphs[glyph_index].y - span.chunk(this).left_x; + _glyphs[glyph_index].y = midpoint[Geom::Y] + tangent[Geom::Y] * tangent_shift + tangent[Geom::X] * _glyphs[glyph_index].y - _lines.front().baseline_y; + _glyphs[glyph_index].rotation += rotation; + } + } + _input_truncated = false; + } else { // outside the bounds of the path: hide the glyphs + _characters[char_index].in_glyph = -1; + _input_truncated = true; + } + g_free(midpoint_otp); + + char_index = next_cluster_char_index; + } + + for (auto & _span : _spans) { + _span.x_start += offset; + _span.x_end += offset; + } + + _path_fitted = &path; +} + +std::unique_ptr Layout::convertToCurves() const +{ + return convertToCurves(begin(), end()); +} + +std::unique_ptr Layout::convertToCurves(iterator const &from_glyph, iterator const &to_glyph) const +{ + auto curve = std::make_unique(); + + for (int glyph_index = from_glyph._glyph_index ; glyph_index < to_glyph._glyph_index ; glyph_index++) { + Geom::Affine glyph_matrix; + Span const &span = _glyphs[glyph_index].span(this); + _getGlyphTransformMatrix(glyph_index, &glyph_matrix); + + Geom::PathVector const * pathv = span.font->PathVector(_glyphs[glyph_index].glyph); + if (pathv) { + Geom::PathVector pathv_trans = (*pathv) * glyph_matrix; + curve->append(SPCurve(std::move(pathv_trans))); + } + } + + return curve; +} + +void Layout::transform(Geom::Affine const &transform) +{ + // this is all massively oversimplified + // I can't actually think of anybody who'll want to use it at the moment, so it'll stay simple + for (auto & _glyph : _glyphs) { + Geom::Point point(_glyph.x, _glyph.y); + point *= transform; + _glyph.x = point[0]; + _glyph.y = point[1]; + } +} + +double Layout::getTextLengthIncrementDue() const +{ + if (textLength._set && textLengthIncrement != 0 && lengthAdjust == Inkscape::Text::Layout::LENGTHADJUST_SPACING) { + return textLengthIncrement; + } + return 0; +} + + +double Layout::getTextLengthMultiplierDue() const +{ + if (textLength._set && textLengthMultiplier != 1 && (lengthAdjust == Inkscape::Text::Layout::LENGTHADJUST_SPACINGANDGLYPHS)) { + return textLengthMultiplier; + } + return 1; +} + +double Layout::getActualLength() const +{ + double length = 0; + for (std::vector::const_iterator it_span = _spans.begin() ; it_span != _spans.end() ; it_span++) { + // take x_end of the last span of each chunk + if (it_span == _spans.end() - 1 || (it_span + 1)->in_chunk != it_span->in_chunk) + length += it_span->x_end; + } + return length; + + +} + +}//namespace Text +}//namespace Inkscape + +std::ostream &operator<<(std::ostream &out, const Inkscape::Text::Layout::FontMetrics &f) { + out << " emSize: " << f.emSize() + << " ascent: " << f.ascent + << " descent: " << f.descent + << " xheight: " << f.xheight; + return out; +} + +std::ostream &operator<<(std::ostream &out, const Inkscape::Text::Layout::FontMetrics *f) { + out << " emSize: " << f->emSize() + << " ascent: " << f->ascent + << " descent: " << f->descent + << " xheight: " << f->xheight; + return out; +} + + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/libnrtype/Layout-TNG-Scanline-Maker.h b/src/libnrtype/Layout-TNG-Scanline-Maker.h new file mode 100644 index 0000000..c8c8232 --- /dev/null +++ b/src/libnrtype/Layout-TNG-Scanline-Maker.h @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::Text::Layout::ScanlineMaker - text layout engine shape measurers + * + * Authors: + * Richard Hughes + * + * Copyright (C) 2005 Richard Hughes + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef __LAYOUT_TNG_SCANLINE_MAKER_H__ +#define __LAYOUT_TNG_SCANLINE_MAKER_H__ + +#include +#include +#include "libnrtype/Layout-TNG.h" + +class Shape; + +namespace Inkscape { +namespace Text { + +/** \brief private to Layout. Generates lists of chunks within a shape. + +This is the abstract base class for taking a given shape and scanning through +it line-by-line to get the horizontal extents of each chunk for a line of a +given height. There are two specialisations: One for real shapes and one that +turns off wrapping by simulating an infinite shape. In due course there will +be a further specialisation to optimise for the common case where the shape +is a rectangle. +*/ +class Layout::ScanlineMaker +{ +public: + virtual ~ScanlineMaker() = default; + + struct ScanRun { + double y; /// that's the top of the scan run, not the baseline + double x_start; // these are not flipped according to the text direction + double x_end; + inline double width() const {return std::abs(x_start - x_end);} + }; + + /** Returns a list of chunks on the current line which can fit text with + the given properties. It is up to the caller to discard any chunks which + are too narrow for its needs. This function may change the y coordinate + between calls if the new height too big to fit in the space remaining in + this shape. Returns an empty vector if there is no space left in the + current shape. */ + virtual std::vector makeScanline(Layout::FontMetrics const &line_height) =0; + + /** Indicates that the caller has successfully filled the current line + and hence that the next call to makeScanline() should return lines on + the next lower line. There is no error return, the next call to + makeScanline() will give an error if there is no more space. */ + virtual void completeLine() =0; + + /** Returns the y coordinate of the top of the scanline that will be + returned by the next call to makeScanline(). */ + virtual double yCoordinate() = 0; + + /** Forces an arbitrary change in the stored y coordinate of the object. + The next call to makeScanline() will return runs whose top is at + the new coordinate. */ + virtual void setNewYCoordinate(double new_y) =0; + + /** Tests whether the caller can fit a new line with the given metrics + into exactly the space returned by the previous call to makeScanline(). + This saves the caller from having to discard its wrapping solution and + starting at the beginning of the line again when a larger font is seen. + The metrics given here are considered to be the ones that are being + used now, and hence is the line advance height used by completeLine(). + */ + virtual bool canExtendCurrentScanline(Layout::FontMetrics const &line_height) =0; + + /** Sets current line block height. Call before completeLine() to correct for + actually used line height (in case some chunks with larger font-size rolled back). + */ + virtual void setLineHeight(Layout::FontMetrics const &line_height) =0; +}; + +/** \brief private to Layout. Generates infinite scanlines for when you don't want wrapping + +This is a 'fake' scanline maker which will always return infinite results, +effectively turning off wrapping. It's a very simple implementation. + +It does have the curious property, however, that the input coordinates are +'real' x and y, but the outputs are rotated according to the +\a block_progression. +*/ +class Layout::InfiniteScanlineMaker : public Layout::ScanlineMaker +{ +public: + InfiniteScanlineMaker(double initial_x, double initial_y, Layout::Direction block_progression); + ~InfiniteScanlineMaker() override; + + /** Returns a single infinite run at the current location */ + std::vector makeScanline(Layout::FontMetrics const &line_height) override; + + /** Increments the current y by the current line height */ + void completeLine() override; + + double yCoordinate() override + {return _y;} + + /** Just changes y */ + void setNewYCoordinate(double new_y) override; + + /** Always true, but has to save the new height */ + bool canExtendCurrentScanline(Layout::FontMetrics const &line_height) override; + + /** Sets current line block height. Call before completeLine() to correct for + actually used line height (in case some chunks with larger font-size rolled back). + */ + void setLineHeight(Layout::FontMetrics const &line_height) override; + +private: + double _x, _y; + Layout::FontMetrics _current_line_height; + bool _negative_block_progression; /// if true, indicates that completeLine() should decrement rather than increment, ie block-progression is either rl or bt +}; + +/** \brief private to Layout. Generates scanlines inside an arbitrary shape + +This is the 'perfect', and hence slowest, implementation of a +Layout::ScanlineMaker, which will return exact bounds for any given +input shape. +*/ +class Layout::ShapeScanlineMaker : public Layout::ScanlineMaker +{ +public: + ShapeScanlineMaker(Shape const *shape, Layout::Direction block_progression); + ~ShapeScanlineMaker() override; + + std::vector makeScanline(Layout::FontMetrics const &line_height) override; + + void completeLine() override; + + double yCoordinate() override; + + void setNewYCoordinate(double new_y) override; + + /** never true */ + bool canExtendCurrentScanline(Layout::FontMetrics const &line_height) override; + + /** Sets current line block height. Call before completeLine() to correct for + actually used line height (in case some chunks with larger font-size rolled back). + */ + void setLineHeight(Layout::FontMetrics const &line_height) override; + +private: + /** To generate scanlines for top-to-bottom text it is easiest if we + simply rotate the given shape by a multiple of 90 degrees. This stores + that. If no rotation was needed we can simply store the pointer we were + given and set shape_needs_freeing appropriately. */ + Shape *_rotated_shape; + + /// see #rotated_shape; + bool _shape_needs_freeing; + + // Shape::BeginRaster() needs floats rather than doubles + float _bounding_box_top, _bounding_box_bottom; + float _y; + float _rasterizer_y; + int _current_rasterization_point; + float _current_line_height; + + bool _negative_block_progression; /// if true, indicates that completeLine() should decrement rather than increment, ie block-progression is either rl or bt +}; + +}//namespace Text +}//namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/libnrtype/Layout-TNG-Scanline-Makers.cpp b/src/libnrtype/Layout-TNG-Scanline-Makers.cpp new file mode 100644 index 0000000..8398bf1 --- /dev/null +++ b/src/libnrtype/Layout-TNG-Scanline-Makers.cpp @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::Text::Layout::ScanlineMaker - text layout engine shape measurers + * + * Authors: + * Richard Hughes + * + * Copyright (C) 2005 Richard Hughes + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "Layout-TNG-Scanline-Maker.h" +#include "livarot/Shape.h" +#include "livarot/float-line.h" +#include + +namespace Inkscape { +namespace Text { + +// *********************** infinite version + +Layout::InfiniteScanlineMaker::InfiniteScanlineMaker(double initial_x, double initial_y, Layout::Direction block_progression) +{ + _current_line_height.setZero(); + switch (block_progression) { + case LEFT_TO_RIGHT: + case RIGHT_TO_LEFT: + _x = initial_y; + _y = initial_x; + break; + default: + _x = initial_x; + _y = initial_y; + break; + } + _negative_block_progression = block_progression == RIGHT_TO_LEFT || block_progression == BOTTOM_TO_TOP; + +} + +Layout::InfiniteScanlineMaker::~InfiniteScanlineMaker() += default; + +std::vector Layout::InfiniteScanlineMaker::makeScanline(Layout::FontMetrics const &line_height) +{ + std::vector runs(1); + runs[0].x_start = _x; + runs[0].x_end = std::numeric_limits::max(); // we could use DBL_MAX, but this just seems safer + runs[0].y = _y; + _current_line_height = line_height; + return runs; +} + +void Layout::InfiniteScanlineMaker::completeLine() +{ + if (_negative_block_progression) + _y -= _current_line_height.emSize(); + else + _y += _current_line_height.emSize(); + _current_line_height.setZero(); +} + +void Layout::InfiniteScanlineMaker::setNewYCoordinate(double new_y) +{ + _y = new_y; +} + +bool Layout::InfiniteScanlineMaker::canExtendCurrentScanline(Layout::FontMetrics const &line_height) +{ + _current_line_height = line_height; + return true; +} + +void Layout::InfiniteScanlineMaker::setLineHeight(Layout::FontMetrics const &line_height) +{ + _current_line_height = line_height; +} + +// *********************** real shapes version + +Layout::ShapeScanlineMaker::ShapeScanlineMaker(Shape const *shape, Layout::Direction block_progression) +{ + if (block_progression == TOP_TO_BOTTOM) { + _rotated_shape = const_cast(shape); + _shape_needs_freeing = false; + } else { + Shape *temp_rotated_shape = new Shape; + _shape_needs_freeing = true; + temp_rotated_shape->Copy(const_cast(shape)); + switch (block_progression) { + case BOTTOM_TO_TOP: temp_rotated_shape->Transform(Geom::Affine(1.0, 0.0, 0.0, -1.0, 0.0, 0.0)); break; // reflect about x axis + case LEFT_TO_RIGHT: temp_rotated_shape->Transform(Geom::Affine(0.0, 1.0, 1.0, 0.0, 0.0, 0.0)); break; // reflect about y=x + case RIGHT_TO_LEFT: temp_rotated_shape->Transform(Geom::Affine(0.0, -1.0, 1.0, 0.0, 0.0, 0.0)); break; // reflect about y=-x + default: break; + } + _rotated_shape = new Shape; + _rotated_shape->ConvertToShape(temp_rotated_shape); + delete temp_rotated_shape; + } + _rotated_shape->CalcBBox(true); + _bounding_box_top = _rotated_shape->topY; + _bounding_box_bottom = _rotated_shape->bottomY; + _y = _rasterizer_y = _bounding_box_top; + _current_rasterization_point = 0; + _rotated_shape->BeginRaster(_y, _current_rasterization_point); + _negative_block_progression = block_progression == RIGHT_TO_LEFT || block_progression == BOTTOM_TO_TOP; +} + + +Layout::ShapeScanlineMaker::~ShapeScanlineMaker() +{ + _rotated_shape->EndRaster(); + if (_shape_needs_freeing) + delete _rotated_shape; +} + +std::vector Layout::ShapeScanlineMaker::makeScanline(Layout::FontMetrics const &line_height) +{ + if (_y > _bounding_box_bottom) + return std::vector(); + + if (_y < _bounding_box_top) + _y = _bounding_box_top; + + FloatLigne line_rasterization; + FloatLigne line_decent_length_runs; + float line_text_height = (float)(line_height.emSize()); + if (line_text_height < 0.001) + line_text_height = 0.001; // Scan() doesn't work for zero height so this will have to do + + _current_line_height = (float)line_height.emSize(); + + // I think what's going on here is that we're moving the top of the scanline to the given position... + _rotated_shape->Scan(_rasterizer_y, _current_rasterization_point, _y, line_text_height); + // ...then actually retrieving the scanline (which alters the first two parameters) + _rotated_shape->Scan(_rasterizer_y, _current_rasterization_point, _y + line_text_height , &line_rasterization, true, line_text_height); + // sanitise the raw rasterisation, which could have weird overlaps + line_rasterization.Flatten(); + // line_rasterization.Affiche(); + // cut out runs that cover less than 90% of the line + line_decent_length_runs.Over(&line_rasterization, 0.9 * line_text_height); + + if (line_decent_length_runs.runs.empty()) + { + if (line_rasterization.runs.empty()) + return std::vector(); // stop the flow + // make up a pointless run: anything that's not an empty vector + std::vector result(1); + result[0].x_start = line_rasterization.runs[0].st; + result[0].x_end = line_rasterization.runs[0].st; + result[0].y = _negative_block_progression ? - _y : _y; + return result; + } + + // convert the FloatLigne to what we use: vector + std::vector result(line_decent_length_runs.runs.size()); + for (unsigned i = 0 ; i < result.size() ; i++) { + result[i].x_start = line_decent_length_runs.runs[i].st; + result[i].x_end = line_decent_length_runs.runs[i].en; + result[i].y = _negative_block_progression ? - _y : _y; + } + + return result; +} + +void Layout::ShapeScanlineMaker::completeLine() +{ + _y += _current_line_height; +} + +double Layout::ShapeScanlineMaker::yCoordinate() +{ + if (_negative_block_progression) return - _y; + return _y; +} + +void Layout::ShapeScanlineMaker::setNewYCoordinate(double new_y) +{ + _y = (float)new_y; + if (_negative_block_progression) _y = - _y; + // what will happen with the rasteriser if we move off the shape? + // it's not an important question because doesn't have a y attribute +} + +bool Layout::ShapeScanlineMaker::canExtendCurrentScanline(Layout::FontMetrics const &/*line_height*/) +{ + //we actually could return true if only the leading changed, but that's too much effort for something that rarely happens + return false; +} + +void Layout::ShapeScanlineMaker::setLineHeight(Layout::FontMetrics const &line_height) +{ + _current_line_height = line_height.emSize(); +} + +}//namespace Text +}//namespace Inkscape diff --git a/src/libnrtype/Layout-TNG.cpp b/src/libnrtype/Layout-TNG.cpp new file mode 100644 index 0000000..96d805d --- /dev/null +++ b/src/libnrtype/Layout-TNG.cpp @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::Text::Layout - text layout engine misc + * + * Authors: + * Richard Hughes + * + * Copyright (C) 2005 Richard Hughes + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "Layout-TNG.h" + +namespace Inkscape { +namespace Text { + +const gunichar Layout::UNICODE_SOFT_HYPHEN = 0x00AD; +const double Layout::LINE_HEIGHT_NORMAL = 1.25; + +Layout::Layout() = default; + +Layout::~Layout() +{ + clear(); +} + +void Layout::clear() +{ + _clearInputObjects(); + _clearOutputObjects(); + + textLength._set = false; + textLengthMultiplier = 1; + textLengthIncrement = 0; + lengthAdjust = LENGTHADJUST_SPACING; +} + +bool Layout::_directions_are_orthogonal(Direction d1, Direction d2) +{ + if (d1 == BOTTOM_TO_TOP) d1 = TOP_TO_BOTTOM; + if (d2 == BOTTOM_TO_TOP) d2 = TOP_TO_BOTTOM; + if (d1 == RIGHT_TO_LEFT) d1 = LEFT_TO_RIGHT; + if (d2 == RIGHT_TO_LEFT) d2 = LEFT_TO_RIGHT; + return d1 != d2; +} + +}//namespace Text +}//namespace Inkscape diff --git a/src/libnrtype/Layout-TNG.h b/src/libnrtype/Layout-TNG.h new file mode 100644 index 0000000..ebcc896 --- /dev/null +++ b/src/libnrtype/Layout-TNG.h @@ -0,0 +1,1207 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::Text::Layout - text layout engine + * + * Authors: + * Richard Hughes + * + * Copyright (C) 2005 Richard Hughes + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef __LAYOUT_TNG_H__ +#define __LAYOUT_TNG_H__ + +//#define DEBUG_TEXTLAYOUT_DUMPASTEXT + +#include <2geom/d2.h> +#include <2geom/affine.h> +#include +#include +#include +#include +#include +#include +#include +#include "style-enums.h" + +namespace Inkscape { + namespace Extension { + namespace Internal { + class CairoRenderContext; + } + } +} + +using Inkscape::Extension::Internal::CairoRenderContext; + +class SPStyle; +class SPObject; +class Shape; +struct SPPrintContext; +class Path; +class SPCurve; +class font_instance; +typedef struct _PangoFontDescription PangoFontDescription; + +namespace Inkscape { +class DrawingGroup; + +namespace Text { + +/** \brief Generates the layout for either wrapped or non-wrapped text and stores the result + +Use this class for all your text output needs. It takes text with formatting +markup as input and turns that into the glyphs and their necessary positions. +It stores the glyphs internally, but maintains enough information to both +retrieve your own rendering information if you wish and to perform visual +text editing where the output refers back to where it came from. + +Usage: +-# Construct +-# Set the text using appendText() and appendControlCode() +-# If you want text wrapping, call appendWrapShape() a few times +-# Call calculateFlow() +-# You can go several directions from here, but the most interesting + things start with creating a Layout::iterator with begin() or end(). + +Terminology, in descending order of size: +- Flow: Not often used, but when it is it means all the text +- Shape: A Shape object which is used to represent one of the regions inside + which to flow the text. Can overlap with... +- Paragraph: Err...A paragraph. Contains one or more... +- Line: An entire horizontal line with a common baseline. Contains one or + more... +- Chunk: You only get more than one of these when a shape is sufficiently + complex that the text has to flow either side of some obstruction in + the middle. A chunk is the base unit for wrapping. Contains one or more... +- Span: A convenient subset of a chunk with the same font, style, + directionality, block progression and input stream. Fill and outline + need not be constant because that's a later rendering stage. +- This is where it gets weird because a span will contain one or more + elements of both of the following, which can overlap with each other in + any way: + - Character: a single Unicode codepoint from an input stream. Many arabic + characters contain multiple glyphs + - Glyph: a rendering primitive for font engines. A ligature glyph will + represent multiple characters. + +Other terminology: +- Input stream: An object representing a single call to appendText() or + appendControlCode(). +- Control code: Metadata in the text stream to signify items that occupy + real space (unlike style changes) but don't belong in the text string. + Paragraph breaks are in this category. See Layout::TextControlCode. +- SVG1.1: The W3C Recommendation "Scalable Vector Graphics (SVG) 1.1" + http://www.w3.org/TR/SVG11/ +- 'left', 'down', etc: These terms are generally used to mean what they + mean in left-to-right, top-to-bottom text but rotated or reflected for + the current directionality. Thus, the 'width' of a ttb line is actually + its height, and the (internally stored) y coordinate of a glyph is + actually its x coordinate. Confusing to the reader but much simpler in + the code. All public methods use real x and y. + +Comments: +- There's a strong emphasis on international support in this class, but + that's primarily because once you can display all the insane things + required by various languages, simple things like styling text are + almost trivial. +- There are a few places (appendText() is one) where pointers are held to + caller-owned objects and used for quite a long time. This is messy but + is safe for our usage scenario and in many cases the cost of copying the + objects is quite high. +- "Why isn't foo here?": Ask yourself if it's possible to implement foo + externally using iterators. However this may not mean that it doesn't + belong as a member, though. +- I've used floats rather than doubles to store relative distances in some + places (internal only) where it would save significant amounts of memory. + The SVG spec allows you to do this as long as intermediate calculations + are done double. Very very long lines might not finish precisely where + you want, but that's to be expected with any typesetting. Also, + SVGLength only uses floats. +- If you look at the six arrays for holding the output data you'll realise + that there's no O(1) way to drill down from a paragraph to find its + starting glyph. This was a conscious decision to reduce complexity and + to save memory. Drilling down isn't actually that slow because a binary + chop will work nicely. Add this to the realisation that most of the + times you do this will be in response to user actions and hence you only + need to be faster than the user and I think the design makes sense. +- There are a massive number of functions acting on Layout::iterator. A + large number are trivial and will be inline, but is it really necessary + to have all these, especially when some can be implemented by the caller + using the others? +- The separation of methods between Layout and Layout::iterator is a + bit arbitrary, because many methods could go in either. I've used the STL + model where the iterator itself can only move around; the base class is + required to do anything interesting. +- I use Pango internally, not Pangomm. The reason for this is lots of + Pangomm methods take Glib::ustrings as input and then output byte offsets + within the strings. There's simply no way to use byte offsets with + ustrings without some very entertaining reinterpret_cast<>s. The Pangomm + docs seem to be lacking quite a lot of things mentioned in the Pango + docs, too. +*/ +class Layout { +public: + class iterator; + friend class iterator; + class Calculator; + friend class Calculator; + class ScanlineMaker; + class InfiniteScanlineMaker; + class ShapeScanlineMaker; + + Layout(); + virtual ~Layout(); + + /** Used to specify any particular text direction required. Used for + both the 'direction' and 'block-progression' CSS attributes. */ + enum Direction {LEFT_TO_RIGHT, RIGHT_TO_LEFT, TOP_TO_BOTTOM, BOTTOM_TO_TOP}; + + /** Used to specify orientation of glyphs in vertical text. */ + enum Orientation {ORIENTATION_UPRIGHT, ORIENTATION_SIDEWAYS}; + + + /** Display alignment for shapes. See appendWrapShape(). */ + enum DisplayAlign {DISPLAY_ALIGN_BEFORE, DISPLAY_ALIGN_CENTER, DISPLAY_ALIGN_AFTER}; + + /** lengthAdjust values */ + enum LengthAdjust {LENGTHADJUST_SPACING, LENGTHADJUST_SPACINGANDGLYPHS}; + + enum WrapMode { + WRAP_NONE, // No wrapping or wrapping via role="line". + WRAP_WHITE_SPACE, // Wrapping via 'white-space' property. + WRAP_INLINE_SIZE, // Wrapping via 'inline-size' property. + WRAP_SHAPE_INSIDE // Wrapping via 'shape-inside' propertry. + } wrap_mode = WRAP_NONE; + + /** The optional attributes which can be applied to a SVG text or + related tag. See appendText(). See SVG1.1 section 10.4 for the + definitions of all these members. See sp_svg_length_list_read() for + the standard way to make these vectors. It is the responsibility of + the caller to deal with the inheritance of these values using its + knowledge of the parse tree. */ + struct OptionalTextTagAttrs { + std::vector x; + std::vector y; + std::vector dx; + std::vector dy; + std::vector rotate; + SVGLength textLength; + LengthAdjust lengthAdjust; + }; + + /** Control codes which can be embedded in the text to be flowed. See + appendControlCode(). */ + enum TextControlCode { + PARAGRAPH_BREAK, /// forces the flow to move on to the next line + SHAPE_BREAK, /// forces the flow to ignore the remainder of the current shape (from #flow_inside_shapes) and continue at the top of the one after. + ARBITRARY_GAP /// inserts an arbitrarily-sized hole in the flow in line with the current text. + }; + + /** For expressing paragraph alignment. These values are rotated in the + case of vertical text, but are not dependent on whether the paragraph is + rtl or ltr, thus LEFT is always either left or top. */ + enum Alignment {LEFT, CENTER, RIGHT, FULL, NONE}; + + /** The CSS spec allows line-height:normal to be whatever the user agent + thinks will look good. This is our value, as a multiple of font-size. */ + static const double LINE_HEIGHT_NORMAL; + + // ************************** describing the stuff to flow ************************* + + /** \name Input + Methods for describing the text you want to flow, its style, and the + shapes to flow in to. + */ + //@{ + + /** Empties everything stored in this class and resets it to its + original state, like when it was created. All iterators on this + object will be invalidated (but can be revalidated using + validateIterator(). */ + void clear(); + + /** Queries whether any calls have been made to appendText() or + appendControlCode() since the object was last cleared. */ + bool inputExists() const + {return !_input_stream.empty();} + + bool _input_truncated = false; + bool inputTruncated() const + {return _input_truncated;} + + /** adds a new piece of text to the end of the current list of text to + be processed. This method can only add text of a consistent style. + To add lots of different styles, call it lots of times. + \param text The text. \b Note: only a \em pointer is stored. Do not + mess with the text until after you have called + calculateFlow(). + \param style The font style. Layout will hold a reference to this + object for the duration of its ownership, ie until you + call clear() or the class is destroyed. Must not be NULL. + \param source Pointer to object that is source of text. + \param optional_attributes A structure containing additional options + for this text. See OptionalTextTagAttrs. The values are + copied to internal storage before this method returns. + \param optional_attributes_offset It is convenient for callers to be + able to use the same \a optional_attributes structure for + several sequential text fields, in which case the vectors + will need to be offset. This parameter causes the nth + element of all the vectors to be read as if it were the + first. + \param text_begin Used for selecting only a substring of \a text + to process. + \param text_end Used for selecting only a substring of \a text + to process. + */ + void appendText(Glib::ustring const &text, SPStyle *style, SPObject *source, OptionalTextTagAttrs const *optional_attributes, unsigned optional_attributes_offset, Glib::ustring::const_iterator text_begin, Glib::ustring::const_iterator text_end); + inline void appendText(Glib::ustring const &text, SPStyle *style, SPObject *source, OptionalTextTagAttrs const *optional_attributes = nullptr, unsigned optional_attributes_offset = 0) + {appendText(text, style, source, optional_attributes, optional_attributes_offset, text.begin(), text.end());} + + /** Control codes are metadata in the text stream to signify items + that occupy real space (unlike style changes) but don't belong in the + text string. See TextControlCode for the types available. + + A control code \em cannot be the first item in the input stream. Use + appendText() with an empty string to set up the paragraph properties. + \param code A member of the TextFlowControlCode enumeration. + \param width The width in pixels that this item occupies. + \param ascent The number of pixels above the text baseline that this + control code occupies. + \param descent The number of pixels below the text baseline that this + control code occupies. + \param source Pointer to object that is source of control code. + Note that for some control codes (eg tab) the values of the \a width, + \a ascender and \a descender are implied by the surrounding text (and + in the case of tabs, the values set in tab_stops) so the values you pass + here are ignored. + */ + void appendControlCode(TextControlCode code, SPObject *source, double width = 0.0, double ascent = 0.0, double descent = 0.0); + + /** Stores another shape inside which to flow the text. If this method + is never called then no automatic wrapping is done and lines will + continue to infinity if necessary. Text can be flowed inside multiple + shapes in sequence, like with frames in a DTP package. If the text flows + past the end of the last shape all remaining text is ignored. + + \param shape The Shape to use next in the flow. The storage for this + is managed by the caller, and need only be valid for + the duration of the call to calculateFlow(). + \param display_align The vertical alignment of the text within this + shape. See XSL1.0 section 7.13.4. The behaviour of + settings other than DISPLAY_ALIGN_BEFORE when using + non-rectangular shapes is undefined. + */ + void appendWrapShape(Shape const *shape, DisplayAlign display_align = DISPLAY_ALIGN_BEFORE); + + // ************************** textLength and friends ************************* + + /** Gives the length target of this layout, as given by textLength attribute. + + FIXME: by putting it here we only support @textLength on text and flowRoot, not on any + spans inside. For spans, we will need to add markers of start and end of a textLength span + into the _input_stream. These spans can nest (SVG 1.1, section 10.5). After a first layout + calculation, we would go through the input stream and, for each end of a textLength span, + go through its items, choose those where it wasn't yet set by a nested span, calculate + their number of characters, divide the length deficit by it, and set set the + textLengthMultiplier for those characters only. For now we do this for the entire layout, + without dealing with spans. + */ + SVGLength textLength; + + /** How do we meet textLength if specified: by letterspacing or by scaling horizontally */ + LengthAdjust lengthAdjust = LENGTHADJUST_SPACING; + + /** By how much each character needs to be wider or narrower, using the specified lengthAdjust + strategy, for the layout to meet its textLength target. Is set to non-zero after the layout + is calculated for the first time, then it is recalculated with each glyph getting its adjustment. */ + /** This one is used by scaling strategies: each glyph width is multiplied by this */ + double textLengthMultiplier = 1; + /** This one is used by letterspacing strategy: to each glyph width, this is added */ + double textLengthIncrement = 0; + + /** Get the actual spacing increment if it's due with the current values of above stuff, otherwise 0 */ + double getTextLengthIncrementDue() const; + /** Get the actual scale multiplier if it's due with the current values of above stuff, otherwise 1 */ + double getTextLengthMultiplierDue() const; + + /** Get actual length of layout, by summing span lengths. For one-line non-flowed text, just + the width; for multiline non-flowed, sum of lengths of all lines; for flowed text, sum of + scanline widths for all non-last lines plus text width of last line. */ + double getActualLength() const; + + + // ************************** doing the actual flowing ************************* + + /** \name Processing + The method to do the actual work of converting text into glyphs. + */ + //@{ + + /** Takes all the stuff you set with the members above here and creates + a load of glyphs for use with the members below here. All iterators on + this object will be invalidated (but can be fixed with validateIterator(). + The implementation just creates a new Layout::Calculator and calls its + Calculator::Calculate() method, so if you want more details on the + internals, go there. + \return false on failure. + */ + bool calculateFlow(); + + //@} + + // ************************** operating on the output glyphs ************************* + + /** \name Output + Methods for reading and interpreting the output glyphs. See also + Layout::iterator. + */ + //@{ + + /** Returns true if there are some glyphs in this object, ie whether + computeFlow() has been called on a non-empty input since the object was + created or the last call to clear(). */ + inline bool outputExists() const + {return !_characters.empty();} + + /** Adds all the output glyphs to \a in_arena using the given \a paintbox. + \param in_arena The arena to add the glyphs group to + \param paintbox The current rendering tile + */ + void show(DrawingGroup *in_arena, Geom::OptRect const &paintbox) const; + + /** Calculates the smallest rectangle completely enclosing all the + glyphs. + \param bounding_box Where to store the box + \param transform The transform to be applied to the entire object + prior to calculating its bounds. + */ + Geom::OptRect bounds(Geom::Affine const &transform, bool with_stroke = false, int start = -1, int length = -1) const; + + /** Sends all the glyphs to the given print context. + \param ctx I have + \param pbox no idea + \param dbox what these + \param bbox parameters + \param ctm do yet + */ + void print(SPPrintContext *ctx, Geom::OptRect const &pbox, Geom::OptRect const &dbox, Geom::OptRect const &bbox, Geom::Affine const &ctm) const; + + /** Renders all the glyphs to the given Cairo rendering context. + \param ctx The Cairo rendering context to be used + */ + void showGlyphs(CairoRenderContext *ctx) const; + + /** Returns the font family of the indexed span */ + Glib::ustring getFontFamily(unsigned span_index) const; + +#if DEBUG_TEXTLAYOUT_DUMPASTEXT + /** debug and unit test method. Creates a textual representation of the + contents of this object. The output is designed to be both human-readable + and comprehensible when diffed with a known-good dump. */ + Glib::ustring dumpAsText() const; +#endif + + /** Moves all the glyphs in the structure so that the baseline of all + the characters sits neatly along the path specified. If the text has + more than one line the results are undefined. The 'align' means to + use the SVG align method as documented in SVG1.1 section 10.13.2. + NB: njh has suggested that it would be cool if we could flow from + shape to path and back again. This is possible, so this method will be + removed at some point. + A pointer to \a path is retained by the class for use by the cursor + positioning functions. */ + void fitToPathAlign(SVGLength const &startOffset, Path const &path); + + /** Convert the specified range of characters into their bezier + outlines. + */ + std::unique_ptr convertToCurves(iterator const &from_glyph, iterator const &to_glyph) const; + std::unique_ptr convertToCurves() const; + + /** Apply the given transform to all the output presently stored in + this object. This only transforms the glyph positions, The glyphs + themselves will not be transformed. */ + void transform(Geom::Affine const &transform); + + //@} + + // ********** + + /** \name Output (Iterators) + Methods for operating with the Layout::iterator class. The method + names ending with 'Index' return 0-based offsets of the number of + items since the beginning of the flow. + */ + //@{ + + /** Returns an iterator pointing at the first glyph of the flowed output. + The first glyph is also the first character, line, paragraph, etc. */ + inline iterator begin() const; + + /** Returns an iterator pointing just past the end of the last glyph, + which is also just past the end of the last chunk, span, etc, etc. */ + inline iterator end() const; + + /** Returns an iterator pointing at the given character index. This + index should be related to the result from a prior call to + iteratorToCharIndex(). */ + inline iterator charIndexToIterator(int char_index) const; + + /** Returns the character index from the start of the flow represented + by the given iterator. This number isn't very useful, except for when + editing text it will stay valid across calls to computeFlow() and will + change in predictable ways when characters are added and removed. It's + also useful when transitioning old code. */ + inline int iteratorToCharIndex(iterator const &it) const; + + /** Checks the validity of the given iterator over the current layout. + If it points to a position out of the bounds for this layout it will + be corrected to the nearest valid position. If you pass an iterator + belonging to a different layout it will be converted to one for this + layout. */ + inline void validateIterator(iterator *it) const; + + /** Returns an iterator pointing to the cursor position for a mouse + click at the given coordinates. */ + iterator getNearestCursorPositionTo(double x, double y) const; + inline iterator getNearestCursorPositionTo(Geom::Point const &point) const; + + /** Returns an iterator pointing to the letter whose bounding box contains + the given coordinates. end() if the point is not over any letter. The + iterator will \em not point at the specific glyph within the character. */ + iterator getLetterAt(double x, double y) const; + inline iterator getLetterAt(Geom::Point &point) const; + + /* Returns an iterator pointing to the character in the output which + was created from the given input. If the character at the given byte + offset was removed (soft hyphens, for example) the next character after + it is returned. If no input was added with the given object, end() is + returned. If more than one input has the same object, the first will + be used regardless of the value of \a text_iterator. If + \a text_iterator is out of bounds, the first or last character belonging + to the given input will be returned accordingly. + iterator sourceToIterator(SPObject *source, Glib::ustring::const_iterator text_iterator) const; + */ + + /** Returns an iterator pointing to the first character in the output + which was created from the given source. If \a source object is invalid, + end() is returned. If more than one input has the same object, the + first one will be used. */ + iterator sourceToIterator(SPObject *source) const; + + // many functions acting on iterators, most of which are obvious + // also most of them don't check that \a it != end(). Be careful. + + /** Returns the bounding box of the given glyph, and its rotation. + The centre of rotation is the horizontal centre of the box at the + text baseline. */ + Geom::OptRect glyphBoundingBox(iterator const &it, double *rotation) const; + + /** Returns the zero-based line number of the character pointed to by + \a it. */ + inline unsigned lineIndex(iterator const &it) const; + + /** Returns the zero-based number of the shape which contains the + character pointed to by \a it. */ + inline unsigned shapeIndex(iterator const &it) const; + + /** Returns true if the character at \a it is a whitespace, as defined + by Pango. This is not meant to be used for picking out words from the + output, use iterator::nextStartOfWord() and friends instead. */ + inline bool isWhitespace(iterator const &it) const; + + /** Returns character pointed to by \a it. If \a it == end() the result is undefined. */ + inline gchar characterAt(iterator const &it) const; + + /** Returns true if the text at \a it is hidden (i.e. overflowed). */ + bool isHidden(iterator const &it) const; + + /** Discovers where the character pointed to by \a it came from, by + retrieving the object that was passed to the call to appendText() or + appendControlCode() which generated that output. If \a it == end() + then NULL is returned as the object. If the character was generated + from a call to appendText() then the optional \a text_iterator + parameter is set to point to the actual character, otherwise + \a text_iterator is unaltered. */ + void getSourceOfCharacter(iterator const &it, SPObject **source, Glib::ustring::iterator *text_iterator = nullptr) const; + + /** For latin text, the left side of the character, on the baseline */ + Geom::Point characterAnchorPoint(iterator const &it) const; + + /** For left aligned text, the leftmost end of the baseline + For rightmost text, the rightmost... you probably got it by now ;-)*/ + std::optional baselineAnchorPoint() const; + + Geom::Path baseline() const; + + /** This is that value to apply to the x,y attributes of tspan role=line + elements, and hence it takes alignment into account. */ + Geom::Point chunkAnchorPoint(iterator const &it) const; + + /** Returns the box extents (not ink extents) of the given character. + The centre of rotation is at the horizontal centre of the box on the + text baseline. */ + Geom::Rect characterBoundingBox(iterator const &it, double *rotation = nullptr) const; + + /** Basically uses characterBoundingBox() on all the characters from + \a start to \a end and returns the union of these boxes. The return value + is a list of zero or more quadrilaterals specified by a group of four + points for each, thus size() is always a multiple of four. */ + std::vector createSelectionShape(iterator const &it_start, iterator const &it_end, Geom::Affine const &transform) const; + + /** Returns true if \a it points to a character which is a valid cursor + position, as defined by Pango. */ + inline bool isCursorPosition(iterator const &it) const; + + /** Gets the ideal cursor shape for a given iterator. The result is + undefined if \a it is not at a valid cursor position. + \param it The location in the output + \param position The pixel location of the centre of the 'bottom' of + the cursor. + \param height The height in pixels of the surrounding text + \param rotation The angle to draw from \a position. Radians, zero up, + increasing clockwise. + */ + void queryCursorShape(iterator const &it, Geom::Point &position, double &height, double &rotation) const; + + /** Returns true if \a it points to a character which is a the start of + a word, as defined by Pango. */ + inline bool isStartOfWord(iterator const &it) const; + + /** Returns true if \a it points to a character which is a the end of + a word, as defined by Pango. */ + inline bool isEndOfWord(iterator const &it) const; + + /** Returns true if \a it points to a character which is a the start of + a sentence, as defined by Pango. */ + inline bool isStartOfSentence(iterator const &it) const; + + /** Returns true if \a it points to a character which is a the end of + a sentence, as defined by Pango. */ + inline bool isEndOfSentence(iterator const &it) const; + + /** Returns the zero-based number of the paragraph containing the + character pointed to by \a it. */ + inline unsigned paragraphIndex(iterator const &it) const; + + /** Returns the actual alignment used for the paragraph containing + the character pointed to by \a it. This means that the CSS 'start' + and 'end' are correctly translated into LEFT or RIGHT according to + the paragraph's directionality. For vertical text, LEFT is top + alignment and RIGHT is bottom. */ + inline Alignment paragraphAlignment(iterator const &it) const; + + /** Returns kerning information which could cause the current output + to be exactly reproduced if the letter and word spacings were zero and + full justification was not used. The x and y arrays are not used, but + they are cleared. The dx applied to the first character in a chunk + will always be zero. If the region between \a from and \a to crosses + a line break then the results may be surprising, and are undefined. + Trailing zeros on the returned arrays will be trimmed. */ + void simulateLayoutUsingKerning(iterator const &from, iterator const &to, OptionalTextTagAttrs *result) const; + + //@} + + + /** + * Keep track of font metrics. Two use cases: + * 1. Keep track of ascent, descent, and x-height of an individual font. + * 2. Keep track of effective ascent and descent that includes half-leading. + * + * Note: Leading refers to the "external" leading which is added (subtracted) due to + * a computed value of 'line-height' that differs from 'font-size'. "Internal" leading + * which is specified inside a font is not used in CSS. The 'font-size' is based on + * the font's em size which is 'ascent' + 'descent'. + * + * This structure was renamed (and modified) from "LineHeight". + * + * It's useful for this to be public so that ScanlineMaker can use it. + */ + class FontMetrics { + + public: + FontMetrics() { reset(); } + + void reset() { + ascent = 0.8; + descent = 0.2; + xheight = 0.5; + ascent_max = 0.8; + descent_max = 0.2; + } + + void set( font_instance *font ); + + // CSS 2.1 dictates that font-size is based on em-size which is defined as ascent + descent + inline double emSize() const {return ascent + descent;} + // Alternatively name function for use 2. + inline double lineSize() const { return ascent + descent; } + inline void setZero() {ascent = descent = xheight = ascent_max = descent_max = 0.0;} + + // For scaling for 'font-size'. + inline FontMetrics& operator*=(double x) { + ascent *= x; descent *= x; xheight *= x; ascent_max *= x; descent_max *= x; + return *this; + } + + /// Save the larger values of ascent and descent between this and other. Needed for laying + /// out a line with mixed font-sizes, fonts, or line spacings. + void max(FontMetrics const &other); + + /// Calculate the effective ascent and descent including half "leading". + void computeEffective( const double &line_height ); + + inline double getTypoAscent() const {return ascent; } + inline double getTypoDescent() const {return descent; } + inline double getXHeight() const {return xheight; } + inline double getMaxAscent() const {return ascent_max; } + inline double getMaxDescent() const {return descent_max; } + + // private: + double ascent; // Typographic ascent. + double descent; // Typographic descent. (Normally positive). + double xheight; // Height of 'x' measured from alphabetic baseline. + double ascent_max; // Maximum ascent of all glyphs in font. + double descent_max; // Maximum descent of all glyphs in font. + + }; // End FontMetrics + + /** The strut is the minimum value used in calculating line height. */ + FontMetrics strut; + +private: + /** Erases all the stuff set by the owner as input, ie #_input_stream + and #_input_wrap_shapes. */ + void _clearInputObjects(); + + /** Erases all the stuff output by computeFlow(). Glyphs and things. */ + void _clearOutputObjects(); + + static const gunichar UNICODE_SOFT_HYPHEN; + + // ******************* input flow + + enum InputStreamItemType {TEXT_SOURCE, CONTROL_CODE}; + + class InputStreamItem { + public: + virtual ~InputStreamItem() = default; + virtual InputStreamItemType Type() =0; + SPObject *source; + }; + + /** Represents a text item in the input stream. See #_input_stream. + Most of the members are copies of the values passed to appendText(). */ + class InputStreamTextSource : public InputStreamItem { + public: + InputStreamItemType Type() override {return TEXT_SOURCE;} + ~InputStreamTextSource() override; + Glib::ustring const *text; /// owned by the caller + Glib::ustring::const_iterator text_begin, text_end; + int text_length; /// in characters, from text_start to text_end only + SPStyle *style; + /** These vectors can (often will) be shorter than the text + in this source, but never longer. */ + std::vector x; + std::vector y; + std::vector dx; + std::vector dy; + std::vector rotate; + SVGLength textLength; + LengthAdjust lengthAdjust; + Glib::ustring lang; + + // a few functions for some of the more complicated style accesses + /// The return value must be freed with pango_font_description_free() + PangoFontDescription *styleGetFontDescription() const; + font_instance *styleGetFontInstance() const; + Direction styleGetBlockProgression() const; + SPCSSTextOrientation styleGetTextOrientation() const; + SPCSSBaseline styleGetDominantBaseline() const; + Alignment styleGetAlignment(Direction para_direction, bool try_text_align) const; + }; + + /** Represents a control code item in the input stream. See + #_input_streams. All the members are copies of the values passed to + appendControlCode(). */ + class InputStreamControlCode : public InputStreamItem { + public: + InputStreamItemType Type() override {return CONTROL_CODE;} + TextControlCode code; + double ascent; + double descent; + double width; + }; + + /** This is our internal storage for all the stuff passed to the + appendText() and appendControlCode() functions. */ + std::vector _input_stream; + + /** The parameters to appendText() are allowed to be a little bit + complex. This copies them to be the right length and starting at zero. + We also don't want to write five bits of identical code just with + different variable names. */ + static void _copyInputVector(std::vector const &input_vector, unsigned input_offset, std::vector *output_vector, size_t max_length); + + /** The overall block-progression of the whole flow. */ + inline Direction _blockProgression() const + { + if(!_input_stream.empty()) + return static_cast(_input_stream.front())->styleGetBlockProgression(); + return TOP_TO_BOTTOM; + } + + /** The overall text-orientation of the whole flow. */ + inline SPCSSTextOrientation _blockTextOrientation() const + { + if(!_input_stream.empty()) + return static_cast(_input_stream.front())->styleGetTextOrientation(); + return SP_CSS_TEXT_ORIENTATION_MIXED; + } + + /** The overall text-orientation of the whole flow. */ + inline SPCSSBaseline _blockBaseline() const + { + if(!_input_stream.empty()) + return static_cast(_input_stream.front())->styleGetDominantBaseline(); + return SP_CSS_BASELINE_AUTO; + } + + /** so that LEFT_TO_RIGHT == RIGHT_TO_LEFT but != TOP_TO_BOTTOM */ + static bool _directions_are_orthogonal(Direction d1, Direction d2); + + /** If the output is empty callers still want to be able to call + queryCursorShape() and get a valid answer so, while #_input_wrap_shapes + can still be considered valid, we need to precompute the cursor shape + for this case. */ + void _calculateCursorShapeForEmpty(); + + struct CursorShape { + Geom::Point position; + double height; + double rotation; + } _empty_cursor_shape; + + // ******************* input shapes + + struct InputWrapShape { + Shape const *shape; /// as passed to Layout::appendWrapShape() + DisplayAlign display_align; /// as passed to Layout::appendWrapShape() + }; + std::vector _input_wrap_shapes; + + // ******************* output + + /** as passed to fitToPathAlign() */ + Path const *_path_fitted = nullptr; + + struct Glyph; + struct Character; + struct Span; + struct Chunk; + struct Line; + struct Paragraph; + + // A glyph + struct Glyph { + int glyph; + unsigned in_character; + bool hidden; + float x; /// relative to the start of the chunk + float y; /// relative to the current line's baseline + float rotation; /// absolute, modulo any object transforms, which we don't know about + Orientation orientation; /// Orientation of glyph in vertical text + float advance; /// for positioning next glyph + float vertical_scale; /// to implement lengthAdjust="spacingAndGlyphs" that must scale glyphs only horizontally; instead we change font size and then undo that change vertically only + inline Span const & span (Layout const *l) const {return l->_spans[l->_characters[in_character].in_span];} + inline Chunk const & chunk(Layout const *l) const {return l->_chunks[l->_spans[l->_characters[in_character].in_span].in_chunk];} + inline Line const & line (Layout const *l) const {return l->_lines[l->_chunks[l->_spans[l->_characters[in_character].in_span].in_chunk].in_line];} + }; + + // A unicode character + struct Character { + unsigned in_span; + float x; /// relative to the start of the *span* (so we can do block-progression) + PangoLogAttr char_attributes; + gchar the_char = '#'; + int in_glyph; /// will be -1 if this character has no visual representation + inline Span const & span (Layout const *l) const {return l->_spans[in_span];} + inline Chunk const & chunk (Layout const *l) const {return l->_chunks[l->_spans[in_span].in_chunk];} + inline Line const & line (Layout const *l) const {return l->_lines[l->_chunks[l->_spans[in_span].in_chunk].in_line];} + inline Paragraph const & paragraph(Layout const *l) const {return l->_paragraphs[l->_lines[l->_chunks[l->_spans[in_span].in_chunk].in_line].in_paragraph];} + // to get the advance width of a character, subtract the x values if it's in the middle of a span, or use span.x_end if it's at the end + }; + + // A collection of characters that share the same style and position start ( or x, y attributes). + struct Span { + unsigned in_chunk; + font_instance *font; + float font_size; + float x_start; /// relative to the start of the chunk + float x_end; /// relative to the start of the chunk + float y_offset; /// relative to line baseline (without baseline shift) + inline float width() const {return std::abs(x_start - x_end);} + FontMetrics line_height; + double baseline_shift; /// relative to the line's baseline (CSS) + SPCSSTextOrientation text_orientation; + Direction direction; /// See CSS3 section 3.2. Either rtl or ltr + Direction block_progression; /// See CSS3 section 3.2. The direction in which lines go. + unsigned in_input_stream_item; + Glib::ustring::const_iterator input_stream_first_character; + inline Chunk const & chunk (Layout const *l) const {return l->_chunks[in_chunk]; } + inline Line const & line (Layout const *l) const {return l->_lines[l->_chunks[in_chunk].in_line]; } + inline Paragraph const & paragraph(Layout const *l) const {return l->_paragraphs[l->_lines[l->_chunks[in_chunk].in_line].in_paragraph];} + }; + + // A part of a line that is not broken. + struct Chunk { + unsigned in_line; + double left_x; + }; + + // A line of text. Depending on the shape, it may contain one or more chunks. + struct Line { + unsigned in_paragraph; + double baseline_y; + unsigned in_shape; + bool hidden; + }; + + // A paragraph. SVG 2 does not contain native paragraphs. + struct Paragraph { + Direction base_direction; /// can be overridden by child Span objects + Alignment alignment; + }; + + std::vector _paragraphs; + std::vector _lines; + std::vector _chunks; + std::vector _spans; + std::vector _characters; + std::vector _glyphs; + + /** gets the overall matrix that transforms the given glyph from local + space to world space. */ + void _getGlyphTransformMatrix(int glyph_index, Geom::Affine *matrix) const; + + // loads of functions to drill down the object tree, all of them + // annoyingly similar and all of them requiring predicate functors. + // I'll be buggered if I can find a way to make it work with + // functions or with a templated functor, so macros it is. +#define EMIT_PREDICATE(name, object_type, index_generator) \ + class name { \ + Layout const * const _flow; \ + public: \ + inline name(Layout const *flow) : _flow(flow) {} \ + inline bool operator()(object_type const &object, unsigned index) \ + {g_assert(_flow); return index_generator < index;} \ + } +// end of macro + EMIT_PREDICATE(PredicateLineToSpan, Span, _flow->_chunks[object.in_chunk].in_line); + EMIT_PREDICATE(PredicateLineToCharacter, Character, _flow->_chunks[_flow->_spans[object.in_span].in_chunk].in_line); + EMIT_PREDICATE(PredicateSpanToCharacter, Character, object.in_span); + EMIT_PREDICATE(PredicateSourceToCharacter, Character, _flow->_spans[object.in_span].in_input_stream_item); + + inline unsigned _lineToSpan(unsigned line_index) const + {return std::lower_bound(_spans.begin(), _spans.end(), line_index, PredicateLineToSpan(this)) - _spans.begin();} + inline unsigned _lineToCharacter(unsigned line_index) const + {return std::lower_bound(_characters.begin(), _characters.end(), line_index, PredicateLineToCharacter(this)) - _characters.begin();} + inline unsigned _spanToCharacter(unsigned span_index) const + {return std::lower_bound(_characters.begin(), _characters.end(), span_index, PredicateSpanToCharacter(this)) - _characters.begin();} + inline unsigned _sourceToCharacter(unsigned source_index) const + {return std::lower_bound(_characters.begin(), _characters.end(), source_index, PredicateSourceToCharacter(this)) - _characters.begin();} + + /** given an x and y coordinate and a line number, returns an iterator + pointing to the closest cursor position on that line to the + coordinate. + ('y' is needed to handle cases where multiline text is simulated via the 'y' attribute.) */ + iterator _cursorXOnLineToIterator(unsigned line_index, double local_x, double local_y = 0) const; + + /** calculates the width of a chunk, which is the largest x + coordinate (start or end) of the spans contained within it. */ + double _getChunkWidth(unsigned chunk_index) const; +}; + +/** \brief Holds a position within the glyph output of Layout. + +Used to access the output of a Layout, query information and generally +move around in it. See Layout for a glossary of the names of functions. + +I'm not going to document all the methods because most of their names make +their function self-evident. + +A lot of the functions would do the same thing in a naive implementation +for latin-only text, for example nextCharacter(), nextCursorPosition() and +cursorRight(). Generally it's fairly obvious which one you should use in a +given situation, but sometimes you might need to put some thought in to it. + +All the methods return false if the requested action would have caused the +current position to move out of bounds. In this case the position is moved +to either begin() or end(), depending on which direction you were going. + +Note that some characters do not have a glyph representation (eg line +breaks), so if you try using prev/nextGlyph() from one of these you're +heading for a crash. +*/ +class Layout::iterator { +public: + friend class Layout; + // this is just so you can create uninitialised iterators - don't actually try to use one + iterator() : + _parent_layout(nullptr), + _glyph_index(-1), + _char_index(0), + _cursor_moving_vertically(false), + _x_coordinate(0.0){} + // no copy constructor required, the default does what we want + bool operator== (iterator const &other) const + {return _glyph_index == other._glyph_index && _char_index == other._char_index;} + bool operator!= (iterator const &other) const + {return _glyph_index != other._glyph_index || _char_index != other._char_index;} + + /* mustn't compare _glyph_index in these operators because for characters + that don't have glyphs (line breaks, elided soft hyphens, etc), the glyph + index is -1 which makes them not well-ordered. To be honest, iterating by + glyphs is not very useful and should be avoided. */ + bool operator< (iterator const &other) const + {return _char_index < other._char_index;} + bool operator<= (iterator const &other) const + {return _char_index <= other._char_index;} + bool operator> (iterator const &other) const + {return _char_index > other._char_index;} + bool operator>= (iterator const &other) const + {return _char_index >= other._char_index;} + + /* **** visual-oriented methods **** */ + + //glyphs + inline bool prevGlyph(); + inline bool nextGlyph(); + + //span + bool prevStartOfSpan(); + bool thisStartOfSpan(); + bool nextStartOfSpan(); + + //chunk + bool prevStartOfChunk(); + bool thisStartOfChunk(); + bool nextStartOfChunk(); + + //line + bool prevStartOfLine(); + bool thisStartOfLine(); + bool nextStartOfLine(); + bool thisEndOfLine(); + + //shape + bool prevStartOfShape(); + bool thisStartOfShape(); + bool nextStartOfShape(); + + /* **** text-oriented methods **** */ + + //characters + inline bool nextCharacter(); + inline bool prevCharacter(); + + bool nextCursorPosition(); + bool prevCursorPosition(); + bool nextLineCursor(int n = 1); + bool prevLineCursor(int n = 1); + + //words + bool nextStartOfWord(); + bool prevStartOfWord(); + bool nextEndOfWord(); + bool prevEndOfWord(); + + //sentences + bool nextStartOfSentence(); + bool prevStartOfSentence(); + bool nextEndOfSentence(); + bool prevEndOfSentence(); + + //paragraphs + bool prevStartOfParagraph(); + bool thisStartOfParagraph(); + bool nextStartOfParagraph(); + //no endOfPara methods because that's just the previous char + + //sources + bool prevStartOfSource(); + bool thisStartOfSource(); + bool nextStartOfSource(); + + //logical cursor movement + bool cursorUp(int n = 1); + bool cursorDown(int n = 1); + bool cursorLeft(); + bool cursorRight(); + + //logical cursor movement (by word or paragraph) + bool cursorUpWithControl(); + bool cursorDownWithControl(); + bool cursorLeftWithControl(); + bool cursorRightWithControl(); + +private: + Layout const *_parent_layout; + int _glyph_index; /// index into Layout::glyphs, or -1 + unsigned _char_index; /// index into Layout::character + bool _cursor_moving_vertically; + /** for cursor up/down movement we must maintain the x position where + we started so the cursor doesn't 'drift' left or right with the repeated + quantization to character boundaries. */ + double _x_coordinate; + + inline iterator(Layout const *p, unsigned c, int g) + : _parent_layout(p), _glyph_index(g), _char_index(c), _cursor_moving_vertically(false), _x_coordinate(0.0) {} + inline iterator(Layout const *p, unsigned c) + : _parent_layout(p), _glyph_index(p->_characters[c].in_glyph), _char_index(c), _cursor_moving_vertically(false), _x_coordinate(0.0) {} + // no dtor required + void beginCursorUpDown(); /// stores the current x coordinate so that the cursor won't drift. See #_x_coordinate + + /** moves forward or backwards one cursor position according to the + directionality of the current paragraph, but ignoring block progression. + Helper for the cursor*() functions. */ + bool _cursorLeftOrRightLocalX(Direction direction); + + /** moves forward or backwards by until the next character with + is_word_start according to the directionality of the current paragraph, + but ignoring block progression. Helper for the cursor*WithControl() + functions. */ + bool _cursorLeftOrRightLocalXByWord(Direction direction); +}; + +// ************************** inline methods + +inline Layout::iterator Layout::begin() const + {return iterator(this, 0, 0);} + +inline Layout::iterator Layout::end() const + {return iterator(this, _characters.size(), _glyphs.size());} + +inline Layout::iterator Layout::charIndexToIterator(int char_index) const +{ + if (char_index < 0) return begin(); + if (char_index >= (int)_characters.size()) return end(); + return iterator(this, char_index); +} + +inline int Layout::iteratorToCharIndex(Layout::iterator const &it) const + {return it._char_index;} + +inline void Layout::validateIterator(Layout::iterator *it) const +{ + it->_parent_layout = this; + if (it->_char_index >= _characters.size()) { + it->_char_index = _characters.size(); + it->_glyph_index = _glyphs.size(); + } else + it->_glyph_index = _characters[it->_char_index].in_glyph; +} + +inline Layout::iterator Layout::getNearestCursorPositionTo(Geom::Point const &point) const + {return getNearestCursorPositionTo(point[0], point[1]);} + +inline Layout::iterator Layout::getLetterAt(Geom::Point &point) const + {return getLetterAt(point[0], point[1]);} + +inline unsigned Layout::lineIndex(iterator const &it) const + {return it._char_index == _characters.size() ? _lines.size() - 1 : _characters[it._char_index].chunk(this).in_line;} + +inline unsigned Layout::shapeIndex(iterator const &it) const + {return it._char_index == _characters.size() ? _input_wrap_shapes.size() - 1 : _characters[it._char_index].line(this).in_shape;} + +inline bool Layout::isWhitespace(iterator const &it) const + {return it._char_index == _characters.size() || _characters[it._char_index].char_attributes.is_white;} + +inline gchar Layout::characterAt(iterator const &it) const +{ + return _characters[it._char_index].the_char; +} + +inline bool Layout::isCursorPosition(iterator const &it) const + {return it._char_index == _characters.size() || _characters[it._char_index].char_attributes.is_cursor_position;} + +inline bool Layout::isStartOfWord(iterator const &it) const + {return it._char_index != _characters.size() && _characters[it._char_index].char_attributes.is_word_start;} + +inline bool Layout::isEndOfWord(iterator const &it) const + {return it._char_index == _characters.size() || _characters[it._char_index].char_attributes.is_word_end;} + +inline bool Layout::isStartOfSentence(iterator const &it) const + {return it._char_index != _characters.size() && _characters[it._char_index].char_attributes.is_sentence_start;} + +inline bool Layout::isEndOfSentence(iterator const &it) const + {return it._char_index == _characters.size() || _characters[it._char_index].char_attributes.is_sentence_end;} + +inline unsigned Layout::paragraphIndex(iterator const &it) const + {return it._char_index == _characters.size() ? _paragraphs.size() - 1 : _characters[it._char_index].line(this).in_paragraph;} + +inline Layout::Alignment Layout::paragraphAlignment(iterator const &it) const + {return (_paragraphs.size() == 0) ? NONE : _paragraphs[paragraphIndex(it)].alignment;} + +inline bool Layout::iterator::nextGlyph() +{ + _cursor_moving_vertically = false; + if (_glyph_index >= (int)_parent_layout->_glyphs.size() - 1) { + if (_glyph_index == (int)_parent_layout->_glyphs.size()) return false; + _char_index = _parent_layout->_characters.size(); + _glyph_index = _parent_layout->_glyphs.size(); + } + else _char_index = _parent_layout->_glyphs[++_glyph_index].in_character; + return true; +} + +inline bool Layout::iterator::prevGlyph() +{ + _cursor_moving_vertically = false; + if (_glyph_index == 0) return false; + _char_index = _parent_layout->_glyphs[--_glyph_index].in_character; + return true; +} + +inline bool Layout::iterator::nextCharacter() +{ + _cursor_moving_vertically = false; + if (_char_index + 1 >= _parent_layout->_characters.size()) { + if (_char_index == _parent_layout->_characters.size()) return false; + _char_index = _parent_layout->_characters.size(); + _glyph_index = _parent_layout->_glyphs.size(); + } + else _glyph_index = _parent_layout->_characters[++_char_index].in_glyph; + return true; +} + +inline bool Layout::iterator::prevCharacter() +{ + _cursor_moving_vertically = false; + if (_char_index == 0) return false; + _glyph_index = _parent_layout->_characters[--_char_index].in_glyph; + return true; +} + +}//namespace Text +}//namespace Inkscape + +std::ostream &operator<<(std::ostream &out, const Inkscape::Text::Layout::FontMetrics &f); +std::ostream &operator<<(std::ostream &out, const Inkscape::Text::Layout::FontMetrics *f); + + +#endif + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/libnrtype/OpenTypeUtil.cpp b/src/libnrtype/OpenTypeUtil.cpp new file mode 100644 index 0000000..9d3576d --- /dev/null +++ b/src/libnrtype/OpenTypeUtil.cpp @@ -0,0 +1,447 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef USE_PANGO_WIN32 + +#include "OpenTypeUtil.h" + + +#include // For debugging +#include +#include + +// FreeType +#include FT_FREETYPE_H +#include FT_MULTIPLE_MASTERS_H +#include FT_SFNT_NAMES_H + +// Harfbuzz +#include +#include +#include + +// SVG in OpenType +#include "io/stream/gzipstream.h" +#include "io/stream/bufferstream.h" + + +// Utilities used in this file + +struct HbSetDeleter { + void operator()(hb_set_t* x) { hb_set_destroy(x); } +}; +using HbSet = std::unique_ptr; + +void dump_tag( guint32 *tag, Glib::ustring prefix = "", bool lf=true ) { + std::cout << prefix + << ((char)((*tag & 0xff000000)>>24)) + << ((char)((*tag & 0x00ff0000)>>16)) + << ((char)((*tag & 0x0000ff00)>> 8)) + << ((char)((*tag & 0x000000ff) )); + if( lf ) { + std::cout << std::endl; + } +} + +Glib::ustring extract_tag( guint32 *tag ) { + Glib::ustring tag_name; + tag_name += ((char)((*tag & 0xff000000)>>24)); + tag_name += ((char)((*tag & 0x00ff0000)>>16)); + tag_name += ((char)((*tag & 0x0000ff00)>> 8)); + tag_name += ((char)((*tag & 0x000000ff) )); + return tag_name; +} + + +// Later (see get_glyphs) we need to lookup the Unicode codepoint for a glyph +// but there's no direct API for that. So, we need a way to iterate over all +// glyph mappings and build a reverse map. +// FIXME: we should handle UVS at some point... or better, work with glyphs directly + +// Allows looking up the lowest Unicode codepoint mapped to a given glyph. +// To do so, it lazily builds a reverse map. +class GlyphToUnicodeMap { +protected: + hb_font_t* font; + HbSet codepointSet; + + std::unordered_map mappings; + bool more = true; // false if we have finished iterating the set + hb_codepoint_t codepoint = HB_SET_VALUE_INVALID; // current iteration +public: + GlyphToUnicodeMap(hb_font_t* font): font(font), codepointSet(hb_set_create()) { + hb_face_collect_unicodes(hb_font_get_face(font), codepointSet.get()); + } + + hb_codepoint_t lookup(hb_codepoint_t glyph) { + // first, try to find it in the mappings we've seen so far + if (auto it = mappings.find(glyph); it != mappings.end()) + return it->second; + + // populate more mappings from the set + while ((more = (more && hb_set_next(codepointSet.get(), &codepoint)))) { + // get the glyph that this codepoint is associated with, if any + hb_codepoint_t tGlyph; + if (!hb_font_get_nominal_glyph(font, codepoint, &tGlyph)) continue; + + // save the mapping, and return if this is the one we were looking for + mappings.emplace(tGlyph, codepoint); + if (tGlyph == glyph) return codepoint; + } + return 0; + } +}; + +void get_glyphs(GlyphToUnicodeMap& glyphMap, HbSet& set, Glib::ustring& characters) { + hb_codepoint_t glyph = -1; + while (hb_set_next(set.get(), &glyph)) { + if (auto codepoint = glyphMap.lookup(glyph)) + characters += codepoint; + } +} + +// Make a list of all tables found in the GSUB +// This list includes all tables regardless of script or language. +// Use Harfbuzz, Pango's equivalent calls are deprecated. +void readOpenTypeGsubTable (hb_font_t* hb_font, + std::map& tables) +{ + hb_face_t* hb_face = hb_font_get_face (hb_font); + + tables.clear(); + + // First time to get size of array + auto script_count = hb_ot_layout_table_get_script_tags(hb_face, HB_OT_TAG_GSUB, 0, nullptr, nullptr); + auto const hb_scripts = g_new(hb_tag_t, script_count + 1); + + // Second time to fill array (this two step process was not necessary with Pango). + hb_ot_layout_table_get_script_tags(hb_face, HB_OT_TAG_GSUB, 0, &script_count, hb_scripts); + + for(unsigned int i = 0; i < script_count; ++i) { + // std::cout << " Script: " << extract_tag(&hb_scripts[i]) << std::endl; + auto language_count = hb_ot_layout_script_get_language_tags(hb_face, HB_OT_TAG_GSUB, i, 0, nullptr, nullptr); + + if(language_count > 0) { + auto const hb_languages = g_new(hb_tag_t, language_count + 1); + hb_ot_layout_script_get_language_tags(hb_face, HB_OT_TAG_GSUB, i, 0, &language_count, hb_languages); + + for(unsigned int j = 0; j < language_count; ++j) { + // std::cout << " Language: " << extract_tag(&hb_languages[j]) << std::endl; + auto feature_count = hb_ot_layout_language_get_feature_tags(hb_face, HB_OT_TAG_GSUB, i, j, 0, nullptr, nullptr); + auto const hb_features = g_new(hb_tag_t, feature_count + 1); + hb_ot_layout_language_get_feature_tags(hb_face, HB_OT_TAG_GSUB, i, j, 0, &feature_count, hb_features); + + for(unsigned int k = 0; k < feature_count; ++k) { + // std::cout << " Feature: " << extract_tag(&hb_features[k]) << std::endl; + tables[ extract_tag(&hb_features[k])]; + } + + g_free(hb_features); + } + + g_free(hb_languages); + + } else { + + // Even if no languages are present there is still the default. + // std::cout << " Language: " << " (dflt)" << std::endl; + auto feature_count = hb_ot_layout_language_get_feature_tags(hb_face, HB_OT_TAG_GSUB, i, + HB_OT_LAYOUT_DEFAULT_LANGUAGE_INDEX, + 0, nullptr, nullptr); + auto const hb_features = g_new(hb_tag_t, feature_count + 1); + hb_ot_layout_language_get_feature_tags(hb_face, HB_OT_TAG_GSUB, i, + HB_OT_LAYOUT_DEFAULT_LANGUAGE_INDEX, + 0, &feature_count, hb_features); + + for(unsigned int k = 0; k < feature_count; ++k) { + // std::cout << " Feature: " << extract_tag(&hb_features[k]) << std::endl; + tables[ extract_tag(&hb_features[k])]; + } + + g_free(hb_features); + } + } + + // Find glyphs in OpenType substitution tables ('gsub'). + // Note that pango's functions are just dummies. Must use harfbuzz. + + GlyphToUnicodeMap glyphMap (hb_font); + + // Loop over all tables + for (auto table: tables) { + + // Only look at style substitution tables ('salt', 'ss01', etc. but not 'ssty'). + // Also look at character substitution tables ('cv01', etc.). + bool style = + table.first == "case" /* Case-Sensitive Forms */ || + table.first == "salt" /* Stylistic Alternatives */ || + table.first == "swsh" /* Swash */ || + table.first == "cwsh" /* Contextual Swash */ || + table.first == "ornm" /* Ornaments */ || + table.first == "nalt" /* Alternative Annotation */ || + table.first == "hist" /* Historical Forms */ || + (table.first[0] == 's' && table.first[1] == 's' && !(table.first[2] == 't')) || + (table.first[0] == 'c' && table.first[1] == 'v'); + + bool ligature = ( table.first == "liga" || // Standard ligatures + table.first == "clig" || // Common ligatures + table.first == "dlig" || // Discretionary ligatures + table.first == "hlig" || // Historical ligatures + table.first == "calt" ); // Contextual alternatives + + bool numeric = ( table.first == "lnum" || // Lining numerals + table.first == "onum" || // Old style + table.first == "pnum" || // Proportional + table.first == "tnum" || // Tabular + table.first == "frac" || // Diagonal fractions + table.first == "afrc" || // Stacked fractions + table.first == "ordn" || // Ordinal fractions + table.first == "zero" ); // Slashed zero + + if (style || ligature || numeric) { + + unsigned int feature_index; + if ( hb_ot_layout_language_find_feature (hb_face, HB_OT_TAG_GSUB, + 0, // Assume one script exists with index 0 + HB_OT_LAYOUT_DEFAULT_LANGUAGE_INDEX, + HB_TAG(table.first[0], + table.first[1], + table.first[2], + table.first[3]), + &feature_index ) ) { + + // std::cout << "Table: " << table.first << std::endl; + // std::cout << " Found feature, number: " << feature_index << std::endl; + unsigned int lookup_indexes[32]; + unsigned int lookup_count = 32; + int count = hb_ot_layout_feature_get_lookups (hb_face, HB_OT_TAG_GSUB, + feature_index, + 0, // Start + &lookup_count, + lookup_indexes ); + // std::cout << " Lookup count: " << count << " total: " << lookup_count << std::endl; + + for (int i = 0; i < count; ++i) { + HbSet glyphs_before (hb_set_create()); + HbSet glyphs_input (hb_set_create()); + HbSet glyphs_after (hb_set_create()); + HbSet glyphs_output (hb_set_create()); + + hb_ot_layout_lookup_collect_glyphs (hb_face, HB_OT_TAG_GSUB, + lookup_indexes[i], + glyphs_before.get(), + glyphs_input.get(), + glyphs_after.get(), + glyphs_output.get() ); + + // std::cout << " Populations: " + // << " " << hb_set_get_population (glyphs_before) + // << " " << hb_set_get_population (glyphs_input) + // << " " << hb_set_get_population (glyphs_after) + // << " " << hb_set_get_population (glyphs_output) + // << std::endl; + + get_glyphs (glyphMap, glyphs_before, tables[table.first].before); + get_glyphs (glyphMap, glyphs_input, tables[table.first].input ); + get_glyphs (glyphMap, glyphs_after, tables[table.first].after ); + get_glyphs (glyphMap, glyphs_output, tables[table.first].output); + + // std::cout << " Before: " << tables[table.first].before.c_str() << std::endl; + // std::cout << " Input: " << tables[table.first].input.c_str() << std::endl; + // std::cout << " After: " << tables[table.first].after.c_str() << std::endl; + // std::cout << " Output: " << tables[table.first].output.c_str() << std::endl; + } // End count (lookups) + + } else { + // std::cout << " Did not find '" << table.first << "'!" << std::endl; + } + } + + } + + g_free(hb_scripts); +} + +// Harfbuzz now as API for variations (Version 2.2, Nov 29 2018). +// Make a list of all Variation axes with ranges. +void readOpenTypeFvarAxes(const FT_Face ft_face, + std::map& axes) { + +#if FREETYPE_MAJOR *10000 + FREETYPE_MINOR*100 + FREETYPE_MICRO >= 20701 + FT_MM_Var* mmvar = nullptr; + FT_Multi_Master mmtype; + if (FT_HAS_MULTIPLE_MASTERS( ft_face ) && // Font has variables + FT_Get_MM_Var( ft_face, &mmvar) == 0 && // We found the data + FT_Get_Multi_Master( ft_face, &mmtype) !=0) { // It's not an Adobe MM font + + FT_Fixed coords[mmvar->num_axis]; + FT_Get_Var_Design_Coordinates( ft_face, mmvar->num_axis, coords ); + + for (size_t i = 0; i < mmvar->num_axis; ++i) { + FT_Var_Axis* axis = &mmvar->axis[i]; + axes[axis->name] = OTVarAxis(FTFixedToDouble(axis->minimum), + FTFixedToDouble(axis->def), + FTFixedToDouble(axis->maximum), + FTFixedToDouble(coords[i]), + i); + } + + // for (auto a: axes) { + // std::cout << " " << a.first + // << " min: " << a.second.minimum + // << " max: " << a.second.maximum + // << " set: " << a.second.set_val << std::endl; + // } + + } + +#endif /* FREETYPE Version */ +} + + +// Harfbuzz now as API for named variations (Version 2.2, Nov 29 2018). +// Make a list of all Named instances with axis values. +void readOpenTypeFvarNamed(const FT_Face ft_face, + std::map& named) { + +#if FREETYPE_MAJOR *10000 + FREETYPE_MINOR*100 + FREETYPE_MICRO >= 20701 + FT_MM_Var* mmvar = nullptr; + FT_Multi_Master mmtype; + if (FT_HAS_MULTIPLE_MASTERS( ft_face ) && // Font has variables + FT_Get_MM_Var( ft_face, &mmvar) == 0 && // We found the data + FT_Get_Multi_Master( ft_face, &mmtype) !=0) { // It's not an Adobe MM font + + std::cout << " Multiple Masters: variables: " << mmvar->num_axis + << " named styles: " << mmvar->num_namedstyles << std::endl; + + // const FT_UInt numNames = FT_Get_Sfnt_Name_Count(ft_face); + // std::cout << " number of names: " << numNames << std::endl; + // FT_SfntName ft_name; + // for (FT_UInt i = 0; i < numNames; ++i) { + + // if (FT_Get_Sfnt_Name(ft_face, i, &ft_name) != 0) { + // continue; + // } + + // Glib::ustring name; + // for (size_t j = 0; j < ft_name.string_len; ++j) { + // name += (char)ft_name.string[j]; + // } + // std::cout << " " << i << ": " << name << std::endl; + // } + + } + +#endif /* FREETYPE Version */ +} + +#define HB_OT_TAG_SVG HB_TAG('S','V','G',' ') + +// Get SVG glyphs out of an OpenType font. +void readOpenTypeSVGTable(hb_font_t* hb_font, + std::map& glyphs) { + + hb_face_t* hb_face = hb_font_get_face (hb_font); + + // Harfbuzz has some support for SVG fonts but it is not exposed until version 2.1 (Oct 30, 2018). + // We do it the hard way! + hb_blob_t *hb_blob = hb_face_reference_table (hb_face, HB_OT_TAG_SVG); + + if (!hb_blob) { + // No SVG table in font! + return; + } + + unsigned int svg_length = hb_blob_get_length (hb_blob); + if (svg_length == 0) { + // No SVG glyphs in table! + return; + } + + const char* data = hb_blob_get_data(hb_blob, &svg_length); + if (!data) { + std::cerr << "readOpenTypeSVGTable: Failed to get data! " << std::endl; + return; + } + + // OpenType fonts use Big Endian +#if 0 + uint16_t version = ((data[0] & 0xff) << 8) + (data[1] & 0xff); + // std::cout << "Version: " << version << std::endl; +#endif + uint32_t offset = ((data[2] & 0xff) << 24) + ((data[3] & 0xff) << 16) + ((data[4] & 0xff) << 8) + (data[5] & 0xff); + + // std::cout << "Offset: " << offset << std::endl; + // Bytes 6-9 are reserved. + + uint16_t entries = ((data[offset] & 0xff) << 8) + (data[offset+1] & 0xff); + // std::cout << "Number of entries: " << entries << std::endl; + + for (int entry = 0; entry < entries; ++entry) { + uint32_t base = offset + 2 + entry * 12; + + uint16_t startGlyphID = ((data[base ] & 0xff) << 8) + (data[base+1] & 0xff); + uint16_t endGlyphID = ((data[base+2] & 0xff) << 8) + (data[base+3] & 0xff); + uint32_t offsetGlyph = ((data[base+4] & 0xff) << 24) + ((data[base+5] & 0xff) << 16) +((data[base+6] & 0xff) << 8) + (data[base+7] & 0xff); + uint32_t lengthGlyph = ((data[base+8] & 0xff) << 24) + ((data[base+9] & 0xff) << 16) +((data[base+10] & 0xff) << 8) + (data[base+11] & 0xff); + + // std::cout << "Entry " << entry << ": Start: " << startGlyphID << " End: " << endGlyphID + // << " Offset: " << offsetGlyph << " Length: " << lengthGlyph << std::endl; + + std::string svg; + + // static cast is needed as hb_blob_get_length returns char but we are comparing to a value greater than allowed by char. + if (lengthGlyph > 1 && // + static_cast(data[offset + offsetGlyph + 0]) == 0x1f && + static_cast(data[offset + offsetGlyph + 1]) == 0x8b) { + // Glyph is gzipped + + std::vector buffer; + for (unsigned int c = offsetGlyph; c < offsetGlyph + lengthGlyph; ++c) { + buffer.push_back(data[offset + c]); + } + + Inkscape::IO::BufferInputStream zipped(buffer); + Inkscape::IO::GzipInputStream gzin(zipped); + for (int character = gzin.get(); character != -1; character = gzin.get()) { + svg+= (char)character; + } + + } else { + // Glyph is not compressed + + for (unsigned int c = offsetGlyph; c < offsetGlyph + lengthGlyph; ++c) { + svg += (unsigned char) data[offset + c]; + } + } + + for (unsigned int i = startGlyphID; i < endGlyphID+1; ++i) { + glyphs[i].svg = svg; + } + + // for (auto glyph : glyphs) { + // std::cout << "Glyph: " << glyph.first << std::endl; + // std::cout << glyph.second.svg << std::endl; + // } + } +} + +#endif /* !USE_PANGO_WIND32 */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/libnrtype/OpenTypeUtil.h b/src/libnrtype/OpenTypeUtil.h new file mode 100644 index 0000000..3385698 --- /dev/null +++ b/src/libnrtype/OpenTypeUtil.h @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_OPENTYPEUTIL_H +#define SEEN_OPENTYPEUTIL_H + +#ifndef USE_PANGO_WIN32 + +#include + +#include +#include FT_FREETYPE_H + +#include + +/* + * A set of utilities to extract data from OpenType fonts. + * + * Isolates dependencies on FreeType, Harfbuzz, and Pango. + * All three provide variable amounts of access to data. + */ + +struct hb_font_t; + +// OpenType substitution +class OTSubstitution { +public: + OTSubstitution() = default;; + Glib::ustring before; + Glib::ustring input; + Glib::ustring after; + Glib::ustring output; +}; + +// An OpenType fvar axis. +class OTVarAxis { +public: + OTVarAxis() + : minimum(0) + , def(500) // Default + , maximum(1000) + , set_val(500) + , index(-1) {}; + + OTVarAxis(double _minimum, double _def, double _maximum, double _set_val, int _index) + : minimum(_minimum) + , def(_def) // Default + , maximum(_maximum) + , set_val(_set_val) + , index (_index) {}; + + double minimum; + double def; + double maximum; + double set_val; + int index; // Index in OpenType file (since we use a map). +}; + +// A particular instance of a variable font. +// A map indexed by axis name with value. +class OTVarInstance { + std::map axes; +}; + +inline double FTFixedToDouble (FT_Fixed value) { + return static_cast(value) / 65536.0; +} + +inline FT_Fixed FTDoubleToFixed (double value) { + return static_cast(value * 65536); +} + + +namespace Inkscape { class Pixbuf; } + +class SVGTableEntry { +public: + SVGTableEntry() : pixbuf(nullptr) {}; + std::string svg; + Inkscape::Pixbuf* pixbuf; +}; + +// This would be better if one had std::vector instead of OTSubstitution where each +// entry corresponded to one substitution (e.g. ff -> ff) but Harfbuzz at the moment cannot return +// individual substitutions. See Harfbuzz issue #673. +void readOpenTypeGsubTable (hb_font_t* hb_font, + std::map& tables); + +void readOpenTypeFvarAxes (const FT_Face ft_face, + std::map& axes); + +void readOpenTypeFvarNamed (const FT_Face ft_face, + std::map& named); + +void readOpenTypeSVGTable (hb_font_t* hb_font, + std::map& glyphs); + +#endif /* !USE_PANGO_WIND32 */ +#endif /* !SEEN_OPENTYPEUTIL_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/libnrtype/font-glyph.h b/src/libnrtype/font-glyph.h new file mode 100644 index 0000000..5dcb933 --- /dev/null +++ b/src/libnrtype/font-glyph.h @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2011 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_LIBNRTYPE_FONT_GLYPH_H +#define SEEN_LIBNRTYPE_FONT_GLYPH_H + +#include <2geom/forward.h> + +// the info for a glyph in a font. it's totally resolution- and fontsize-independent +struct font_glyph { + double h_advance, h_width; // width != advance because of kerning adjustements + double v_advance, v_width; + double bbox[4]; // bbox of the path (and the artbpath), not the bbox of the glyph + // as the fonts sometimes contain + Geom::PathVector* pathvector; // outline as 2geom pathvector, for text->curve stuff (should be unified with livarot) +}; + + +#endif /* !SEEN_LIBNRTYPE_FONT_GLYPH_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/libnrtype/font-instance.h b/src/libnrtype/font-instance.h new file mode 100644 index 0000000..fb99d15 --- /dev/null +++ b/src/libnrtype/font-instance.h @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_LIBNRTYPE_FONT_INSTANCE_H +#define SEEN_LIBNRTYPE_FONT_INSTANCE_H + +#include + +#include +#include + +#include <2geom/d2.h> + +#include "FontFactory.h" +#include "font-style.h" +#include "OpenTypeUtil.h" + +namespace Inkscape { class Pixbuf; } + +class font_factory; +struct font_glyph; + +// the font_instance are the template of several raster_font; they provide metrics and outlines +// that are drawn by the raster_font, so the raster_font needs info relative to the way the +// font need to be drawn. note that fontsize is a scale factor in the transform matrix +// of the style +class font_instance { +public: + // the real source of the font + PangoFont* pFont = nullptr; + // depending on the rendering backend, different temporary data + + // that's the font's fingerprint; this particular PangoFontDescription gives the entry at which this font_instance + // resides in the font_factory loadedFaces unordered_map + PangoFontDescription* descr = nullptr; + // refcount + int refCount = 0; + // font_factory owning this font_instance + font_factory* parent = nullptr; + + // common glyph definitions for all the rasterfonts + std::map id_to_no; + int nbGlyph = 0; + int maxGlyph = 0; + + font_glyph* glyphs = nullptr; + + // font is loaded with GSUB in 2 pass + bool fulloaded = false; + + // Map of OpenType tables found in font. + std::map openTypeTables; + + // Maps for font variations. + std::map openTypeVarAxes; // Axes with ranges + + // Map of SVG in OpenType glyphs + std::map openTypeSVGGlyphs; + + // Does OpenType font contain SVG glyphs? + bool fontHasSVG = false; + + font_instance(); + virtual ~font_instance(); + + void Ref(); + void Unref(); + + bool IsOutlineFont(); // utility + void InstallFace(PangoFont* iFace); // utility; should reset the pFont field if loading failed + // in case the PangoFont is a bitmap font, for example. that way, the calling function + // will be able to check the validity of the font before installing it in loadedFaces + void InitTheFace(bool loadgsub = false); + + int MapUnicodeChar(gunichar c); // calls the relevant unicode->glyph index function + void LoadGlyph(int glyph_id); // the main backend-dependent function + // loads the given glyph's info + + // nota: all coordinates returned by these functions are on a [0..1] scale; you need to multiply + // by the fontsize to get the real sizes + + // Return 2geom pathvector for glyph. Deallocated when font instance dies. + Geom::PathVector* PathVector(int glyph_id); + + // Return font has SVG OpenType enties. + bool FontHasSVG() { return fontHasSVG; }; + + // Return pixbuf of SVG glyph or nullptr if no SVG glyph exists. + Inkscape::Pixbuf* PixBuf(int glyph_id); + + + // Horizontal advance if 'vertical' is false, vertical advance if true. + double Advance(int glyph_id, bool vertical); + + double GetTypoAscent() { return _ascent; } + double GetTypoDescent() { return _descent; } + double GetXHeight() { return _xheight; } + double GetMaxAscent() { return _ascent_max; } + double GetMaxDescent() { return _descent_max; } + const double* GetBaselines() { return _baselines; } + int GetDesignUnits() { return _design_units; } + + bool FontMetrics(double &ascent, double &descent, double &leading); + bool FontDecoration(double &underline_position, double &underline_thickness, + double &linethrough_position, double &linethrough_thickness); + bool FontSlope(double &run, double &rise); + // for generating slanted cursors for oblique fonts + Geom::OptRect BBox(int glyph_id); + +private: + void FreeTheFace(); + // Find ascent, descent, x-height, and baselines. + void FindFontMetrics(); + + // Temp: make public +public: +#ifdef USE_PANGO_WIN32 + HFONT theFace = nullptr; +#else + // we need to keep around an rw copy of the (read-only) hb font to extract the freetype face + hb_font_t* hb_font_copy = nullptr; + FT_Face theFace = nullptr; + // it's a pointer in fact; no worries to ref/unref it, pango does its magic + // as long as pFont is valid, theFace is too +#endif + +private: + // Font metrics in em-box units + double _ascent; // Typographic ascent. + double _descent; // Typographic descent. + double _xheight; // x-height of font. + double _ascent_max; // Maximum ascent of all glyphs in font. + double _descent_max; // Maximum descent of all glyphs in font. + int _design_units; // Design units, (units per em, typically 1000 or 2048). + + // Baselines + double _baselines[SP_CSS_BASELINE_SIZE]; +}; + + +#endif /* !SEEN_LIBNRTYPE_FONT_INSTANCE_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/libnrtype/font-lister.cpp b/src/libnrtype/font-lister.cpp new file mode 100644 index 0000000..a929228 --- /dev/null +++ b/src/libnrtype/font-lister.cpp @@ -0,0 +1,1271 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include + +#include + +#include + +#include "font-lister.h" +#include "FontFactory.h" + +#include "desktop.h" +#include "desktop-style.h" +#include "document.h" +#include "inkscape.h" +#include "preferences.h" + +#include "object/sp-object.h" + +// Following are needed to limit the source of updating font data to text and containers. +#include "object/sp-root.h" +#include "object/sp-object-group.h" +#include "object/sp-anchor.h" +#include "object/sp-text.h" +#include "object/sp-tspan.h" +#include "object/sp-textpath.h" +#include "object/sp-tref.h" +#include "object/sp-flowtext.h" +#include "object/sp-flowdiv.h" + +#include "xml/repr.h" + + +//#define DEBUG_FONT + +// CSS dictates that font family names are case insensitive. +// This should really implement full Unicode case unfolding. +bool familyNamesAreEqual(const Glib::ustring &a, const Glib::ustring &b) +{ + return (a.casefold().compare(b.casefold()) == 0); +} + +static const char* sp_font_family_get_name(PangoFontFamily* family) +{ + const char* name = pango_font_family_get_name(family); + if (strncmp(name, "Sans", 4) == 0 && strlen(name) == 4) + return "sans-serif"; + if (strncmp(name, "Serif", 5) == 0 && strlen(name) == 5) + return "serif"; + if (strncmp(name, "Monospace", 9) == 0 && strlen(name) == 9) + return "monospace"; + return name; +} + +namespace Inkscape { + +FontLister::FontLister() + : current_family_row (0) + , current_family ("sans-serif") + , current_style ("Normal") + , block (false) +{ + font_list_store = Gtk::ListStore::create(FontList); + font_list_store->freeze_notify(); + + /* Create default styles for use when font-family is unknown on system. */ + default_styles = g_list_append(nullptr, new StyleNames("Normal")); + default_styles = g_list_append(default_styles, new StyleNames("Italic")); + default_styles = g_list_append(default_styles, new StyleNames("Bold")); + default_styles = g_list_append(default_styles, new StyleNames("Bold Italic")); + + // Get sorted font families from Pango + std::vector familyVector; + font_factory::Default()->GetUIFamilies(familyVector); + + // Traverse through the family names and set up the list store + for (auto & i : familyVector) { + const char* displayName = sp_font_family_get_name(i); + + if (displayName == nullptr || *displayName == '\0') { + continue; + } + + Glib::ustring familyName = displayName; + if (!familyName.empty()) { + Gtk::TreeModel::iterator treeModelIter = font_list_store->append(); + (*treeModelIter)[FontList.family] = familyName; + + // we don't set this now (too slow) but the style will be cached if the user + // ever decides to use this font + (*treeModelIter)[FontList.styles] = NULL; + // store the pango representation for generating the style + (*treeModelIter)[FontList.pango_family] = i; + (*treeModelIter)[FontList.onSystem] = true; + } + } + + font_list_store->thaw_notify(); + + style_list_store = Gtk::ListStore::create(FontStyleList); + + // Initialize style store with defaults + style_list_store->freeze_notify(); + style_list_store->clear(); + for (GList *l = default_styles; l; l = l->next) { + Gtk::TreeModel::iterator treeModelIter = style_list_store->append(); + (*treeModelIter)[FontStyleList.cssStyle] = ((StyleNames *)l->data)->CssName; + (*treeModelIter)[FontStyleList.displayStyle] = ((StyleNames *)l->data)->DisplayName; + } + style_list_store->thaw_notify(); +} + +FontLister::~FontLister() +{ + // Delete default_styles + for (GList *l = default_styles; l; l = l->next) { + delete ((StyleNames *)l->data); + } + + // Delete other styles + Gtk::TreeModel::iterator iter = font_list_store->get_iter("0"); + while (iter != font_list_store->children().end()) { + Gtk::TreeModel::Row row = *iter; + GList *styles = row[FontList.styles]; + for (GList *l = styles; l; l = l->next) { + delete ((StyleNames *)l->data); + } + ++iter; + } +} + +FontLister *FontLister::get_instance() +{ + static Inkscape::FontLister *instance = new Inkscape::FontLister(); + return instance; +} + +// To do: remove model (not needed for C++ version). +// Ensures the style list for a particular family has been created. +void FontLister::ensureRowStyles(Glib::RefPtr model, Gtk::TreeModel::iterator const iter) +{ + Gtk::TreeModel::Row row = *iter; + if (!row[FontList.styles]) { + if (row[FontList.pango_family]) { + row[FontList.styles] = font_factory::Default()->GetUIStyles(row[FontList.pango_family]); + } else { + row[FontList.styles] = default_styles; + } + } +} + +Glib::ustring FontLister::get_font_family_markup(Gtk::TreeIter const &iter) +{ + Gtk::TreeModel::Row row = *iter; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + Glib::ustring family = row[FontList.family]; + bool onSystem = row[FontList.onSystem]; + + Glib::ustring family_escaped = Glib::Markup::escape_text( family ); + Glib::ustring markup; + + if (!onSystem) { + markup = ""; + + // See if font-family is on system (separately for each family in font stack). + std::vector tokens = Glib::Regex::split_simple("\\s*,\\s*", family); + + for (auto token: tokens) { + bool found = false; + Gtk::TreeModel::Children children = get_font_list()->children(); + for (auto iter2: children) { + Gtk::TreeModel::Row row2 = *iter2; + Glib::ustring family2 = row2[FontList.family]; + bool onSystem2 = row2[FontList.onSystem]; + if (onSystem2 && familyNamesAreEqual(token, family2)) { + found = true; + break; + } + } + + if (found) { + markup += Glib::Markup::escape_text (token); + markup += ", "; + } else { + markup += ""; + markup += Glib::Markup::escape_text (token); + markup += ""; + markup += ", "; + } + } + + // Remove extra comma and space from end. + if (markup.size() >= 2) { + markup.resize(markup.size() - 2); + } + markup += ""; + + } else { + markup = family_escaped; + } + + int show_sample = prefs->getInt("/tools/text/show_sample_in_list", 1); + if (show_sample) { + + Glib::ustring sample = prefs->getString("/tools/text/font_sample"); + + markup += " "; + markup += sample; + markup += ""; + } + + // std::cout << "Markup: " << markup << std::endl; + return markup; +} + +// Example of how to use "foreach_iter" +// bool +// FontLister::print_document_font( const Gtk::TreeModel::iterator &iter ) { +// Gtk::TreeModel::Row row = *iter; +// if( !row[FontList.onSystem] ) { +// std::cout << " Not on system: " << row[FontList.family] << std::endl; +// return false; +// } +// return true; +// } +// font_list_store->foreach_iter( sigc::mem_fun(*this, &FontLister::print_document_font )); + +/* Used to insert a font that was not in the document and not on the system into the font list. */ +void FontLister::insert_font_family(Glib::ustring new_family) +{ + GList *styles = default_styles; + + /* In case this is a fallback list, check if first font-family on system. */ + std::vector tokens = Glib::Regex::split_simple(",", new_family); + if (!tokens.empty() && !tokens[0].empty()) { + Gtk::TreeModel::iterator iter2 = font_list_store->get_iter("0"); + while (iter2 != font_list_store->children().end()) { + Gtk::TreeModel::Row row = *iter2; + if (row[FontList.onSystem] && familyNamesAreEqual(tokens[0], row[FontList.family])) { + if (!row[FontList.styles]) { + row[FontList.styles] = font_factory::Default()->GetUIStyles(row[FontList.pango_family]); + } + styles = row[FontList.styles]; + break; + } + ++iter2; + } + } + + Gtk::TreeModel::iterator treeModelIter = font_list_store->prepend(); + (*treeModelIter)[FontList.family] = new_family; + (*treeModelIter)[FontList.styles] = styles; + (*treeModelIter)[FontList.onSystem] = false; + (*treeModelIter)[FontList.pango_family] = NULL; + + current_family = new_family; + current_family_row = 0; + current_style = "Normal"; + + emit_update(); +} + +void FontLister::update_font_list(SPDocument *document) +{ + SPObject *root = document->getRoot(); + if (!root) { + return; + } + + font_list_store->freeze_notify(); + + /* Find if current row is in document or system part of list */ + gboolean row_is_system = false; + if (current_family_row > -1) { + Gtk::TreePath path; + path.push_back(current_family_row); + Gtk::TreeModel::iterator iter = font_list_store->get_iter(path); + if (iter) { + row_is_system = (*iter)[FontList.onSystem]; + // std::cout << " In: row: " << current_family_row << " " << (*iter)[FontList.family] << std::endl; + } + } + + /* Clear all old document font-family entries */ + Gtk::TreeModel::iterator iter = font_list_store->get_iter("0"); + while (iter != font_list_store->children().end()) { + Gtk::TreeModel::Row row = *iter; + if (!row[FontList.onSystem]) { + // std::cout << " Not on system: " << row[FontList.family] << std::endl; + iter = font_list_store->erase(iter); + } else { + // std::cout << " First on system: " << row[FontList.family] << std::endl; + break; + } + } + + /* Get "font-family"s and styles used in document. */ + std::map> font_data; + update_font_data_recursive(*root, font_data); + + /* Insert separator */ + if (!font_data.empty()) { + Gtk::TreeModel::iterator treeModelIter = font_list_store->prepend(); + (*treeModelIter)[FontList.family] = "#"; + (*treeModelIter)[FontList.onSystem] = false; + } + + /* Insert font-family's in document. */ + for (auto i: font_data) { + + GList *styles = default_styles; + + /* See if font-family (or first in fallback list) is on system. If so, get styles. */ + std::vector tokens = Glib::Regex::split_simple(",", i.first); + if (!tokens.empty() && !tokens[0].empty()) { + + Gtk::TreeModel::iterator iter2 = font_list_store->get_iter("0"); + while (iter2 != font_list_store->children().end()) { + Gtk::TreeModel::Row row = *iter2; + if (row[FontList.onSystem] && familyNamesAreEqual(tokens[0], row[FontList.family])) { + // Found font on system, set style list to system font style list. + if (!row[FontList.styles]) { + row[FontList.styles] = font_factory::Default()->GetUIStyles(row[FontList.pango_family]); + } + + // Add new styles (from 'font-variation-settings', these are not include in GetUIStyles()). + for (auto j: i.second) { + // std::cout << " Inserting: " << j << std::endl; + + bool exists = false; + for(GList *temp = row[FontList.styles]; temp; temp = temp->next) { + if( ((StyleNames*)temp->data)->CssName.compare( j ) == 0 ) { + exists = true; + break; + } + } + + if (!exists) { + row[FontList.styles] = g_list_append(row[FontList.styles], new StyleNames(j,j)); + } + } + + styles = row[FontList.styles]; + break; + } + ++iter2; + } + } + + Gtk::TreeModel::iterator treeModelIter = font_list_store->prepend(); + (*treeModelIter)[FontList.family] = reinterpret_cast(g_strdup((i.first).c_str())); + (*treeModelIter)[FontList.styles] = styles; + (*treeModelIter)[FontList.onSystem] = false; // false if document font + (*treeModelIter)[FontList.pango_family] = NULL; // CHECK ME (set to pango_family if on system?) + + } + + font_family_row_update(row_is_system ? font_data.size() : 0); + // std::cout << " Out: row: " << current_family_row << " " << current_family << std::endl; + + font_list_store->thaw_notify(); + emit_update(); +} + +void FontLister::update_font_data_recursive(SPObject& r, std::map> &font_data) +{ + // Text nodes (i.e. the content of or ) do not have their own style. + if (r.getRepr()->type() == Inkscape::XML::NodeType::TEXT_NODE) { + return; + } + + PangoFontDescription* descr = ink_font_description_from_style( r.style ); + const gchar* font_family_char = pango_font_description_get_family(descr); + if (font_family_char) { + Glib::ustring font_family(font_family_char); + pango_font_description_unset_fields( descr, PANGO_FONT_MASK_FAMILY); + + gchar* font_style_char = pango_font_description_to_string(descr); + Glib::ustring font_style(font_style_char); + g_free(font_style_char); + + if (!font_family.empty() && !font_style.empty()) { + font_data[font_family].insert(font_style); + } + } else { + // We're starting from root and looking at all elements... we should probably white list text/containers. + std::cerr << "FontLister::update_font_data_recursive: descr without font family! " << (r.getId()?r.getId():"null") << std::endl; + } + pango_font_description_free(descr); + + if (SP_IS_GROUP(&r) || + SP_IS_ANCHOR(&r) || + SP_IS_ROOT(&r) || + SP_IS_TEXT(&r) || + SP_IS_TSPAN(&r) || + SP_IS_TEXTPATH(&r) || + SP_IS_TREF(&r) || + SP_IS_FLOWTEXT(&r) || + SP_IS_FLOWDIV(&r) || + SP_IS_FLOWPARA(&r) || + SP_IS_FLOWLINE(&r)) { + for (auto& child: r.children) { + update_font_data_recursive(child, font_data); + } + } +} + +void FontLister::emit_update() +{ + if (block) return; + + block = true; + update_signal.emit (); + block = false; +} + + +Glib::ustring FontLister::canonize_fontspec(Glib::ustring fontspec) +{ + + // Pass fontspec to and back from Pango to get a the fontspec in + // canonical form. -inkscape-font-specification relies on the + // Pango constructed fontspec not changing form. If it does, + // this is the place to fix it. + PangoFontDescription *descr = pango_font_description_from_string(fontspec.c_str()); + gchar *canonized = pango_font_description_to_string(descr); + Glib::ustring Canonized = canonized; + g_free(canonized); + pango_font_description_free(descr); + + // Pango canonized strings remove space after comma between family names. Put it back. + // But don't add a space inside a 'font-variation-settings' declaration (this breaks Pango). + size_t i = 0; + while ((i = Canonized.find_first_of(",@", i)) != std::string::npos ) { + if (Canonized[i] == '@') // Found start of 'font-variation-settings'. + break; + Canonized.replace(i, 1, ", "); + i += 2; + } + + return Canonized; +} + +Glib::ustring FontLister::system_fontspec(Glib::ustring fontspec) +{ + // Find what Pango thinks is the closest match. + Glib::ustring out = fontspec; + + PangoFontDescription *descr = pango_font_description_from_string(fontspec.c_str()); + font_instance *res = (font_factory::Default())->Face(descr); + if (res && res->pFont) { + PangoFontDescription *nFaceDesc = pango_font_describe(res->pFont); + out = sp_font_description_get_family(nFaceDesc); + } + pango_font_description_free(descr); + + return out; +} + +std::pair FontLister::ui_from_fontspec(Glib::ustring fontspec) +{ + PangoFontDescription *descr = pango_font_description_from_string(fontspec.c_str()); + const gchar *family = pango_font_description_get_family(descr); + if (!family) + family = "sans-serif"; + Glib::ustring Family = family; + + // PANGO BUG... + // A font spec of Delicious, 500 Italic should result in a family of 'Delicious' + // and a style of 'Medium Italic'. It results instead with: a family of + // 'Delicious, 500' with a style of 'Medium Italic'. We chop of any weight numbers + // at the end of the family: match ",[1-9]00^". + Glib::RefPtr weight = Glib::Regex::create(",[1-9]00$"); + Family = weight->replace(Family, 0, "", Glib::REGEX_MATCH_PARTIAL); + + // Pango canonized strings remove space after comma between family names. Put it back. + size_t i = 0; + while ((i = Family.find(",", i)) != std::string::npos) { + Family.replace(i, 1, ", "); + i += 2; + } + + pango_font_description_unset_fields(descr, PANGO_FONT_MASK_FAMILY); + gchar *style = pango_font_description_to_string(descr); + Glib::ustring Style = style; + pango_font_description_free(descr); + g_free(style); + + return std::make_pair(Family, Style); +} + +/* Now we do a song and dance to find the correct row as the row corresponding + * to the current_family may have changed. We can't simply search for the + * family name in the list since it can occur twice, once in the document + * font family part and once in the system font family part. Above we determined + * which part it is in. + */ +void FontLister::font_family_row_update(int start) +{ + if (this->current_family_row > -1 && start > -1) { + int length = this->font_list_store->children().size(); + for (int i = 0; i < length; ++i) { + int row = i + start; + if (row >= length) + row -= length; + Gtk::TreePath path; + path.push_back(row); + Gtk::TreeModel::iterator iter = this->font_list_store->get_iter(path); + if (iter) { + if (familyNamesAreEqual(this->current_family, (*iter)[FontList.family])) { + this->current_family_row = row; + break; + } + } + } + } +} + +std::pair FontLister::selection_update() +{ +#ifdef DEBUG_FONT + std::cout << "\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << std::endl; + std::cout << "FontLister::selection_update: entrance" << std::endl; +#endif + // Get fontspec from a selection, preferences, or thin air. + Glib::ustring fontspec; + SPStyle query(SP_ACTIVE_DOCUMENT); + + // Directly from stored font specification. + int result = + sp_desktop_query_style(SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_FONT_SPECIFICATION); + + //std::cout << " Attempting selected style" << std::endl; + if (result != QUERY_STYLE_NOTHING && query.font_specification.set) { + fontspec = query.font_specification.value(); + //std::cout << " fontspec from query :" << fontspec << ":" << std::endl; + } + + // From style + if (fontspec.empty()) { + //std::cout << " Attempting desktop style" << std::endl; + int rfamily = sp_desktop_query_style(SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_FONTFAMILY); + int rstyle = sp_desktop_query_style(SP_ACTIVE_DESKTOP, &query, QUERY_STYLE_PROPERTY_FONTSTYLE); + + // Must have text in selection + if (rfamily != QUERY_STYLE_NOTHING && rstyle != QUERY_STYLE_NOTHING) { + fontspec = fontspec_from_style(&query); + } + //std::cout << " fontspec from style :" << fontspec << ":" << std::endl; + } + + // From preferences + if (fontspec.empty()) { + //std::cout << " Attempting preferences" << std::endl; + query.readFromPrefs("/tools/text"); + fontspec = fontspec_from_style(&query); + //std::cout << " fontspec from prefs :" << fontspec << ":" << std::endl; + } + + // From thin air + if (fontspec.empty()) { + //std::cout << " Attempting thin air" << std::endl; + fontspec = current_family + ", " + current_style; + //std::cout << " fontspec from thin air :" << fontspec << ":" << std::endl; + } + + // Need to update font family row too + font_family_row_update(); + + std::pair ui = ui_from_fontspec(fontspec); + set_font_family(ui.first); + set_font_style(ui.second); + +#ifdef DEBUG_FONT + std::cout << " family_row: :" << current_family_row << ":" << std::endl; + std::cout << " family: :" << current_family << ":" << std::endl; + std::cout << " style: :" << current_style << ":" << std::endl; + std::cout << "FontLister::selection_update: exit" << std::endl; + std::cout << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" << std::endl; +#endif + + emit_update(); + + return std::make_pair(current_family, current_style); +} + + +// Set fontspec. If check is false, best style match will not be done. +void FontLister::set_fontspec(Glib::ustring new_fontspec, bool /*check*/) +{ + std::pair ui = ui_from_fontspec(new_fontspec); + Glib::ustring new_family = ui.first; + Glib::ustring new_style = ui.second; + +#ifdef DEBUG_FONT + std::cout << "FontLister::set_fontspec: family: " << new_family + << " style:" << new_style << std::endl; +#endif + + set_font_family(new_family, false, false); + set_font_style(new_style, false); + + emit_update(); +} + + +// TODO: use to determine font-selector best style +// TODO: create new function new_font_family(Gtk::TreeModel::iterator iter) +std::pair FontLister::new_font_family(Glib::ustring new_family, bool /*check_style*/) +{ +#ifdef DEBUG_FONT + std::cout << "\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << std::endl; + std::cout << "FontLister::new_font_family: " << new_family << std::endl; +#endif + + // No need to do anything if new family is same as old family. + if (familyNamesAreEqual(new_family, current_family)) { +#ifdef DEBUG_FONT + std::cout << "FontLister::new_font_family: exit: no change in family." << std::endl; + std::cout << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" << std::endl; +#endif + return std::make_pair(current_family, current_style); + } + + // We need to do two things: + // 1. Update style list for new family. + // 2. Select best valid style match to old style. + + // For finding style list, use list of first family in font-family list. + GList *styles = nullptr; + Gtk::TreeModel::iterator iter = font_list_store->get_iter("0"); + while (iter != font_list_store->children().end()) { + + Gtk::TreeModel::Row row = *iter; + + if (familyNamesAreEqual(new_family, row[FontList.family])) { + if (!row[FontList.styles]) { + row[FontList.styles] = font_factory::Default()->GetUIStyles(row[FontList.pango_family]); + } + styles = row[FontList.styles]; + break; + } + ++iter; + } + + // Newly typed in font-family may not yet be in list... use default list. + // TODO: if font-family is list, check if first family in list is on system + // and set style accordingly. + if (styles == nullptr) { + styles = default_styles; + } + + // Update style list. + style_list_store->freeze_notify(); + style_list_store->clear(); + + for (GList *l = styles; l; l = l->next) { + Gtk::TreeModel::iterator treeModelIter = style_list_store->append(); + (*treeModelIter)[FontStyleList.cssStyle] = ((StyleNames *)l->data)->CssName; + (*treeModelIter)[FontStyleList.displayStyle] = ((StyleNames *)l->data)->DisplayName; + } + + style_list_store->thaw_notify(); + + // Find best match to the style from the old font-family to the + // styles available with the new font. + // TODO: Maybe check if an exact match exists before using Pango. + Glib::ustring best_style = get_best_style_match(new_family, current_style); + +#ifdef DEBUG_FONT + std::cout << "FontLister::new_font_family: exit: " << new_family << " " << best_style << std::endl; + std::cout << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" << std::endl; +#endif + return std::make_pair(new_family, best_style); +} + +std::pair FontLister::set_font_family(Glib::ustring new_family, bool check_style, + bool emit) +{ + +#ifdef DEBUG_FONT + std::cout << "\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << std::endl; + std::cout << "FontLister::set_font_family: " << new_family << std::endl; +#endif + + std::pair ui = new_font_family(new_family, check_style); + current_family = ui.first; + current_style = ui.second; + +#ifdef DEBUG_FONT + std::cout << " family_row: :" << current_family_row << ":" << std::endl; + std::cout << " family: :" << current_family << ":" << std::endl; + std::cout << " style: :" << current_style << ":" << std::endl; + std::cout << "FontLister::set_font_family: end" << std::endl; + std::cout << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" << std::endl; +#endif + if (emit) { + emit_update(); + } + return ui; +} + + +std::pair FontLister::set_font_family(int row, bool check_style, bool emit) +{ + +#ifdef DEBUG_FONT + std::cout << "\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << std::endl; + std::cout << "FontLister::set_font_family( row ): " << row << std::endl; +#endif + + current_family_row = row; + Gtk::TreePath path; + path.push_back(row); + Glib::ustring new_family = current_family; + Gtk::TreeModel::iterator iter = font_list_store->get_iter(path); + if (iter) { + new_family = (*iter)[FontList.family]; + } + + std::pair ui = set_font_family(new_family, check_style, emit); + +#ifdef DEBUG_FONT + std::cout << "FontLister::set_font_family( row ): end" << std::endl; + std::cout << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" << std::endl; +#endif + return ui; +} + + +void FontLister::set_font_style(Glib::ustring new_style, bool emit) +{ + +// TODO: Validate input using Pango. If Pango doesn't recognize a style it will +// attach the "invalid" style to the font-family. + +#ifdef DEBUG_FONT + std::cout << "\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << std::endl; + std::cout << "FontLister:set_font_style: " << new_style << std::endl; +#endif + current_style = new_style; + +#ifdef DEBUG_FONT + std::cout << " family: " << current_family << std::endl; + std::cout << " style: " << current_style << std::endl; + std::cout << "FontLister::set_font_style: end" << std::endl; + std::cout << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" << std::endl; +#endif + if (emit) { + emit_update(); + } +} + + +// We do this ourselves as we can't rely on FontFactory. +void FontLister::fill_css(SPCSSAttr *css, Glib::ustring fontspec) +{ + if (fontspec.empty()) { + fontspec = get_fontspec(); + } + + std::pair ui = ui_from_fontspec(fontspec); + + Glib::ustring family = ui.first; + + + // Font spec is single quoted... for the moment + Glib::ustring fontspec_quoted(fontspec); + css_quote(fontspec_quoted); + sp_repr_css_set_property(css, "-inkscape-font-specification", fontspec_quoted.c_str()); + + // Font families needs to be properly quoted in CSS (used unquoted in font-lister) + css_font_family_quote(family); + sp_repr_css_set_property(css, "font-family", family.c_str()); + + PangoFontDescription *desc = pango_font_description_from_string(fontspec.c_str()); + PangoWeight weight = pango_font_description_get_weight(desc); + switch (weight) { + case PANGO_WEIGHT_THIN: + sp_repr_css_set_property(css, "font-weight", "100"); + break; + case PANGO_WEIGHT_ULTRALIGHT: + sp_repr_css_set_property(css, "font-weight", "200"); + break; + case PANGO_WEIGHT_LIGHT: + sp_repr_css_set_property(css, "font-weight", "300"); + break; + case PANGO_WEIGHT_SEMILIGHT: + sp_repr_css_set_property(css, "font-weight", "350"); + break; + case PANGO_WEIGHT_BOOK: + sp_repr_css_set_property(css, "font-weight", "380"); + break; + case PANGO_WEIGHT_NORMAL: + sp_repr_css_set_property(css, "font-weight", "normal"); + break; + case PANGO_WEIGHT_MEDIUM: + sp_repr_css_set_property(css, "font-weight", "500"); + break; + case PANGO_WEIGHT_SEMIBOLD: + sp_repr_css_set_property(css, "font-weight", "600"); + break; + case PANGO_WEIGHT_BOLD: + sp_repr_css_set_property(css, "font-weight", "bold"); + break; + case PANGO_WEIGHT_ULTRABOLD: + sp_repr_css_set_property(css, "font-weight", "800"); + break; + case PANGO_WEIGHT_HEAVY: + sp_repr_css_set_property(css, "font-weight", "900"); + break; + case PANGO_WEIGHT_ULTRAHEAVY: + sp_repr_css_set_property(css, "font-weight", "1000"); + break; + } + + PangoStyle style = pango_font_description_get_style(desc); + switch (style) { + case PANGO_STYLE_NORMAL: + sp_repr_css_set_property(css, "font-style", "normal"); + break; + case PANGO_STYLE_OBLIQUE: + sp_repr_css_set_property(css, "font-style", "oblique"); + break; + case PANGO_STYLE_ITALIC: + sp_repr_css_set_property(css, "font-style", "italic"); + break; + } + + PangoStretch stretch = pango_font_description_get_stretch(desc); + switch (stretch) { + case PANGO_STRETCH_ULTRA_CONDENSED: + sp_repr_css_set_property(css, "font-stretch", "ultra-condensed"); + break; + case PANGO_STRETCH_EXTRA_CONDENSED: + sp_repr_css_set_property(css, "font-stretch", "extra-condensed"); + break; + case PANGO_STRETCH_CONDENSED: + sp_repr_css_set_property(css, "font-stretch", "condensed"); + break; + case PANGO_STRETCH_SEMI_CONDENSED: + sp_repr_css_set_property(css, "font-stretch", "semi-condensed"); + break; + case PANGO_STRETCH_NORMAL: + sp_repr_css_set_property(css, "font-stretch", "normal"); + break; + case PANGO_STRETCH_SEMI_EXPANDED: + sp_repr_css_set_property(css, "font-stretch", "semi-expanded"); + break; + case PANGO_STRETCH_EXPANDED: + sp_repr_css_set_property(css, "font-stretch", "expanded"); + break; + case PANGO_STRETCH_EXTRA_EXPANDED: + sp_repr_css_set_property(css, "font-stretch", "extra-expanded"); + break; + case PANGO_STRETCH_ULTRA_EXPANDED: + sp_repr_css_set_property(css, "font-stretch", "ultra-expanded"); + break; + } + + PangoVariant variant = pango_font_description_get_variant(desc); + switch (variant) { + case PANGO_VARIANT_NORMAL: + sp_repr_css_set_property(css, "font-variant", "normal"); + break; + case PANGO_VARIANT_SMALL_CAPS: + sp_repr_css_set_property(css, "font-variant", "small-caps"); + break; + } + + // Convert Pango variations string to CSS format + const char* str = pango_font_description_get_variations(desc); + + std::string variations; + + if (str) { + + std::vector tokens = Glib::Regex::split_simple(",", str); + + Glib::RefPtr regex = Glib::Regex::create("(\\w{4})=([-+]?\\d*\\.?\\d+([eE][-+]?\\d+)?)"); + Glib::MatchInfo matchInfo; + for (auto token: tokens) { + regex->match(token, matchInfo); + if (matchInfo.matches()) { + variations += "'"; + variations += matchInfo.fetch(1).raw(); + variations += "' "; + variations += matchInfo.fetch(2).raw(); + variations += ", "; + } + } + if (variations.length() >= 2) { // Remove last comma/space + variations.pop_back(); + variations.pop_back(); + } + } + + if (!variations.empty()) { + sp_repr_css_set_property(css, "font-variation-settings", variations.c_str()); + } else { + sp_repr_css_unset_property(css, "font-variation-settings" ); + } + pango_font_description_free(desc); +} + + +Glib::ustring FontLister::fontspec_from_style(SPStyle *style) +{ + + PangoFontDescription* descr = ink_font_description_from_style( style ); + Glib::ustring fontspec = pango_font_description_to_string( descr ); + pango_font_description_free(descr); + + //std::cout << "FontLister:fontspec_from_style: " << fontspec << std::endl; + + return fontspec; +} + + +Gtk::TreeModel::Row FontLister::get_row_for_font(Glib::ustring family) +{ + + Gtk::TreeModel::iterator iter = font_list_store->get_iter("0"); + while (iter != font_list_store->children().end()) { + + Gtk::TreeModel::Row row = *iter; + + if (familyNamesAreEqual(family, row[FontList.family])) { + return row; + } + + ++iter; + } + + throw FAMILY_NOT_FOUND; +} + +Gtk::TreePath FontLister::get_path_for_font(Glib::ustring family) +{ + return font_list_store->get_path(get_row_for_font(family)); +} + +bool FontLister::is_path_for_font(Gtk::TreePath path, Glib::ustring family) +{ + Gtk::TreeModel::iterator iter = font_list_store->get_iter(path); + if (iter) { + return familyNamesAreEqual(family, (*iter)[FontList.family]); + } + + return false; +} + +Gtk::TreeModel::Row FontLister::get_row_for_style(Glib::ustring style) +{ + + Gtk::TreeModel::iterator iter = style_list_store->get_iter("0"); + while (iter != style_list_store->children().end()) { + + Gtk::TreeModel::Row row = *iter; + + if (familyNamesAreEqual(style, row[FontStyleList.cssStyle])) { + return row; + } + + ++iter; + } + + throw STYLE_NOT_FOUND; +} + +static gint compute_distance(const PangoFontDescription *a, const PangoFontDescription *b) +{ + + // Weight: multiples of 100 + gint distance = abs(pango_font_description_get_weight(a) - + pango_font_description_get_weight(b)); + + distance += 10000 * abs(pango_font_description_get_stretch(a) - + pango_font_description_get_stretch(b)); + + PangoStyle style_a = pango_font_description_get_style(a); + PangoStyle style_b = pango_font_description_get_style(b); + if (style_a != style_b) { + if ((style_a == PANGO_STYLE_OBLIQUE && style_b == PANGO_STYLE_ITALIC) || + (style_b == PANGO_STYLE_OBLIQUE && style_a == PANGO_STYLE_ITALIC)) { + distance += 1000; // Oblique and italic are almost the same + } else { + distance += 100000; // Normal vs oblique/italic, not so similar + } + } + + // Normal vs small-caps + distance += 1000000 * abs(pango_font_description_get_variant(a) - + pango_font_description_get_variant(b)); + return distance; +} + +// This is inspired by pango_font_description_better_match, but that routine +// always returns false if variant or stretch are different. This means, for +// example, that PT Sans Narrow with style Bold Condensed is never matched +// to another font-family with Bold style. +gboolean font_description_better_match(PangoFontDescription *target, PangoFontDescription *old_desc, PangoFontDescription *new_desc) +{ + if (old_desc == nullptr) + return true; + if (new_desc == nullptr) + return false; + + int old_distance = compute_distance(target, old_desc); + int new_distance = compute_distance(target, new_desc); + //std::cout << "font_description_better_match: old: " << old_distance << std::endl; + //std::cout << " new: " << new_distance << std::endl; + + return (new_distance < old_distance); +} + +// void +// font_description_dump( PangoFontDescription* target ) { +// std::cout << " Font: " << pango_font_description_to_string( target ) << std::endl; +// std::cout << " style: " << pango_font_description_get_style( target ) << std::endl; +// std::cout << " weight: " << pango_font_description_get_weight( target ) << std::endl; +// std::cout << " variant: " << pango_font_description_get_variant( target ) << std::endl; +// std::cout << " stretch: " << pango_font_description_get_stretch( target ) << std::endl; +// std::cout << " gravity: " << pango_font_description_get_gravity( target ) << std::endl; +// } + +/* Returns style string */ +// TODO: Remove or turn into function to be used by new_font_family. +Glib::ustring FontLister::get_best_style_match(Glib::ustring family, Glib::ustring target_style) +{ + +#ifdef DEBUG_FONT + std::cout << "\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" << std::endl; + std::cout << "FontLister::get_best_style_match: " << family << " : " << target_style << std::endl; +#endif + + Glib::ustring fontspec = family + ", " + target_style; + + Gtk::TreeModel::Row row; + try + { + row = get_row_for_font(family); + } + catch (...) + { + std::cerr << "FontLister::get_best_style_match(): can't find family: " << family << std::endl; + return (target_style); + } + + PangoFontDescription *target = pango_font_description_from_string(fontspec.c_str()); + PangoFontDescription *best = nullptr; + + //font_description_dump( target ); + + GList *styles = default_styles; + if (row[FontList.onSystem] && !row[FontList.styles]) { + row[FontList.styles] = font_factory::Default()->GetUIStyles(row[FontList.pango_family]); + styles = row[FontList.styles]; + } + + for (GList *l = styles; l; l = l->next) { + Glib::ustring fontspec = family + ", " + ((StyleNames *)l->data)->CssName; + PangoFontDescription *candidate = pango_font_description_from_string(fontspec.c_str()); + //font_description_dump( candidate ); + //std::cout << " " << font_description_better_match( target, best, candidate ) << std::endl; + if (font_description_better_match(target, best, candidate)) { + pango_font_description_free(best); + best = candidate; + //std::cout << " ... better: " << std::endl; + } else { + pango_font_description_free(candidate); + //std::cout << " ... not better: " << std::endl; + } + } + + Glib::ustring best_style = target_style; + if (best) { + pango_font_description_unset_fields(best, PANGO_FONT_MASK_FAMILY); + best_style = pango_font_description_to_string(best); + } + + if (target) + pango_font_description_free(target); + if (best) + pango_font_description_free(best); + + +#ifdef DEBUG_FONT + std::cout << " Returning: " << best_style << std::endl; + std::cout << "FontLister::get_best_style_match: exit" << std::endl; + std::cout << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" << std::endl; +#endif + return best_style; +} + +const Glib::RefPtr FontLister::get_font_list() const +{ + return font_list_store; +} + +const Glib::RefPtr FontLister::get_style_list() const +{ + return style_list_store; +} + +} // namespace Inkscape + +// Helper functions + +// Separator function (if true, a separator will be drawn) +bool font_lister_separator_func(const Glib::RefPtr& model, + const Gtk::TreeModel::iterator& iter) { + + // Of what use is 'model', can we avoid using font_lister? + Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance(); + Gtk::TreeModel::Row row = *iter; + Glib::ustring entry = row[font_lister->FontList.family]; + return entry == "#"; +} + +// Needed until Text toolbar updated +gboolean font_lister_separator_func2(GtkTreeModel *model, GtkTreeIter *iter, gpointer /*data*/) +{ + gchar *text = nullptr; + gtk_tree_model_get(model, iter, 0, &text, -1); // Column 0: FontList.family + bool result = (text && strcmp(text, "#") == 0); + g_free(text); + return result; +} + +// first call do nothing +void font_lister_cell_data_func (Gtk::CellRenderer *renderer, Gtk::TreeIter const &iter) +{ +} + +// Draw system fonts in dark blue, missing fonts with red strikeout. +// Used by both FontSelector and Text toolbar. +void font_lister_cell_data_func_markup (Gtk::CellRenderer *renderer, Gtk::TreeIter const &iter) +{ + Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance(); + Glib::ustring markup = font_lister->get_font_family_markup(iter); + renderer->set_property("markup", markup); +} + +// Needed until Text toolbar updated +void font_lister_cell_data_func2(GtkCellLayout * /*cell_layout*/, + GtkCellRenderer *cell, + GtkTreeModel *model, + GtkTreeIter *iter, + gpointer data) +{ + gchar *family; + gboolean onSystem = false; + gtk_tree_model_get(model, iter, 0, &family, 2, &onSystem, -1); + gchar* family_escaped = g_markup_escape_text(family, -1); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool dark = prefs->getBool("/theme/darkTheme", false); + Glib::ustring markup; + + if (!onSystem) { + markup = ""; + + /* See if font-family on system */ + std::vector tokens = Glib::Regex::split_simple("\\s*,\\s*", family); + for (auto token : tokens) { + + GtkTreeIter iter; + gboolean valid; + gboolean onSystem = true; + gboolean found = false; + for (valid = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter); + valid; + valid = gtk_tree_model_iter_next(GTK_TREE_MODEL(model), &iter)) { + + gchar *token_family = nullptr; + gtk_tree_model_get(model, &iter, 0, &token_family, 2, &onSystem, -1); + if (onSystem && familyNamesAreEqual(token, token_family)) { + found = true; + g_free(token_family); + break; + } + g_free(token_family); + } + if (found) { + markup += g_markup_escape_text(token.c_str(), -1); + markup += ", "; + } else { + if (dark) { + markup += ""; + } else { + markup += ""; + } + markup += g_markup_escape_text(token.c_str(), -1); + markup += ""; + markup += ", "; + } + } + // Remove extra comma and space from end. + if (markup.size() >= 2) { + markup.resize(markup.size() - 2); + } + markup += ""; + // std::cout << markup << std::endl; + } else { + markup = family_escaped; + } + + int show_sample = prefs->getInt("/tools/text/show_sample_in_list", 1); + if (show_sample) { + + Glib::ustring sample = prefs->getString("/tools/text/font_sample"); + gchar* sample_escaped = g_markup_escape_text(sample.data(), -1); + if (data) { + markup += " get_font_family(); + Glib::ustring style = row[font_lister->FontStyleList.cssStyle]; + + Glib::ustring style_escaped = Glib::Markup::escape_text( style ); + Glib::ustring font_desc = family + ", " + style; + Glib::ustring markup; + + markup = "" + style_escaped + ""; + std::cout << " markup: " << markup << std::endl; + + renderer->set_property("markup", markup); +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/libnrtype/font-lister.h b/src/libnrtype/font-lister.h new file mode 100644 index 0000000..5d73119 --- /dev/null +++ b/src/libnrtype/font-lister.h @@ -0,0 +1,364 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef FONT_LISTER_H +#define FONT_LISTER_H + +/* + * Font selection widgets + * + * Authors: + * Chris Lahey + * Lauris Kaplinski + * Tavmjong Bah + * + * Copyright (C) 1999-2001 Ximian, Inc. + * Copyright (C) 2002 Lauris Kaplinski + * Copyright (C) 2013 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include + +#include +#include // For strescape() + +#include +#include +#include + +class SPObject; +class SPDocument; +class SPCSSAttr; +class SPStyle; + +namespace Gtk { +class CellRenderer; +} + +namespace Inkscape { + +/** + * This class enumerates fonts using libnrtype into reusable data stores and + * allows for random access to the font-family list and the font-style list. + * Setting the font-family updates the font-style list. "Style" in this case + * refers to everything but family and size (e.g. italic/oblique, weight). + * + * This class handles font-family lists and fonts that are not on the system, + * where there is not an entry in the fontInstanceMap. + * + * This class uses the idea of "font_spec". This is a plain text string as used by + * Pango. It is similar to the CSS font shorthand except that font-family comes + * first and in this class the font-size is not used. + * + * This class uses the FontFactory class to get a list of system fonts + * and to find best matches via Pango. The Pango interface is only setup + * to deal with fonts that are on the system so care must be taken. For + * example, best matches should only be done with the first font-family + * in a font-family list. If the first font-family is not on the system + * then a generic font-family should be used (sans-serif -> Sans). + * + * This class is used by the UI interface (text-toolbar, font-select, etc.). + * Those items can change the selected font family and style here. When that + * happens. this class emits a signal for those items to update their displayed + * values. + * + * This class is a singleton (one instance per Inkscape session). Since fonts + * used in a document are added to the list, there really should be one + * instance per document. + * + * "Font" includes family and style. It should not be used when one + * means font-family. + */ + +class FontLister { +public: + enum Exceptions { + FAMILY_NOT_FOUND, + STYLE_NOT_FOUND + }; + + virtual ~FontLister(); + + /** + * GtkTreeModelColumnRecord for the font-family list Gtk::ListStore + */ + struct FontListClass : public Gtk::TreeModelColumnRecord { + /** + * Column containing the family name + */ + Gtk::TreeModelColumn family; + + /** + * Column containing the styles for each family name. + */ + Gtk::TreeModelColumn styles; + + /** + * Column containing flag if font is on system + */ + Gtk::TreeModelColumn onSystem; + + /** + * Not actually a column. + * Necessary for quick initialization of FontLister, + * we initially store the pango family and if the + * font style is actually used we'll cache it in + * %styles. + */ + Gtk::TreeModelColumn pango_family; + + FontListClass() + { + add(family); + add(styles); + add(onSystem); + add(pango_family); + } + }; + + FontListClass FontList; + + struct FontStyleListClass : public Gtk::TreeModelColumnRecord { + /** + * Column containing the styles as Font designer used. + */ + Gtk::TreeModelColumn displayStyle; + + /** + * Column containing the styles in CSS/Pango format. + */ + Gtk::TreeModelColumn cssStyle; + + FontStyleListClass() + { + add(cssStyle); + add(displayStyle); + } + }; + + FontStyleListClass FontStyleList; + + /** + * @return the ListStore with the family names + * + * The return is const and the function is declared as const. + * The ListStore is ready to be used after class instantiation + * and should not be modified. + */ + const Glib::RefPtr get_font_list() const; + + /** + * @return the ListStore with the styles + */ + const Glib::RefPtr get_style_list() const; + + /** + * Inserts a font family or font-fallback list (for use when not + * already in document or on system). + */ + void insert_font_family(Glib::ustring new_family); + + /** + * Updates font list to include fonts in document. + */ + void update_font_list(SPDocument *document); + +public: + static Inkscape::FontLister *get_instance(); + + /** + * Takes a hand written font spec and returns a Pango generated one in + * standard form. + */ + Glib::ustring canonize_fontspec(Glib::ustring fontspec); + + /** + * Find closest system font to given font. + */ + Glib::ustring system_fontspec(Glib::ustring fontspec); + + /** + * Gets font-family and style from fontspec. + * font-family and style returned. + */ + std::pair ui_from_fontspec(Glib::ustring fontspec); + + /** + * Sets font-family and style after a selection change. + * New font-family and style returned. + */ + std::pair selection_update(); + + /** + * Sets current_fontspec, etc. If check is false, won't + * try to find best style match (assumes style in fontspec + * valid for given font-family). + */ + void set_fontspec(Glib::ustring fontspec, bool check = true); + + Glib::ustring get_fontspec() { return (canonize_fontspec(current_family + ", " + current_style)); } + + /** + * Changes font-family, updating style list and attempting to find + * closest style to current_style style (if check_style is true). + * New font-family and style returned. + * Does NOT update current_family and current_style. + * (For potential use in font-selector which doesn't update until + * "Apply" button clicked.) + */ + std::pair new_font_family(Glib::ustring family, bool check_style = true); + + /** + * Sets font-family, updating style list and attempting + * to find closest style to old current_style. + * New font-family and style returned. + * Updates current_family and current_style. + * Calls new_font_family(). + * (For use in text-toolbar where update is immediate.) + */ + std::pair set_font_family(Glib::ustring family, bool check_style = true, + bool emit = true); + + /** + * Sets font-family from row in list store. + * The row can be used to determine if we are in the + * document or system part of the font-family list. + * This is needed to handle scrolling through the + * font-family list correctly. + * Calls set_font_family(). + */ + std::pair set_font_family(int row, bool check_style = true, bool emit = true); + + Glib::ustring get_font_family() + { + return current_family; + } + + int get_font_family_row() + { + return current_family_row; + } + + /** + * Sets style. Does not validate style for family. + */ + void set_font_style(Glib::ustring style, bool emit = true); + + Glib::ustring get_font_style() + { + return current_style; + } + + Glib::ustring fontspec_from_style(SPStyle *style); + + /** + * Fill css using given fontspec (doesn't need to be member function). + */ + void fill_css(SPCSSAttr *css, Glib::ustring fontspec = ""); + + Gtk::TreeModel::Row get_row_for_font() { return get_row_for_font (current_family); } + + Gtk::TreeModel::Row get_row_for_font(Glib::ustring family); + + Gtk::TreePath get_path_for_font(Glib::ustring family); + + bool is_path_for_font(Gtk::TreePath path, Glib::ustring family); + + Gtk::TreeModel::Row get_row_for_style() { return get_row_for_style (current_style); } + + Gtk::TreeModel::Row get_row_for_style(Glib::ustring style); + + Gtk::TreePath get_path_for_style(Glib::ustring style); + + std::pair get_paths(Glib::ustring family, Glib::ustring style); + + /** + * Return best style match for new font given style for old font. + */ + Glib::ustring get_best_style_match(Glib::ustring family, Glib::ustring style); + + /** + * Ensures the style list for a particular family has been created. + */ + void ensureRowStyles(Glib::RefPtr model, Gtk::TreeModel::iterator const iter); + + /** + * Get markup for font-family. + */ + Glib::ustring get_font_family_markup(Gtk::TreeIter const &iter); + + /** + * Let users of FontLister know to update GUI. + * This is to allow synchronization of changes across multiple widgets. + * Handlers should block signals. + * Input is fontspec to set. + */ + sigc::connection connectUpdate(sigc::slot slot) { + return update_signal.connect(slot); + } + + bool blocked() { return block; } + +private: + FontLister(); + + void update_font_data_recursive(SPObject& r, std::map> &font_data); + + void font_family_row_update(int start=0); + + Glib::RefPtr font_list_store; + Glib::RefPtr style_list_store; + + /** + * Info for currently selected font (what is shown in the UI). + * May include font-family lists and fonts not on system. + */ + int current_family_row; + Glib::ustring current_family; + Glib::ustring current_style; + + /** + * If a font-family is not on system, this list of styles is used. + */ + GList *default_styles; + + bool block; + void emit_update(); + sigc::signal update_signal; +}; + +} // namespace Inkscape + +// Helper functions +bool font_lister_separator_func (const Glib::RefPtr& model, + const Gtk::TreeModel::iterator& iter); + +gboolean font_lister_separator_func2(GtkTreeModel *model, + GtkTreeIter *iter, + gpointer /*data*/); + +void font_lister_cell_data_func (Gtk::CellRenderer *renderer, Gtk::TreeIter const &iter); + +void font_lister_cell_data_func_markup (Gtk::CellRenderer *renderer, Gtk::TreeIter const &iter); + +void font_lister_cell_data_func2(GtkCellLayout * /*cell_layout*/, + GtkCellRenderer *cell, + GtkTreeModel *model, + GtkTreeIter *iter, + gpointer /*data*/); + +void font_lister_style_cell_data_func (Gtk::CellRenderer *renderer, Gtk::TreeIter const &iter); + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/libnrtype/font-style.h b/src/libnrtype/font-style.h new file mode 100644 index 0000000..23a26d6 --- /dev/null +++ b/src/libnrtype/font-style.h @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_LIBNRTYPE_FONT_STYLE_H +#define SEEN_LIBNRTYPE_FONT_STYLE_H + +#include <2geom/affine.h> +#include + +// structure that holds data describing how to render glyphs of a font + +// Different raster styles. +struct font_style { + Geom::Affine transform; // the ctm. contains the font-size + bool vertical; // should be rendered vertically or not? + // good font support would take the glyph alternates for vertical mode, when present + double stroke_width; // if 0, the glyph is filled; otherwise stroked + JoinType stroke_join; + ButtType stroke_cap; + float stroke_miter_limit; + int nbDash; + double dash_offset; + double* dashes; +}; + + +#endif /* !SEEN_LIBNRTYPE_FONT_STYLE_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : -- cgit v1.2.3