Skip to content
Merged
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
12 changes: 7 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,13 @@

### Breaking

- Text height is now based on the OS/2 typographic metrics
(`sTypoAscender` / `sTypoDescender`) instead of the hhea table values.
This results in tighter line spacing for fonts whose hhea values
include extra spacing that was effectively double-counted with the
`lineHeight` multiplier.
- The text height is now correctly based on the OS/2 typographic metrics
(`sTypoAscender` / `sTypoDescender` / `sTypoLineGap`) instead of the
hhea table values.
- The default `lineHeight` multiplier has changed from `1.2` to `1`.
Together with the switch from hhea to OS/2 typographic metrics, these
changes make line spacing follow the font's own vertical metrics
instead of applying a fixed CSS-style multiplier.

## [0.5.6] - 2025-01-19

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ text spans. The following text properties are supported:
`900`. The literal values `normal` and `bold` are also supported.
- `fontSize`: The font size in pt.
- `lineHeight`: The line height as a multiple of the font size (default:
`1.2`).
`1`).
- `color`: The text [color](#colors).
- `link`: Renders the text as a link to the given target. Can be a URL
or an [anchor](#anchors) reference.
Expand Down
1 change: 1 addition & 0 deletions examples/anchors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const def: DocumentDefinition = {
margin: { x: '20mm', y: '0.5cm' },
defaultStyle: {
fontSize: 12,
lineHeight: 1.1,
},
header: columns([text('PDF Maker'), text('Anchors', { textAlign: 'right', width: 'auto' })], {
margin: { x: '20mm', top: '1cm' },
Expand Down
5 changes: 4 additions & 1 deletion src/api/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,10 @@ export type TextProps = {
fontSize?: number;

/**
* The line height as a multiple of the font size. Defaults to `1.2`.
* The line height as a factor of the text height. The text height is
* derived from the font's vertical metrics (ascent, descent, and line
* gap). Values greater than `1` increase the spacing between lines,
* values less than `1` decrease it. Defaults to `1`.
*/
lineHeight?: number;

Expand Down
20 changes: 10 additions & 10 deletions src/layout/layout-columns.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,11 @@ describe('layout-columns', () => {

expect(frame).toEqual({
children: [
expect.objectContaining({ x: 20 + 5, y: 30 + 7, width: 200 - 5 - 6, height: 12 }),
expect.objectContaining({ x: 20 + 200 + 5, y: 30 + 7, width: 200 - 5 - 6, height: 12 }),
expect.objectContaining({ x: 20 + 5, y: 30 + 7, width: 200 - 5 - 6, height: 10 }),
expect.objectContaining({ x: 20 + 200 + 5, y: 30 + 7, width: 200 - 5 - 6, height: 10 }),
],
width: box.width,
height: 12 + 7 + 8,
height: 10 + 7 + 8,
});
});

Expand All @@ -145,8 +145,8 @@ describe('layout-columns', () => {
expect(frame).toEqual({
children: [
expect.objectContaining({ x: 20 + 5, y: 30 + 7, width: 89, height: 25 }),
expect.objectContaining({ x: 20 + 100 + 5, y: 30 + 7, width: 150 - 5 - 6, height: 12 }),
expect.objectContaining({ x: 20 + 250 + 5, y: 30 + 7, width: 150 - 5 - 6, height: 12 }),
expect.objectContaining({ x: 20 + 100 + 5, y: 30 + 7, width: 150 - 5 - 6, height: 10 }),
expect.objectContaining({ x: 20 + 250 + 5, y: 30 + 7, width: 150 - 5 - 6, height: 10 }),
],
width: box.width,
height: 25 + 7 + 8,
Expand All @@ -165,11 +165,11 @@ describe('layout-columns', () => {

expect(frame).toEqual({
children: [
expect.objectContaining({ x: 20 + 5, y: 30 + 7, width: 200 - 5 - 6, height: 12 }),
expect.objectContaining({ x: 20 + 200 + 5, y: 30 + 7, width: 200 - 5 - 6, height: 12 }),
expect.objectContaining({ x: 20 + 5, y: 30 + 7, width: 200 - 5 - 6, height: 10 }),
expect.objectContaining({ x: 20 + 200 + 5, y: 30 + 7, width: 200 - 5 - 6, height: 10 }),
],
width: box.width,
height: 27,
height: 25,
});
});

Expand All @@ -186,8 +186,8 @@ describe('layout-columns', () => {
expect(frame).toEqual({
children: [
expect.objectContaining({ y: box.y, height: 100 }),
expect.objectContaining({ y: box.y + (100 - 12) / 2, height: 12 }),
expect.objectContaining({ y: box.y + 100 - 12, height: 12 }),
expect.objectContaining({ y: box.y + (100 - 10) / 2, height: 10 }),
expect.objectContaining({ y: box.y + 100 - 10, height: 10 }),
],
width: box.width,
height: 100,
Expand Down
16 changes: 8 additions & 8 deletions src/layout/layout-text.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ describe('layout-text', () => {

const { frame } = await layoutTextContent(block, box, ctx);

expect(frame).toEqual(expect.objectContaining({ width: box.width, height: 12 }));
expect(frame).toEqual(expect.objectContaining({ width: box.width, height: 10 }));
});

it('creates frame with intrinsic width for block with autoWidth', async () => {
Expand All @@ -41,7 +41,7 @@ describe('layout-text', () => {

const { frame } = await layoutTextContent(block, box, ctx);

expect(frame).toEqual(expect.objectContaining({ width: 30, height: 12 }));
expect(frame).toEqual(expect.objectContaining({ width: 30, height: 10 }));
});

it('does not include padding in frame height', async () => {
Expand All @@ -51,7 +51,7 @@ describe('layout-text', () => {

const { frame } = await layoutTextContent(block, box, ctx);

expect(frame.height).toEqual(12);
expect(frame.height).toEqual(10);
});

it('includes text baseline', async () => {
Expand All @@ -64,7 +64,7 @@ describe('layout-text', () => {
type: 'text',
rows: [
{
...{ x: 20, y: 30, width: 90, height: 12, baseline: 9 },
...{ x: 20, y: 30, width: 90, height: 10, baseline: 8 },
segments: [expect.objectContaining({ font: defaultFont, fontSize: 10 })],
},
],
Expand All @@ -88,7 +88,7 @@ describe('layout-text', () => {
type: 'text',
rows: [
{
...{ x: 20, y: 30, width: 270, height: 18, baseline: 13.5 },
...{ x: 20, y: 30, width: 270, height: 15, baseline: 12 },
segments: [
expect.objectContaining({ font: defaultFont, fontSize: 5 }),
expect.objectContaining({ font: defaultFont, fontSize: 10 }),
Expand All @@ -109,7 +109,7 @@ describe('layout-text', () => {

expect(frame.objects).toEqual([
{ type: 'text', rows: [expect.objectContaining({ x: 20, y: 30 })] },
{ type: 'link', x: 20, y: 30 + 1, width: 30, height: 10, url: 'test-link' },
{ type: 'link', x: 20, y: 30, width: 30, height: 10, url: 'test-link' },
]);
});

Expand All @@ -125,7 +125,7 @@ describe('layout-text', () => {

expect(frame.objects).toEqual([
{ type: 'text', rows: [expect.objectContaining({ x: 20, y: 30 })] },
{ type: 'link', x: 20, y: 30 + 1, width: 70, height: 10, url: 'test-link' },
{ type: 'link', x: 20, y: 30, width: 70, height: 10, url: 'test-link' },
]);
});

Expand Down Expand Up @@ -189,7 +189,7 @@ describe('layout-text', () => {
expect.objectContaining({
x: margin.right + (box.width - 30) / 2,
width: 30,
height: 12,
height: 10,
}),
],
},
Expand Down
16 changes: 8 additions & 8 deletions src/layout/layout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ describe('layout', () => {
const pages = await layoutPages(def, ctx);

expect(pages[0].content.children).toEqual([
expect.objectContaining({ height: 14 * 1.2 }),
expect.objectContaining({ height: 14 * 1.2 }),
expect.objectContaining({ height: 14 }),
expect.objectContaining({ height: 14 }),
]);
});

Expand Down Expand Up @@ -120,7 +120,7 @@ describe('layout', () => {
x: 20,
y: 20,
width: pageWidth - 40,
height: 12,
height: 10,
}),
);
expect(pages[0].footer).toBeUndefined();
Expand All @@ -141,9 +141,9 @@ describe('layout', () => {
expect(pages[0].footer).toEqual(
expect.objectContaining({
x: 20,
y: pageHeight - 20 - 12,
y: pageHeight - 20 - 10,
width: pageWidth - 40,
height: 12,
height: 10,
}),
);
});
Expand All @@ -165,15 +165,15 @@ describe('layout', () => {
x: 20,
y: 20,
width: pageWidth - 40,
height: 12,
height: 10,
}),
);
expect(pages[0].footer).toEqual(
expect.objectContaining({
x: 20,
y: pageHeight - 20 - 12,
y: pageHeight - 20 - 10,
width: pageWidth - 40,
height: 12,
height: 10,
}),
);
});
Expand Down
2 changes: 1 addition & 1 deletion src/text.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ describe('text', () => {
width: 3 * 18,
height: 18,
fontSize: 18,
lineHeight: 1.2,
lineHeight: 1,
font: normalFont,
}),
]);
Expand Down
6 changes: 2 additions & 4 deletions src/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { Color } from './read/read-color.ts';
import { scriptToOpenTypeTag, segmentByScript } from './script-detection.ts';

const defaultFontSize = 18;
const defaultLineHeight = 1.2;
const defaultLineHeight = 1.0;

export type TextSegment = {
type: 'text' | 'whitespace' | 'newline';
Expand Down Expand Up @@ -280,9 +280,7 @@ function getGlyphRunWidth(glyphs: ShapedGlyph[], fontSize: number): number {
}

function getTextHeight(font: PDFFont, fontSize: number): number {
const ascent = font.ascent;
const descent = font.descent;
const height = ascent - descent;
const height = font.ascent - font.descent + font.lineGap;
return (height * fontSize) / 1000;
}

Expand Down