diff --git a/.changeset/six-paws-tie.md b/.changeset/six-paws-tie.md
new file mode 100644
index 000000000..b06013f18
--- /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..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.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 32e897ab2..15e6a0d59 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 '@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.ts b/packages/layout/src/steps/resolveInheritance.ts
index 192a9ded7..937591d68 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 0684bce65..2ac618e70 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 bf912dcef..2dda7b40f 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..a0e2196c1 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;
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 eb9395c9c..fe5012f50 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 '@react-pdf/font';
+import type { FontFeatureSettings } from '@react-pdf/stylesheet';
+import type { Glyph as FontkitGlyph } from 'fontkit';
import { Factor as JustificationFactor } from './engines/justification/types';
export type Coordinate = {
@@ -50,7 +51,7 @@ export type Attributes = {
bullet?: unknown;
characterSpacing?: number;
color?: string;
- direction?: 'rtl' | 'ltr';
+ direction?: FontFeatureSettings;
features?: unknown[];
fill?: boolean;
font?: Font[];