From 3842936edfbdfa931cb323259a624308f1c55798 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Thu, 12 Mar 2026 22:57:25 +0100 Subject: [PATCH 1/4] Split the `src/shared/obj-bin-transform.js` file into separate files for the main/worker threads (PR 20197 follow-up) On the worker-thread only the static `write` methods are actually used, and on the main-thread only class instances are being created. Hence this, after PR 20197, leads to a bunch of dead code in both of the *built* `pdf.mjs` and `pdf.worker.js` files. This patch reduces the size of the `gulp mozcentral` output by `21 419` bytes, i.e. `21` kilo-bytes, which I believe is way too large of a saving to not do this. (I can't even remember the last time we managed to reduce build-size this much with a single patch.) --- src/core/evaluator.js | 20 +- src/core/obj_bin_transform_core.js | 414 ++++++++++ src/display/api.js | 2 +- src/display/obj_bin_transform_display.js | 491 ++++++++++++ src/shared/obj-bin-transform.js | 919 ----------------------- src/shared/obj_bin_transform_utils.js | 71 ++ test/unit/obj_bin_transform_spec.js | 45 +- 7 files changed, 1012 insertions(+), 950 deletions(-) create mode 100644 src/core/obj_bin_transform_core.js create mode 100644 src/display/obj_bin_transform_display.js delete mode 100644 src/shared/obj-bin-transform.js create mode 100644 src/shared/obj_bin_transform_utils.js diff --git a/src/core/evaluator.js b/src/core/evaluator.js index 7626d99221979..eddde0788d4fe 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -31,6 +31,11 @@ import { } from "../shared/util.js"; import { CMapFactory, IdentityCMap } from "./cmap.js"; import { Cmd, Dict, EOF, isName, Name, Ref, RefSet } from "./primitives.js"; +import { + compileFontInfo, + compileFontPathInfo, + compilePatternInfo, +} from "./obj_bin_transform_core.js"; import { compileType3Glyph, FontFlags, @@ -44,11 +49,6 @@ import { lookupMatrix, lookupNormalRect, } from "./core_utils.js"; -import { - FontInfo, - FontPathInfo, - PatternInfo, -} from "../shared/obj-bin-transform.js"; import { getEncoding, MacRomanEncoding, @@ -1531,10 +1531,8 @@ class PartialEvaluator { localShadingPatternCache.set(shading, id); if (this.parsingType3Font) { - const transfers = []; - const patternBuffer = PatternInfo.write(patternIR); - transfers.push(patternBuffer); - this.handler.send("commonobj", [id, "Pattern", patternBuffer], transfers); + const buffer = compilePatternInfo(patternIR); + this.handler.send("commonobj", [id, "Pattern", buffer], [buffer]); } else { this.handler.send("obj", [id, this.pageIndex, "Pattern", patternIR]); } @@ -4755,7 +4753,7 @@ class PartialEvaluator { if (font.renderer.hasBuiltPath(fontChar)) { return; } - const buffer = FontPathInfo.write(font.renderer.getPathJs(fontChar)); + const buffer = compileFontPathInfo(font.renderer.getPathJs(fontChar)); handler.send("commonobj", [glyphName, "FontPath", buffer], [buffer]); } catch (reason) { if (evaluatorOptions.ignoreErrors) { @@ -4812,7 +4810,7 @@ class TranslatedFont { if (fontData.data.charProcOperatorList) { fontData.charProcOperatorList = fontData.data.charProcOperatorList; } - fontData.data = FontInfo.write(fontData.data); + fontData.data = compileFontInfo(fontData.data); transfer.push(fontData.data); } handler.send("commonobj", [this.loadedName, "Font", fontData], transfer); diff --git a/src/core/obj_bin_transform_core.js b/src/core/obj_bin_transform_core.js new file mode 100644 index 0000000000000..e2f70214debfc --- /dev/null +++ b/src/core/obj_bin_transform_core.js @@ -0,0 +1,414 @@ +/* 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, FeatureTest } from "../shared/util.js"; +import { + CSS_FONT_INFO, + FONT_INFO, + PATTERN_INFO, + SYSTEM_FONT_INFO, +} from "../shared/obj_bin_transform_utils.js"; + +function compileCssFontInfo(info) { + const encoder = new TextEncoder(); + const encodedStrings = {}; + let stringsLength = 0; + for (const prop of CSS_FONT_INFO.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 CSS_FONT_INFO.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, "compileCssFontInfo: Buffer overflow"); + return buffer; +} + +function compileSystemFontInfo(info) { + const encoder = new TextEncoder(); + const encodedStrings = {}; + let stringsLength = 0; + for (const prop of SYSTEM_FONT_INFO.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 SYSTEM_FONT_INFO.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, "compileSystemFontInfo: Buffer overflow"); + return buffer.transferToFixedLength(offset); +} + +function compileFontInfo(font) { + const systemFontInfoBuffer = font.systemFontInfo + ? compileSystemFontInfo(font.systemFontInfo) + : null; + const cssFontInfoBuffer = font.cssFontInfo + ? compileCssFontInfo(font.cssFontInfo) + : null; + + const encoder = new TextEncoder(); + const encodedStrings = {}; + let stringsLength = 0; + for (const prop of FONT_INFO.strings) { + encodedStrings[prop] = encoder.encode(font[prop]); + stringsLength += 4 + encodedStrings[prop].length; + } + + const lengthEstimate = + FONT_INFO.OFFSET_STRINGS + + 4 + + stringsLength + + 4 + + (systemFontInfoBuffer?.byteLength ?? 0) + + 4 + + (cssFontInfoBuffer?.byteLength ?? 0) + + 4 + + (font.data?.length ?? 0); + + const buffer = new ArrayBuffer(lengthEstimate); + const data = new Uint8Array(buffer); + const view = new DataView(buffer); + let offset = 0; + + const numBools = FONT_INFO.bools.length; + let boolByte = 0, + boolBit = 0; + for (let i = 0; i < numBools; i++) { + const value = font[FONT_INFO.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 === FONT_INFO.OFFSET_NUMBERS, + "compileFontInfo: Boolean properties offset mismatch" + ); + + for (const prop of FONT_INFO.numbers) { + view.setFloat64(offset, font[prop]); + offset += 8; + } + assert( + offset === FONT_INFO.OFFSET_BBOX, + "compileFontInfo: 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 === FONT_INFO.OFFSET_FONT_MATRIX, + "compileFontInfo: 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 === FONT_INFO.OFFSET_DEFAULT_VMETRICS, + "compileFontInfo: 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 === FONT_INFO.OFFSET_STRINGS, + "compileFontInfo: DefaultVMetrics properties offset mismatch" + ); + + view.setUint32(FONT_INFO.OFFSET_STRINGS, 0); + offset += 4; + for (const prop of FONT_INFO.strings) { + const encoded = encodedStrings[prop]; + const length = encoded.length; + view.setUint32(offset, length); + data.set(encoded, offset + 4); + offset += 4 + length; + } + view.setUint32( + FONT_INFO.OFFSET_STRINGS, + offset - FONT_INFO.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, + "compileFontInfo: 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, + "compileFontInfo: 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, "compileFontInfo: Buffer overflow"); + return buffer.transferToFixedLength(offset); +} + +function compilePatternInfo(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(PATTERN_INFO.KIND, kind); + dataView.setUint8(PATTERN_INFO.HAS_BBOX, bbox ? 1 : 0); + dataView.setUint8(PATTERN_INFO.HAS_BACKGROUND, background ? 1 : 0); + dataView.setUint8(PATTERN_INFO.SHADING_TYPE, shadingType); // Only for mesh pattern, null otherwise + dataView.setUint32(PATTERN_INFO.N_COORD, nCoord, true); + dataView.setUint32(PATTERN_INFO.N_COLOR, nColor, true); + dataView.setUint32(PATTERN_INFO.N_STOP, nStop, true); + dataView.setUint32(PATTERN_INFO.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; +} + +function compileFontPathInfo(path) { + let data; + let buffer; + if ( + (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) || + FeatureTest.isFloat16ArraySupported + ) { + buffer = new ArrayBuffer(path.length * 2); + data = new Float16Array(buffer); + } else { + buffer = new ArrayBuffer(path.length * 4); + data = new Float32Array(buffer); + } + data.set(path); + return buffer; +} + +export { + compileCssFontInfo, + compileFontInfo, + compileFontPathInfo, + compilePatternInfo, + compileSystemFontInfo, +}; diff --git a/src/display/api.js b/src/display/api.js index 23c994276453e..9b452aa124f4a 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -56,7 +56,7 @@ import { FontInfo, FontPathInfo, PatternInfo, -} from "../shared/obj-bin-transform.js"; +} from "./obj_bin_transform_display.js"; import { getDataProp, getFactoryUrlProp, diff --git a/src/display/obj_bin_transform_display.js b/src/display/obj_bin_transform_display.js new file mode 100644 index 0000000000000..9376f09a9929b --- /dev/null +++ b/src/display/obj_bin_transform_display.js @@ -0,0 +1,491 @@ +/* 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, FeatureTest, MeshFigureType } from "../shared/util.js"; +import { + CSS_FONT_INFO, + FONT_INFO, + PATTERN_INFO, + SYSTEM_FONT_INFO, +} from "../shared/obj_bin_transform_utils.js"; + +class CssFontInfo { + #buffer; + + #decoder = new TextDecoder(); + + #view; + + constructor(buffer) { + this.#buffer = buffer; + this.#view = new DataView(this.#buffer); + } + + #readString(index) { + assert(index < CSS_FONT_INFO.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; + + #decoder = new TextDecoder(); + + #view; + + constructor(buffer) { + this.#buffer = buffer; + this.#view = new DataView(this.#buffer); + } + + get guessFallback() { + return this.#view.getUint8(0) !== 0; + } + + #readString(index) { + assert(index < SYSTEM_FONT_INFO.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 { + #buffer; + + #decoder = new TextDecoder(); + + #view; + + constructor({ data, extra }) { + this.#buffer = data; + this.#view = new DataView(this.#buffer); + if (extra) { + Object.assign(this, extra); + } + } + + #readBoolean(index) { + assert(index < FONT_INFO.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 < FONT_INFO.numbers.length, "Invalid number index"); + return this.#view.getFloat64(FONT_INFO.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 = FONT_INFO.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 = FONT_INFO.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 = FONT_INFO.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 < FONT_INFO.strings.length, "Invalid string index"); + let offset = FONT_INFO.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); + } + + #getDataOffsets() { + let offset = FONT_INFO.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); + + return { offset, length }; + } + + get data() { + const { offset, length } = this.#getDataOffsets(); + return length === 0 + ? undefined + : new Uint8Array(this.#buffer, offset + 4, length); + } + + clearData() { + const { offset, length } = this.#getDataOffsets(); + if (length === 0) { + return; // The data is either not present, or it was previously cleared. + } + this.#view.setUint32(offset, 0); // Zero the data-length. + + // Replace the buffer/view with only its contents *before* the data-block. + this.#buffer = new Uint8Array(this.#buffer, 0, offset + 4).slice().buffer; + this.#view = new DataView(this.#buffer); + } + + get cssFontInfo() { + let offset = FONT_INFO.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 = FONT_INFO.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); + } +} + +class PatternInfo { + constructor(buffer) { + this.buffer = buffer; + this.view = new DataView(buffer); + this.data = new Uint8Array(buffer); + } + + getIR() { + const dataView = this.view; + const kind = this.data[PATTERN_INFO.KIND]; + const hasBBox = !!this.data[PATTERN_INFO.HAS_BBOX]; + const hasBackground = !!this.data[PATTERN_INFO.HAS_BACKGROUND]; + const nCoord = dataView.getUint32(PATTERN_INFO.N_COORD, true); + const nColor = dataView.getUint32(PATTERN_INFO.N_COLOR, true); + const nStop = dataView.getUint32(PATTERN_INFO.N_STOP, true); + const nFigures = dataView.getUint32(PATTERN_INFO.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[PATTERN_INFO.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}`); + } +} + +class FontPathInfo { + #buffer; + + constructor(buffer) { + this.#buffer = buffer; + } + + get path() { + if ( + (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) || + FeatureTest.isFloat16ArraySupported + ) { + return new Float16Array(this.#buffer); + } + return new Float32Array(this.#buffer); + } +} + +export { CssFontInfo, FontInfo, FontPathInfo, PatternInfo, SystemFontInfo }; diff --git a/src/shared/obj-bin-transform.js b/src/shared/obj-bin-transform.js deleted file mode 100644 index 7c549b470d9dc..0000000000000 --- a/src/shared/obj-bin-transform.js +++ /dev/null @@ -1,919 +0,0 @@ -/* 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, FeatureTest, MeshFigureType } from "./util.js"; - -class CssFontInfo { - #buffer; - - #decoder = new TextDecoder(); - - #view; - - 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); - } - - #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; - - #decoder = new TextDecoder(); - - #view; - - 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); - } - - 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 = new TextDecoder(); - - #view; - - constructor({ data, extra }) { - this.#buffer = data; - 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); - } - - #getDataOffsets() { - 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); - - return { offset, length }; - } - - get data() { - const { offset, length } = this.#getDataOffsets(); - return length === 0 - ? undefined - : new Uint8Array(this.#buffer, offset + 4, length); - } - - clearData() { - const { offset, length } = this.#getDataOffsets(); - if (length === 0) { - return; // The data is either not present, or it was previously cleared. - } - this.#view.setUint32(offset, 0); // Zero the data-length. - - // Replace the buffer/view with only its contents *before* the data-block. - this.#buffer = new Uint8Array(this.#buffer, 0, offset + 4).slice().buffer; - this.#view = new DataView(this.#buffer); - } - - 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?.byteLength ?? 0) + - 4 + - (cssFontInfoBuffer?.byteLength ?? 0) + - 4 + - (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}`); - } -} - -class FontPathInfo { - static write(path) { - let data; - let buffer; - if ( - (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) || - FeatureTest.isFloat16ArraySupported - ) { - buffer = new ArrayBuffer(path.length * 2); - data = new Float16Array(buffer); - } else { - buffer = new ArrayBuffer(path.length * 4); - data = new Float32Array(buffer); - } - data.set(path); - return buffer; - } - - #buffer; - - constructor(buffer) { - this.#buffer = buffer; - } - - get path() { - if ( - (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) || - FeatureTest.isFloat16ArraySupported - ) { - return new Float16Array(this.#buffer); - } - return new Float32Array(this.#buffer); - } -} - -export { CssFontInfo, FontInfo, FontPathInfo, PatternInfo, SystemFontInfo }; diff --git a/src/shared/obj_bin_transform_utils.js b/src/shared/obj_bin_transform_utils.js new file mode 100644 index 0000000000000..b959374dffcc4 --- /dev/null +++ b/src/shared/obj_bin_transform_utils.js @@ -0,0 +1,71 @@ +/* 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. + */ + +class CSS_FONT_INFO { + static strings = ["fontFamily", "fontWeight", "italicAngle"]; +} + +class SYSTEM_FONT_INFO { + static strings = ["css", "loadedName", "baseFontName", "src"]; +} + +class FONT_INFO { + 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; +} + +class PATTERN_INFO { + 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 +} + +export { CSS_FONT_INFO, FONT_INFO, PATTERN_INFO, SYSTEM_FONT_INFO }; diff --git a/test/unit/obj_bin_transform_spec.js b/test/unit/obj_bin_transform_spec.js index 157efa3cb06ab..9f8277798669c 100644 --- a/test/unit/obj_bin_transform_spec.js +++ b/test/unit/obj_bin_transform_spec.js @@ -13,16 +13,23 @@ * limitations under the License. */ +import { + compileCssFontInfo, + compileFontInfo, + compileFontPathInfo, + compilePatternInfo, + compileSystemFontInfo, +} from "../../src/core/obj_bin_transform_core.js"; import { CssFontInfo, FontInfo, FontPathInfo, PatternInfo, SystemFontInfo, -} from "../../src/shared/obj-bin-transform.js"; +} from "../../src/display/obj_bin_transform_display.js"; import { FeatureTest, MeshFigureType } from "../../src/shared/util.js"; -describe("obj-bin-transform", function () { +describe("obj_bin_transform", function () { describe("Font data", function () { const cssFontInfo = { fontFamily: "Sample Family", @@ -78,7 +85,7 @@ describe("obj-bin-transform", function () { for (const string of ["Sample Family", "not a number", "angle"]) { sizeEstimate += 4 + encoder.encode(string).length; } - const buffer = CssFontInfo.write(cssFontInfo); + const buffer = compileCssFontInfo(cssFontInfo); expect(buffer.byteLength).toEqual(sizeEstimate); const deserialized = new CssFontInfo(buffer); expect(deserialized.fontFamily).toEqual("Sample Family"); @@ -102,7 +109,7 @@ describe("obj-bin-transform", function () { ]) { sizeEstimate += 4 + encoder.encode(string).length; } - const buffer = SystemFontInfo.write(systemFontInfo); + const buffer = compileSystemFontInfo(systemFontInfo); expect(buffer.byteLength).toEqual(sizeEstimate); const deserialized = new SystemFontInfo(buffer); expect(deserialized.guessFallback).toEqual(false); @@ -124,7 +131,7 @@ describe("obj-bin-transform", function () { sizeEstimate += 4 + 4 * (4 + encoder.encode("string").length); sizeEstimate += 4 + 4; // cssFontInfo and systemFontInfo sizeEstimate += 4 + fontInfo.data.length; - const buffer = FontInfo.write(fontInfo); + const buffer = compileFontInfo(fontInfo); expect(buffer.byteLength).toEqual(sizeEstimate); const deserialized = new FontInfo({ data: buffer }); expect(deserialized.black).toEqual(true); @@ -156,7 +163,7 @@ describe("obj-bin-transform", function () { }); it("nesting should work as expected", function () { - const buffer = FontInfo.write({ + const buffer = compileFontInfo({ ...fontInfo, cssFontInfo, systemFontInfo, @@ -231,7 +238,7 @@ describe("obj-bin-transform", function () { describe("Pattern serialization and deserialization", function () { it("must serialize and deserialize axial gradients correctly", function () { - const buffer = PatternInfo.write(axialPatternIR); + const buffer = compilePatternInfo(axialPatternIR); expect(buffer).toBeInstanceOf(ArrayBuffer); expect(buffer.byteLength).toBeGreaterThan(0); @@ -253,7 +260,7 @@ describe("obj-bin-transform", function () { }); it("must serialize and deserialize radial gradients correctly", function () { - const buffer = PatternInfo.write(radialPatternIR); + const buffer = compilePatternInfo(radialPatternIR); expect(buffer).toBeInstanceOf(ArrayBuffer); expect(buffer.byteLength).toBeGreaterThan(0); @@ -276,7 +283,7 @@ describe("obj-bin-transform", function () { }); it("must serialize and deserialize mesh patterns with figures correctly", function () { - const buffer = PatternInfo.write(meshPatternIR); + const buffer = compilePatternInfo(meshPatternIR); expect(buffer).toBeInstanceOf(ArrayBuffer); expect(buffer.byteLength).toBeGreaterThan(0); @@ -335,7 +342,7 @@ describe("obj-bin-transform", function () { null, ]; - const buffer = PatternInfo.write(noFiguresIR); + const buffer = compilePatternInfo(noFiguresIR); const patternInfo = new PatternInfo(buffer); const reconstructedIR = patternInfo.getIR(); @@ -344,7 +351,7 @@ describe("obj-bin-transform", function () { }); it("must preserve figure data integrity across serialization", function () { - const buffer = PatternInfo.write(meshPatternIR); + const buffer = compilePatternInfo(meshPatternIR); const patternInfo = new PatternInfo(buffer); const reconstructedIR = patternInfo.getIR(); @@ -362,9 +369,9 @@ describe("obj-bin-transform", function () { }); it("must calculate correct buffer sizes for different pattern types", function () { - const axialBuffer = PatternInfo.write(axialPatternIR); - const radialBuffer = PatternInfo.write(radialPatternIR); - const meshBuffer = PatternInfo.write(meshPatternIR); + const axialBuffer = compilePatternInfo(axialPatternIR); + const radialBuffer = compilePatternInfo(radialPatternIR); + const meshBuffer = compilePatternInfo(meshPatternIR); expect(axialBuffer.byteLength).toBeLessThan(radialBuffer.byteLength); expect(meshBuffer.byteLength).toBeGreaterThan(axialBuffer.byteLength); @@ -394,7 +401,7 @@ describe("obj-bin-transform", function () { null, ]; - const buffer = PatternInfo.write(customFiguresIR); + const buffer = compilePatternInfo(customFiguresIR); const patternInfo = new PatternInfo(buffer); const reconstructedIR = patternInfo.getIR(); @@ -415,7 +422,7 @@ describe("obj-bin-transform", function () { new Uint8Array([255, 128, 64]), ]; - const buffer = PatternInfo.write(meshWithBgIR); + const buffer = compilePatternInfo(meshWithBgIR); const patternInfo = new PatternInfo(buffer); const reconstructedIR = patternInfo.getIR(); @@ -432,7 +439,7 @@ describe("obj-bin-transform", function () { null, ]; - const buffer2 = PatternInfo.write(meshNoBgIR); + const buffer2 = compilePatternInfo(meshNoBgIR); const patternInfo2 = new PatternInfo(buffer2); const reconstructedIR2 = patternInfo2.getIR(); @@ -451,7 +458,7 @@ describe("obj-bin-transform", function () { null, ]; - const buffer = PatternInfo.write(customMeshIR); + const buffer = compilePatternInfo(customMeshIR); const patternInfo = new PatternInfo(buffer); const reconstructedIR = patternInfo.getIR(); @@ -477,7 +484,7 @@ describe("obj-bin-transform", function () { ]); it("should create a FontPathInfo instance from an array of path commands", function () { - const buffer = FontPathInfo.write(path); + const buffer = compileFontPathInfo(path); const fontPathInfo = new FontPathInfo(buffer); expect(fontPathInfo.path).toEqual(path); }); From 67f3972bf0f55185831bcf2998848736255146f9 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Fri, 13 Mar 2026 09:47:20 +0100 Subject: [PATCH 2/4] Add a private `FontInfo` helper method for reading array-data (PR 20197 follow-up) Currently the `bbox`, `fontMatrix`, and `defaultVMetrics` getters duplicate almost the same code, which we can avoid by adding a new helper method (similar to existing ones for reading numbers and strings). The added `assert` in the new helper method also caught a bug in how the `defaultVMetrics` length was compiled. --- src/core/obj_bin_transform_core.js | 2 +- src/display/obj_bin_transform_display.js | 63 +++++++++++------------- 2 files changed, 31 insertions(+), 34 deletions(-) diff --git a/src/core/obj_bin_transform_core.js b/src/core/obj_bin_transform_core.js index e2f70214debfc..b9cbd75acdb8c 100644 --- a/src/core/obj_bin_transform_core.js +++ b/src/core/obj_bin_transform_core.js @@ -192,7 +192,7 @@ function compileFontInfo(font) { ); if (font.defaultVMetrics) { - view.setUint8(offset++, 1); + view.setUint8(offset++, 3); for (const metric of font.defaultVMetrics) { view.setInt16(offset, metric, true); offset += 2; diff --git a/src/display/obj_bin_transform_display.js b/src/display/obj_bin_transform_display.js index 9376f09a9929b..2068b13ed545e 100644 --- a/src/display/obj_bin_transform_display.js +++ b/src/display/obj_bin_transform_display.js @@ -198,49 +198,46 @@ class FontInfo { return this.#readNumber(2); } - get bbox() { - let offset = FONT_INFO.OFFSET_BBOX; - const numCoords = this.#view.getUint8(offset); - if (numCoords === 0) { + #readArray(offset, arrLen, lookupName, increment) { + const len = this.#view.getUint8(offset); + if (len === 0) { return undefined; } + assert(len === arrLen, "Invalid array length."); offset += 1; - const bbox = []; - for (let i = 0; i < 4; i++) { - bbox.push(this.#view.getInt16(offset, true)); - offset += 2; + const arr = new Array(len); + for (let i = 0; i < len; i++) { + arr[i] = this.#view[lookupName](offset, true); + offset += increment; } - return bbox; + return arr; + } + + get bbox() { + return this.#readArray( + /* offset = */ FONT_INFO.OFFSET_BBOX, + /* arrLen = */ 4, + /* lookup = */ "getInt16", + /* increment = */ 2 + ); } get fontMatrix() { - let offset = FONT_INFO.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; + return this.#readArray( + /* offset = */ FONT_INFO.OFFSET_FONT_MATRIX, + /* arrLen = */ 6, + /* lookup = */ "getFloat64", + /* increment = */ 8 + ); } get defaultVMetrics() { - let offset = FONT_INFO.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; + return this.#readArray( + /* offset = */ FONT_INFO.OFFSET_DEFAULT_VMETRICS, + /* arrLen = */ 3, + /* lookup = */ "getInt16", + /* increment = */ 2 + ); } #readString(index) { From bbf178c5eef3df1376d0921d69ec393cd21d21fb Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Fri, 13 Mar 2026 13:53:00 +0100 Subject: [PATCH 3/4] Only focus the undo bar when the focus isn't inside It should fix the test "must work properly when selecting undo by keyboard" which calls focus() but it can be steal by fixed callback in setTimeout. --- web/editor_undo_bar.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/editor_undo_bar.js b/web/editor_undo_bar.js index 1cb3ed90a357e..c2d44e9e5c7df 100644 --- a/web/editor_undo_bar.js +++ b/web/editor_undo_bar.js @@ -105,7 +105,9 @@ class EditorUndoBar { // Without the setTimeout, VoiceOver will read out the document title // instead of the popup label. this.#focusTimeout = setTimeout(() => { - this.#container.focus(); + if (!this.#container.contains(document.activeElement)) { + this.#container.focus(); + } this.#focusTimeout = null; }, 100); } From 1d25607b9c4dd3918075a89b1ca5fad039ac9f21 Mon Sep 17 00:00:00 2001 From: calixteman Date: Thu, 12 Mar 2026 22:53:48 +0100 Subject: [PATCH 4/4] Move 'Save as...' menu to 'Export selected...' --- l10n/en-US/viewer.ftl | 2 +- test/integration/reorganize_pages_spec.mjs | 8 +++--- web/pdf_thumbnail_viewer.js | 29 ++++++++++++++++------ web/viewer.html | 4 +-- web/viewer.js | 4 ++- 5 files changed, 32 insertions(+), 15 deletions(-) diff --git a/l10n/en-US/viewer.ftl b/l10n/en-US/viewer.ftl index c78bacc75ca9b..544b4d6348a2e 100644 --- a/l10n/en-US/viewer.ftl +++ b/l10n/en-US/viewer.ftl @@ -730,7 +730,7 @@ pdfjs-views-manager-pages-status-action-button-label = Manage pdfjs-views-manager-pages-status-copy-button-label = Copy pdfjs-views-manager-pages-status-cut-button-label = Cut pdfjs-views-manager-pages-status-delete-button-label = Delete -pdfjs-views-manager-pages-status-save-as-button-label = Save as… +pdfjs-views-manager-pages-status-export-selected-button-label = Export selected… # Variables: # $count (Number) - the number of selected pages to be cut. diff --git a/test/integration/reorganize_pages_spec.mjs b/test/integration/reorganize_pages_spec.mjs index 2b7f4901726d7..fba151951af28 100644 --- a/test/integration/reorganize_pages_spec.mjs +++ b/test/integration/reorganize_pages_spec.mjs @@ -996,7 +996,7 @@ describe("Reorganize Pages View", () => { "#viewsManagerStatusActionCopy:not(:disabled)" ); await page.waitForSelector( - "#viewsManagerStatusActionSaveAs:not(:disabled)" + "#viewsManagerStatusActionExport:not(:disabled)" ); await page.keyboard.press("Escape"); @@ -1739,7 +1739,7 @@ describe("Reorganize Pages View", () => { `.thumbnail:has(${getThumbnailSelector(3)}) input` ); - const handleSaveAs = await createPromise(page, resolve => { + const handleExport = await createPromise(page, resolve => { window.PDFViewerApplication.eventBus.on( "saveextractedpages", ({ data }) => { @@ -1752,8 +1752,8 @@ describe("Reorganize Pages View", () => { }); await page.click("#viewsManagerStatusActionButton"); - await waitAndClick(page, "#viewsManagerStatusActionSaveAs"); - const pagesData = await awaitPromise(handleSaveAs); + await waitAndClick(page, "#viewsManagerStatusActionExport"); + const pagesData = await awaitPromise(handleExport); expect(pagesData) .withContext(`In ${browserName}`) .toEqual([ diff --git a/web/pdf_thumbnail_viewer.js b/web/pdf_thumbnail_viewer.js index 1547c3f314e1f..dc763f48d9ea8 100644 --- a/web/pdf_thumbnail_viewer.js +++ b/web/pdf_thumbnail_viewer.js @@ -115,7 +115,7 @@ class PDFThumbnailViewer { #pagesMapper = null; - #manageSaveAsButton = null; + #manageExportButton = null; #manageDeleteButton = null; @@ -197,7 +197,14 @@ class PDFThumbnailViewer { // this.#addFileButton = addFileButton; if (this.#enableSplitMerge && manageMenu) { - const { button, menu, copy, cut, delete: del, saveAs } = manageMenu; + const { + button, + menu, + copy, + cut, + delete: del, + exportSelected, + } = manageMenu; this.eventBus.on( "pagesloaded", () => { @@ -206,9 +213,17 @@ class PDFThumbnailViewer { { once: true } ); - this._manageMenu = new Menu(menu, button, [copy, cut, del, saveAs]); - this.#manageSaveAsButton = saveAs; - saveAs.addEventListener("click", this.#saveExtractedPages.bind(this)); + this._manageMenu = new Menu(menu, button, [ + copy, + cut, + del, + exportSelected, + ]); + this.#manageExportButton = exportSelected; + exportSelected.addEventListener( + "click", + this.#saveExtractedPages.bind(this) + ); this.#manageDeleteButton = del; del.addEventListener("click", this.#deletePages.bind(this, "delete")); this.#manageCopyButton = copy; @@ -890,13 +905,13 @@ class PDFThumbnailViewer { #updateMenuEntries() { const size = this.#selectedPages?.size || 0; - this.#manageSaveAsButton.disabled = this.#manageCopyButton.disabled = !size; + this.#manageExportButton.disabled = this.#manageCopyButton.disabled = !size; this.#manageDeleteButton.disabled = this.#manageCutButton.disabled = !this.#canDelete(); } #toggleMenuEntries(enable) { - this.#manageSaveAsButton.disabled = + this.#manageExportButton.disabled = this.#manageDeleteButton.disabled = this.#manageCopyButton.disabled = this.#manageCutButton.disabled = diff --git a/web/viewer.html b/web/viewer.html index fb3c351a0f745..3eeb5c25c063a 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -233,8 +233,8 @@
  • -
  • diff --git a/web/viewer.js b/web/viewer.js index 492890adebc60..b0c2d112019e1 100644 --- a/web/viewer.js +++ b/web/viewer.js @@ -165,7 +165,9 @@ function getViewerConfiguration() { copy: document.getElementById("viewsManagerStatusActionCopy"), cut: document.getElementById("viewsManagerStatusActionCut"), delete: document.getElementById("viewsManagerStatusActionDelete"), - saveAs: document.getElementById("viewsManagerStatusActionSaveAs"), + exportSelected: document.getElementById( + "viewsManagerStatusActionExport" + ), }, }, findBar: {