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/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..b9cbd75acdb8c --- /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++, 3); + 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..2068b13ed545e --- /dev/null +++ b/src/display/obj_bin_transform_display.js @@ -0,0 +1,488 @@ +/* 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); + } + + #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 arr = new Array(len); + for (let i = 0; i < len; i++) { + arr[i] = this.#view[lookupName](offset, true); + offset += increment; + } + return arr; + } + + get bbox() { + return this.#readArray( + /* offset = */ FONT_INFO.OFFSET_BBOX, + /* arrLen = */ 4, + /* lookup = */ "getInt16", + /* increment = */ 2 + ); + } + + get fontMatrix() { + return this.#readArray( + /* offset = */ FONT_INFO.OFFSET_FONT_MATRIX, + /* arrLen = */ 6, + /* lookup = */ "getFloat64", + /* increment = */ 8 + ); + } + + get defaultVMetrics() { + return this.#readArray( + /* offset = */ FONT_INFO.OFFSET_DEFAULT_VMETRICS, + /* arrLen = */ 3, + /* lookup = */ "getInt16", + /* increment = */ 2 + ); + } + + #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/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/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); }); 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); } 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 @@