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 Jan 11, 2025
1 parent 52ed58e commit ea77557
Show file tree
Hide file tree
Showing 8 changed files with 305 additions and 28 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
### Unreleased

- Fix precision rounding issues in LineWrapper
- Add support for dynamic sizing

### [v0.16.0] - 2024-12-29

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
96 changes: 92 additions & 4 deletions lib/mixins/fonts.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import PDFFontFactory from '../font_factory';
import { CM_TO_IN, IN_TO_PT, MM_TO_CM, PC_TO_PT, PX_TO_IN } from '../utils';

const isEqualFont = (font1, font2) => {
// compare font checksum
Expand All @@ -15,20 +16,31 @@ const isEqualFont = (font1, font2) => {
}

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 All @@ -50,6 +62,8 @@ export default {
}
}

this._fontSource = src;
this._fontFamily = family;
if (size != null) {
this.fontSize(size);
}
Expand Down Expand Up @@ -84,7 +98,7 @@ export default {
},

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

Expand All @@ -102,5 +116,79 @@ 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
* @param {number} [percentageWidth] - The value to use for computing size based on `%`
*
* @returns number
*/
sizeToPoint(size, defaultValue = 0, page = this.page, percentageWidth = undefined) {
if (!percentageWidth) percentageWidth = this._fontSize;
if (typeof defaultValue !== 'number')
defaultValue = this.sizeToPoint(defaultValue);
if (size === undefined) return defaultValue;
if (typeof size === 'number') return size;
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 = percentageWidth / 100;
break;
case 'pt':
default:
multiplier = 1;
}

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

import { normalizeSides } from './utils';

/**
* @type {SideDefinition<Size>}
*/
const DEFAULT_MARGINS = {
top: 72,
left: 72,
bottom: 72,
right: 72
right: 72,
};

const SIZES = {
Expand Down Expand Up @@ -69,20 +74,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 +83,17 @@ class PDFPage {

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

if (options.font) document.font(options.font, options.fontFamily);
if (options.fontSize) 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
102 changes: 102 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,105 @@ export function PDFNumber(n) {
// @see ISO 32000-1 Annex C.2 (real numbers)
return Math.fround(n);
}

/**
* 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 S
* @template D
* @template O
* @template {S | D} T
* @param {SideDefinition<S>} sides - The sides to convert
* @param {SideDefinition<D>} defaultDefinition - The value to use when no definition is provided
* @param {function(T): 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 ||
(typeof sides === "object" && Object.keys(sides).length === 0)
) {
sides = defaultDefinition;
}
if (typeof sides !== "object" || sides === null || sides === undefined) {
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
Loading

0 comments on commit ea77557

Please sign in to comment.