diff --git a/.changeset/olive-walls-breathe.md b/.changeset/olive-walls-breathe.md new file mode 100644 index 000000000..d271eb6df --- /dev/null +++ b/.changeset/olive-walls-breathe.md @@ -0,0 +1,5 @@ +--- +"@react-pdf/pdfkit": minor +--- + +refactor: align mulptiles filesWith upstream diff --git a/packages/pdfkit/src/mixins/annotations.js b/packages/pdfkit/src/mixins/annotations.js index 959d16c0d..ad9b70474 100644 --- a/packages/pdfkit/src/mixins/annotations.js +++ b/packages/pdfkit/src/mixins/annotations.js @@ -1,3 +1,5 @@ +import PDFAnnotationReference from '../structure_annotation'; + export default { annotate(x, y, w, h, options) { options.Type = 'Annot'; @@ -19,6 +21,9 @@ export default { options.Dest = new String(options.Dest); } + const structParent = options.structParent; + delete options.structParent; + // Capitalize keys for (let key in options) { const val = options[key]; @@ -27,6 +32,12 @@ export default { const ref = this.ref(options); this.page.annotations.push(ref); + + if (structParent && typeof structParent.add === 'function') { + const annotRef = new PDFAnnotationReference(ref); + structParent.add(annotRef); + } + ref.end(); return this; }, @@ -47,7 +58,7 @@ export default { options.Subtype = 'Link'; options.A = this.ref({ S: 'GoTo', - D: new String(name) + D: new String(name), }); options.A.end(); return this.annotate(x, y, w, h, options); @@ -62,7 +73,7 @@ export default { if (url >= 0 && url < pages.Kids.length) { options.A = this.ref({ S: 'GoTo', - D: [pages.Kids[url], 'XYZ', null, null, null] + D: [pages.Kids[url], 'XYZ', null, null, null], }); options.A.end(); } else { @@ -72,11 +83,15 @@ export default { // Link to an external url options.A = this.ref({ S: 'URI', - URI: new String(url) + URI: new String(url), }); options.A.end(); } + if (options.structParent && !options.Contents) { + options.Contents = new String(''); + } + return this.annotate(x, y, w, h, options); }, @@ -164,5 +179,5 @@ export default { y2 = m1 * x2 + m3 * y2 + m5; return [x1, y1, x2, y2]; - } + }, }; diff --git a/packages/pdfkit/src/mixins/fonts.js b/packages/pdfkit/src/mixins/fonts.js index b15eafb67..bb2da5909 100644 --- a/packages/pdfkit/src/mixins/fonts.js +++ b/packages/pdfkit/src/mixins/fonts.js @@ -1,24 +1,57 @@ 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 + if ( + font1.font._tables?.head?.checkSumAdjustment !== + font2.font._tables?.head?.checkSumAdjustment + ) { + return false; + } + + // compare font name table + if ( + JSON.stringify(font1.font._tables?.name?.records) !== + JSON.stringify(font2.font._tables?.name?.records) + ) { + return false; + } + + return true; +}; export default { - initFonts() { + 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 - return this.font('Helvetica'); + if (defaultFont) { + this.font(defaultFont, defaultFontFamily); + } }, font(src, family, size) { - let cacheKey; - let font; + let cacheKey, font; if (typeof family === 'number') { size = family; family = null; @@ -35,6 +68,8 @@ export default { } } + this._fontSource = src; + this._fontFamily = family; if (size != null) { this.fontSize(size); } @@ -51,7 +86,10 @@ export default { // check for existing font familes with the same name already in the PDF // useful if the font was passed as a buffer - if ((font = this._fontFamilies[this._font.name])) { + if ( + (font = this._fontFamilies[this._font.name]) && + isEqualFont(this._font, font) + ) { this._font = font; return this; } @@ -61,31 +99,118 @@ export default { this._fontFamilies[cacheKey] = this._font; } - if (this._font.name) { + if (this._font.name && !this._fontFamilies[this._font.name]) { this._fontFamilies[this._font.name] = this._font; } + // if the font wasn't registered under any key (e.g. loaded via raw buffer + // with no cacheKey and a postscript name that collides with an + // already-registered font), register it under its id so it always gets + // finalized and doesn't leave a dangling references + if ( + !cacheKey && + (!this._font.name || this._fontFamilies[this._font.name] !== this._font) + ) { + this._fontFamilies[this._font.id] = this._font; + } + return this; }, fontSize(_fontSize) { - this._fontSize = _fontSize; + this._fontSize = this.sizeToPoint(_fontSize); return this; }, currentLineHeight(includeGap) { - if (includeGap == null) { - includeGap = false; - } return this._font.lineHeight(this._fontSize, includeGap); }, registerFont(name, src, family) { this._registeredFonts[name] = { src, - family + family, }; 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]); + }, }; diff --git a/packages/pdfkit/src/mixins/text.js b/packages/pdfkit/src/mixins/text.js index 0f57f6675..f83d8ba7a 100644 --- a/packages/pdfkit/src/mixins/text.js +++ b/packages/pdfkit/src/mixins/text.js @@ -4,14 +4,31 @@ import { cosine, sine } from '../utils'; const { number } = PDFObject; +/** + * Format a list label based on the list type + * @param {number} n + * @param {'numbered' | 'lettered'} listType + * @returns {string} + */ +function formatListLabel(n, listType) { + if (listType === 'numbered') { + return `${n}.`; + } + + // lettered + var letter = String.fromCharCode(((n - 1) % 26) + 65); + var times = Math.floor((n - 1) / 26 + 1); + var text = Array(times + 1).join(letter); + return `${text}.`; +} + export default { initText() { this._line = this._line.bind(this); - // Current coordinates this.x = 0; this.y = 0; - return (this._lineGap = 0); + this._lineGap = 0; }, lineGap(_lineGap) { @@ -50,8 +67,8 @@ export default { if (options.structParent) { options.structParent.add( this.struct(options.structType || 'P', [ - this.markStructureContent(options.structType || 'P') - ]) + this.markStructureContent(options.structType || 'P'), + ]), ); } }; @@ -113,7 +130,127 @@ export default { * @param options - Any text options (The same you would apply to `doc.text()`) * @returns {{x: number, y: number, width: number, height: number}} */ - boundsOfString(string, x, y, options) {}, + boundsOfString(string, x, y, options) { + options = this._initOptions(x, y, options); + ({ x, y } = this); + const lineGap = options.lineGap ?? this._lineGap ?? 0; + const lineHeight = this.currentLineHeight(true) + lineGap; + let contentWidth = 0; + // Convert text to a string + string = String(string ?? ''); + + // if the wordSpacing option is specified, remove multiple consecutive spaces + if (options.wordSpacing) { + string = string.replace(/\s{2,}/g, ' '); + } + + // word wrapping + if (options.width) { + let wrapper = new LineWrapper(this, options); + wrapper.on('line', (text, options) => { + this.y += lineHeight; + text = text.replace(/\n/g, ''); + + if (text.length) { + // handle options + let wordSpacing = options.wordSpacing ?? 0; + const characterSpacing = options.characterSpacing ?? 0; + + // justify alignments + if (options.width && options.align === 'justify') { + // calculate the word spacing value + const words = text.trim().split(/\s+/); + const textWidth = this.widthOfString( + text.replace(/\s+/g, ''), + options, + ); + const spaceWidth = this.widthOfString(' ') + characterSpacing; + wordSpacing = Math.max( + 0, + (options.lineWidth - textWidth) / Math.max(1, words.length - 1) - + spaceWidth, + ); + } + + // calculate the actual rendered width of the string after word and character spacing + contentWidth = Math.max( + contentWidth, + options.textWidth + + wordSpacing * (options.wordCount - 1) + + characterSpacing * (text.length - 1), + ); + } + }); + wrapper.wrap(string, options); + } else { + // render paragraphs as single lines + for (let line of string.split('\n')) { + const lineWidth = this.widthOfString(line, options); + this.y += lineHeight; + contentWidth = Math.max(contentWidth, lineWidth); + } + } + + let contentHeight = this.y - y; + // Clamp height to max height + if (options.height) contentHeight = Math.min(contentHeight, options.height); + + this.x = x; + this.y = y; + + /** + * Rotates around top left corner + * [x1,y1] > [x2,y2] + * ⌃ ⌄ + * [x4,y4] < [x3,y3] + */ + if (options.rotation === 0) { + // No rotation so we can use the existing values + return { x, y, width: contentWidth, height: contentHeight }; + // Use fast computation without explicit trig + } else if (options.rotation === 90) { + return { + x: x, + y: y - contentWidth, + width: contentHeight, + height: contentWidth, + }; + } else if (options.rotation === 180) { + return { + x: x - contentWidth, + y: y - contentHeight, + width: contentWidth, + height: contentHeight, + }; + } else if (options.rotation === 270) { + return { + x: x - contentHeight, + y: y, + width: contentHeight, + height: contentWidth, + }; + } + + // Non-trivial values so time for trig + const cos = cosine(options.rotation); + const sin = sine(options.rotation); + + const x1 = x; + const y1 = y; + const x2 = x + contentWidth * cos; + const y2 = y - contentWidth * sin; + const x3 = x + contentWidth * cos + contentHeight * sin; + const y3 = y - contentWidth * sin + contentHeight * cos; + const x4 = x + contentHeight * sin; + const y4 = y + contentHeight * cos; + + const xMin = Math.min(x1, x2, x3, x4); + const xMax = Math.max(x1, x2, x3, x4); + const yMin = Math.min(y1, y2, y3, y4); + const yMax = Math.max(y1, y2, y3, y4); + + return { x: xMin, y: yMin, width: xMax - xMin, height: yMax - yMin }; + }, heightOfString(text, options) { const { x, y } = this; @@ -123,7 +260,7 @@ export default { const lineGap = options.lineGap || this._lineGap || 0; this._text(text, this.x, this.y, options, () => { - return (this.y += this.currentLineHeight(true) + lineGap); + this.y += this.currentLineHeight(true) + lineGap; }); const height = this.y - y; @@ -133,7 +270,7 @@ export default { return height; }, - list(list, x, y, options, wrapper) { + list(list, x, y, options) { options = this._initOptions(x, y, options); const listType = options.listType || 'bullet'; @@ -170,20 +307,8 @@ export default { flatten(list); - const label = function (n) { - switch (listType) { - case 'numbered': - return `${n}.`; - case 'lettered': - var letter = String.fromCharCode(((n - 1) % 26) + 65); - var times = Math.floor((n - 1) / 26 + 1); - var text = Array(times + 1).join(letter); - return `${text}.`; - } - }; - const drawListItem = function (listItem, i) { - wrapper = new LineWrapper(this, options); + const wrapper = new LineWrapper(this, options); wrapper.on('line', this._line); level = 1; @@ -215,8 +340,8 @@ export default { if (item && (labelType || bodyType)) { item.add( this.struct(labelType || bodyType, [ - this.markStructureContent(labelType || bodyType) - ]) + this.markStructureContent(labelType || bodyType), + ]), ); } switch (listType) { @@ -226,14 +351,14 @@ export default { break; case 'numbered': case 'lettered': - var text = label(numbers[i - 1]); + var text = formatListLabel(numbers[i - 1], listType); this._fragment(text, this.x - indent, this.y, options); break; } if (item && labelType && bodyType) { item.add( - this.struct(bodyType, [this.markStructureContent(bodyType)]) + this.struct(bodyType, [this.markStructureContent(bodyType)]), ); } if (item && item !== options.structParent) { @@ -244,13 +369,13 @@ export default { wrapper.on('sectionStart', () => { const pos = indent + itemIndent * (level - 1); this.x += pos; - return (wrapper.lineWidth -= pos); + wrapper.lineWidth -= pos; }); wrapper.on('sectionEnd', () => { const pos = indent + itemIndent * (level - 1); this.x -= pos; - return (wrapper.lineWidth += pos); + wrapper.lineWidth += pos; }); wrapper.wrap(listItem, options); @@ -264,7 +389,7 @@ export default { }, _initOptions(x = {}, y, options = {}) { - if (typeof x === 'object') { + if (x && typeof x === 'object') { options = x; x = null; } @@ -308,7 +433,7 @@ export default { } // 1/4 inch // Normalize rotation to between 0 - 360 - result.rotation = Number(options.rotation ?? 0) % 360; + result.rotation = Number(result.rotation ?? 0) % 360; if (result.rotation < 0) result.rotation += 360; return result; @@ -316,12 +441,12 @@ export default { _line(text, options = {}, wrapper) { this._fragment(text, this.x, this.y, options); - const lineGap = options.lineGap || this._lineGap || 0; - if (!wrapper) { - return (this.x += this.widthOfString(text, options)); + if (wrapper) { + const lineGap = options.lineGap || this._lineGap || 0; + this.y += this.currentLineHeight(true) + lineGap; } else { - return (this.y += this.currentLineHeight(true) + lineGap); + this.x += this.widthOfString(text, options); } }, @@ -358,7 +483,7 @@ export default { wordSpacing = Math.max( 0, (options.lineWidth - textWidth) / Math.max(1, words.length - 1) - - spaceWidth + spaceWidth, ); break; } @@ -406,7 +531,21 @@ export default { // create link annotations if the link option is given if (options.link != null) { - this.link(x, y, renderedWidth, this.currentLineHeight(), options.link); + const linkOptions = {}; + if ( + this._currentStructureElement && + this._currentStructureElement.dictionary.data.S === 'Link' + ) { + linkOptions.structParent = this._currentStructureElement; + } + this.link( + x, + y, + renderedWidth, + this.currentLineHeight(), + options.link, + linkOptions, + ); } if (options.goTo != null) { this.goTo(x, y, renderedWidth, this.currentLineHeight(), options.goTo); @@ -514,7 +653,7 @@ export default { for (let word of words) { const [encodedWord, positionsWord] = this._font.encode( word, - options.features + options.features, ); encoded = encoded.concat(encodedWord); positions = positions.concat(positionsWord); @@ -548,7 +687,7 @@ export default { commands.push(`<${hex}> ${number(-advance)}`); } - return (last = cur); + last = cur; }; // Flushes the current TJ commands to the output stream @@ -557,7 +696,7 @@ export default { if (commands.length > 0) { this.addContent(`[${commands.join(' ')}] TJ`); - return (commands.length = 0); + commands.length = 0; } }; @@ -572,8 +711,8 @@ export default { // Move the text position and flush just the current character this.addContent( `1 0 0 1 ${number(x + pos.xOffset * scale)} ${number( - y + pos.yOffset * scale - )} Tm` + y + pos.yOffset * scale, + )} Tm`, ); flush(i + 1); @@ -601,6 +740,6 @@ export default { this.addContent('ET'); // restore flipped coordinate system - return this.restore(); - } + this.restore(); + }, }; diff --git a/packages/pdfkit/src/page.js b/packages/pdfkit/src/page.js index 0ba84043f..9a8b0be39 100644 --- a/packages/pdfkit/src/page.js +++ b/packages/pdfkit/src/page.js @@ -3,6 +3,8 @@ PDFPage - represents a single page in the PDF document By Devon Govett */ +import { normalizeSides } from './utils'; + /** * @type {SideDefinition} */ @@ -10,7 +12,7 @@ const DEFAULT_MARGINS = { top: 72, left: 72, bottom: 72, - right: 72 + right: 72, }; const SIZES = { @@ -63,7 +65,7 @@ const SIZES = { FOLIO: [612.0, 936.0], LEGAL: [612.0, 1008.0], LETTER: [612.0, 792.0], - TABLOID: [792.0, 1224.0] + TABLOID: [792.0, 1224.0], }; class PDFPage { @@ -74,20 +76,6 @@ class PDFPage { this.layout = options.layout || 'portrait'; this.userUnit = options.userUnit || 1.0; - // 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 @@ -100,9 +88,17 @@ class PDFPage { 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'] + ProcSet: ['PDF', 'Text', 'ImageB', 'ImageC', 'ImageI'], }); // The page dictionary @@ -112,7 +108,7 @@ class PDFPage { MediaBox: [0, 0, this.width, this.height], Contents: this.content, Resources: this.resources, - UserUnit: this.userUnit + UserUnit: this.userUnit, }); this.markings = []; @@ -156,6 +152,24 @@ class PDFPage { : (data.StructParents = this.document.createStructParentTreeNextKey()); } + /** + * The width of the safe contents of a page + * + * @returns {number} + */ + get contentWidth() { + return this.width - this.margins.left - this.margins.right; + } + + /** + * The height of the safe contents of a page + * + * @returns {number} + */ + get contentHeight() { + return this.height - this.margins.top - this.margins.bottom; + } + maxY() { return this.height - this.margins.bottom; } diff --git a/packages/pdfkit/src/structure_element.js b/packages/pdfkit/src/structure_element.js index cebbdbdc2..016443793 100644 --- a/packages/pdfkit/src/structure_element.js +++ b/packages/pdfkit/src/structure_element.js @@ -4,6 +4,7 @@ By Ben Schmidt */ import PDFStructureContent from './structure_content'; +import PDFAnnotationReference from './structure_annotation'; class PDFStructureElement { constructor(document, type, options = {}, children = null) { @@ -14,7 +15,7 @@ class PDFStructureElement { this._flushed = false; this.dictionary = document.ref({ // Type: "StructElem", - S: type + S: type, }); const data = this.dictionary.data; @@ -39,6 +40,24 @@ class PDFStructureElement { if (typeof options.actual !== 'undefined') { data.ActualText = new String(options.actual); } + if ( + typeof options.bbox !== 'undefined' || + typeof options.placement !== 'undefined' + ) { + const attrs = { O: 'Layout' }; + attrs.Placement = + typeof options.placement !== 'undefined' ? options.placement : 'Block'; + if (typeof options.bbox !== 'undefined') { + const height = this.document.page.height; + attrs.BBox = [ + options.bbox[0], + height - options.bbox[3], + options.bbox[2], + height - options.bbox[1], + ]; + } + data.A = attrs; + } this._children = []; @@ -71,6 +90,10 @@ class PDFStructureElement { this._addContentToParentTree(child); } + if (child instanceof PDFAnnotationReference) { + this._addAnnotationToParentTree(child.annotationRef); + } + if (typeof child === 'function' && this._attached) { // _contentForClosure() adds the content to the parent tree child = this._contentForClosure(child); @@ -90,6 +113,15 @@ class PDFStructureElement { }); } + _addAnnotationToParentTree(annotRef) { + const parentTreeKey = this.document.createStructParentTreeNextKey(); + + annotRef.data.StructParent = parentTreeKey; + + const parentTree = this.document.getStructParentTree(); + parentTree.add(parentTreeKey, this.dictionary); + } + setParent(parentRef) { if (this.dictionary.data.P) { throw new Error(`Structure element added to more than one parent`); @@ -137,13 +169,25 @@ class PDFStructureElement { return ( child instanceof PDFStructureElement || child instanceof PDFStructureContent || + child instanceof PDFAnnotationReference || typeof child === 'function' ); } _contentForClosure(closure) { const content = this.document.markStructureContent(this.dictionary.data.S); + + const prevStructElement = this.document._currentStructureElement; + this.document._currentStructureElement = this; + + const wasEnded = this._ended; + this._ended = false; + closure(); + + this._ended = wasEnded; + + this.document._currentStructureElement = prevStructElement; this.document.endMarkedContent(); this._addContentToParentTree(content); @@ -204,11 +248,21 @@ class PDFStructureElement { this.dictionary.data.K.push({ Type: 'MCR', Pg: pageRef, - MCID: mcid + MCID: mcid, }); } }); } + + if (child instanceof PDFAnnotationReference) { + const pageRef = this.document.page.dictionary; + const objr = { + Type: 'OBJR', + Obj: child.annotationRef, + Pg: pageRef, + }; + this.dictionary.data.K.push(objr); + } } } diff --git a/packages/pdfkit/src/utils.js b/packages/pdfkit/src/utils.js index 7b825e422..6cab97f70 100644 --- a/packages/pdfkit/src/utils.js +++ b/packages/pdfkit/src/utils.js @@ -1,8 +1,25 @@ +const fArray = new Float32Array(1); +const uArray = new Uint32Array(fArray.buffer); + export function PDFNumber(n) { // PDF numbers are strictly 32bit - // so convert this number to the nearest 32bit number + // so convert this number to a 32bit number // @see ISO 32000-1 Annex C.2 (real numbers) - return Math.fround(n); + const rounded = Math.fround(n); + if (rounded <= n) return rounded; + + // Will have to perform 32bit float truncation + fArray[0] = n; + + // Get the 32-bit representation as integer and shift bits + if (n <= 0) { + uArray[0] += 1; + } else { + uArray[0] -= 1; + } + + // Return the float value + return fArray[0]; } /** @@ -12,11 +29,10 @@ export function PDFNumber(n) { */ /** - * Measurement of how wide something is, false means 0 and true means 1 - * - * @typedef {Size | boolean} Wideness + * @typedef {Array | string | Array} PDFColor */ +/** @typedef {string | Buffer | Uint8Array | ArrayBuffer} PDFFontSource */ /** * Side definitions * - To define all sides, use a single value @@ -49,19 +65,17 @@ export function PDFNumber(n) { export function normalizeSides( sides, defaultDefinition = undefined, - transformer = (v) => v + transformer = (v) => v, ) { if ( - sides === undefined || - sides === null || + sides == null || (typeof sides === 'object' && Object.keys(sides).length === 0) ) { sides = defaultDefinition; } - if (typeof sides !== 'object' || sides === null) { - sides = [sides, sides, sides, sides]; - } - if (Array.isArray(sides)) { + if (sides == null || typeof sides !== 'object') { + sides = { top: sides, right: sides, bottom: sides, left: sides }; + } else if (Array.isArray(sides)) { if (sides.length === 2) { sides = { vertical: sides[0], horizontal: sides[1] }; } else { @@ -69,7 +83,7 @@ export function normalizeSides( top: sides[0], right: sides[1], bottom: sides[2], - left: sides[3] + left: sides[3], }; } } @@ -79,7 +93,7 @@ export function normalizeSides( top: sides.vertical, right: sides.horizontal, bottom: sides.vertical, - left: sides.horizontal + left: sides.horizontal, }; } @@ -87,7 +101,7 @@ export function normalizeSides( top: transformer(sides.top), right: transformer(sides.right), bottom: transformer(sides.bottom), - left: transformer(sides.left) + left: transformer(sides.left), }; }