Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .changeset/vertical-text-cjk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
"@react-pdf/textkit": minor
"@react-pdf/stylesheet": minor
"@react-pdf/font": minor
"@react-pdf/layout": minor
"@react-pdf/render": minor
"@react-pdf/renderer": minor
---

feat: add vertical writing mode and built-in CJK font support

### Vertical Writing Mode

Added `writingMode` style property for `<Text>` elements, supporting vertical top-to-bottom text layout commonly used in CJK (Chinese, Japanese, Korean) typography.

```jsx
<Text style={{ writingMode: 'vertical-rl', fontSize: 24 }}>
모든 사람은 의견의 자유와 표현의 자유에 대한 권리를 가진다.
</Text>
```

Supported values:
- `horizontal-tb` (default) — standard left-to-right, top-to-bottom
- `vertical-rl` — top-to-bottom, columns from right to left
- `vertical-lr` — top-to-bottom, columns from left to right

### Built-in CJK Fonts

CJK fonts (Noto Sans) are now registered automatically and lazily loaded from Google Fonts. No manual `Font.register()` needed.

```jsx
import { CJK } from '@react-pdf/renderer';

// Explicit font selection
<Text style={{ fontFamily: CJK.KOREAN }}>한국어 텍스트</Text>

// Or just use CJK text — fonts are auto-detected and loaded
<Text>日本語テキスト</Text>
```

Available fonts: `CJK.CHINESE_SIMPLIFIED`, `CJK.CHINESE_TRADITIONAL`, `CJK.JAPANESE`, `CJK.KOREAN`.

### Font IntelliSense

`fontFamily` now provides autocomplete for all built-in fonts (Helvetica, Courier, Times-Roman, and CJK Noto Sans families) while still accepting custom registered font names.
46 changes: 46 additions & 0 deletions packages/examples/vite/src/examples/font-family-fallback/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Text,
StyleSheet,
Font,
CJK,
} from '@react-pdf/renderer';

import RobotoFont from '../../../public/Roboto-Regular.ttf';
Expand Down Expand Up @@ -126,6 +127,51 @@ const MyDoc = () => {
<Text style={{ fontFamily: 'Courier' }}>Courier</Text>
</Text>
</View>

<View style={styles.card}>
<Text style={styles.cardLabel}>
Built-in CJK — Korean (auto-registered)
</Text>
<Text style={{ fontFamily: CJK.KOREAN, fontSize: 14 }}>
한국어 텍스트
</Text>
</View>

<View style={styles.card}>
<Text style={styles.cardLabel}>
Built-in CJK — Japanese (auto-registered)
</Text>
<Text style={{ fontFamily: CJK.JAPANESE, fontSize: 14 }}>
日本語テキスト — フォント登録不要
</Text>
</View>

<View style={styles.card}>
<Text style={styles.cardLabel}>
Built-in CJK — Simplified Chinese (auto-registered)
</Text>
<Text style={{ fontFamily: CJK.CHINESE_SIMPLIFIED, fontSize: 14 }}>
简体中文文本 — 无需字体注册
</Text>
</View>

<View style={styles.card}>
<Text style={styles.cardLabel}>
CJK auto-detection — no fontFamily specified
</Text>
<Text style={{ fontSize: 14 }}>
Mixed: Hello 한국어 日本語 中文 world
</Text>
</View>

<View style={styles.card}>
<Text style={styles.cardLabel}>
Roboto + CJK fallback (mixed scripts)
</Text>
<Text style={{ fontFamily: ['Roboto', CJK.KOREAN], fontSize: 14 }}>
Roboto Latin with 한국어 fallback
</Text>
</View>
</Page>
);
};
Expand Down
2 changes: 2 additions & 0 deletions packages/examples/vite/src/examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import responsiveImages from './responsive-images';
import math from './math';
import passwordProtection from './password-protection';
import softHyphens from './soft-hyphens';
import verticalText from './vertical-text';

const EXAMPLES = [
scripts,
Expand Down Expand Up @@ -54,6 +55,7 @@ const EXAMPLES = [
math,
passwordProtection,
softHyphens,
verticalText,
];

export default EXAMPLES;
198 changes: 198 additions & 0 deletions packages/examples/vite/src/examples/vertical-text/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import React from 'react';
import {
Document,
Page,
View,
Text,
StyleSheet,
CJK,
} from '@react-pdf/renderer';

const styles = StyleSheet.create({
body: {
padding: 40,
backgroundColor: '#fafafa',
},
title: {
fontSize: 18,
fontWeight: 'bold',
color: '#1a1a1a',
},
subtitle: {
fontSize: 9,
color: '#888',
marginBottom: 20,
},
sectionTitle: {
fontSize: 12,
fontWeight: 'bold',
color: '#444',
marginTop: 16,
marginBottom: 8,
},
row: {
flexDirection: 'row',
gap: 12,
marginBottom: 12,
},
card: {
backgroundColor: '#ffffff',
borderRadius: 5,
padding: 12,
borderWidth: 1,
borderColor: '#e8e8e8',
flex: 1,
},
cardLabel: {
fontSize: 8,
color: '#999',
textTransform: 'uppercase',
letterSpacing: 0.5,
marginBottom: 8,
},
});

const VerticalText = () => {
return (
<Document>
<Page style={styles.body}>
<Text style={styles.title}>Vertical Writing Mode</Text>
<Text style={styles.subtitle}>
Demonstrating writingMode: vertical-rl and vertical-lr for CJK text
</Text>

<Text style={styles.sectionTitle}>
vertical-rl (right to left columns)
</Text>

<View style={styles.row}>
<View style={styles.card}>
<Text style={styles.cardLabel}>Korean</Text>
<Text
style={{
writingMode: 'vertical-rl',
fontFamily: CJK.KOREAN,
fontSize: 16,
height: 200,
}}
>
모든 사람은 의견의 자유와 표현의 자유에 대한 권리를 가진다.
</Text>
</View>

<View style={styles.card}>
<Text style={styles.cardLabel}>Japanese</Text>
<Text
style={{
writingMode: 'vertical-rl',
fontFamily: CJK.JAPANESE,
fontSize: 16,
height: 200,
}}
>
すべての人間は、生まれながらにして自由であり、かつ尊厳と権。
</Text>
</View>
</View>

<View style={styles.row}>
<View style={styles.card}>
<Text style={styles.cardLabel}>Simplified Chinese</Text>
<Text
style={{
writingMode: 'vertical-rl',
fontFamily: CJK.CHINESE_SIMPLIFIED,
fontSize: 16,
height: 200,
}}
>
人人生而自由,在尊严和权利。
</Text>
</View>

<View style={styles.card}>
<Text style={styles.cardLabel}>Traditional Chinese</Text>
<Text
style={{
writingMode: 'vertical-rl',
fontFamily: CJK.CHINESE_TRADITIONAL,
fontSize: 16,
height: 200,
}}
>
人人生而自由,在尊嚴和權利。
</Text>
</View>
</View>
</Page>

<Page style={styles.body}>
<Text style={styles.title}>Vertical Writing Mode</Text>
<Text style={styles.subtitle}>
Demonstrating writingMode: vertical-rl and vertical-lr for CJK text
</Text>

<Text style={styles.sectionTitle}>
vertical-lr (left to right columns)
</Text>

<View style={styles.row}>
<View style={styles.card}>
<Text style={styles.cardLabel}>Japanese</Text>
<Text
style={{
writingMode: 'vertical-lr',
fontFamily: CJK.JAPANESE,
fontSize: 14,
height: 160,
}}
>
すべての人間は、生まれながらにして自由であり、かつ尊厳と権。
</Text>
</View>

<View style={styles.card}>
<Text style={styles.cardLabel}>Latin text</Text>
<Text
style={{
writingMode: 'vertical-lr',
fontSize: 14,
height: 80,
}}
>
Lorem ipsum dolor sit amet consectetur adipisicing elit.
Aspernatur repellat minima itaque.
</Text>
</View>
</View>

<Text style={styles.sectionTitle}>
Auto-detected CJK (no fontFamily)
</Text>

<View style={styles.card}>
<Text style={styles.cardLabel}>
Mixed CJK — fonts detected automatically
</Text>
<Text
style={{
writingMode: 'vertical-rl',
fontSize: 14,
height: 180,
}}
>
한국어 日本語 中文 mixed text 人人生而自由,在尊嚴和權利。 모든
사람은 의견의 자유와 표현의 자유에 대한 권리를 가진다.
</Text>
</View>
</Page>
</Document>
);
};

export default {
id: 'vertical-text',
name: 'Vertical Text',
description: '',
Document: VerticalText,
};
62 changes: 62 additions & 0 deletions packages/font/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,57 @@ import {
SingleLoad,
} from './types';

/**
* Built-in CJK font family names.
* These are registered automatically and fetched lazily from Google Fonts.
*
* @example
* import { CJK } from '@react-pdf/font';
* // or: import { CJK } from '@react-pdf/renderer';
* <Text style={{ fontFamily: CJK.KOREAN }}>한국어</Text>
*/
export const CJK = {
/** Simplified Chinese (Mainland China) */
CHINESE_SIMPLIFIED: 'Noto Sans SC',
/** Traditional Chinese (Taiwan, Macau) */
CHINESE_TRADITIONAL: 'Noto Sans TC',
/** Japanese (Hiragana, Katakana, Kanji) */
JAPANESE: 'Noto Sans JP',
/** Korean (Hangul, Hanja) */
KOREAN: 'Noto Sans KR',
} as const;

/** @deprecated Use `CJK` instead */
export const CJK_FONT_FAMILIES = CJK;

/** All CJK font family names as an array */
export const CJK_FONT_NAMES: string[] = Object.values(CJK);

// Google Fonts CSS API base – resolved to static .ttf URLs at registration time.
// Using the CSS2 API with TTF format ensures stable, versioned font file URLs.
const CJK_FONT_SOURCES: Record<string, { normal: string; bold: string }> = {
[CJK.CHINESE_SIMPLIFIED]: {
normal:
'https://fonts.gstatic.com/s/notosanssc/v40/k3kCo84MPvpLmixcA63oeAL7Iqp5IZJF9bmaG9_FnYw.ttf',
bold: 'https://fonts.gstatic.com/s/notosanssc/v40/k3kCo84MPvpLmixcA63oeAL7Iqp5IZJF9bmaGzjCnYw.ttf',
},
[CJK.CHINESE_TRADITIONAL]: {
normal:
'https://fonts.gstatic.com/s/notosanstc/v39/-nFuOG829Oofr2wohFbTp9ifNAn722rq0MXz76Cy_Co.ttf',
bold: 'https://fonts.gstatic.com/s/notosanstc/v39/-nFuOG829Oofr2wohFbTp9ifNAn722rq0MXz70e1_Co.ttf',
},
[CJK.JAPANESE]: {
normal:
'https://fonts.gstatic.com/s/notosansjp/v56/-F6jfjtqLzI2JPCgQBnw7HFyzSD-AsregP8VFBEj75s.ttf',
bold: 'https://fonts.gstatic.com/s/notosansjp/v56/-F6jfjtqLzI2JPCgQBnw7HFyzSD-AsregP8VFPYk75s.ttf',
},
[CJK.KOREAN]: {
normal:
'https://fonts.gstatic.com/s/notosanskr/v39/PbyxFmXiEBPT4ITbgNA5Cgms3VYcOA-vvnIzzuoyeLQ.ttf',
bold: 'https://fonts.gstatic.com/s/notosanskr/v39/PbyxFmXiEBPT4ITbgNA5Cgms3VYcOA-vvnIzzg01eLQ.ttf',
},
};

class FontStore {
fontFamilies: Record<string, FontFamily> = {};

Expand Down Expand Up @@ -91,6 +142,17 @@ class FontStore {
src: 'Times-BoldItalic',
});

// Register built-in CJK fonts (lazy-loaded from Google Fonts CDN)
for (const [family, urls] of Object.entries(CJK_FONT_SOURCES)) {
this.register({
family,
fonts: [
{ src: urls.normal, fontStyle: 'normal', fontWeight: 400 },
{ src: urls.bold, fontStyle: 'normal', fontWeight: 700 },
],
});
}

// Load default fonts

this.load({
Expand Down
Loading