Skip to content

Feature/cjk vertical pdf renderer#3368

Open
Nando-Cardoso wants to merge 14 commits intodiegomura:masterfrom
Nando-Cardoso:feature/CJK-vertical-pdf-renderer
Open

Feature/cjk vertical pdf renderer#3368
Nando-Cardoso wants to merge 14 commits intodiegomura:masterfrom
Nando-Cardoso:feature/CJK-vertical-pdf-renderer

Conversation

@Nando-Cardoso
Copy link
Copy Markdown

@Nando-Cardoso Nando-Cardoso commented Apr 12, 2026

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

Summary

This PR adds vertical text layout support (writingMode: 'vertical-rl' | 'vertical-lr') and built-in CJK (Chinese, Japanese, Korean) font families that are auto-registered and lazily loaded from Google Fonts.

Motivation

CJK typography traditionally uses vertical writing (top-to-bottom, columns right-to-left). Users currently have no way to render vertical text in react-pdf, and using CJK fonts requires manual Font.register() with external URLs. This PR addresses both needs as a single cohesive feature.

Screenshots

Page 1 — vertical-rl Page 2 — vertical-lr & auto-detection
![vertical-rl example]Screenshot 2026-04-12 100943 ![vertical-lr example]Screenshot 2026-04-12 100956

Usage

Vertical Text

<Text style={{ writingMode: 'vertical-rl', fontSize: 24, width: 500, height: 300 }}>
  모든 사람은 의견의 자유와 표현의 자유에 대한 권리를 가진다.
</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

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

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

// Auto-detection — CJK fonts are loaded automatically when CJK characters are detected
<Text>日本語テキスト</Text>

Available: 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 via the (string & {}) pattern.

Changes by Package

@react-pdf/stylesheet

  • Add WritingMode type and writingMode property to TextStyle
  • Add BuiltInFontFamily / FontFamily types for IntelliSense autocomplete
  • Add writingMode handler in style resolver
  • Add tests

@react-pdf/textkit

  • Add writingMode to Attributes type

@react-pdf/font

  • Add CJK constant with font family names
  • Add CJK_FONT_NAMES array and CJK_FONT_SOURCES with Google Fonts URLs
  • Auto-register CJK fonts in FontStore constructor (lazy-loaded)
  • Export CJK, CJK_FONT_FAMILIES (deprecated alias), CJK_FONT_NAMES

@react-pdf/layout

  • Add vertical text layout: swap container dimensions, transform line boxes back to vertical columns
  • Add verticalColumnHeight() using per-glyph Math.max(xAdvance, fontSize)
  • Use 999999 instead of Infinity to prevent NaN from IEEE 754 arithmetic
  • Update linesHeight / linesWidth / measureText for vertical dimension semantics
  • Add CJK codepoint detection (containsCJK, getCJKFallbackFontFamilies)
  • Add CJK fonts as fallbacks in attributed string font resolution
  • Auto-detect and preload CJK fonts when CJK characters found in text content
  • Fix pagination: pass contentArea to splitText and force forward progress for oversized lines
  • Add tests for vertical layout, linesHeight, linesWidth

@react-pdf/render

  • Add renderGlyphsVertical() for top-to-bottom glyph positioning with centering
  • Update renderRun, renderLine, renderText for vertical mode
  • Support vertical background rendering for text runs

@react-pdf/renderer

  • Export CJK and CJK_FONT_FAMILIES from the main entry point
  • Add TypeScript declarations with JSDoc

Tests

  • 105 tests across changed packages (all passing)
  • 1046 total tests across all packages (all passing)
  • Tests cover: writingMode style resolution, vertical layout dimension swapping, linesHeight/linesWidth for vertical, empty text handling

Commits

  1. feat(stylesheet): add writingMode style property and built-in font types
  2. feat(font): add built-in CJK font families with auto-registration
  3. feat(layout): add vertical text layout with CJK auto-detection
  4. fix(layout): prevent infinite pagination loop with oversized text
  5. feat(render): add vertical text rendering
  6. feat(renderer): export CJK font families API
  7. chore: add changeset for vertical writing mode and CJK fonts

- Add WritingMode type ('horizontal-tb' | 'vertical-rl' | 'vertical-lr')
- Add BuiltInFontFamily type for IntelliSense autocomplete of built-in fonts
- Add FontFamily type using (string & {}) pattern for custom + built-in fonts
- Add writingMode to TextStyle and textkit Attributes types
- Add writingMode handler in stylesheet text resolver
- Add tests for writingMode style resolution
- Add CJK constant with CHINESE_SIMPLIFIED, CHINESE_TRADITIONAL, JAPANESE, KOREAN
- Add CJK_FONT_FAMILIES as deprecated alias for backward compatibility
- Add CJK_FONT_NAMES array of all CJK font family names
- Register CJK fonts (Noto Sans SC/TC/JP/KR) in FontStore constructor
- Fonts are lazily loaded from Google Fonts CDN with normal and bold weights
- Add vertical writing mode support in text layout engine
  - Swap container width/height for vertical text (characters flow top-to-bottom)
  - Use 999999 instead of Infinity to avoid NaN from IEEE 754 arithmetic
  - Transform horizontal line boxes back to vertical column boxes
  - Compute column height using per-glyph Math.max(xAdvance, fontSize)
- Update linesHeight/linesWidth for vertical mode dimension semantics
- Update measureText to handle vertical text measurement correctly
- Add CJK codepoint detection utilities (containsCJK, getCJKFallbackFontFamilies)
- Add CJK fonts as fallbacks in attributed string font resolution
- Auto-detect and preload CJK fonts when CJK characters found in text
- Add tests for vertical layout, linesHeight, and linesWidth
- Pass contentArea to splitText (was previously only passed to splitView)
- Force at least one line onto current page when first line exceeds
  contentArea, preventing endless push-to-next-page loop
- Return [current, null] when no lines remain for next page, stopping
  pagination when all text content has been consumed
- Add renderGlyphsVertical for top-to-bottom glyph positioning
  - Center glyphs horizontally within column using advanceWidth
  - Use Math.max(xAdvance, fontSize) per glyph to prevent overlap
  - Return total advance for inter-run CTM advancement
- Update renderRun to handle vertical mode with CTM translation
- Update renderLine to position at column (x, y) without lineAscent offset
- Update renderText to detect writingMode and adjust initial translation
- Support vertical background rendering for text runs
- Export CJK and CJK_FONT_FAMILIES from renderer package
- Add TypeScript declarations for CJK constant with JSDoc
- Add deprecated CJK_FONT_FAMILIES alias in type declarations
Copilot AI review requested due to automatic review settings April 12, 2026 07:32
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 12, 2026

🦋 Changeset detected

Latest commit: dd510d6

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 11 packages
Name Type
@react-pdf/textkit Minor
@react-pdf/stylesheet Minor
@react-pdf/font Minor
@react-pdf/layout Minor
@react-pdf/render Minor
@react-pdf/renderer Minor
@react-pdf/types Patch
@react-pdf/math Major
next-14 Patch
next-15 Patch
@react-pdf/vite-example Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds vertical writing mode support for <Text> (writingMode: horizontal-tb | vertical-rl | vertical-lr) and introduces built-in CJK Noto Sans font families that are auto-registered and can be auto-loaded when CJK text is detected.

Changes:

  • Extend style/type systems to support writingMode and built-in font-family IntelliSense.
  • Implement vertical text layout + measurement adjustments (line/column transforms, pagination safeguards).
  • Add CJK font constants/registration and CJK detection + preloading during asset resolution; update rendering to support vertical glyph placement.

Reviewed changes

Copilot reviewed 20 out of 21 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
packages/textkit/src/types.ts Adds writingMode to textkit Attributes.
packages/stylesheet/tests/text.test.ts Adds tests for writingMode style resolution.
packages/stylesheet/src/types.ts Introduces WritingMode, built-in font family unions, and updates TextStyle.fontFamily typing.
packages/stylesheet/src/resolve/text.ts Adds writingMode handler in text style resolver.
packages/renderer/src/index.js Re-exports CJK and deprecated alias from renderer entry.
packages/renderer/index.d.ts Adds TS declarations/JSDoc for CJK and CJK_FONT_FAMILIES.
packages/render/src/primitives/renderText.ts Adds vertical glyph rendering path and vertical-aware line/run rendering hooks.
packages/layout/tests/text/linesWidth.test.ts Adds tests for linesWidth behavior in vertical vs horizontal modes.
packages/layout/tests/text/linesHeight.test.ts Adds tests for linesHeight behavior in vertical vs horizontal modes.
packages/layout/tests/text/layoutText.test.ts Adds basic tests for vertical layout behavior and empty text.
packages/layout/src/text/splitText.ts Adds contentArea parameter and safeguards against infinite pagination loops.
packages/layout/src/text/measureText.ts Adds vertical-writing measure semantics for Yoga.
packages/layout/src/text/linesWidth.ts Updates width calculation semantics for vertical columns.
packages/layout/src/text/linesHeight.ts Updates height calculation semantics for vertical columns.
packages/layout/src/text/layoutText.ts Swaps container dimensions for vertical layout and transforms line boxes into vertical columns.
packages/layout/src/text/getAttributedString.ts Adds CJK fallback font families and passes writingMode down to textkit attributes.
packages/layout/src/text/cjk.ts Adds CJK codepoint detection + fallback font family accessor.
packages/layout/src/steps/resolvePagination.ts Passes contentArea into splitText for pagination safety.
packages/layout/src/steps/resolveAssets.ts Detects CJK text content and preloads built-in CJK fonts.
packages/font/src/index.ts Defines/exports CJK, registers CJK fonts (lazy-loaded from Google Fonts), and exports names list.
.changeset/vertical-text-cjk.md Changeset documenting vertical writing mode + built-in CJK fonts.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/render/src/primitives/renderText.ts
Comment thread packages/render/src/primitives/renderText.ts
Comment thread packages/layout/src/text/layoutText.ts Outdated
Comment thread packages/layout/src/text/getAttributedString.ts
Comment thread packages/layout/src/text/measureText.ts Outdated
Comment thread packages/render/src/primitives/renderText.ts
Nando-Cardoso and others added 7 commits April 12, 2026 09:51
- Add 'Vertical Text' example tab with vertical-rl, vertical-lr, and
  auto-detected CJK demonstrations across Korean, Japanese, and Chinese
- Add CJK font examples to 'Font Family Fallback' tab showing built-in
  CJK fonts, auto-detection, and mixed-script fallback chains
…yphsVertical

Position offsets (xOffset, yOffset) are in font units (1000-em), not points.
Apply the same scale factor used by horizontal renderGlyphs so kerning and
ligature offsets are correctly converted to points in vertical mode.
…olumn

Background was painted as (0, 0, column.width, column.height) for every run,
overpainting subsequent runs and blank space. Now each run's background height
is the sum of Math.max(pos.xAdvance, fontSize) for its glyphs — exactly the
vertical space that run occupies before the CTM advances.
…cal-rl

columnWidth was read once from lines[0].box.height and reused for all columns
when computing the x position in vertical-rl mode. Mixed font sizes or line
heights produce columns of different widths, causing incorrect x placement for
all columns after the first. Now each column reads its own box.height.
…t user fonts

The blanket try/catch in getAttributedString silently swallowed errors for
all font families including user-specified ones, masking registration issues
and silently falling back to Helvetica. Now only the internally-injected CJK
fallback families (which may not be loaded yet) are wrapped in try/catch.
User-specified fontFamily values preserve the original throwing behavior,
consistent with how svg/layoutText.ts handles font resolution.
rewrite comments on vertical-mode to reflect current vertical width semantics.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…al mode

Link hit-boxes were using horizontal run metrics (0, -height-descent, xAdvance, height)
even in vertical mode, so annotations would not align with rendered text.
In vertical mode the run occupies (0, 0, columnWidth, runAdvance) relative to the
current CTM — x:0 is the column left edge, y:0 is the top of the run before the
CTM translate, width is fontSize (one em = column width), and height is the
pre-computed vertical advance from calcVerticalRunAdvance.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants