From 3bd1a7946e71c5c33d6d235b5711a63e2a0a8304 Mon Sep 17 00:00:00 2001 From: Stefan Wittwer Date: Sun, 5 May 2024 11:34:08 +0200 Subject: [PATCH 1/4] feat: add font feature settings support Added font feature settings to TextStyle Adapted example to new structure --- .changeset/six-paws-tie.md | 7 ++ .../examples/font-feature-settings/index.jsx | 86 +++++++++++++++++++ packages/examples/vite/src/examples/index.js | 4 +- packages/examples/vite/src/index.jsx | 2 +- .../layout/src/steps/resolveInheritance.js | 1 + packages/layout/src/svg/inheritProps.js | 1 + .../layout/src/text/getAttributedString.js | 2 + .../tests/steps/resolveInhritance.test.js | 4 + packages/stylesheet/src/types.ts | 1 + packages/textkit/src/layout/generateGlyphs.ts | 4 +- 10 files changed, 108 insertions(+), 4 deletions(-) create mode 100644 .changeset/six-paws-tie.md create mode 100644 packages/examples/vite/src/examples/font-feature-settings/index.jsx diff --git a/.changeset/six-paws-tie.md b/.changeset/six-paws-tie.md new file mode 100644 index 000000000..37be21054 --- /dev/null +++ b/.changeset/six-paws-tie.md @@ -0,0 +1,7 @@ +--- +"@react-pdf/textkit": minor +"@react-pdf/layout": minor +"@react-pdf/types": minor +--- + +Add support for fontFeatureSettings to customise ligatures, tabular number display, and other font features diff --git a/packages/examples/vite/src/examples/font-feature-settings/index.jsx b/packages/examples/vite/src/examples/font-feature-settings/index.jsx new file mode 100644 index 000000000..42d06d619 --- /dev/null +++ b/packages/examples/vite/src/examples/font-feature-settings/index.jsx @@ -0,0 +1,86 @@ +/* eslint react/prop-types: 0 */ +/* eslint react/jsx-sort-props: 0 */ + +import { Document, Font, Page, StyleSheet, Text } from '@react-pdf/renderer'; +import React from 'react'; + +import RobotoFont from '../../../public/Roboto-Regular.ttf'; +import RubikFont from '../../../public/Rubik-Regular.ttf'; + +const styles = StyleSheet.create({ + body: { + paddingTop: 35, + paddingBottom: 45, + paddingHorizontal: 35, + position: 'relative', + fontSize: 14, + }, + headline: { + fontFamily: 'Roboto', + fontSize: '24', + paddingVertical: 12, + }, + rubik: { + fontFamily: 'Rubik', + }, + roboto: { + fontFamily: 'Roboto', + }, + tabular: { + fontFeatureSettings: ['tnum'], + }, + smallCapitals: { + fontFeatureSettings: ['smcp'], + }, + disableCommonLigatures: { + fontFeatureSettings: { liga: 0 }, + }, +}); + +Font.register({ + family: 'Rubik', + fonts: [{ src: RubikFont, fontWeight: 400 }], +}); +Font.register({ + family: 'Roboto', + fonts: [{ src: RobotoFont, fontWeight: 400 }], +}); + +const MyDoc = () => { + const longNumberExample = "012'345'678'901"; + const commonLigaturesExample = 'A firefighter from Sheffield'; + return ( + + Rubik + {longNumberExample} – Default features + + {longNumberExample} – Tabular numbers + + Roboto + + {commonLigaturesExample} – Default features + + + {commonLigaturesExample} – Common ligatures off + + + {commonLigaturesExample} – Small capitals + + + ); +}; + +const FontFeatureSettings = () => { + return ( + + + + ); +}; + +export default { + id: 'font-feature-settings', + name: 'Font Feature Settings', + description: '', + Document: FontFeatureSettings, +}; diff --git a/packages/examples/vite/src/examples/index.js b/packages/examples/vite/src/examples/index.js index 5b140e082..581ba1b0e 100644 --- a/packages/examples/vite/src/examples/index.js +++ b/packages/examples/vite/src/examples/index.js @@ -2,7 +2,9 @@ import duplicatedImages from './duplicated-images'; import ellipsis from './ellipsis'; import emoji from './emoji'; import fontFamilyFallback from './font-family-fallback'; +import fontFeatureSettings from './font-feature-settings'; import fontWeight from './font-weight'; +import forms from './forms'; import fractals from './fractals'; import goTo from './go-to'; import imageStressTest from './image-stress-test'; @@ -18,7 +20,6 @@ import resume from './resume'; import svg from './svg'; import svgTransform from './svg-transform'; import transformOrigin from './transform-origin'; -import forms from './forms'; const EXAMPLES = [ duplicatedImages, @@ -26,6 +27,7 @@ const EXAMPLES = [ emoji, fontFamilyFallback, fontWeight, + fontFeatureSettings, fractals, goTo, JpgOrientation, diff --git a/packages/examples/vite/src/index.jsx b/packages/examples/vite/src/index.jsx index 32e897ab2..15e6a0d59 100644 --- a/packages/examples/vite/src/index.jsx +++ b/packages/examples/vite/src/index.jsx @@ -1,8 +1,8 @@ import './index.css'; +import { PDFViewer } from '@react-pdf/renderer'; import React, { useEffect, useState } from 'react'; import { createRoot } from 'react-dom/client'; -import { PDFViewer } from '@react-pdf/renderer'; import EXAMPLES from './examples'; diff --git a/packages/layout/src/steps/resolveInheritance.js b/packages/layout/src/steps/resolveInheritance.js index 7ee6359ac..d83baf045 100644 --- a/packages/layout/src/steps/resolveInheritance.js +++ b/packages/layout/src/steps/resolveInheritance.js @@ -7,6 +7,7 @@ const BASE_INHERITABLE_PROPERTIES = [ 'fontSize', 'fontStyle', 'fontWeight', + 'fontFeatureSettings', 'letterSpacing', 'opacity', 'textDecoration', diff --git a/packages/layout/src/svg/inheritProps.js b/packages/layout/src/svg/inheritProps.js index 690f2fa53..8c0a9b222 100644 --- a/packages/layout/src/svg/inheritProps.js +++ b/packages/layout/src/svg/inheritProps.js @@ -22,6 +22,7 @@ const BASE_SVG_INHERITED_PROPS = [ 'fontSize', 'fontStyle', 'fontWeight', + 'fontFeatureSettings', 'letterSpacing', 'opacity', 'textDecoration', diff --git a/packages/layout/src/text/getAttributedString.js b/packages/layout/src/text/getAttributedString.js index 917daf7d7..62602b11f 100644 --- a/packages/layout/src/text/getAttributedString.js +++ b/packages/layout/src/text/getAttributedString.js @@ -32,6 +32,7 @@ const getFragments = (fontStore, instance, parentLink, level = 0) => { fontWeight, fontStyle, fontSize = 18, + fontFeatureSettings, textAlign, lineHeight, textDecoration, @@ -83,6 +84,7 @@ const getFragments = (fontStore, instance, parentLink, level = 0) => { underlineColor: textDecorationColor || color, link: parentLink || instance.props?.src || instance.props?.href, align: textAlign || (direction === 'rtl' ? 'right' : 'left'), + features: fontFeatureSettings, }; for (let i = 0; i < instance.children.length; i += 1) { diff --git a/packages/layout/tests/steps/resolveInhritance.test.js b/packages/layout/tests/steps/resolveInhritance.test.js index 4e14e013a..9887442ba 100644 --- a/packages/layout/tests/steps/resolveInhritance.test.js +++ b/packages/layout/tests/steps/resolveInhritance.test.js @@ -154,4 +154,8 @@ describe('layout resolveInheritance', () => { test('Should inherit textAlign value', shouldInherit('textAlign')); test('Should inherit visibility value', shouldInherit('visibility')); test('Should inherit wordSpacing value', shouldInherit('wordSpacing')); + test( + 'Should inherit fontFeatureSettings value', + shouldInherit('fontFeatureSettings'), + ); }); diff --git a/packages/stylesheet/src/types.ts b/packages/stylesheet/src/types.ts index 3da75c26a..4d192f4a4 100644 --- a/packages/stylesheet/src/types.ts +++ b/packages/stylesheet/src/types.ts @@ -284,6 +284,7 @@ export type TextStyle = { fontFamily?: string | string[]; fontStyle?: FontStyle; fontWeight?: FontWeight; + fontFeatureSettings?: string[] | Record; letterSpacing?: number | string; lineHeight?: number | string; maxLines?: number; diff --git a/packages/textkit/src/layout/generateGlyphs.ts b/packages/textkit/src/layout/generateGlyphs.ts index 7018efcae..a48538df8 100644 --- a/packages/textkit/src/layout/generateGlyphs.ts +++ b/packages/textkit/src/layout/generateGlyphs.ts @@ -42,7 +42,7 @@ const layoutRun = (string: string) => { */ return (run: Run) => { const { start, end, attributes = {} } = run; - const { font } = attributes; + const { font, features } = attributes; if (!font) return { ...run, glyphs: [], glyphIndices: [], positions: [] }; @@ -53,7 +53,7 @@ const layoutRun = (string: string) => { // passing LTR To force fontkit to not reverse the string const glyphRun = font.layout( runString, - undefined, + features, undefined, undefined, 'ltr', From 3872d3214783dfe4b477b7af9be8da4dbda6c52e Mon Sep 17 00:00:00 2001 From: Mendy Landa <54242706+MendyLanda@users.noreply.github.com> Date: Sun, 15 Jun 2025 15:52:51 +0300 Subject: [PATCH 2/4] Update packages/examples/vite/src/examples/font-feature-settings/index.jsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../examples/vite/src/examples/font-feature-settings/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/examples/vite/src/examples/font-feature-settings/index.jsx b/packages/examples/vite/src/examples/font-feature-settings/index.jsx index 42d06d619..97467324c 100644 --- a/packages/examples/vite/src/examples/font-feature-settings/index.jsx +++ b/packages/examples/vite/src/examples/font-feature-settings/index.jsx @@ -17,7 +17,7 @@ const styles = StyleSheet.create({ }, headline: { fontFamily: 'Roboto', - fontSize: '24', + fontSize: 24, paddingVertical: 12, }, rubik: { From 36c48236081b2c4ca66ce1ed089ee1e2225a8a0b Mon Sep 17 00:00:00 2001 From: Mendy Landa Date: Sun, 15 Jun 2025 15:55:07 +0300 Subject: [PATCH 3/4] features lacks a type on Run.attributes --- packages/textkit/src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/textkit/src/types.ts b/packages/textkit/src/types.ts index d3aff1ba3..c41af6e73 100644 --- a/packages/textkit/src/types.ts +++ b/packages/textkit/src/types.ts @@ -51,7 +51,7 @@ export type Attributes = { characterSpacing?: number; color?: string; direction?: 'rtl' | 'ltr'; - features?: unknown[]; + features?: string[] | Record; fill?: boolean; font?: Font[]; fontSize?: number; From 0bfb69ccb2eb6149ee1546ef4c58f6ec2277901b Mon Sep 17 00:00:00 2001 From: Mendy Landa Date: Sun, 15 Jun 2025 16:48:52 +0300 Subject: [PATCH 4/4] types and nitpicks --- .changeset/six-paws-tie.md | 2 +- packages/stylesheet/src/types.ts | 34 +++++++++++++++++++++++++++++++- packages/textkit/src/types.ts | 3 ++- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/.changeset/six-paws-tie.md b/.changeset/six-paws-tie.md index 37be21054..cd8f2536a 100644 --- a/.changeset/six-paws-tie.md +++ b/.changeset/six-paws-tie.md @@ -4,4 +4,4 @@ "@react-pdf/types": minor --- -Add support for fontFeatureSettings to customise ligatures, tabular number display, and other font features +Add support for fontFeatureSettings to customize ligatures, tabular number display, and other font features diff --git a/packages/stylesheet/src/types.ts b/packages/stylesheet/src/types.ts index 4575a1c6d..95aeea848 100644 --- a/packages/stylesheet/src/types.ts +++ b/packages/stylesheet/src/types.ts @@ -321,13 +321,44 @@ export type TextTransform = export type VerticalAlign = 'sub' | 'super'; +export type FontFeatureSetting = + | 'liga' + | 'dlig' + | 'onum' + | 'lnum' + | 'tnum' + | 'zero' + | 'frac' + | 'sups' + | 'subs' + | 'smcp' + | 'c2sc' + | 'case' + | 'hlig' + | 'calt' + | 'swsh' + | 'hist' + | 'ss**' + | 'kern' + | 'locl' + | 'rlig' + | 'medi' + | 'init' + | 'isol' + | 'fina' + | 'mark' + | 'mkmk'; +export type FontFeatureSettings = + | FontFeatureSetting[] + | Record; + export type TextStyle = { direction?: 'ltr' | 'rtl'; fontSize?: number | string; fontFamily?: string | string[]; fontStyle?: FontStyle; fontWeight?: FontWeight; - fontFeatureSettings?: string[] | Record; + fontFeatureSettings?: FontFeatureSettings; letterSpacing?: number | string; lineHeight?: number | string; maxLines?: number; @@ -348,6 +379,7 @@ export type TextSafeStyle = TextExpandedStyle & { fontWeight?: number; letterSpacing?: number; lineHeight?: number; + fontFeatureSettings?: FontFeatureSettings; }; // Margins diff --git a/packages/textkit/src/types.ts b/packages/textkit/src/types.ts index c41af6e73..33d5b54ac 100644 --- a/packages/textkit/src/types.ts +++ b/packages/textkit/src/types.ts @@ -1,5 +1,6 @@ import type { Glyph as FontkitGlyph } from 'fontkit'; import type { Font } from '@rpdf/font'; +import type { FontFeatureSettings } from '@rpdf/stylesheet'; import { Factor as JustificationFactor } from './engines/justification/types'; export type Coordinate = { @@ -51,7 +52,7 @@ export type Attributes = { characterSpacing?: number; color?: string; direction?: 'rtl' | 'ltr'; - features?: string[] | Record; + features?: FontFeatureSettings; fill?: boolean; font?: Font[]; fontSize?: number;