diff --git a/android/src/main/java/com/horcrux/svg/FontData.java b/android/src/main/java/com/horcrux/svg/FontData.java index 8103d7dc..f2df6445 100644 --- a/android/src/main/java/com/horcrux/svg/FontData.java +++ b/android/src/main/java/com/horcrux/svg/FontData.java @@ -20,6 +20,7 @@ class FontData { private static final String WORD_SPACING = "wordSpacing"; private static final String LETTER_SPACING = "letterSpacing"; private static final String TEXT_DECORATION = "textDecoration"; + private static final String FONT_FEATURE_SETTINGS = "fontFeatureSettings"; private static final String FONT_VARIANT_LIGATURES = "fontVariantLigatures"; final double fontSize; @@ -27,6 +28,7 @@ class FontData { final FontStyle fontStyle; final ReadableMap fontData; final FontWeight fontWeight; + final String fontFeatureSettings; final FontVariantLigatures fontVariantLigatures; final TextAnchor textAnchor; @@ -45,6 +47,7 @@ class FontData { fontFamily = ""; fontStyle = FontStyle.normal; fontWeight = FontWeight.Normal; + fontFeatureSettings = ""; fontVariantLigatures = FontVariantLigatures.normal; textAnchor = TextAnchor.start; @@ -88,6 +91,7 @@ class FontData { fontFamily = font.hasKey(FONT_FAMILY) ? font.getString(FONT_FAMILY) : parent.fontFamily; fontStyle = font.hasKey(FONT_STYLE) ? FontStyle.valueOf(font.getString(FONT_STYLE)) : parent.fontStyle; fontWeight = font.hasKey(FONT_WEIGHT) ? FontWeight.getEnum(font.getString(FONT_WEIGHT)) : parent.fontWeight; + fontFeatureSettings = font.hasKey(FONT_FEATURE_SETTINGS) ? font.getString(FONT_FEATURE_SETTINGS) : parent.fontFeatureSettings; fontVariantLigatures = font.hasKey(FONT_VARIANT_LIGATURES) ? FontVariantLigatures.valueOf(font.getString(FONT_VARIANT_LIGATURES)) : parent.fontVariantLigatures; textAnchor = font.hasKey(TEXT_ANCHOR) ? TextAnchor.valueOf(font.getString(TEXT_ANCHOR)) : parent.textAnchor; diff --git a/android/src/main/java/com/horcrux/svg/TSpanShadowNode.java b/android/src/main/java/com/horcrux/svg/TSpanShadowNode.java index 85cb4432..4752faea 100644 --- a/android/src/main/java/com/horcrux/svg/TSpanShadowNode.java +++ b/android/src/main/java/com/horcrux/svg/TSpanShadowNode.java @@ -223,12 +223,41 @@ class TSpanShadowNode extends TextShadowNode { Media: visual Computed value: as specified Animatable: no + + https://drafts.csswg.org/css-fonts-3/#default-features + + 7.1. Default features + + For OpenType fonts, user agents must enable the default features defined in the OpenType + documentation for a given script and writing mode. + + Required ligatures, common ligatures and contextual forms must be enabled by default + (OpenType features: rlig, liga, clig, calt), + along with localized forms (OpenType feature: locl), + and features required for proper display of composed characters and marks + (OpenType features: ccmp, mark, mkmk). + + These features must always be enabled, even when the value of the ‘font-variant’ and + ‘font-feature-settings’ properties is ‘normal’. + + Individual features are only disabled when explicitly overridden by the author, + as when ‘font-variant-ligatures’ is set to ‘no-common-ligatures’. + + TODO For handling complex scripts such as Arabic, Mongolian or Devanagari additional features + are required. + + TODO For upright text within vertical text runs, + vertical alternates (OpenType feature: vert) must be enabled. */ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + String required = "'rlig', 'liga', 'clig', 'calt', 'locl', 'ccmp', 'mark', 'mkmk',"; + String defaultFeatures = required + "'kern', "; if (allowOptionalLigatures) { - paint.setFontFeatureSettings("'kern', 'liga', 'clig', 'dlig', 'hlig', 'cala', 'rlig'"); + String additionalLigatures = "'hlig', 'cala', "; + paint.setFontFeatureSettings(defaultFeatures + additionalLigatures + font.fontFeatureSettings); } else { - paint.setFontFeatureSettings("'kern', 'liga' 0, 'clig' 0, 'dlig' 0, 'hlig' 0, 'cala' 0, 'rlig'"); + String disableDiscretionaryLigatures = "'liga' 0, 'clig' 0, 'dlig' 0, 'hlig' 0, 'cala' 0, "; + paint.setFontFeatureSettings(defaultFeatures + disableDiscretionaryLigatures + font.fontFeatureSettings); } } // OpenType.js font data @@ -601,10 +630,32 @@ class TSpanShadowNode extends TextShadowNode { switch (baselineShiftString) { case "sub": // TODO + if (fontData != null && fontData.hasKey("tables") && fontData.hasKey("unitsPerEm")) { + int unitsPerEm = fontData.getInt("unitsPerEm"); + ReadableMap tables = fontData.getMap("tables"); + if (tables.hasKey("os2")) { + ReadableMap os2 = tables.getMap("os2"); + if (os2.hasKey("ySubscriptYOffset")) { + double subOffset = os2.getDouble("ySubscriptYOffset"); + baselineShift += fontSize * subOffset / unitsPerEm; + } + } + } break; case "super": // TODO + if (fontData != null && fontData.hasKey("tables") && fontData.hasKey("unitsPerEm")) { + int unitsPerEm = fontData.getInt("unitsPerEm"); + ReadableMap tables = fontData.getMap("tables"); + if (tables.hasKey("os2")) { + ReadableMap os2 = tables.getMap("os2"); + if (os2.hasKey("ySuperscriptYOffset")) { + double superOffset = os2.getDouble("ySuperscriptYOffset"); + baselineShift -= fontSize * superOffset / unitsPerEm; + } + } + } break; case "baseline": @@ -625,9 +676,6 @@ class TSpanShadowNode extends TextShadowNode { final float[] startPointMatrixData = new float[9]; final float[] endPointMatrixData = new float[9]; - String previous = ""; - double previousCharWidth = 0; - for (int index = 0; index < length; index++) { char currentChar = chars[index]; String current = String.valueOf(currentChar); @@ -638,23 +686,21 @@ class TSpanShadowNode extends TextShadowNode { advances horizontally when the glyph is drawn using horizontal text layout). */ boolean hasLigature = false; - if (allowOptionalLigatures) { - if (alreadyRenderedGraphemeCluster) { - current = ""; - } else { - int nextIndex = index; - while (++nextIndex < length) { - float nextWidth = advances[nextIndex]; - if (nextWidth > 0) { - break; - } - String nextLigature = current + String.valueOf(chars[nextIndex]); - boolean hasNextLigature = PaintCompat.hasGlyph(paint, nextLigature); - if (hasNextLigature) { - ligature[nextIndex] = true; - current = nextLigature; - hasLigature = true; - } + if (alreadyRenderedGraphemeCluster) { + current = ""; + } else { + int nextIndex = index; + while (++nextIndex < length) { + float nextWidth = advances[nextIndex]; + if (nextWidth > 0) { + break; + } + String nextLigature = current + String.valueOf(chars[nextIndex]); + boolean hasNextLigature = PaintCompat.hasGlyph(paint, nextLigature); + if (hasNextLigature) { + ligature[nextIndex] = true; + current = nextLigature; + hasLigature = true; } } } @@ -670,16 +716,8 @@ class TSpanShadowNode extends TextShadowNode { using the user agent's distance along the path algorithm. */ if (autoKerning) { - if (allowOptionalLigatures) { - double kerned = advances[index] * scaleSpacingAndGlyphs; - kerning = kerned - charWidth; - } else { - double bothCharsWidth = paint.measureText(previous + current) * scaleSpacingAndGlyphs; - double kerned = bothCharsWidth - previousCharWidth; - kerning = kerned - charWidth; - previousCharWidth = charWidth; - previous = current; - } + double kerned = advances[index] * scaleSpacingAndGlyphs; + kerning = kerned - charWidth; } boolean isWordSeparator = currentChar == ' '; @@ -699,8 +737,8 @@ class TSpanShadowNode extends TextShadowNode { continue; } - advance = advance * side; - charWidth = charWidth * side; + advance *= side; + charWidth *= side; double cursor = offset + (x + dx) * side; double startPoint = cursor - advance; diff --git a/lib/attributes.js b/lib/attributes.js index d32f2b6d..2164ab51 100644 --- a/lib/attributes.js +++ b/lib/attributes.js @@ -32,7 +32,8 @@ function fontDiffer(a, b) { a.wordSpacing !== b.wordSpacing || a.kerning !== b.kerning || a.fontVariantLigatures !== b.fontVariantLigatures || - a.fontData !== b.fontData + a.fontData !== b.fontData || + a.fontFeatureSettings !== b.fontFeatureSettings ); } diff --git a/lib/extract/extractText.js b/lib/extract/extractText.js index 019a97ab..1df21b13 100644 --- a/lib/extract/extractText.js +++ b/lib/extract/extractText.js @@ -56,6 +56,7 @@ export function extractFont(props) { wordSpacing, kerning, fontVariantLigatures, + fontFeatureSettings, } = props; let { fontSize, @@ -80,6 +81,7 @@ export function extractFont(props) { wordSpacing, kerning, fontVariantLigatures, + fontFeatureSettings, }, prop => !_.isNil(prop)); if (typeof font === 'string') { diff --git a/lib/props.js b/lib/props.js index 8705a193..282fa349 100644 --- a/lib/props.js +++ b/lib/props.js @@ -260,6 +260,106 @@ const alignmentBaseline = PropTypes.oneOf(['baseline', 'text-bottom', 'alphabeti */ const baselineShift = PropTypes.oneOfType([PropTypes.oneOf(['sub', 'super', 'baseline']), PropTypes.arrayOf(numberProp), PropTypes.string]); +/* + 6.12. Low-level font feature settings control: the font-feature-settings property + + Name: font-feature-settings + Value: normal | # + Initial: normal + Applies to: all elements + Inherited: yes + Percentages: N/A + Media: visual + Computed value: as specified + Animatable: no + + This property provides low-level control over OpenType font features. + + It is intended as a way of providing access to font features + that are not widely used but are needed for a particular use case. + + Authors should generally use ‘font-variant’ and its related subproperties + whenever possible and only use this property for special cases where its use + is the only way of accessing a particular infrequently used font feature. + + enable small caps and use second swash alternate + font-feature-settings: "smcp", "swsh" 2; + A value of ‘normal’ means that no change in glyph selection or positioning occurs due to this property. + + Feature tag values have the following syntax: + + = [ | on | off ]? + The is a case-sensitive OpenType feature tag. As specified in the OpenType specification, + feature tags contain four ASCII characters. + + Tag strings longer or shorter than four characters, + or containing characters outside the U+20–7E codepoint range are invalid. + + Feature tags need only match a feature tag defined in the font, + so they are not limited to explicitly registered OpenType features. + + Fonts defining custom feature tags should follow the tag name rules + defined in the OpenType specification [OPENTYPE-FEATURES]. + + Feature tags not present in the font are ignored; + a user agent must not attempt to synthesize fallback behavior based on these feature tags. + + The one exception is that user agents may synthetically support the kern feature with fonts + that contain kerning data in the form of a ‘kern’ table but lack kern feature support in the ‘GPOS’ table. + + In general, authors should use the ‘font-kerning’ property to explicitly enable or disable kerning + since this property always affects fonts with either type of kerning data. + + If present, a value indicates an index used for glyph selection. + + An value must be 0 or greater. + + A value of 0 indicates that the feature is disabled. + + For boolean features, a value of 1 enables the feature. + + For non-boolean features, a value of 1 or greater enables the feature and indicates the feature selection index. + + A value of ‘on’ is synonymous with 1 and ‘off’ is synonymous with 0. + + If the value is omitted, a value of 1 is assumed. + + font-feature-settings: "dlig" 1; /* dlig=1 enable discretionary ligatures * / + font-feature-settings: "smcp" on; /* smcp=1 enable small caps * / + font-feature-settings: 'c2sc'; /* c2sc=1 enable caps to small caps * / + font-feature-settings: "liga" off; /* liga=0 no common ligatures * / + font-feature-settings: "tnum", 'hist'; /* tnum=1, hist=1 enable tabular numbers and historical forms * / + font-feature-settings: "tnum" "hist"; /* invalid, need a comma-delimited list * / + font-feature-settings: "silly" off; /* invalid, tag too long * / + font-feature-settings: "PKRN"; /* PKRN=1 enable custom feature * / + font-feature-settings: dlig; /* invalid, tag must be a string * / + + When values greater than the range supported by the font are specified, the behavior is explicitly undefined. + + For boolean features, in general these will enable the feature. + + For non-boolean features, out of range values will in general be equivalent to a 0 value. + + However, in both cases the exact behavior will depend upon the way the font is designed + (specifically, which type of lookup is used to define the feature). + + Although specifically defined for OpenType feature tags, + feature tags for other modern font formats that support font features may be added in the future. + + Where possible, features defined for other font formats + should attempt to follow the pattern of registered OpenType tags. + + The Japanese text below will be rendered with half-width kana characters: + + body { font-feature-settings: "hwid"; /* Half-width OpenType feature * / } + +

毎日カレー食べてるのに、飽きない

+ + https://drafts.csswg.org/css-fonts-3/#propdef-font-feature-settings + https://developer.mozilla.org/en/docs/Web/CSS/font-feature-settings +*/ +const fontFeatureSettings = PropTypes.string; + const textSpecificProps = { ...pathProps, ...fontProps, @@ -269,6 +369,7 @@ const textSpecificProps = { lengthAdjust, textLength, fontData: PropTypes.object, + fontFeatureSettings, }; // https://svgwg.org/svg2-draft/text.html#TSpanAttributes