diff --git a/.changeset/six-paws-tie.md b/.changeset/six-paws-tie.md new file mode 100644 index 000000000..cd8f2536a --- /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 customize 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..97467324c --- /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.ts b/packages/examples/vite/src/examples/index.ts index 5b140e082..581ba1b0e 100644 --- a/packages/examples/vite/src/examples/index.ts +++ b/packages/examples/vite/src/examples/index.ts @@ -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.tsx b/packages/examples/vite/src/index.tsx index 278fbd610..9ef0855a1 100644 --- a/packages/examples/vite/src/index.tsx +++ b/packages/examples/vite/src/index.tsx @@ -1,8 +1,8 @@ import './index.css'; +import { PDFViewer } from '@rpdf/renderer'; import React, { useEffect, useState } from 'react'; import { createRoot } from 'react-dom/client'; -import { PDFViewer } from '@rpdf/renderer'; import EXAMPLES from './examples'; @@ -25,13 +25,13 @@ const ExamplesPage = () => { const { Document } = EXAMPLES[index]; return ( -
+
-
+
diff --git a/packages/layout/src/steps/resolveInheritance.ts b/packages/layout/src/steps/resolveInheritance.ts index 874080153..179d62758 100644 --- a/packages/layout/src/steps/resolveInheritance.ts +++ b/packages/layout/src/steps/resolveInheritance.ts @@ -12,6 +12,7 @@ const BASE_INHERITABLE_PROPERTIES = [ 'fontSize', 'fontStyle', 'fontWeight', + 'fontFeatureSettings', 'letterSpacing', 'opacity', 'textDecoration', diff --git a/packages/layout/src/svg/inheritProps.ts b/packages/layout/src/svg/inheritProps.ts index b41e90a4e..8eaebc010 100644 --- a/packages/layout/src/svg/inheritProps.ts +++ b/packages/layout/src/svg/inheritProps.ts @@ -23,6 +23,7 @@ const BASE_SVG_INHERITED_PROPS = [ 'fontSize', 'fontStyle', 'fontWeight', + 'fontFeatureSettings', 'letterSpacing', 'opacity', 'textDecoration', diff --git a/packages/layout/src/text/getAttributedString.ts b/packages/layout/src/text/getAttributedString.ts index 3678c614c..c155d50dc 100644 --- a/packages/layout/src/text/getAttributedString.ts +++ b/packages/layout/src/text/getAttributedString.ts @@ -47,6 +47,7 @@ const getFragments = ( fontWeight, fontStyle, fontSize = 18, + fontFeatureSettings, textAlign, lineHeight, textDecoration, @@ -100,6 +101,7 @@ const getFragments = ( // @ts-expect-error allow this props access 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.ts b/packages/layout/tests/steps/resolveInhritance.test.ts index 2f5ffbce8..474d58a78 100644 --- a/packages/layout/tests/steps/resolveInhritance.test.ts +++ b/packages/layout/tests/steps/resolveInhritance.test.ts @@ -175,4 +175,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 0e62723d7..95aeea848 100644 --- a/packages/stylesheet/src/types.ts +++ b/packages/stylesheet/src/types.ts @@ -321,12 +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?: FontFeatureSettings; letterSpacing?: number | string; lineHeight?: number | string; maxLines?: number; @@ -347,6 +379,7 @@ export type TextSafeStyle = TextExpandedStyle & { fontWeight?: number; letterSpacing?: number; lineHeight?: number; + fontFeatureSettings?: FontFeatureSettings; }; // Margins diff --git a/packages/textkit/src/layout/generateGlyphs.ts b/packages/textkit/src/layout/generateGlyphs.ts index 461934799..913be4a58 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[0].layout( runString, - undefined, + features, undefined, undefined, 'ltr', diff --git a/packages/textkit/src/types.ts b/packages/textkit/src/types.ts index d3aff1ba3..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?: unknown[]; + features?: FontFeatureSettings; fill?: boolean; font?: Font[]; fontSize?: number;