[WIP] Serialize font data into an ArrayBuffer

This PR serializes font data into an ArrayBuffer
that is then transfered from the worker to the
main thread. It's more efficient than the current
solution which clones the "export data" object
which includes the font data as a Uint8Array.

It prepares us to switch to a SharedArrayBuffer
in the future, which would allow us to share
the font data with multiple agents, which would be
crucial for the upcoming "renderer" worker.
This commit is contained in:
Ujjwal Sharma 2025-07-22 16:13:22 +02:00
parent 3432c1933e
commit 4bed7370f4
8 changed files with 920 additions and 19 deletions

View File

@ -72,6 +72,7 @@ import { BaseStream } from "./base_stream.js";
import { bidi } from "./bidi.js"; import { bidi } from "./bidi.js";
import { ColorSpace } from "./colorspace.js"; import { ColorSpace } from "./colorspace.js";
import { ColorSpaceUtils } from "./colorspace_utils.js"; import { ColorSpaceUtils } from "./colorspace_utils.js";
import { FontInfo } from "../shared/obj-bin-transform.js";
import { getFontSubstitution } from "./font_substitutions.js"; import { getFontSubstitution } from "./font_substitutions.js";
import { getGlyphsUnicode } from "./glyphlist.js"; import { getGlyphsUnicode } from "./glyphlist.js";
import { getMetrics } from "./metrics.js"; import { getMetrics } from "./metrics.js";
@ -4709,12 +4710,21 @@ class TranslatedFont {
return; return;
} }
this.#sent = true; this.#sent = true;
const fontData = this.font.exportData();
handler.send("commonobj", [ const transfer = [];
this.loadedName, if (fontData.data) {
"Font", if (fontData.data.charProcOperatorList) {
this.font.exportData(), 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) { fallback(handler, evaluatorOptions) {

View File

@ -1144,19 +1144,27 @@ class Font {
} }
exportData() { exportData() {
const exportDataProps = this.fontExtraProperties
? [...EXPORT_DATA_PROPERTIES, ...EXPORT_DATA_EXTRA_PROPERTIES]
: EXPORT_DATA_PROPERTIES;
const data = Object.create(null); const data = Object.create(null);
for (const prop of exportDataProps) { for (const prop of EXPORT_DATA_PROPERTIES) {
const value = this[prop]; const value = this[prop];
// Ignore properties that haven't been explicitly set. // Ignore properties that haven't been explicitly set.
if (value !== undefined) { if (value !== undefined) {
data[prop] = value; 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) { fallbackToSystemFont(properties) {

View File

@ -67,6 +67,7 @@ import { DOMCMapReaderFactory } from "display-cmap_reader_factory";
import { DOMFilterFactory } from "./filter_factory.js"; import { DOMFilterFactory } from "./filter_factory.js";
import { DOMStandardFontDataFactory } from "display-standard_fontdata_factory"; import { DOMStandardFontDataFactory } from "display-standard_fontdata_factory";
import { DOMWasmFactory } from "display-wasm_factory"; import { DOMWasmFactory } from "display-wasm_factory";
import { FontInfo } from "../shared/obj-bin-transform.js";
import { GlobalWorkerOptions } from "./worker_options.js"; import { GlobalWorkerOptions } from "./worker_options.js";
import { Metadata } from "./metadata.js"; import { Metadata } from "./metadata.js";
import { OptionalContentConfig } from "./optional_content_config.js"; import { OptionalContentConfig } from "./optional_content_config.js";
@ -2760,11 +2761,17 @@ class WorkerTransport {
break; break;
} }
const fontData = new FontInfo(exportedData);
const inspectFont = const inspectFont =
this._params.pdfBug && globalThis.FontInspector?.enabled this._params.pdfBug && globalThis.FontInspector?.enabled
? (font, url) => globalThis.FontInspector.fontAdded(font, url) ? (font, url) => globalThis.FontInspector.fontAdded(font, url)
: null; : null;
const font = new FontFaceObject(exportedData, inspectFont); const font = new FontFaceObject(
fontData,
inspectFont,
exportedData.extra,
exportedData.charProcOperatorList
);
this.fontLoader this.fontLoader
.bind(font) .bind(font)
@ -2776,7 +2783,7 @@ class WorkerTransport {
// rather than waiting for a `PDFDocumentProxy.cleanup` call. // rather than waiting for a `PDFDocumentProxy.cleanup` call.
// Since `font.data` could be very large, e.g. in some cases // Since `font.data` could be very large, e.g. in some cases
// multiple megabytes, this will help reduce memory usage. // multiple megabytes, this will help reduce memory usage.
font.data = null; font.clearData();
} }
this.commonObjs.resolve(id, font); this.commonObjs.resolve(id, font);
}); });

View File

@ -355,12 +355,11 @@ class FontLoader {
} }
class FontFaceObject { class FontFaceObject {
constructor(translatedData, inspectFont = null) { #fontData;
constructor(translatedData, inspectFont = null, extra, charProcOperatorList) {
this.compiledGlyphs = Object.create(null); this.compiledGlyphs = Object.create(null);
// importing translated data this.#fontData = translatedData;
for (const i in translatedData) {
this[i] = translatedData[i];
}
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
if (typeof this.disableFontFace !== "boolean") { if (typeof this.disableFontFace !== "boolean") {
unreachable("disableFontFace must be available."); unreachable("disableFontFace must be available.");
@ -370,6 +369,12 @@ class FontFaceObject {
} }
} }
this._inspectFont = inspectFont; this._inspectFont = inspectFont;
if (extra) {
Object.assign(this, extra);
}
if (charProcOperatorList) {
this.charProcOperatorList = charProcOperatorList;
}
} }
createNativeFontFace() { createNativeFontFace() {
@ -438,6 +443,102 @@ class FontFaceObject {
} }
return (this.compiledGlyphs[character] = path); 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 }; export { FontFaceObject, FontLoader };

View File

@ -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 };

View File

@ -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");
});
});
});

View File

@ -10,6 +10,7 @@
"app_options_spec.js", "app_options_spec.js",
"autolinker_spec.js", "autolinker_spec.js",
"bidi_spec.js", "bidi_spec.js",
"bin_font_info_spec.js",
"canvas_factory_spec.js", "canvas_factory_spec.js",
"cff_parser_spec.js", "cff_parser_spec.js",
"cmap_spec.js", "cmap_spec.js",

View File

@ -53,6 +53,7 @@ async function initializePDFJS(callback) {
"pdfjs-test/unit/app_options_spec.js", "pdfjs-test/unit/app_options_spec.js",
"pdfjs-test/unit/autolinker_spec.js", "pdfjs-test/unit/autolinker_spec.js",
"pdfjs-test/unit/bidi_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/canvas_factory_spec.js",
"pdfjs-test/unit/cff_parser_spec.js", "pdfjs-test/unit/cff_parser_spec.js",
"pdfjs-test/unit/cmap_spec.js", "pdfjs-test/unit/cmap_spec.js",