pdf.js/src/shared/obj-bin-transform.js
Aditi fa631806bf Serialize pattern data into ArrayBuffer
Follow up on https://github.com/mozilla/pdf.js/pull/20197,
This serializes pattern data into an ArrayBuffer which is
then transferred from the worker to the main thread.

It sets up the stage for us to eventually switch to a
SharedArrayBuffer in the future.
2025-10-11 01:58:07 +05:30

885 lines
24 KiB
JavaScript

/* 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, MeshFigureType } 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);
}
}
class PatternInfo {
static #KIND = 0; // 1=axial, 2=radial, 3=mesh
static #HAS_BBOX = 1; // 0/1
static #HAS_BACKGROUND = 2; // 0/1 (background for mesh patterns)
static #SHADING_TYPE = 3; // shadingType (only for mesh patterns)
static #N_COORD = 4; // number of coordinate pairs
static #N_COLOR = 8; // number of rgb triplets
static #N_STOP = 12; // number of gradient stops
static #N_FIGURES = 16; // number of figures
constructor(buffer) {
this.buffer = buffer;
this.view = new DataView(buffer);
this.data = new Uint8Array(buffer);
}
static write(ir) {
let kind,
bbox = null,
coords = [],
colors = [],
colorStops = [],
figures = [],
shadingType = null, // only needed for mesh patterns
background = null; // background for mesh patterns
switch (ir[0]) {
case "RadialAxial":
kind = ir[1] === "axial" ? 1 : 2;
bbox = ir[2];
colorStops = ir[3];
if (kind === 1) {
coords.push(...ir[4], ...ir[5]);
} else {
coords.push(ir[4][0], ir[4][1], ir[6], ir[5][0], ir[5][1], ir[7]);
}
break;
case "Mesh":
kind = 3;
shadingType = ir[1];
coords = ir[2];
colors = ir[3];
figures = ir[4] || [];
bbox = ir[6];
background = ir[7];
break;
default:
throw new Error(`Unsupported pattern type: ${ir[0]}`);
}
const nCoord = Math.floor(coords.length / 2);
const nColor = Math.floor(colors.length / 3);
const nStop = colorStops.length;
const nFigures = figures.length;
let figuresSize = 0;
for (const figure of figures) {
figuresSize += 1;
figuresSize = Math.ceil(figuresSize / 4) * 4; // Ensure 4-byte alignment
figuresSize += 4 + figure.coords.length * 4;
figuresSize += 4 + figure.colors.length * 4;
if (figure.verticesPerRow !== undefined) {
figuresSize += 4;
}
}
const byteLen =
20 +
nCoord * 8 +
nColor * 3 +
nStop * 8 +
(bbox ? 16 : 0) +
(background ? 3 : 0) +
figuresSize;
const buffer = new ArrayBuffer(byteLen);
const dataView = new DataView(buffer);
const u8data = new Uint8Array(buffer);
dataView.setUint8(PatternInfo.#KIND, kind);
dataView.setUint8(PatternInfo.#HAS_BBOX, bbox ? 1 : 0);
dataView.setUint8(PatternInfo.#HAS_BACKGROUND, background ? 1 : 0);
dataView.setUint8(PatternInfo.#SHADING_TYPE, shadingType); // Only for mesh pattern, null otherwise
dataView.setUint32(PatternInfo.#N_COORD, nCoord, true);
dataView.setUint32(PatternInfo.#N_COLOR, nColor, true);
dataView.setUint32(PatternInfo.#N_STOP, nStop, true);
dataView.setUint32(PatternInfo.#N_FIGURES, nFigures, true);
let offset = 20;
const coordsView = new Float32Array(buffer, offset, nCoord * 2);
coordsView.set(coords);
offset += nCoord * 8;
u8data.set(colors, offset);
offset += nColor * 3;
for (const [pos, hex] of colorStops) {
dataView.setFloat32(offset, pos, true);
offset += 4;
dataView.setUint32(offset, parseInt(hex.slice(1), 16), true);
offset += 4;
}
if (bbox) {
for (const v of bbox) {
dataView.setFloat32(offset, v, true);
offset += 4;
}
}
if (background) {
u8data.set(background, offset);
offset += 3;
}
for (let i = 0; i < figures.length; i++) {
const figure = figures[i];
dataView.setUint8(offset, figure.type);
offset += 1;
// Ensure 4-byte alignment
offset = Math.ceil(offset / 4) * 4;
dataView.setUint32(offset, figure.coords.length, true);
offset += 4;
const figureCoordsView = new Int32Array(
buffer,
offset,
figure.coords.length
);
figureCoordsView.set(figure.coords);
offset += figure.coords.length * 4;
dataView.setUint32(offset, figure.colors.length, true);
offset += 4;
const colorsView = new Int32Array(buffer, offset, figure.colors.length);
colorsView.set(figure.colors);
offset += figure.colors.length * 4;
if (figure.verticesPerRow !== undefined) {
dataView.setUint32(offset, figure.verticesPerRow, true);
offset += 4;
}
}
return buffer;
}
getIR() {
const dataView = this.view;
const kind = this.data[PatternInfo.#KIND];
const hasBBox = !!this.data[PatternInfo.#HAS_BBOX];
const hasBackground = !!this.data[PatternInfo.#HAS_BACKGROUND];
const nCoord = dataView.getUint32(PatternInfo.#N_COORD, true);
const nColor = dataView.getUint32(PatternInfo.#N_COLOR, true);
const nStop = dataView.getUint32(PatternInfo.#N_STOP, true);
const nFigures = dataView.getUint32(PatternInfo.#N_FIGURES, true);
let offset = 20;
const coords = new Float32Array(this.buffer, offset, nCoord * 2);
offset += nCoord * 8;
const colors = new Uint8Array(this.buffer, offset, nColor * 3);
offset += nColor * 3;
const stops = [];
for (let i = 0; i < nStop; ++i) {
const p = dataView.getFloat32(offset, true);
offset += 4;
const rgb = dataView.getUint32(offset, true);
offset += 4;
stops.push([p, `#${rgb.toString(16).padStart(6, "0")}`]);
}
let bbox = null;
if (hasBBox) {
bbox = [];
for (let i = 0; i < 4; ++i) {
bbox.push(dataView.getFloat32(offset, true));
offset += 4;
}
}
let background = null;
if (hasBackground) {
background = new Uint8Array(this.buffer, offset, 3);
offset += 3;
}
const figures = [];
for (let i = 0; i < nFigures; ++i) {
const type = dataView.getUint8(offset);
offset += 1;
// Ensure 4-byte alignment
offset = Math.ceil(offset / 4) * 4;
const coordsLength = dataView.getUint32(offset, true);
offset += 4;
const figureCoords = new Int32Array(this.buffer, offset, coordsLength);
offset += coordsLength * 4;
const colorsLength = dataView.getUint32(offset, true);
offset += 4;
const figureColors = new Int32Array(this.buffer, offset, colorsLength);
offset += colorsLength * 4;
const figure = {
type,
coords: figureCoords,
colors: figureColors,
};
if (type === MeshFigureType.LATTICE) {
figure.verticesPerRow = dataView.getUint32(offset, true);
offset += 4;
}
figures.push(figure);
}
if (kind === 1) {
// axial
return [
"RadialAxial",
"axial",
bbox,
stops,
Array.from(coords.slice(0, 2)),
Array.from(coords.slice(2, 4)),
null,
null,
];
}
if (kind === 2) {
return [
"RadialAxial",
"radial",
bbox,
stops,
[coords[0], coords[1]],
[coords[3], coords[4]],
coords[2],
coords[5],
];
}
if (kind === 3) {
const shadingType = this.data[PatternInfo.#SHADING_TYPE];
let bounds = null;
if (coords.length > 0) {
let minX = coords[0],
maxX = coords[0];
let minY = coords[1],
maxY = coords[1];
for (let i = 0; i < coords.length; i += 2) {
const x = coords[i],
y = coords[i + 1];
minX = minX > x ? x : minX;
minY = minY > y ? y : minY;
maxX = maxX < x ? x : maxX;
maxY = maxY < y ? y : maxY;
}
bounds = [minX, minY, maxX, maxY];
}
return [
"Mesh",
shadingType,
coords,
colors,
figures,
bounds,
bbox,
background,
];
}
throw new Error(`Unsupported pattern kind: ${kind}`);
}
}
export { CssFontInfo, FontInfo, PatternInfo, SystemFontInfo };