diff --git a/src/core/evaluator.js b/src/core/evaluator.js index 58580855e..256a50626 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -72,6 +72,7 @@ import { BaseStream } from "./base_stream.js"; import { bidi } from "./bidi.js"; import { ColorSpace } from "./colorspace.js"; import { ColorSpaceUtils } from "./colorspace_utils.js"; +import { FontInfo } from "../shared/obj-bin-transform.js"; import { getFontSubstitution } from "./font_substitutions.js"; import { getGlyphsUnicode } from "./glyphlist.js"; import { getMetrics } from "./metrics.js"; @@ -4709,12 +4710,21 @@ class TranslatedFont { return; } this.#sent = true; - - handler.send("commonobj", [ - this.loadedName, - "Font", - this.font.exportData(), - ]); + const fontData = this.font.exportData(); + const transfer = []; + if (fontData.data) { + if (fontData.data.charProcOperatorList) { + fontData.charProcOperatorList = fontData.data.charProcOperatorList; + } + fontData.data = FontInfo.write(fontData.data); + transfer.push(fontData.data); + } + handler.send("commonobj", [this.loadedName, "Font", fontData], transfer); + // future path: switch to a SharedArrayBuffer + // const sab = new SharedArrayBuffer(data.byteLength); + // const view = new Uint8Array(sab); + // view.set(new Uint8Array(data)); + // handler.send("commonobj", [this.loadedName, "Font", sab]); } fallback(handler, evaluatorOptions) { diff --git a/src/core/fonts.js b/src/core/fonts.js index bfb9943c6..0450e1c67 100644 --- a/src/core/fonts.js +++ b/src/core/fonts.js @@ -1144,19 +1144,27 @@ class Font { } exportData() { - const exportDataProps = this.fontExtraProperties - ? [...EXPORT_DATA_PROPERTIES, ...EXPORT_DATA_EXTRA_PROPERTIES] - : EXPORT_DATA_PROPERTIES; - const data = Object.create(null); - for (const prop of exportDataProps) { + for (const prop of EXPORT_DATA_PROPERTIES) { const value = this[prop]; // Ignore properties that haven't been explicitly set. if (value !== undefined) { data[prop] = value; } } - return data; + + if (!this.fontExtraProperties) { + return { data }; + } + + const extra = Object.create(null); + for (const prop of EXPORT_DATA_EXTRA_PROPERTIES) { + const value = this[prop]; + if (value !== undefined) { + extra[prop] = value; + } + } + return { data, extra }; } fallbackToSystemFont(properties) { diff --git a/src/display/api.js b/src/display/api.js index e2f0ef04b..9283adecb 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -67,6 +67,7 @@ import { DOMCMapReaderFactory } from "display-cmap_reader_factory"; import { DOMFilterFactory } from "./filter_factory.js"; import { DOMStandardFontDataFactory } from "display-standard_fontdata_factory"; import { DOMWasmFactory } from "display-wasm_factory"; +import { FontInfo } from "../shared/obj-bin-transform.js"; import { GlobalWorkerOptions } from "./worker_options.js"; import { Metadata } from "./metadata.js"; import { OptionalContentConfig } from "./optional_content_config.js"; @@ -2760,11 +2761,17 @@ class WorkerTransport { break; } + const fontData = new FontInfo(exportedData); const inspectFont = this._params.pdfBug && globalThis.FontInspector?.enabled ? (font, url) => globalThis.FontInspector.fontAdded(font, url) : null; - const font = new FontFaceObject(exportedData, inspectFont); + const font = new FontFaceObject( + fontData, + inspectFont, + exportedData.extra, + exportedData.charProcOperatorList + ); this.fontLoader .bind(font) @@ -2776,7 +2783,7 @@ class WorkerTransport { // rather than waiting for a `PDFDocumentProxy.cleanup` call. // Since `font.data` could be very large, e.g. in some cases // multiple megabytes, this will help reduce memory usage. - font.data = null; + font.clearData(); } this.commonObjs.resolve(id, font); }); diff --git a/src/display/font_loader.js b/src/display/font_loader.js index e41357fce..7d48d8c71 100644 --- a/src/display/font_loader.js +++ b/src/display/font_loader.js @@ -355,12 +355,11 @@ class FontLoader { } class FontFaceObject { - constructor(translatedData, inspectFont = null) { + #fontData; + + constructor(translatedData, inspectFont = null, extra, charProcOperatorList) { this.compiledGlyphs = Object.create(null); - // importing translated data - for (const i in translatedData) { - this[i] = translatedData[i]; - } + this.#fontData = translatedData; if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { if (typeof this.disableFontFace !== "boolean") { unreachable("disableFontFace must be available."); @@ -370,6 +369,12 @@ class FontFaceObject { } } this._inspectFont = inspectFont; + if (extra) { + Object.assign(this, extra); + } + if (charProcOperatorList) { + this.charProcOperatorList = charProcOperatorList; + } } createNativeFontFace() { @@ -438,6 +443,102 @@ class FontFaceObject { } return (this.compiledGlyphs[character] = path); } + + get black() { + return this.#fontData.black; + } + + get bold() { + return this.#fontData.bold; + } + + get disableFontFace() { + return this.#fontData.disableFontFace ?? false; + } + + get fontExtraProperties() { + return this.#fontData.fontExtraProperties ?? false; + } + + get isInvalidPDFjsFont() { + return this.#fontData.isInvalidPDFjsFont; + } + + get isType3Font() { + return this.#fontData.isType3Font; + } + + get italic() { + return this.#fontData.italic; + } + + get missingFile() { + return this.#fontData.missingFile; + } + + get remeasure() { + return this.#fontData.remeasure; + } + + get vertical() { + return this.#fontData.vertical; + } + + get ascent() { + return this.#fontData.ascent; + } + + get defaultWidth() { + return this.#fontData.defaultWidth; + } + + get descent() { + return this.#fontData.descent; + } + + get bbox() { + return this.#fontData.bbox; + } + + get fontMatrix() { + return this.#fontData.fontMatrix; + } + + get fallbackName() { + return this.#fontData.fallbackName; + } + + get loadedName() { + return this.#fontData.loadedName; + } + + get mimetype() { + return this.#fontData.mimetype; + } + + get name() { + return this.#fontData.name; + } + + get data() { + return this.#fontData.data; + } + + clearData() { + this.#fontData.clearData(); + } + + get cssFontInfo() { + return this.#fontData.cssFontInfo; + } + + get systemFontInfo() { + return this.#fontData.systemFontInfo; + } + + get defaultVMetrics() { + return this.#fontData.defaultVMetrics; + } } export { FontFaceObject, FontLoader }; diff --git a/src/shared/obj-bin-transform.js b/src/shared/obj-bin-transform.js new file mode 100644 index 000000000..f47f1769b --- /dev/null +++ b/src/shared/obj-bin-transform.js @@ -0,0 +1,609 @@ +/* Copyright 2025 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { assert } from "./util.js"; + +class CssFontInfo { + #buffer; + + #view; + + #decoder; + + static strings = ["fontFamily", "fontWeight", "italicAngle"]; + + static write(info) { + const encoder = new TextEncoder(); + const encodedStrings = {}; + let stringsLength = 0; + for (const prop of CssFontInfo.strings) { + const encoded = encoder.encode(info[prop]); + encodedStrings[prop] = encoded; + stringsLength += 4 + encoded.length; + } + + const buffer = new ArrayBuffer(stringsLength); + const data = new Uint8Array(buffer); + const view = new DataView(buffer); + let offset = 0; + + for (const prop of CssFontInfo.strings) { + const encoded = encodedStrings[prop]; + const length = encoded.length; + view.setUint32(offset, length); + data.set(encoded, offset + 4); + offset += 4 + length; + } + assert(offset === buffer.byteLength, "CssFontInfo.write: Buffer overflow"); + return buffer; + } + + constructor(buffer) { + this.#buffer = buffer; + this.#view = new DataView(this.#buffer); + this.#decoder = new TextDecoder(); + } + + #readString(index) { + assert(index < CssFontInfo.strings.length, "Invalid string index"); + let offset = 0; + for (let i = 0; i < index; i++) { + offset += this.#view.getUint32(offset) + 4; + } + const length = this.#view.getUint32(offset); + return this.#decoder.decode( + new Uint8Array(this.#buffer, offset + 4, length) + ); + } + + get fontFamily() { + return this.#readString(0); + } + + get fontWeight() { + return this.#readString(1); + } + + get italicAngle() { + return this.#readString(2); + } +} + +class SystemFontInfo { + #buffer; + + #view; + + #decoder; + + static strings = ["css", "loadedName", "baseFontName", "src"]; + + static write(info) { + const encoder = new TextEncoder(); + const encodedStrings = {}; + let stringsLength = 0; + for (const prop of SystemFontInfo.strings) { + const encoded = encoder.encode(info[prop]); + encodedStrings[prop] = encoded; + stringsLength += 4 + encoded.length; + } + stringsLength += 4; + let encodedStyleStyle, + encodedStyleWeight, + lengthEstimate = 1 + stringsLength; + if (info.style) { + encodedStyleStyle = encoder.encode(info.style.style); + encodedStyleWeight = encoder.encode(info.style.weight); + lengthEstimate += + 4 + encodedStyleStyle.length + 4 + encodedStyleWeight.length; + } + + const buffer = new ArrayBuffer(lengthEstimate); + const data = new Uint8Array(buffer); + const view = new DataView(buffer); + let offset = 0; + + view.setUint8(offset++, info.guessFallback ? 1 : 0); + view.setUint32(offset, 0); + offset += 4; + stringsLength = 0; + for (const prop of SystemFontInfo.strings) { + const encoded = encodedStrings[prop]; + const length = encoded.length; + stringsLength += 4 + length; + view.setUint32(offset, length); + data.set(encoded, offset + 4); + offset += 4 + length; + } + view.setUint32(offset - stringsLength - 4, stringsLength); + + if (info.style) { + view.setUint32(offset, encodedStyleStyle.length); + data.set(encodedStyleStyle, offset + 4); + offset += 4 + encodedStyleStyle.length; + view.setUint32(offset, encodedStyleWeight.length); + data.set(encodedStyleWeight, offset + 4); + offset += 4 + encodedStyleWeight.length; + } + assert( + offset <= buffer.byteLength, + "SubstitionInfo.write: Buffer overflow" + ); + return buffer.transferToFixedLength(offset); + } + + constructor(buffer) { + this.#buffer = buffer; + this.#view = new DataView(this.#buffer); + this.#decoder = new TextDecoder(); + } + + get guessFallback() { + return this.#view.getUint8(0) !== 0; + } + + #readString(index) { + assert(index < SystemFontInfo.strings.length, "Invalid string index"); + let offset = 5; + for (let i = 0; i < index; i++) { + offset += this.#view.getUint32(offset) + 4; + } + const length = this.#view.getUint32(offset); + return this.#decoder.decode( + new Uint8Array(this.#buffer, offset + 4, length) + ); + } + + get css() { + return this.#readString(0); + } + + get loadedName() { + return this.#readString(1); + } + + get baseFontName() { + return this.#readString(2); + } + + get src() { + return this.#readString(3); + } + + get style() { + let offset = 1; + offset += 4 + this.#view.getUint32(offset); + const styleLength = this.#view.getUint32(offset); + const style = this.#decoder.decode( + new Uint8Array(this.#buffer, offset + 4, styleLength) + ); + offset += 4 + styleLength; + const weightLength = this.#view.getUint32(offset); + const weight = this.#decoder.decode( + new Uint8Array(this.#buffer, offset + 4, weightLength) + ); + return { style, weight }; + } +} + +class FontInfo { + static bools = [ + "black", + "bold", + "disableFontFace", + "fontExtraProperties", + "isInvalidPDFjsFont", + "isType3Font", + "italic", + "missingFile", + "remeasure", + "vertical", + ]; + + static numbers = ["ascent", "defaultWidth", "descent"]; + + static strings = ["fallbackName", "loadedName", "mimetype", "name"]; + + static #OFFSET_NUMBERS = Math.ceil((this.bools.length * 2) / 8); + + static #OFFSET_BBOX = this.#OFFSET_NUMBERS + this.numbers.length * 8; + + static #OFFSET_FONT_MATRIX = this.#OFFSET_BBOX + 1 + 2 * 4; + + static #OFFSET_DEFAULT_VMETRICS = this.#OFFSET_FONT_MATRIX + 1 + 8 * 6; + + static #OFFSET_STRINGS = this.#OFFSET_DEFAULT_VMETRICS + 1 + 2 * 3; + + #buffer; + + #decoder; + + #view; + + constructor({ data, extra }) { + this.#buffer = data; + this.#decoder = new TextDecoder(); + this.#view = new DataView(this.#buffer); + if (extra) { + Object.assign(this, extra); + } + } + + #readBoolean(index) { + assert(index < FontInfo.bools.length, "Invalid boolean index"); + const byteOffset = Math.floor(index / 4); + const bitOffset = (index * 2) % 8; + const value = (this.#view.getUint8(byteOffset) >> bitOffset) & 0x03; + return value === 0x00 ? undefined : value === 0x02; + } + + get black() { + return this.#readBoolean(0); + } + + get bold() { + return this.#readBoolean(1); + } + + get disableFontFace() { + return this.#readBoolean(2); + } + + get fontExtraProperties() { + return this.#readBoolean(3); + } + + get isInvalidPDFjsFont() { + return this.#readBoolean(4); + } + + get isType3Font() { + return this.#readBoolean(5); + } + + get italic() { + return this.#readBoolean(6); + } + + get missingFile() { + return this.#readBoolean(7); + } + + get remeasure() { + return this.#readBoolean(8); + } + + get vertical() { + return this.#readBoolean(9); + } + + #readNumber(index) { + assert(index < FontInfo.numbers.length, "Invalid number index"); + return this.#view.getFloat64(FontInfo.#OFFSET_NUMBERS + index * 8); + } + + get ascent() { + return this.#readNumber(0); + } + + get defaultWidth() { + return this.#readNumber(1); + } + + get descent() { + return this.#readNumber(2); + } + + get bbox() { + let offset = FontInfo.#OFFSET_BBOX; + const numCoords = this.#view.getUint8(offset); + if (numCoords === 0) { + return undefined; + } + offset += 1; + const bbox = []; + for (let i = 0; i < 4; i++) { + bbox.push(this.#view.getInt16(offset, true)); + offset += 2; + } + return bbox; + } + + get fontMatrix() { + let offset = FontInfo.#OFFSET_FONT_MATRIX; + const numPoints = this.#view.getUint8(offset); + if (numPoints === 0) { + return undefined; + } + offset += 1; + const fontMatrix = []; + for (let i = 0; i < 6; i++) { + fontMatrix.push(this.#view.getFloat64(offset, true)); + offset += 8; + } + return fontMatrix; + } + + get defaultVMetrics() { + let offset = FontInfo.#OFFSET_DEFAULT_VMETRICS; + const numMetrics = this.#view.getUint8(offset); + if (numMetrics === 0) { + return undefined; + } + offset += 1; + const defaultVMetrics = []; + for (let i = 0; i < 3; i++) { + defaultVMetrics.push(this.#view.getInt16(offset, true)); + offset += 2; + } + return defaultVMetrics; + } + + #readString(index) { + assert(index < FontInfo.strings.length, "Invalid string index"); + let offset = FontInfo.#OFFSET_STRINGS + 4; + for (let i = 0; i < index; i++) { + offset += this.#view.getUint32(offset) + 4; + } + const length = this.#view.getUint32(offset); + const stringData = new Uint8Array(length); + stringData.set(new Uint8Array(this.#buffer, offset + 4, length)); + return this.#decoder.decode(stringData); + } + + get fallbackName() { + return this.#readString(0); + } + + get loadedName() { + return this.#readString(1); + } + + get mimetype() { + return this.#readString(2); + } + + get name() { + return this.#readString(3); + } + + get data() { + let offset = FontInfo.#OFFSET_STRINGS; + const stringsLength = this.#view.getUint32(offset); + offset += 4 + stringsLength; + const systemFontInfoLength = this.#view.getUint32(offset); + offset += 4 + systemFontInfoLength; + const cssFontInfoLength = this.#view.getUint32(offset); + offset += 4 + cssFontInfoLength; + const length = this.#view.getUint32(offset); + if (length === 0) { + return undefined; + } + return new Uint8Array(this.#buffer, offset + 4, length); + } + + clearData() { + let offset = FontInfo.#OFFSET_STRINGS; + const stringsLength = this.#view.getUint32(offset); + offset += 4 + stringsLength; + const systemFontInfoLength = this.#view.getUint32(offset); + offset += 4 + systemFontInfoLength; + const cssFontInfoLength = this.#view.getUint32(offset); + offset += 4 + cssFontInfoLength; + const length = this.#view.getUint32(offset); + const data = new Uint8Array(this.#buffer, offset + 4, length); + data.fill(0); + this.#view.setUint32(offset, 0); + // this.#buffer.resize(offset); + } + + get cssFontInfo() { + let offset = FontInfo.#OFFSET_STRINGS; + const stringsLength = this.#view.getUint32(offset); + offset += 4 + stringsLength; + const systemFontInfoLength = this.#view.getUint32(offset); + offset += 4 + systemFontInfoLength; + const cssFontInfoLength = this.#view.getUint32(offset); + if (cssFontInfoLength === 0) { + return null; + } + const cssFontInfoData = new Uint8Array(cssFontInfoLength); + cssFontInfoData.set( + new Uint8Array(this.#buffer, offset + 4, cssFontInfoLength) + ); + return new CssFontInfo(cssFontInfoData.buffer); + } + + get systemFontInfo() { + let offset = FontInfo.#OFFSET_STRINGS; + const stringsLength = this.#view.getUint32(offset); + offset += 4 + stringsLength; + const systemFontInfoLength = this.#view.getUint32(offset); + if (systemFontInfoLength === 0) { + return null; + } + const systemFontInfoData = new Uint8Array(systemFontInfoLength); + systemFontInfoData.set( + new Uint8Array(this.#buffer, offset + 4, systemFontInfoLength) + ); + return new SystemFontInfo(systemFontInfoData.buffer); + } + + static write(font) { + const systemFontInfoBuffer = font.systemFontInfo + ? SystemFontInfo.write(font.systemFontInfo) + : null; + const cssFontInfoBuffer = font.cssFontInfo + ? CssFontInfo.write(font.cssFontInfo) + : null; + + const encoder = new TextEncoder(); + const encodedStrings = {}; + let stringsLength = 0; + for (const prop of FontInfo.strings) { + encodedStrings[prop] = encoder.encode(font[prop]); + stringsLength += 4 + encodedStrings[prop].length; + } + + const lengthEstimate = + FontInfo.#OFFSET_STRINGS + + 4 + + stringsLength + + 4 + + (systemFontInfoBuffer ? systemFontInfoBuffer.byteLength : 0) + + 4 + + (cssFontInfoBuffer ? cssFontInfoBuffer.byteLength : 0) + + 4 + + (font.data ? font.data.length : 0); + + const buffer = new ArrayBuffer(lengthEstimate); + const data = new Uint8Array(buffer); + const view = new DataView(buffer); + let offset = 0; + + const numBools = FontInfo.bools.length; + let boolByte = 0, + boolBit = 0; + for (let i = 0; i < numBools; i++) { + const value = font[FontInfo.bools[i]]; + // eslint-disable-next-line no-nested-ternary + const bits = value === undefined ? 0x00 : value ? 0x02 : 0x01; + boolByte |= bits << boolBit; + boolBit += 2; + if (boolBit === 8 || i === numBools - 1) { + view.setUint8(offset++, boolByte); + boolByte = 0; + boolBit = 0; + } + } + assert( + offset === FontInfo.#OFFSET_NUMBERS, + "FontInfo.write: Boolean properties offset mismatch" + ); + + for (const prop of FontInfo.numbers) { + view.setFloat64(offset, font[prop]); + offset += 8; + } + assert( + offset === FontInfo.#OFFSET_BBOX, + "FontInfo.write: Number properties offset mismatch" + ); + + if (font.bbox) { + view.setUint8(offset++, 4); + for (const coord of font.bbox) { + view.setInt16(offset, coord, true); + offset += 2; + } + } else { + view.setUint8(offset++, 0); + offset += 2 * 4; // TODO: optimize this padding away + } + + assert( + offset === FontInfo.#OFFSET_FONT_MATRIX, + "FontInfo.write: BBox properties offset mismatch" + ); + + if (font.fontMatrix) { + view.setUint8(offset++, 6); + for (const point of font.fontMatrix) { + view.setFloat64(offset, point, true); + offset += 8; + } + } else { + view.setUint8(offset++, 0); + offset += 8 * 6; // TODO: optimize this padding away + } + + assert( + offset === FontInfo.#OFFSET_DEFAULT_VMETRICS, + "FontInfo.write: FontMatrix properties offset mismatch" + ); + + if (font.defaultVMetrics) { + view.setUint8(offset++, 1); + for (const metric of font.defaultVMetrics) { + view.setInt16(offset, metric, true); + offset += 2; + } + } else { + view.setUint8(offset++, 0); + offset += 3 * 2; // TODO: optimize this padding away + } + + assert( + offset === FontInfo.#OFFSET_STRINGS, + "FontInfo.write: DefaultVMetrics properties offset mismatch" + ); + + view.setUint32(FontInfo.#OFFSET_STRINGS, 0); + offset += 4; + for (const prop of FontInfo.strings) { + const encoded = encodedStrings[prop]; + const length = encoded.length; + view.setUint32(offset, length); + data.set(encoded, offset + 4); + offset += 4 + length; + } + view.setUint32( + FontInfo.#OFFSET_STRINGS, + offset - FontInfo.#OFFSET_STRINGS - 4 + ); + + if (!systemFontInfoBuffer) { + view.setUint32(offset, 0); + offset += 4; + } else { + const length = systemFontInfoBuffer.byteLength; + view.setUint32(offset, length); + assert( + offset + 4 + length <= buffer.byteLength, + "FontInfo.write: Buffer overflow at systemFontInfo" + ); + data.set(new Uint8Array(systemFontInfoBuffer), offset + 4); + offset += 4 + length; + } + + if (!cssFontInfoBuffer) { + view.setUint32(offset, 0); + offset += 4; + } else { + const length = cssFontInfoBuffer.byteLength; + view.setUint32(offset, length); + assert( + offset + 4 + length <= buffer.byteLength, + "FontInfo.write: Buffer overflow at cssFontInfo" + ); + data.set(new Uint8Array(cssFontInfoBuffer), offset + 4); + offset += 4 + length; + } + + if (font.data === undefined) { + view.setUint32(offset, 0); + offset += 4; + } else { + view.setUint32(offset, font.data.length); + data.set(font.data, offset + 4); + offset += 4 + font.data.length; + } + + assert(offset <= buffer.byteLength, "FontInfo.write: Buffer overflow"); + return buffer.transferToFixedLength(offset); + } +} + +export { CssFontInfo, FontInfo, SystemFontInfo }; diff --git a/test/unit/bin_font_info_spec.js b/test/unit/bin_font_info_spec.js new file mode 100644 index 000000000..9b46462ef --- /dev/null +++ b/test/unit/bin_font_info_spec.js @@ -0,0 +1,164 @@ +/* Copyright 2025 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + CssFontInfo, + FontInfo, + SystemFontInfo, +} from "../../src/shared/obj-bin-transform.js"; + +const cssFontInfo = { + fontFamily: "Sample Family", + fontWeight: "not a number", + italicAngle: "angle", + uselessProp: "doesn't matter", +}; + +const systemFontInfo = { + guessFallback: false, + css: "some string", + loadedName: "another string", + baseFontName: "base name", + src: "source", + style: { + style: "normal", + weight: "400", + uselessProp: "doesn't matter", + }, + uselessProp: "doesn't matter", +}; + +const fontInfo = { + black: true, + bold: true, + disableFontFace: true, + fontExtraProperties: true, + isInvalidPDFjsFont: true, + isType3Font: true, + italic: true, + missingFile: true, + remeasure: true, + vertical: true, + ascent: 1, + defaultWidth: 1, + descent: 1, + bbox: [1, 1, 1, 1], + fontMatrix: [1, 1, 1, 1, 1, 1], + defaultVMetrics: [1, 1, 1], + fallbackName: "string", + loadedName: "string", + mimetype: "string", + name: "string", + data: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), + uselessProp: "something", +}; + +describe("font data serialization and deserialization", function () { + describe("CssFontInfo", function () { + it("must roundtrip correctly for CssFontInfo", function () { + const encoder = new TextEncoder(); + let sizeEstimate = 0; + for (const string of ["Sample Family", "not a number", "angle"]) { + sizeEstimate += 4 + encoder.encode(string).length; + } + const buffer = CssFontInfo.write(cssFontInfo); + expect(buffer.byteLength).toEqual(sizeEstimate); + const deserialized = new CssFontInfo(buffer); + expect(deserialized.fontFamily).toEqual("Sample Family"); + expect(deserialized.fontWeight).toEqual("not a number"); + expect(deserialized.italicAngle).toEqual("angle"); + expect(deserialized.uselessProp).toBeUndefined(); + }); + }); + + describe("SystemFontInfo", function () { + it("must roundtrip correctly for SystemFontInfo", function () { + const encoder = new TextEncoder(); + let sizeEstimate = 1 + 4; + for (const string of [ + "some string", + "another string", + "base name", + "source", + "normal", + "400", + ]) { + sizeEstimate += 4 + encoder.encode(string).length; + } + const buffer = SystemFontInfo.write(systemFontInfo); + expect(buffer.byteLength).toEqual(sizeEstimate); + const deserialized = new SystemFontInfo(buffer); + expect(deserialized.guessFallback).toEqual(false); + expect(deserialized.css).toEqual("some string"); + expect(deserialized.loadedName).toEqual("another string"); + expect(deserialized.baseFontName).toEqual("base name"); + expect(deserialized.src).toEqual("source"); + expect(deserialized.style.style).toEqual("normal"); + expect(deserialized.style.weight).toEqual("400"); + expect(deserialized.style.uselessProp).toBeUndefined(); + expect(deserialized.uselessProp).toBeUndefined(); + }); + }); + + describe("FontInfo", function () { + it("must roundtrip correctly for FontInfo", function () { + let sizeEstimate = 92; // fixed offset until the strings + const encoder = new TextEncoder(); + sizeEstimate += 4 + 4 * (4 + encoder.encode("string").length); + sizeEstimate += 4 + 4; // cssFontInfo and systemFontInfo + sizeEstimate += 4 + fontInfo.data.length; + const buffer = FontInfo.write(fontInfo); + expect(buffer.byteLength).toEqual(sizeEstimate); + const deserialized = new FontInfo({ data: buffer }); + expect(deserialized.black).toEqual(true); + expect(deserialized.bold).toEqual(true); + expect(deserialized.disableFontFace).toEqual(true); + expect(deserialized.fontExtraProperties).toEqual(true); + expect(deserialized.isInvalidPDFjsFont).toEqual(true); + expect(deserialized.isType3Font).toEqual(true); + expect(deserialized.italic).toEqual(true); + expect(deserialized.missingFile).toEqual(true); + expect(deserialized.remeasure).toEqual(true); + expect(deserialized.vertical).toEqual(true); + expect(deserialized.ascent).toEqual(1); + expect(deserialized.defaultWidth).toEqual(1); + expect(deserialized.descent).toEqual(1); + expect(deserialized.bbox).toEqual([1, 1, 1, 1]); + expect(deserialized.fontMatrix).toEqual([1, 1, 1, 1, 1, 1]); + expect(deserialized.defaultVMetrics).toEqual([1, 1, 1]); + expect(deserialized.fallbackName).toEqual("string"); + expect(deserialized.loadedName).toEqual("string"); + expect(deserialized.mimetype).toEqual("string"); + expect(deserialized.name).toEqual("string"); + expect(Array.from(deserialized.data)).toEqual([ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, + ]); + expect(deserialized.uselessProp).toBeUndefined(); + expect(deserialized.cssFontInfo).toBeNull(); + expect(deserialized.systemFontInfo).toBeNull(); + }); + + it("nesting should work as expected", function () { + const buffer = FontInfo.write({ + ...fontInfo, + cssFontInfo, + systemFontInfo, + }); + const deserialized = new FontInfo({ data: buffer }); + expect(deserialized.cssFontInfo.fontWeight).toEqual("not a number"); + expect(deserialized.systemFontInfo.src).toEqual("source"); + }); + }); +}); diff --git a/test/unit/clitests.json b/test/unit/clitests.json index 1328b6124..5533ddd5f 100644 --- a/test/unit/clitests.json +++ b/test/unit/clitests.json @@ -10,6 +10,7 @@ "app_options_spec.js", "autolinker_spec.js", "bidi_spec.js", + "bin_font_info_spec.js", "canvas_factory_spec.js", "cff_parser_spec.js", "cmap_spec.js", diff --git a/test/unit/jasmine-boot.js b/test/unit/jasmine-boot.js index 7dd0a0986..e72990d3f 100644 --- a/test/unit/jasmine-boot.js +++ b/test/unit/jasmine-boot.js @@ -53,6 +53,7 @@ async function initializePDFJS(callback) { "pdfjs-test/unit/app_options_spec.js", "pdfjs-test/unit/autolinker_spec.js", "pdfjs-test/unit/bidi_spec.js", + "pdfjs-test/unit/bin_font_info_spec.js", "pdfjs-test/unit/canvas_factory_spec.js", "pdfjs-test/unit/cff_parser_spec.js", "pdfjs-test/unit/cmap_spec.js",