Skip to content

Commit

Permalink
Add support for dynamic sizing
Browse files Browse the repository at this point in the history
- Enable defining sizes using any units (defaulting to Points)
- This also allows us to define sizes based on the current font context i.e. em's
- The new public `sizeToPoint` method allows users to also interact with these sizes to generate the correct point sizes
  • Loading branch information
hollandjake committed Dec 24, 2024
1 parent 7527b7a commit 632a544
Show file tree
Hide file tree
Showing 8 changed files with 275 additions and 33 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

- Update fontkit to 2.0
- Update linebreak to 1.1
- Add support for dynamic sizing

### [v0.15.2] - 2024-12-15

Expand Down
22 changes: 16 additions & 6 deletions docs/getting_started.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,18 +101,28 @@ Passing a page options object to the `PDFDocument` constructor will
set the default paper size and layout for every page in the document, which is
then overridden by individual options passed to the `addPage` method.

You can set the page margins in two ways. The first is by setting the `margin`
property (singular) to a number, which applies that margin to all edges. The
other way is to set the `margins` property (plural) to an object with `top`,
`bottom`, `left`, and `right` values. The default is a 1 inch (72 point) margin
You can set the page margins in two ways. The first is by setting the `margin` / `margins`
property to a single value, which applies that to all edges. The
other way is to provide an object with `top`, `right`, `bottom`, and `left` values.
By default, using a number this will be in points (the default PDF unit),
however you can provide any of the following units inside a string
and this will be converted for you:
`em`, `in`, `px`, `cm`, `mm`, `pc`, `ex`, `ch`, `rem`, `vw`, `vmin`, `vmax`, `%`, `pt`.
For those which are based on text sizes this will take the size of the font for the page
(excluding `rem` which is always the document root font size)
The default is a 1 inch (72 point) margin
on all sides.

For example:

// Add a 50 point margin on all sides
doc.addPage({
margin: 50});
doc.addPage({ margin: 50 });

// Add a 2 inch margin on all sides
doc.addPage({ margin: '2in' });

// Add a 2em(28pt) margin using the font size
doc.addPage({ fontSize: 14, margin: '2em' });

// Add different margins on each side
doc.addPage({
Expand Down
95 changes: 91 additions & 4 deletions lib/mixins/fonts.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,32 @@
import PDFFontFactory from '../font_factory';
import { CM_TO_IN, IN_TO_PT, MM_TO_CM, PC_TO_PT, PX_TO_IN } from '../utils';

export default {
initFonts(defaultFont = 'Helvetica') {
initFonts(
defaultFont = 'Helvetica',
defaultFontFamily = null,
defaultFontSize = 12
) {
// Lookup table for embedded fonts
this._fontFamilies = {};
this._fontCount = 0;

// Font state
this._fontSize = 12;
// Useful to export the font builder so that someone can create a snapshot of the current state
// (e.g. Reverting back to the previous font)
this._fontSource = defaultFont;
this._fontFamily = defaultFontFamily;
this._fontSize = defaultFontSize;
this._font = null;

// rem size is fixed per document as the document is the root element
this._remSize = defaultFontSize;

this._registeredFonts = {};

// Set the default font
if (defaultFont) {
this.font(defaultFont);
this.font(defaultFont, defaultFontFamily);
}
},

Expand Down Expand Up @@ -70,7 +82,7 @@ export default {
},

fontSize(_fontSize) {
this._fontSize = _fontSize;
this._fontSize = this.sizeToPoint(_fontSize);
return this;
},

Expand All @@ -88,5 +100,80 @@ export default {
};

return this;
},

/**
* Convert a {@link Size} into a point measurement
*
* @param {Size | boolean | undefined} size - The size to convert
* @param {Size | boolean | undefined} defaultValue - The default value when undefined
* @param {PDFPage} page - The page used for computing font sizes
*
* @returns number
*/
sizeToPoint(size, defaultValue = 0, page = this.page) {
if (typeof defaultValue !== 'number')
defaultValue = this.sizeToPoint(defaultValue);
if (size === undefined) return defaultValue;
if (typeof size === 'number') {
if (size > 0) return size;
return defaultValue;
}
if (typeof size === 'boolean') return Number(size);

const match = String(size).match(
/((\d+)?(\.\d+)?)(em|in|px|cm|mm|pc|ex|ch|rem|vw|vh|vmin|vmax|%|pt)?/
);
if (!match) throw new Error(`Unsupported size '${size}'`);
let multiplier;
switch (match[4]) {
case 'em':
multiplier = this._fontSize;
break;
case 'in':
multiplier = IN_TO_PT;
break;
case 'px':
multiplier = PX_TO_IN * IN_TO_PT;
break;
case 'cm':
multiplier = CM_TO_IN * IN_TO_PT;
break;
case 'mm':
multiplier = MM_TO_CM * CM_TO_IN * IN_TO_PT;
break;
case 'pc':
multiplier = PC_TO_PT;
break;
case 'ex':
multiplier = this.currentLineHeight();
break;
case 'ch':
multiplier = this.widthOfString('0');
break;
case 'rem':
multiplier = this._remSize;
break;
case 'vw':
multiplier = page.width / 100;
break;
case 'vh':
multiplier = page.height / 100;
break;
case 'vmin':
multiplier = Math.min(page.width, page.height) / 100;
break;
case 'vmax':
multiplier = Math.max(page.width, page.height) / 100;
break;
case '%':
multiplier = this._fontSize / 100;
break;
case 'pt':
default:
multiplier = 1;
}

return multiplier * Number(match[1]);
}
};
37 changes: 17 additions & 20 deletions lib/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ PDFPage - represents a single page in the PDF document
By Devon Govett
*/

const DEFAULT_MARGINS = {
top: 72,
left: 72,
bottom: 72,
right: 72
};
import { normalizeSides } from './utils';

/**
* @type {SideDefinition<Size>}
*/
const DEFAULT_MARGINS = '1in';

const SIZES = {
'4A0': [4767.87, 6740.79],
Expand Down Expand Up @@ -69,20 +69,6 @@ class PDFPage {
this.size = options.size || 'letter';
this.layout = options.layout || 'portrait';

// process margins
if (typeof options.margin === 'number') {
this.margins = {
top: options.margin,
left: options.margin,
bottom: options.margin,
right: options.margin
};

// default to 1 inch margins
} else {
this.margins = options.margins || DEFAULT_MARGINS;
}

// calculate page dimensions
const dimensions = Array.isArray(this.size)
? this.size
Expand All @@ -92,6 +78,17 @@ class PDFPage {

this.content = this.document.ref();

if (options.font !== undefined) document.font(options.font, options.fontFamily);
if (options.fontSize !== undefined) document.fontSize(options.fontSize);

// process margins
// Margin calculation must occur after font assignment to ensure any dynamic sizes are calculated correctly
this.margins = normalizeSides(
options.margin ?? options.margins,
DEFAULT_MARGINS,
x => document.sizeToPoint(x, 0, this)
)

// Initialize the Font, XObject, and ExtGState dictionaries
this.resources = this.document.ref({
ProcSet: ['PDF', 'Text', 'ImageB', 'ImageC', 'ImageI']
Expand Down
73 changes: 73 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* Measurement of size
*
* @typedef {number | `${number}` | `${number}${'em' | 'in' | 'px' | 'cm' | 'mm' | 'pc' | 'ex' | 'ch' | 'rem' | 'vw' | 'vmin' | 'vmax' | '%' | 'pt'}`} Size
*/

/**
* Measurement of how wide something is, false means 0 and true means 1
*
* @typedef {Size | boolean} Wideness
*/

/**
* Side definitions
* - To define all sides, use a single value
* - To define up-down left-right, use a `[Y, X]` array
* - To define each side, use `[top, right, bottom, left]` array
* - Or `{vertical: SideValue, horizontal: SideValue}`
* - Or `{top: SideValue, right: SideValue, bottom: SideValue, left: SideValue}`
*
* @template T
* @typedef {T| [T, T]| [T, T, T, T]| { vertical: T; horizontal: T }| { top: T; right: T; bottom: T; left: T }} SideDefinition<T>
**/

/**
* @template T
* @typedef {{ top: T; right: T; bottom: T; left: T }} ExpandedSideDefinition<T>
*/

/**
* Convert any side definition into a static structure
*
* @template T
* @template D
* @template O
* @param {SideDefinition<T>} sides The sides to convert
* @param {SideDefinition<D>} defaultDefinition The value to use when no definition is provided
* @param {(v: T | D) => O} transformer The transformation to apply to the sides once normalized
* @returns {ExpandedSideDefinition<O>}
*/
export function normalizeSides(
sides,
defaultDefinition = undefined,
transformer = v => v
) {
if (sides === undefined) sides = defaultDefinition;
if (typeof sides !== 'object' || sides === null) sides = [sides, sides, sides, sides];
if (Array.isArray(sides)) {
if (sides.length === 2) sides = { vertical: sides[0], horizontal: sides[1] };
else sides = { top: sides[0], right: sides[1], bottom: sides[2], left: sides[3] };
}

if ('vertical' in sides && 'horizontal' in sides) {
sides = { top: sides.vertical, right: sides.horizontal, bottom: sides.vertical, left: sides.horizontal };
}

if (!('top' in sides && 'right' in sides && 'bottom' in sides && 'left' in sides)) {
sides = { top: sides, right: sides, bottom: sides, left: sides };
}

return {
top: transformer(sides.top),
right: transformer(sides.right),
bottom: transformer(sides.bottom),
left: transformer(sides.left)
};
}

export const MM_TO_CM = 1 / 10; // 1MM = 1CM
export const CM_TO_IN = 1 / 2.54; // 1CM = 1/2.54 IN
export const PX_TO_IN = 1 / 96; // 1 PX = 1/96 IN
export const IN_TO_PT = 72; // 1 IN = 72 PT
export const PC_TO_PT = 12; // 1 PC = 12 PT
4 changes: 2 additions & 2 deletions tests/unit/document.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ describe('PDFDocument', () => {
test('not defined', () => {
new PDFDocument();

expect(fontSpy).toBeCalledWith('Helvetica');
expect(fontSpy).toBeCalledWith('Helvetica', null);
});

test('a string value', () => {
new PDFDocument({ font: 'Roboto' });

expect(fontSpy).toBeCalledWith('Roboto');
expect(fontSpy).toBeCalledWith('Roboto', null);
});

test('a falsy value', () => {
Expand Down
43 changes: 42 additions & 1 deletion tests/unit/font.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ describe('EmbeddedFont', () => {
});
});

describe.only('toUnicodeMap', () => {
describe('toUnicodeMap', () => {
test('bfrange lines should not cross highcode boundary', () => {
const doc = new PDFDocument({ compress: false });
const font = PDFFontFactory.open(
Expand Down Expand Up @@ -96,3 +96,44 @@ describe('EmbeddedFont', () => {
});
});
});

describe('sizeToPoint', () => {
let doc;
beforeEach(() => {
doc = new PDFDocument({
font: 'Helvetica',
fontSize: 12,
size: [250, 500],
margin: { top: 10, right: 5, bottom: 10, left: 5 },
});
});

test.each([
[1, 1],
['1', 1],
[true, 1],
[false, 0],
['1em', 12],
['1in', 72],
['1px', 0.75],
['1cm', 28.3465],
['1mm', 2.8346],
['1pc', 12],
['1ex', 11.1],
['1ch', 6.672],
['1vw', 2.5],
['1vh', 5],
['1vmin', 2.5],
['1vmax', 5],
['1%', 0.12],
['1pt', 1],
])('%o -> %s', (size, expected) => {
expect(doc.sizeToPoint(size)).toBeCloseTo(expected, 4);
});

test('1rem -> 12', () => {
doc.fontSize(15);
expect(doc.sizeToPoint('1em')).toEqual(15);
expect(doc.sizeToPoint('1rem')).toEqual(12);
});
});
Loading

0 comments on commit 632a544

Please sign in to comment.