From 3ced0dec1ba41dbf4da1ef6cfd6b3cfe4122d29c Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Wed, 19 Jan 2022 11:43:41 +0100 Subject: [PATCH] [api-major] Remove the SVG back-end (PR 15173 follow-up) This has been deprecated since version `2.15.349`, which is a year ago. Removing this will also simplify some upcoming changes, specifically outputting of JavaScript modules in the builds. --- examples/node/domstubs.js | 285 ------ examples/node/pdf2svg.js | 128 --- gulpfile.mjs | 11 +- src/display/api.js | 2 - src/display/stubs.js | 2 - src/display/svg.js | 1700 --------------------------------- src/pdf.js | 2 - test/unit/clitests.json | 1 - test/unit/display_svg_spec.js | 163 ---- test/unit/jasmine-boot.js | 1 - test/unit/pdf_spec.js | 2 - test/unit/unit_test.html | 1 - tsconfig.json | 1 - web/pdfjs.js | 2 - web/viewer-geckoview.html | 1 - web/viewer.html | 1 - 16 files changed, 1 insertion(+), 2302 deletions(-) delete mode 100644 examples/node/domstubs.js delete mode 100644 examples/node/pdf2svg.js delete mode 100644 src/display/svg.js delete mode 100644 test/unit/display_svg_spec.js diff --git a/examples/node/domstubs.js b/examples/node/domstubs.js deleted file mode 100644 index 373fd1c26..000000000 --- a/examples/node/domstubs.js +++ /dev/null @@ -1,285 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -function xmlEncode(s) { - let i = 0, - ch; - s = String(s); - while ( - i < s.length && - (ch = s[i]) !== "&" && - ch !== "<" && - ch !== '"' && - ch !== "\n" && - ch !== "\r" && - ch !== "\t" - ) { - i++; - } - if (i >= s.length) { - return s; - } - let buf = s.substring(0, i); - while (i < s.length) { - ch = s[i++]; - switch (ch) { - case "&": - buf += "&"; - break; - case "<": - buf += "<"; - break; - case '"': - buf += """; - break; - case "\n": - buf += " "; - break; - case "\r": - buf += " "; - break; - case "\t": - buf += " "; - break; - default: - buf += ch; - break; - } - } - return buf; -} - -function DOMElement(name) { - this.nodeName = name; - this.childNodes = []; - this.attributes = {}; - this.textContent = ""; - - if (name === "style") { - this.sheet = { - cssRules: [], - insertRule(rule) { - this.cssRules.push(rule); - }, - }; - } -} - -DOMElement.prototype = { - getAttribute: function DOMElement_getAttribute(name) { - if (name in this.attributes) { - return this.attributes[name]; - } - return null; - }, - - getAttributeNS: function DOMElement_getAttributeNS(NS, name) { - // Fast path - if (name in this.attributes) { - return this.attributes[name]; - } - // Slow path - used by test/unit/display_svg_spec.js - // Assuming that there is only one matching attribute for a given name, - // across all namespaces. - if (NS) { - const suffix = ":" + name; - for (const fullName in this.attributes) { - if (fullName.slice(-suffix.length) === suffix) { - return this.attributes[fullName]; - } - } - } - return null; - }, - - setAttribute: function DOMElement_setAttribute(name, value) { - this.attributes[name] = value || ""; - }, - - setAttributeNS: function DOMElement_setAttributeNS(NS, name, value) { - this.setAttribute(name, value); - }, - - append: function DOMElement_append(...elements) { - const childNodes = this.childNodes; - for (const element of elements) { - if (!childNodes.includes(element)) { - childNodes.push(element); - } - } - }, - - appendChild: function DOMElement_appendChild(element) { - const childNodes = this.childNodes; - if (!childNodes.includes(element)) { - childNodes.push(element); - } - }, - - hasChildNodes: function DOMElement_hasChildNodes() { - return this.childNodes.length !== 0; - }, - - cloneNode: function DOMElement_cloneNode() { - const newNode = new DOMElement(this.nodeName); - newNode.childNodes = this.childNodes; - newNode.attributes = this.attributes; - newNode.textContent = this.textContent; - return newNode; - }, - - // This method is offered for convenience. It is recommended to directly use - // getSerializer because that allows you to process the chunks as they come - // instead of requiring the whole image to fit in memory. - toString: function DOMElement_toString() { - const buf = []; - const serializer = this.getSerializer(); - let chunk; - while ((chunk = serializer.getNext()) !== null) { - buf.push(chunk); - } - return buf.join(""); - }, - - getSerializer: function DOMElement_getSerializer() { - return new DOMElementSerializer(this); - }, -}; - -function DOMElementSerializer(node) { - this._node = node; - this._state = 0; - this._loopIndex = 0; - this._attributeKeys = null; - this._childSerializer = null; -} -DOMElementSerializer.prototype = { - /** - * Yields the next chunk in the serialization of the element. - * - * @returns {string|null} null if the element has fully been serialized. - */ - getNext: function DOMElementSerializer_getNext() { - const node = this._node; - switch (this._state) { - case 0: // Start opening tag. - ++this._state; - return "<" + node.nodeName; - case 1: // Add SVG namespace if this is the root element. - ++this._state; - if (node.nodeName === "svg:svg") { - return ( - ' xmlns:xlink="http://www.w3.org/1999/xlink"' + - ' xmlns:svg="http://www.w3.org/2000/svg"' - ); - } - /* falls through */ - case 2: // Initialize variables for looping over attributes. - ++this._state; - this._loopIndex = 0; - this._attributeKeys = Object.keys(node.attributes); - /* falls through */ - case 3: // Serialize any attributes and end opening tag. - if (this._loopIndex < this._attributeKeys.length) { - const name = this._attributeKeys[this._loopIndex++]; - return " " + name + '="' + xmlEncode(node.attributes[name]) + '"'; - } - ++this._state; - return ">"; - case 4: // Serialize textContent for tspan/style elements. - if (node.nodeName === "svg:tspan" || node.nodeName === "svg:style") { - this._state = 6; - return xmlEncode(node.textContent); - } - ++this._state; - this._loopIndex = 0; - /* falls through */ - case 5: // Serialize child nodes (only for non-tspan/style elements). - while (true) { - const value = - this._childSerializer && this._childSerializer.getNext(); - if (value !== null) { - return value; - } - const nextChild = node.childNodes[this._loopIndex++]; - if (nextChild) { - this._childSerializer = new DOMElementSerializer(nextChild); - } else { - this._childSerializer = null; - ++this._state; - break; - } - } - /* falls through */ - case 6: // Ending tag. - ++this._state; - return ""; - case 7: // Done. - return null; - default: - throw new Error("Unexpected serialization state: " + this._state); - } - }, -}; - -const document = { - childNodes: [], - - get currentScript() { - return { src: "" }; - }, - - get documentElement() { - return this; - }, - - createElementNS(NS, element) { - const elObject = new DOMElement(element); - return elObject; - }, - - createElement(element) { - return this.createElementNS("", element); - }, - - getElementsByTagName(element) { - if (element === "head") { - return [this.head || (this.head = new DOMElement("head"))]; - } - return []; - }, -}; - -function Image() { - this._src = null; - this.onload = null; -} -Image.prototype = { - get src() { - return this._src; - }, - set src(value) { - this._src = value; - if (this.onload) { - this.onload(); - } - }, -}; - -exports.document = document; -exports.Image = Image; - -const exported_symbols = Object.keys(exports); - -exports.setStubs = function (namespace) { - exported_symbols.forEach(function (key) { - console.assert(!(key in namespace), "property should not be set: " + key); - namespace[key] = exports[key]; - }); -}; -exports.unsetStubs = function (namespace) { - exported_symbols.forEach(function (key) { - console.assert(key in namespace, "property should be set: " + key); - delete namespace[key]; - }); -}; diff --git a/examples/node/pdf2svg.js b/examples/node/pdf2svg.js deleted file mode 100644 index 0fa1cb41c..000000000 --- a/examples/node/pdf2svg.js +++ /dev/null @@ -1,128 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -// -// Node tool to dump SVG output into a file. -// - -const fs = require("fs"); -const util = require("util"); -const path = require("path"); -const stream = require("stream"); - -// HACK few hacks to let PDF.js be loaded not as a module in global space. -require("./domstubs.js").setStubs(global); - -// Run `gulp dist-install` to generate 'pdfjs-dist' npm package files. -const pdfjsLib = require("pdfjs-dist/legacy/build/pdf.js"); - -// Some PDFs need external cmaps. -const CMAP_URL = "../../node_modules/pdfjs-dist/cmaps/"; -const CMAP_PACKED = true; - -// Loading file from file system into typed array -const pdfPath = - process.argv[2] || "../../web/compressed.tracemonkey-pldi-09.pdf"; -const data = new Uint8Array(fs.readFileSync(pdfPath)); - -const outputDirectory = "./svgdump"; - -try { - // Note: This creates a directory only one level deep. If you want to create - // multiple subdirectories on the fly, use the mkdirp module from npm. - fs.mkdirSync(outputDirectory); -} catch (e) { - if (e.code !== "EEXIST") { - throw e; - } -} - -// Dumps svg outputs to a folder called svgdump -function getFilePathForPage(pageNum) { - const name = path.basename(pdfPath, path.extname(pdfPath)); - return path.join(outputDirectory, `${name}-${pageNum}.svg`); -} - -/** - * A readable stream which offers a stream representing the serialization of a - * given DOM element (as defined by domstubs.js). - * - * @param {object} options - * @param {DOMElement} options.svgElement The element to serialize - */ -function ReadableSVGStream(options) { - if (!(this instanceof ReadableSVGStream)) { - return new ReadableSVGStream(options); - } - stream.Readable.call(this, options); - this.serializer = options.svgElement.getSerializer(); -} -util.inherits(ReadableSVGStream, stream.Readable); -// Implements https://nodejs.org/api/stream.html#stream_readable_read_size_1 -ReadableSVGStream.prototype._read = function () { - let chunk; - while ((chunk = this.serializer.getNext()) !== null) { - if (!this.push(chunk)) { - return; - } - } - this.push(null); -}; - -// Streams the SVG element to the given file path. -function writeSvgToFile(svgElement, filePath) { - let readableSvgStream = new ReadableSVGStream({ - svgElement, - }); - const writableStream = fs.createWriteStream(filePath); - return new Promise(function (resolve, reject) { - readableSvgStream.once("error", reject); - writableStream.once("error", reject); - writableStream.once("finish", resolve); - readableSvgStream.pipe(writableStream); - }).catch(function (err) { - readableSvgStream = null; // Explicitly null because of v8 bug 6512. - writableStream.end(); - throw err; - }); -} - -// Will be using async/await to load document, pages and misc data. -const loadingTask = pdfjsLib.getDocument({ - data, - cMapUrl: CMAP_URL, - cMapPacked: CMAP_PACKED, - fontExtraProperties: true, -}); -(async function () { - const doc = await loadingTask.promise; - const numPages = doc.numPages; - console.log("# Document Loaded"); - console.log(`Number of Pages: ${numPages}`); - console.log(); - - for (let pageNum = 1; pageNum <= numPages; pageNum++) { - try { - const page = await doc.getPage(pageNum); - console.log(`# Page ${pageNum}`); - const viewport = page.getViewport({ scale: 1.0 }); - console.log(`Size: ${viewport.width}x${viewport.height}`); - console.log(); - - const opList = await page.getOperatorList(); - const svgGfx = new pdfjsLib.SVGGraphics( - page.commonObjs, - page.objs, - /* forceDataSchema = */ true - ); - svgGfx.embedFonts = true; - const svg = await svgGfx.getSVG(opList, viewport); - await writeSvgToFile(svg, getFilePathForPage(pageNum)); - // Release page resources. - page.cleanup(); - } catch (err) { - console.log(`Error: ${err}`); - } - } - console.log("# End of Document"); -})(); diff --git a/gulpfile.mjs b/gulpfile.mjs index f1e719bcc..8cc36bd2e 100644 --- a/gulpfile.mjs +++ b/gulpfile.mjs @@ -242,7 +242,6 @@ function createWebpackConfig( "display-network": "src/display/stubs.js", "display-node_stream": "src/display/stubs.js", "display-node_utils": "src/display/stubs.js", - "display-svg": "src/display/stubs.js", }; const viewerAlias = { "web-alt_text_manager": "web/alt_text_manager.js", @@ -276,7 +275,6 @@ function createWebpackConfig( libraryAlias["display-network"] = "src/display/network.js"; libraryAlias["display-node_stream"] = "src/display/node_stream.js"; libraryAlias["display-node_utils"] = "src/display/node_utils.js"; - libraryAlias["display-svg"] = "src/display/svg.js"; viewerAlias["web-com"] = "web/genericcom.js"; viewerAlias["web-print_service"] = "web/pdf_print_service.js"; @@ -1637,7 +1635,6 @@ function buildLibHelper(bundleDefines, inputStream, outputDir) { "display-network": "./network", "display-node_stream": "./node_stream", "display-node_utils": "./node_utils", - "display-svg": "./svg", }, }; const licenseHeaderLibre = fs @@ -1670,12 +1667,7 @@ function buildLib(defines, dir) { { base: "src/" } ), gulp.src( - [ - "examples/node/domstubs.js", - "external/webL10n/l10n.js", - "web/*.js", - "!web/{pdfjs,viewer}.js", - ], + ["external/webL10n/l10n.js", "web/*.js", "!web/{pdfjs,viewer}.js"], { base: "." } ), gulp.src("test/unit/*.js", { base: "." }), @@ -2217,7 +2209,6 @@ function packageJson() { http: false, https: false, url: false, - zlib: false, }, format: "amd", // to not allow system.js to choose 'cjs' repository: { diff --git a/src/display/api.js b/src/display/api.js index 9030e0743..1426bdd1a 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -73,7 +73,6 @@ import { PDFDataTransportStream } from "./transport_stream.js"; import { PDFFetchStream } from "display-fetch_stream"; import { PDFNetworkStream } from "display-network"; import { PDFNodeStream } from "display-node_stream"; -import { SVGGraphics } from "display-svg"; import { XfaText } from "./xfa_text.js"; const DEFAULT_RANGE_CHUNK_SIZE = 65536; // 2^16 = 65536 @@ -3468,6 +3467,5 @@ export { PDFWorker, PDFWorkerUtil, RenderTask, - SVGGraphics, version, }; diff --git a/src/display/stubs.js b/src/display/stubs.js index 3ac497b61..14e9f9a79 100644 --- a/src/display/stubs.js +++ b/src/display/stubs.js @@ -21,7 +21,6 @@ const NullL10n = null; const PDFFetchStream = null; const PDFNetworkStream = null; const PDFNodeStream = null; -const SVGGraphics = null; export { NodeCanvasFactory, @@ -32,5 +31,4 @@ export { PDFFetchStream, PDFNetworkStream, PDFNodeStream, - SVGGraphics, }; diff --git a/src/display/svg.js b/src/display/svg.js deleted file mode 100644 index c81084071..000000000 --- a/src/display/svg.js +++ /dev/null @@ -1,1700 +0,0 @@ -/* Copyright 2012 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. - */ -/* globals __non_webpack_require__ */ - -import { deprecated, DOMSVGFactory } from "./display_utils.js"; -import { - FONT_IDENTITY_MATRIX, - IDENTITY_MATRIX, - ImageKind, - isNodeJS, - OPS, - TextRenderingMode, - Util, - warn, -} from "../shared/util.js"; - -if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("GENERIC")) { - throw new Error( - 'Module "SVGGraphics" shall not be used outside GENERIC builds.' - ); -} - -const SVG_DEFAULTS = { - fontStyle: "normal", - fontWeight: "normal", - fillColor: "#000000", -}; -const XML_NS = "http://www.w3.org/XML/1998/namespace"; -const XLINK_NS = "http://www.w3.org/1999/xlink"; -const LINE_CAP_STYLES = ["butt", "round", "square"]; -const LINE_JOIN_STYLES = ["miter", "round", "bevel"]; - -const createObjectURL = function ( - data, - contentType = "", - forceDataSchema = false -) { - if (URL.createObjectURL && typeof Blob !== "undefined" && !forceDataSchema) { - return URL.createObjectURL(new Blob([data], { type: contentType })); - } - // Blob/createObjectURL is not available, falling back to data schema. - const digits = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; - - let buffer = `data:${contentType};base64,`; - for (let i = 0, ii = data.length; i < ii; i += 3) { - const b1 = data[i] & 0xff; - const b2 = data[i + 1] & 0xff; - const b3 = data[i + 2] & 0xff; - const d1 = b1 >> 2, - d2 = ((b1 & 3) << 4) | (b2 >> 4); - const d3 = i + 1 < ii ? ((b2 & 0xf) << 2) | (b3 >> 6) : 64; - const d4 = i + 2 < ii ? b3 & 0x3f : 64; - buffer += digits[d1] + digits[d2] + digits[d3] + digits[d4]; - } - return buffer; -}; - -const convertImgDataToPng = (function () { - const PNG_HEADER = new Uint8Array([ - 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, - ]); - const CHUNK_WRAPPER_SIZE = 12; - - const crcTable = new Int32Array(256); - for (let i = 0; i < 256; i++) { - let c = i; - for (let h = 0; h < 8; h++) { - c = c & 1 ? 0xedb88320 ^ ((c >> 1) & 0x7fffffff) : (c >> 1) & 0x7fffffff; - } - crcTable[i] = c; - } - - function crc32(data, start, end) { - let crc = -1; - for (let i = start; i < end; i++) { - const a = (crc ^ data[i]) & 0xff; - const b = crcTable[a]; - crc = (crc >>> 8) ^ b; - } - return crc ^ -1; - } - - function writePngChunk(type, body, data, offset) { - let p = offset; - const len = body.length; - - data[p] = (len >> 24) & 0xff; - data[p + 1] = (len >> 16) & 0xff; - data[p + 2] = (len >> 8) & 0xff; - data[p + 3] = len & 0xff; - p += 4; - - data[p] = type.charCodeAt(0) & 0xff; - data[p + 1] = type.charCodeAt(1) & 0xff; - data[p + 2] = type.charCodeAt(2) & 0xff; - data[p + 3] = type.charCodeAt(3) & 0xff; - p += 4; - - data.set(body, p); - p += body.length; - - const crc = crc32(data, offset + 4, p); - data[p] = (crc >> 24) & 0xff; - data[p + 1] = (crc >> 16) & 0xff; - data[p + 2] = (crc >> 8) & 0xff; - data[p + 3] = crc & 0xff; - } - - function adler32(data, start, end) { - let a = 1; - let b = 0; - for (let i = start; i < end; ++i) { - a = (a + (data[i] & 0xff)) % 65521; - b = (b + a) % 65521; - } - return (b << 16) | a; - } - - /** - * @param {Uint8Array} literals The input data. - * @returns {Uint8Array} The DEFLATE-compressed data stream in zlib format. - * This is the required format for compressed streams in the PNG format: - * http://www.libpng.org/pub/png/spec/1.2/PNG-Compression.html - */ - function deflateSync(literals) { - if (!isNodeJS) { - // zlib is certainly not available outside of Node.js. We can either use - // the pako library for client-side DEFLATE compression, or use the - // canvas API of the browser to obtain a more optimal PNG file. - return deflateSyncUncompressed(literals); - } - try { - // NOTE: This implementation is far from perfect, but already way better - // than not applying any compression. - // - // A better algorithm will try to choose a good predictor/filter and - // then choose a suitable zlib compression strategy (e.g. 3,Z_RLE). - // - // Node v0.11.12 zlib.deflateSync is introduced (and returns a Buffer). - // Node v3.0.0 Buffer inherits from Uint8Array. - // Node v8.0.0 zlib.deflateSync accepts Uint8Array as input. - const input = - // eslint-disable-next-line no-undef - parseInt(process.versions.node) >= 8 ? literals : Buffer.from(literals); - const output = __non_webpack_require__("zlib").deflateSync(input, { - level: 9, - }); - return output instanceof Uint8Array ? output : new Uint8Array(output); - } catch (e) { - warn("Not compressing PNG because zlib.deflateSync is unavailable: " + e); - } - - return deflateSyncUncompressed(literals); - } - - // An implementation of DEFLATE with compression level 0 (Z_NO_COMPRESSION). - function deflateSyncUncompressed(literals) { - let len = literals.length; - const maxBlockLength = 0xffff; - - const deflateBlocks = Math.ceil(len / maxBlockLength); - const idat = new Uint8Array(2 + len + deflateBlocks * 5 + 4); - let pi = 0; - idat[pi++] = 0x78; // compression method and flags - idat[pi++] = 0x9c; // flags - - let pos = 0; - while (len > maxBlockLength) { - // writing non-final DEFLATE blocks type 0 and length of 65535 - idat[pi++] = 0x00; - idat[pi++] = 0xff; - idat[pi++] = 0xff; - idat[pi++] = 0x00; - idat[pi++] = 0x00; - idat.set(literals.subarray(pos, pos + maxBlockLength), pi); - pi += maxBlockLength; - pos += maxBlockLength; - len -= maxBlockLength; - } - - // writing non-final DEFLATE blocks type 0 - idat[pi++] = 0x01; - idat[pi++] = len & 0xff; - idat[pi++] = (len >> 8) & 0xff; - idat[pi++] = ~len & 0xffff & 0xff; - idat[pi++] = ((~len & 0xffff) >> 8) & 0xff; - idat.set(literals.subarray(pos), pi); - pi += literals.length - pos; - - const adler = adler32(literals, 0, literals.length); // checksum - idat[pi++] = (adler >> 24) & 0xff; - idat[pi++] = (adler >> 16) & 0xff; - idat[pi++] = (adler >> 8) & 0xff; - idat[pi++] = adler & 0xff; - return idat; - } - - function encode(imgData, kind, forceDataSchema, isMask) { - const width = imgData.width; - const height = imgData.height; - let bitDepth, colorType, lineSize; - const bytes = imgData.data; - - switch (kind) { - case ImageKind.GRAYSCALE_1BPP: - colorType = 0; - bitDepth = 1; - lineSize = (width + 7) >> 3; - break; - case ImageKind.RGB_24BPP: - colorType = 2; - bitDepth = 8; - lineSize = width * 3; - break; - case ImageKind.RGBA_32BPP: - colorType = 6; - bitDepth = 8; - lineSize = width * 4; - break; - default: - throw new Error("invalid format"); - } - - // prefix every row with predictor 0 - const literals = new Uint8Array((1 + lineSize) * height); - let offsetLiterals = 0, - offsetBytes = 0; - for (let y = 0; y < height; ++y) { - literals[offsetLiterals++] = 0; // no prediction - literals.set( - bytes.subarray(offsetBytes, offsetBytes + lineSize), - offsetLiterals - ); - offsetBytes += lineSize; - offsetLiterals += lineSize; - } - - if (kind === ImageKind.GRAYSCALE_1BPP && isMask) { - // inverting for image masks - offsetLiterals = 0; - for (let y = 0; y < height; y++) { - offsetLiterals++; // skipping predictor - for (let i = 0; i < lineSize; i++) { - literals[offsetLiterals++] ^= 0xff; - } - } - } - - const ihdr = new Uint8Array([ - (width >> 24) & 0xff, - (width >> 16) & 0xff, - (width >> 8) & 0xff, - width & 0xff, - (height >> 24) & 0xff, - (height >> 16) & 0xff, - (height >> 8) & 0xff, - height & 0xff, - bitDepth, // bit depth - colorType, // color type - 0x00, // compression method - 0x00, // filter method - 0x00, // interlace method - ]); - const idat = deflateSync(literals); - - // PNG consists of: header, IHDR+data, IDAT+data, and IEND. - const pngLength = - PNG_HEADER.length + CHUNK_WRAPPER_SIZE * 3 + ihdr.length + idat.length; - const data = new Uint8Array(pngLength); - let offset = 0; - data.set(PNG_HEADER, offset); - offset += PNG_HEADER.length; - writePngChunk("IHDR", ihdr, data, offset); - offset += CHUNK_WRAPPER_SIZE + ihdr.length; - writePngChunk("IDATA", idat, data, offset); - offset += CHUNK_WRAPPER_SIZE + idat.length; - writePngChunk("IEND", new Uint8Array(0), data, offset); - - return createObjectURL(data, "image/png", forceDataSchema); - } - - // eslint-disable-next-line no-shadow - return function convertImgDataToPng(imgData, forceDataSchema, isMask) { - const kind = - imgData.kind === undefined ? ImageKind.GRAYSCALE_1BPP : imgData.kind; - return encode(imgData, kind, forceDataSchema, isMask); - }; -})(); - -class SVGExtraState { - constructor() { - this.fontSizeScale = 1; - this.fontWeight = SVG_DEFAULTS.fontWeight; - this.fontSize = 0; - - this.textMatrix = IDENTITY_MATRIX; - this.fontMatrix = FONT_IDENTITY_MATRIX; - this.leading = 0; - this.textRenderingMode = TextRenderingMode.FILL; - this.textMatrixScale = 1; - - // Current point (in user coordinates) - this.x = 0; - this.y = 0; - - // Start of text line (in text coordinates) - this.lineX = 0; - this.lineY = 0; - - // Character and word spacing - this.charSpacing = 0; - this.wordSpacing = 0; - this.textHScale = 1; - this.textRise = 0; - - // Default foreground and background colors - this.fillColor = SVG_DEFAULTS.fillColor; - this.strokeColor = "#000000"; - - this.fillAlpha = 1; - this.strokeAlpha = 1; - this.lineWidth = 1; - this.lineJoin = ""; - this.lineCap = ""; - this.miterLimit = 0; - - this.dashArray = []; - this.dashPhase = 0; - - this.dependencies = []; - - // Clipping - this.activeClipUrl = null; - this.clipGroup = null; - - this.maskId = ""; - } - - clone() { - return Object.create(this); - } - - setCurrentPoint(x, y) { - this.x = x; - this.y = y; - } -} - -function opListToTree(opList) { - let opTree = []; - const tmp = []; - - for (const opListElement of opList) { - if (opListElement.fn === "save") { - opTree.push({ fnId: 92, fn: "group", items: [] }); - tmp.push(opTree); - opTree = opTree.at(-1).items; - continue; - } - - if (opListElement.fn === "restore") { - opTree = tmp.pop(); - } else { - opTree.push(opListElement); - } - } - return opTree; -} - -/** - * Format a float number as a string. - * - * @param value {number} - The float number to format. - * @returns {string} - */ -function pf(value) { - if (Number.isInteger(value)) { - return value.toString(); - } - const s = value.toFixed(10); - let i = s.length - 1; - if (s[i] !== "0") { - return s; - } - - // Remove trailing zeros. - do { - i--; - } while (s[i] === "0"); - return s.substring(0, s[i] === "." ? i : i + 1); -} - -/** - * Format a transform matrix as a string. The standard rotation, scale and - * translation matrices are replaced by their shorter forms, and for - * identity matrices an empty string is returned to save memory. - * - * @param m {Array} - The transform matrix to format. - * @returns {string} - */ -function pm(m) { - if (m[4] === 0 && m[5] === 0) { - if (m[1] === 0 && m[2] === 0) { - if (m[0] === 1 && m[3] === 1) { - return ""; - } - return `scale(${pf(m[0])} ${pf(m[3])})`; - } - if (m[0] === m[3] && m[1] === -m[2]) { - const a = (Math.acos(m[0]) * 180) / Math.PI; - return `rotate(${pf(a)})`; - } - } else if (m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1) { - return `translate(${pf(m[4])} ${pf(m[5])})`; - } - return ( - `matrix(${pf(m[0])} ${pf(m[1])} ${pf(m[2])} ${pf(m[3])} ${pf(m[4])} ` + - `${pf(m[5])})` - ); -} - -// The counts below are relevant for all pages, so they have to be global -// instead of being members of `SVGGraphics` (which is recreated for -// each page). -let clipCount = 0; -let maskCount = 0; -let shadingCount = 0; - -class SVGGraphics { - constructor(commonObjs, objs, forceDataSchema = false) { - deprecated( - "The SVG back-end is no longer maintained and *may* be removed in the future." - ); - this.svgFactory = new DOMSVGFactory(); - - this.current = new SVGExtraState(); - this.transformMatrix = IDENTITY_MATRIX; // Graphics state matrix - this.transformStack = []; - this.extraStack = []; - this.commonObjs = commonObjs; - this.objs = objs; - this.pendingClip = null; - this.pendingEOFill = false; - - this.embedFonts = false; - this.embeddedFonts = Object.create(null); - this.cssStyle = null; - this.forceDataSchema = !!forceDataSchema; - - // In `src/shared/util.js` the operator names are mapped to IDs. - // The list below represents the reverse of that, i.e., it maps IDs - // to operator names. - this._operatorIdMapping = []; - for (const op in OPS) { - this._operatorIdMapping[OPS[op]] = op; - } - } - - getObject(data, fallback = null) { - if (typeof data === "string") { - return data.startsWith("g_") - ? this.commonObjs.get(data) - : this.objs.get(data); - } - return fallback; - } - - save() { - this.transformStack.push(this.transformMatrix); - const old = this.current; - this.extraStack.push(old); - this.current = old.clone(); - } - - restore() { - this.transformMatrix = this.transformStack.pop(); - this.current = this.extraStack.pop(); - this.pendingClip = null; - this.tgrp = null; - } - - group(items) { - this.save(); - this.executeOpTree(items); - this.restore(); - } - - loadDependencies(operatorList) { - const fnArray = operatorList.fnArray; - const argsArray = operatorList.argsArray; - - for (let i = 0, ii = fnArray.length; i < ii; i++) { - if (fnArray[i] !== OPS.dependency) { - continue; - } - - for (const obj of argsArray[i]) { - const objsPool = obj.startsWith("g_") ? this.commonObjs : this.objs; - const promise = new Promise(resolve => { - objsPool.get(obj, resolve); - }); - this.current.dependencies.push(promise); - } - } - return Promise.all(this.current.dependencies); - } - - transform(a, b, c, d, e, f) { - const transformMatrix = [a, b, c, d, e, f]; - this.transformMatrix = Util.transform( - this.transformMatrix, - transformMatrix - ); - this.tgrp = null; - } - - getSVG(operatorList, viewport) { - this.viewport = viewport; - - const svgElement = this._initialize(viewport); - return this.loadDependencies(operatorList).then(() => { - this.transformMatrix = IDENTITY_MATRIX; - this.executeOpTree(this.convertOpList(operatorList)); - return svgElement; - }); - } - - convertOpList(operatorList) { - const operatorIdMapping = this._operatorIdMapping; - const argsArray = operatorList.argsArray; - const fnArray = operatorList.fnArray; - const opList = []; - for (let i = 0, ii = fnArray.length; i < ii; i++) { - const fnId = fnArray[i]; - opList.push({ - fnId, - fn: operatorIdMapping[fnId], - args: argsArray[i], - }); - } - return opListToTree(opList); - } - - executeOpTree(opTree) { - for (const opTreeElement of opTree) { - const fn = opTreeElement.fn; - const fnId = opTreeElement.fnId; - const args = opTreeElement.args; - - switch (fnId | 0) { - case OPS.beginText: - this.beginText(); - break; - case OPS.dependency: - // Handled in `loadDependencies`, so no warning should be shown. - break; - case OPS.setLeading: - this.setLeading(args); - break; - case OPS.setLeadingMoveText: - this.setLeadingMoveText(args[0], args[1]); - break; - case OPS.setFont: - this.setFont(args); - break; - case OPS.showText: - this.showText(args[0]); - break; - case OPS.showSpacedText: - this.showText(args[0]); - break; - case OPS.endText: - this.endText(); - break; - case OPS.moveText: - this.moveText(args[0], args[1]); - break; - case OPS.setCharSpacing: - this.setCharSpacing(args[0]); - break; - case OPS.setWordSpacing: - this.setWordSpacing(args[0]); - break; - case OPS.setHScale: - this.setHScale(args[0]); - break; - case OPS.setTextMatrix: - this.setTextMatrix( - args[0], - args[1], - args[2], - args[3], - args[4], - args[5] - ); - break; - case OPS.setTextRise: - this.setTextRise(args[0]); - break; - case OPS.setTextRenderingMode: - this.setTextRenderingMode(args[0]); - break; - case OPS.setLineWidth: - this.setLineWidth(args[0]); - break; - case OPS.setLineJoin: - this.setLineJoin(args[0]); - break; - case OPS.setLineCap: - this.setLineCap(args[0]); - break; - case OPS.setMiterLimit: - this.setMiterLimit(args[0]); - break; - case OPS.setFillRGBColor: - this.setFillRGBColor(args[0], args[1], args[2]); - break; - case OPS.setStrokeRGBColor: - this.setStrokeRGBColor(args[0], args[1], args[2]); - break; - case OPS.setStrokeColorN: - this.setStrokeColorN(args); - break; - case OPS.setFillColorN: - this.setFillColorN(args); - break; - case OPS.shadingFill: - this.shadingFill(args[0]); - break; - case OPS.setDash: - this.setDash(args[0], args[1]); - break; - case OPS.setRenderingIntent: - this.setRenderingIntent(args[0]); - break; - case OPS.setFlatness: - this.setFlatness(args[0]); - break; - case OPS.setGState: - this.setGState(args[0]); - break; - case OPS.fill: - this.fill(); - break; - case OPS.eoFill: - this.eoFill(); - break; - case OPS.stroke: - this.stroke(); - break; - case OPS.fillStroke: - this.fillStroke(); - break; - case OPS.eoFillStroke: - this.eoFillStroke(); - break; - case OPS.clip: - this.clip("nonzero"); - break; - case OPS.eoClip: - this.clip("evenodd"); - break; - case OPS.paintSolidColorImageMask: - this.paintSolidColorImageMask(); - break; - case OPS.paintImageXObject: - this.paintImageXObject(args[0]); - break; - case OPS.paintInlineImageXObject: - this.paintInlineImageXObject(args[0]); - break; - case OPS.paintImageMaskXObject: - this.paintImageMaskXObject(args[0]); - break; - case OPS.paintFormXObjectBegin: - this.paintFormXObjectBegin(args[0], args[1]); - break; - case OPS.paintFormXObjectEnd: - this.paintFormXObjectEnd(); - break; - case OPS.closePath: - this.closePath(); - break; - case OPS.closeStroke: - this.closeStroke(); - break; - case OPS.closeFillStroke: - this.closeFillStroke(); - break; - case OPS.closeEOFillStroke: - this.closeEOFillStroke(); - break; - case OPS.nextLine: - this.nextLine(); - break; - case OPS.transform: - this.transform(args[0], args[1], args[2], args[3], args[4], args[5]); - break; - case OPS.constructPath: - this.constructPath(args[0], args[1]); - break; - case OPS.endPath: - this.endPath(); - break; - case 92: - this.group(opTreeElement.items); - break; - default: - warn(`Unimplemented operator ${fn}`); - break; - } - } - } - - setWordSpacing(wordSpacing) { - this.current.wordSpacing = wordSpacing; - } - - setCharSpacing(charSpacing) { - this.current.charSpacing = charSpacing; - } - - nextLine() { - this.moveText(0, this.current.leading); - } - - setTextMatrix(a, b, c, d, e, f) { - const current = this.current; - current.textMatrix = current.lineMatrix = [a, b, c, d, e, f]; - current.textMatrixScale = Math.hypot(a, b); - - current.x = current.lineX = 0; - current.y = current.lineY = 0; - - current.xcoords = []; - current.ycoords = []; - current.tspan = this.svgFactory.createElement("svg:tspan"); - current.tspan.setAttributeNS(null, "font-family", current.fontFamily); - current.tspan.setAttributeNS( - null, - "font-size", - `${pf(current.fontSize)}px` - ); - current.tspan.setAttributeNS(null, "y", pf(-current.y)); - - current.txtElement = this.svgFactory.createElement("svg:text"); - current.txtElement.append(current.tspan); - } - - beginText() { - const current = this.current; - current.x = current.lineX = 0; - current.y = current.lineY = 0; - current.textMatrix = IDENTITY_MATRIX; - current.lineMatrix = IDENTITY_MATRIX; - current.textMatrixScale = 1; - current.tspan = this.svgFactory.createElement("svg:tspan"); - current.txtElement = this.svgFactory.createElement("svg:text"); - current.txtgrp = this.svgFactory.createElement("svg:g"); - current.xcoords = []; - current.ycoords = []; - } - - moveText(x, y) { - const current = this.current; - current.x = current.lineX += x; - current.y = current.lineY += y; - - current.xcoords = []; - current.ycoords = []; - current.tspan = this.svgFactory.createElement("svg:tspan"); - current.tspan.setAttributeNS(null, "font-family", current.fontFamily); - current.tspan.setAttributeNS( - null, - "font-size", - `${pf(current.fontSize)}px` - ); - current.tspan.setAttributeNS(null, "y", pf(-current.y)); - } - - showText(glyphs) { - const current = this.current; - const font = current.font; - const fontSize = current.fontSize; - if (fontSize === 0) { - return; - } - - const fontSizeScale = current.fontSizeScale; - const charSpacing = current.charSpacing; - const wordSpacing = current.wordSpacing; - const fontDirection = current.fontDirection; - const textHScale = current.textHScale * fontDirection; - const vertical = font.vertical; - const spacingDir = vertical ? 1 : -1; - const defaultVMetrics = font.defaultVMetrics; - const widthAdvanceScale = fontSize * current.fontMatrix[0]; - - let x = 0; - for (const glyph of glyphs) { - if (glyph === null) { - // Word break - x += fontDirection * wordSpacing; - continue; - } else if (typeof glyph === "number") { - x += (spacingDir * glyph * fontSize) / 1000; - continue; - } - - const spacing = (glyph.isSpace ? wordSpacing : 0) + charSpacing; - const character = glyph.fontChar; - let scaledX, scaledY; - let width = glyph.width; - if (vertical) { - let vx; - const vmetric = glyph.vmetric || defaultVMetrics; - vx = glyph.vmetric ? vmetric[1] : width * 0.5; - vx = -vx * widthAdvanceScale; - const vy = vmetric[2] * widthAdvanceScale; - - width = vmetric ? -vmetric[0] : width; - scaledX = vx / fontSizeScale; - scaledY = (x + vy) / fontSizeScale; - } else { - scaledX = x / fontSizeScale; - scaledY = 0; - } - - if (glyph.isInFont || font.missingFile) { - current.xcoords.push(current.x + scaledX); - if (vertical) { - current.ycoords.push(-current.y + scaledY); - } - current.tspan.textContent += character; - } else { - // TODO: To assist with text selection, we should replace the missing - // character with a space character if charWidth is not zero. - // But we cannot just do "character = ' '", because the ' ' character - // might actually map to a different glyph. - } - - const charWidth = vertical - ? width * widthAdvanceScale - spacing * fontDirection - : width * widthAdvanceScale + spacing * fontDirection; - - x += charWidth; - } - current.tspan.setAttributeNS(null, "x", current.xcoords.map(pf).join(" ")); - if (vertical) { - current.tspan.setAttributeNS( - null, - "y", - current.ycoords.map(pf).join(" ") - ); - } else { - current.tspan.setAttributeNS(null, "y", pf(-current.y)); - } - - if (vertical) { - current.y -= x; - } else { - current.x += x * textHScale; - } - - current.tspan.setAttributeNS(null, "font-family", current.fontFamily); - current.tspan.setAttributeNS( - null, - "font-size", - `${pf(current.fontSize)}px` - ); - if (current.fontStyle !== SVG_DEFAULTS.fontStyle) { - current.tspan.setAttributeNS(null, "font-style", current.fontStyle); - } - if (current.fontWeight !== SVG_DEFAULTS.fontWeight) { - current.tspan.setAttributeNS(null, "font-weight", current.fontWeight); - } - - const fillStrokeMode = - current.textRenderingMode & TextRenderingMode.FILL_STROKE_MASK; - if ( - fillStrokeMode === TextRenderingMode.FILL || - fillStrokeMode === TextRenderingMode.FILL_STROKE - ) { - if (current.fillColor !== SVG_DEFAULTS.fillColor) { - current.tspan.setAttributeNS(null, "fill", current.fillColor); - } - if (current.fillAlpha < 1) { - current.tspan.setAttributeNS(null, "fill-opacity", current.fillAlpha); - } - } else if (current.textRenderingMode === TextRenderingMode.ADD_TO_PATH) { - // Workaround for Firefox: We must set fill="transparent" because - // fill="none" would generate an empty clipping path. - current.tspan.setAttributeNS(null, "fill", "transparent"); - } else { - current.tspan.setAttributeNS(null, "fill", "none"); - } - - if ( - fillStrokeMode === TextRenderingMode.STROKE || - fillStrokeMode === TextRenderingMode.FILL_STROKE - ) { - const lineWidthScale = 1 / (current.textMatrixScale || 1); - this._setStrokeAttributes(current.tspan, lineWidthScale); - } - - // Include the text rise in the text matrix since the `pm` function - // creates the SVG element's `translate` entry (work on a copy to avoid - // altering the original text matrix). - let textMatrix = current.textMatrix; - if (current.textRise !== 0) { - textMatrix = textMatrix.slice(); - textMatrix[5] += current.textRise; - } - - current.txtElement.setAttributeNS( - null, - "transform", - `${pm(textMatrix)} scale(${pf(textHScale)}, -1)` - ); - current.txtElement.setAttributeNS(XML_NS, "xml:space", "preserve"); - current.txtElement.append(current.tspan); - current.txtgrp.append(current.txtElement); - - this._ensureTransformGroup().append(current.txtElement); - } - - setLeadingMoveText(x, y) { - this.setLeading(-y); - this.moveText(x, y); - } - - addFontStyle(fontObj) { - if (!fontObj.data) { - throw new Error( - "addFontStyle: No font data available, " + - 'ensure that the "fontExtraProperties" API parameter is set.' - ); - } - if (!this.cssStyle) { - this.cssStyle = this.svgFactory.createElement("svg:style"); - this.cssStyle.setAttributeNS(null, "type", "text/css"); - this.defs.append(this.cssStyle); - } - - const url = createObjectURL( - fontObj.data, - fontObj.mimetype, - this.forceDataSchema - ); - this.cssStyle.textContent += - `@font-face { font-family: "${fontObj.loadedName}";` + - ` src: url(${url}); }\n`; - } - - setFont(details) { - const current = this.current; - const fontObj = this.commonObjs.get(details[0]); - let size = details[1]; - current.font = fontObj; - - if ( - this.embedFonts && - !fontObj.missingFile && - !this.embeddedFonts[fontObj.loadedName] - ) { - this.addFontStyle(fontObj); - this.embeddedFonts[fontObj.loadedName] = fontObj; - } - current.fontMatrix = fontObj.fontMatrix || FONT_IDENTITY_MATRIX; - - let bold = "normal"; - if (fontObj.black) { - bold = "900"; - } else if (fontObj.bold) { - bold = "bold"; - } - const italic = fontObj.italic ? "italic" : "normal"; - - if (size < 0) { - size = -size; - current.fontDirection = -1; - } else { - current.fontDirection = 1; - } - current.fontSize = size; - current.fontFamily = fontObj.loadedName; - current.fontWeight = bold; - current.fontStyle = italic; - - current.tspan = this.svgFactory.createElement("svg:tspan"); - current.tspan.setAttributeNS(null, "y", pf(-current.y)); - current.xcoords = []; - current.ycoords = []; - } - - endText() { - const current = this.current; - if ( - current.textRenderingMode & TextRenderingMode.ADD_TO_PATH_FLAG && - current.txtElement?.hasChildNodes() - ) { - // If no glyphs are shown (i.e. no child nodes), no clipping occurs. - current.element = current.txtElement; - this.clip("nonzero"); - this.endPath(); - } - } - - // Path properties - setLineWidth(width) { - if (width > 0) { - this.current.lineWidth = width; - } - } - - setLineCap(style) { - this.current.lineCap = LINE_CAP_STYLES[style]; - } - - setLineJoin(style) { - this.current.lineJoin = LINE_JOIN_STYLES[style]; - } - - setMiterLimit(limit) { - this.current.miterLimit = limit; - } - - setStrokeAlpha(strokeAlpha) { - this.current.strokeAlpha = strokeAlpha; - } - - setStrokeRGBColor(r, g, b) { - this.current.strokeColor = Util.makeHexColor(r, g, b); - } - - setFillAlpha(fillAlpha) { - this.current.fillAlpha = fillAlpha; - } - - setFillRGBColor(r, g, b) { - this.current.fillColor = Util.makeHexColor(r, g, b); - this.current.tspan = this.svgFactory.createElement("svg:tspan"); - this.current.xcoords = []; - this.current.ycoords = []; - } - - setStrokeColorN(args) { - this.current.strokeColor = this._makeColorN_Pattern(args); - } - - setFillColorN(args) { - this.current.fillColor = this._makeColorN_Pattern(args); - } - - shadingFill(args) { - const { width, height } = this.viewport; - const inv = Util.inverseTransform(this.transformMatrix); - const [x0, y0, x1, y1] = Util.getAxialAlignedBoundingBox( - [0, 0, width, height], - inv - ); - - const rect = this.svgFactory.createElement("svg:rect"); - rect.setAttributeNS(null, "x", x0); - rect.setAttributeNS(null, "y", y0); - rect.setAttributeNS(null, "width", x1 - x0); - rect.setAttributeNS(null, "height", y1 - y0); - rect.setAttributeNS(null, "fill", this._makeShadingPattern(args)); - if (this.current.fillAlpha < 1) { - rect.setAttributeNS(null, "fill-opacity", this.current.fillAlpha); - } - this._ensureTransformGroup().append(rect); - } - - /** - * @private - */ - _makeColorN_Pattern(args) { - if (args[0] === "TilingPattern") { - return this._makeTilingPattern(args); - } - return this._makeShadingPattern(args); - } - - /** - * @private - */ - _makeTilingPattern(args) { - const color = args[1]; - const operatorList = args[2]; - const matrix = args[3] || IDENTITY_MATRIX; - const [x0, y0, x1, y1] = args[4]; - const xstep = args[5]; - const ystep = args[6]; - const paintType = args[7]; - - const tilingId = `shading${shadingCount++}`; - const [tx0, ty0, tx1, ty1] = Util.normalizeRect([ - ...Util.applyTransform([x0, y0], matrix), - ...Util.applyTransform([x1, y1], matrix), - ]); - const [xscale, yscale] = Util.singularValueDecompose2dScale(matrix); - const txstep = xstep * xscale; - const tystep = ystep * yscale; - - const tiling = this.svgFactory.createElement("svg:pattern"); - tiling.setAttributeNS(null, "id", tilingId); - tiling.setAttributeNS(null, "patternUnits", "userSpaceOnUse"); - tiling.setAttributeNS(null, "width", txstep); - tiling.setAttributeNS(null, "height", tystep); - tiling.setAttributeNS(null, "x", `${tx0}`); - tiling.setAttributeNS(null, "y", `${ty0}`); - - // Save current state. - const svg = this.svg; - const transformMatrix = this.transformMatrix; - const fillColor = this.current.fillColor; - const strokeColor = this.current.strokeColor; - - const bbox = this.svgFactory.create(tx1 - tx0, ty1 - ty0); - this.svg = bbox; - this.transformMatrix = matrix; - if (paintType === 2) { - const cssColor = Util.makeHexColor(...color); - this.current.fillColor = cssColor; - this.current.strokeColor = cssColor; - } - this.executeOpTree(this.convertOpList(operatorList)); - - // Restore saved state. - this.svg = svg; - this.transformMatrix = transformMatrix; - this.current.fillColor = fillColor; - this.current.strokeColor = strokeColor; - - tiling.append(bbox.childNodes[0]); - this.defs.append(tiling); - return `url(#${tilingId})`; - } - - /** - * @private - */ - _makeShadingPattern(args) { - if (typeof args === "string") { - args = this.objs.get(args); - } - switch (args[0]) { - case "RadialAxial": - const shadingId = `shading${shadingCount++}`; - const colorStops = args[3]; - let gradient; - - switch (args[1]) { - case "axial": - const point0 = args[4]; - const point1 = args[5]; - gradient = this.svgFactory.createElement("svg:linearGradient"); - gradient.setAttributeNS(null, "id", shadingId); - gradient.setAttributeNS(null, "gradientUnits", "userSpaceOnUse"); - gradient.setAttributeNS(null, "x1", point0[0]); - gradient.setAttributeNS(null, "y1", point0[1]); - gradient.setAttributeNS(null, "x2", point1[0]); - gradient.setAttributeNS(null, "y2", point1[1]); - break; - case "radial": - const focalPoint = args[4]; - const circlePoint = args[5]; - const focalRadius = args[6]; - const circleRadius = args[7]; - gradient = this.svgFactory.createElement("svg:radialGradient"); - gradient.setAttributeNS(null, "id", shadingId); - gradient.setAttributeNS(null, "gradientUnits", "userSpaceOnUse"); - gradient.setAttributeNS(null, "cx", circlePoint[0]); - gradient.setAttributeNS(null, "cy", circlePoint[1]); - gradient.setAttributeNS(null, "r", circleRadius); - gradient.setAttributeNS(null, "fx", focalPoint[0]); - gradient.setAttributeNS(null, "fy", focalPoint[1]); - gradient.setAttributeNS(null, "fr", focalRadius); - break; - default: - throw new Error(`Unknown RadialAxial type: ${args[1]}`); - } - for (const colorStop of colorStops) { - const stop = this.svgFactory.createElement("svg:stop"); - stop.setAttributeNS(null, "offset", colorStop[0]); - stop.setAttributeNS(null, "stop-color", colorStop[1]); - gradient.append(stop); - } - this.defs.append(gradient); - return `url(#${shadingId})`; - case "Mesh": - warn("Unimplemented pattern Mesh"); - return null; - case "Dummy": - return "hotpink"; - default: - throw new Error(`Unknown IR type: ${args[0]}`); - } - } - - setDash(dashArray, dashPhase) { - this.current.dashArray = dashArray; - this.current.dashPhase = dashPhase; - } - - constructPath(ops, args) { - const current = this.current; - let x = current.x, - y = current.y; - let d = []; - let j = 0; - - for (const op of ops) { - switch (op | 0) { - case OPS.rectangle: - x = args[j++]; - y = args[j++]; - const width = args[j++]; - const height = args[j++]; - const xw = x + width; - const yh = y + height; - d.push( - "M", - pf(x), - pf(y), - "L", - pf(xw), - pf(y), - "L", - pf(xw), - pf(yh), - "L", - pf(x), - pf(yh), - "Z" - ); - break; - case OPS.moveTo: - x = args[j++]; - y = args[j++]; - d.push("M", pf(x), pf(y)); - break; - case OPS.lineTo: - x = args[j++]; - y = args[j++]; - d.push("L", pf(x), pf(y)); - break; - case OPS.curveTo: - x = args[j + 4]; - y = args[j + 5]; - d.push( - "C", - pf(args[j]), - pf(args[j + 1]), - pf(args[j + 2]), - pf(args[j + 3]), - pf(x), - pf(y) - ); - j += 6; - break; - case OPS.curveTo2: - d.push( - "C", - pf(x), - pf(y), - pf(args[j]), - pf(args[j + 1]), - pf(args[j + 2]), - pf(args[j + 3]) - ); - x = args[j + 2]; - y = args[j + 3]; - j += 4; - break; - case OPS.curveTo3: - x = args[j + 2]; - y = args[j + 3]; - d.push("C", pf(args[j]), pf(args[j + 1]), pf(x), pf(y), pf(x), pf(y)); - j += 4; - break; - case OPS.closePath: - d.push("Z"); - break; - } - } - - d = d.join(" "); - - if ( - current.path && - ops.length > 0 && - ops[0] !== OPS.rectangle && - ops[0] !== OPS.moveTo - ) { - // If a path does not start with an OPS.rectangle or OPS.moveTo, it has - // probably been divided into two OPS.constructPath operators by - // OperatorList. Append the commands to the previous path element. - d = current.path.getAttributeNS(null, "d") + d; - } else { - current.path = this.svgFactory.createElement("svg:path"); - this._ensureTransformGroup().append(current.path); - } - - current.path.setAttributeNS(null, "d", d); - current.path.setAttributeNS(null, "fill", "none"); - - // Saving a reference in current.element so that it can be addressed - // in 'fill' and 'stroke' - current.element = current.path; - current.setCurrentPoint(x, y); - } - - endPath() { - const current = this.current; - - // Painting operators end a path. - current.path = null; - - if (!this.pendingClip) { - return; - } - if (!current.element) { - this.pendingClip = null; - return; - } - - // Add the current path to a clipping path. - const clipId = `clippath${clipCount++}`; - const clipPath = this.svgFactory.createElement("svg:clipPath"); - clipPath.setAttributeNS(null, "id", clipId); - clipPath.setAttributeNS(null, "transform", pm(this.transformMatrix)); - - // A deep clone is needed when text is used as a clipping path. - const clipElement = current.element.cloneNode(true); - if (this.pendingClip === "evenodd") { - clipElement.setAttributeNS(null, "clip-rule", "evenodd"); - } else { - clipElement.setAttributeNS(null, "clip-rule", "nonzero"); - } - this.pendingClip = null; - clipPath.append(clipElement); - this.defs.append(clipPath); - - if (current.activeClipUrl) { - // The previous clipping group content can go out of order -- resetting - // cached clipGroups. - current.clipGroup = null; - for (const prev of this.extraStack) { - prev.clipGroup = null; - } - // Intersect with the previous clipping path. - clipPath.setAttributeNS(null, "clip-path", current.activeClipUrl); - } - current.activeClipUrl = `url(#${clipId})`; - - this.tgrp = null; - } - - clip(type) { - this.pendingClip = type; - } - - closePath() { - const current = this.current; - if (current.path) { - const d = `${current.path.getAttributeNS(null, "d")}Z`; - current.path.setAttributeNS(null, "d", d); - } - } - - setLeading(leading) { - this.current.leading = -leading; - } - - setTextRise(textRise) { - this.current.textRise = textRise; - } - - setTextRenderingMode(textRenderingMode) { - this.current.textRenderingMode = textRenderingMode; - } - - setHScale(scale) { - this.current.textHScale = scale / 100; - } - - setRenderingIntent(intent) { - // This operation is ignored since we haven't found a use case for it yet. - } - - setFlatness(flatness) { - // This operation is ignored since we haven't found a use case for it yet. - } - - setGState(states) { - for (const [key, value] of states) { - switch (key) { - case "LW": - this.setLineWidth(value); - break; - case "LC": - this.setLineCap(value); - break; - case "LJ": - this.setLineJoin(value); - break; - case "ML": - this.setMiterLimit(value); - break; - case "D": - this.setDash(value[0], value[1]); - break; - case "RI": - this.setRenderingIntent(value); - break; - case "FL": - this.setFlatness(value); - break; - case "Font": - this.setFont(value); - break; - case "CA": - this.setStrokeAlpha(value); - break; - case "ca": - this.setFillAlpha(value); - break; - default: - warn(`Unimplemented graphic state operator ${key}`); - break; - } - } - } - - fill() { - const current = this.current; - if (current.element) { - current.element.setAttributeNS(null, "fill", current.fillColor); - current.element.setAttributeNS(null, "fill-opacity", current.fillAlpha); - this.endPath(); - } - } - - stroke() { - const current = this.current; - if (current.element) { - this._setStrokeAttributes(current.element); - current.element.setAttributeNS(null, "fill", "none"); - this.endPath(); - } - } - - /** - * @private - */ - _setStrokeAttributes(element, lineWidthScale = 1) { - const current = this.current; - let dashArray = current.dashArray; - if (lineWidthScale !== 1 && dashArray.length > 0) { - dashArray = dashArray.map(function (value) { - return lineWidthScale * value; - }); - } - element.setAttributeNS(null, "stroke", current.strokeColor); - element.setAttributeNS(null, "stroke-opacity", current.strokeAlpha); - element.setAttributeNS(null, "stroke-miterlimit", pf(current.miterLimit)); - element.setAttributeNS(null, "stroke-linecap", current.lineCap); - element.setAttributeNS(null, "stroke-linejoin", current.lineJoin); - element.setAttributeNS( - null, - "stroke-width", - pf(lineWidthScale * current.lineWidth) + "px" - ); - element.setAttributeNS( - null, - "stroke-dasharray", - dashArray.map(pf).join(" ") - ); - element.setAttributeNS( - null, - "stroke-dashoffset", - pf(lineWidthScale * current.dashPhase) + "px" - ); - } - - eoFill() { - this.current.element?.setAttributeNS(null, "fill-rule", "evenodd"); - this.fill(); - } - - fillStroke() { - // Order is important since stroke wants fill to be none. - // First stroke, then if fill needed, it will be overwritten. - this.stroke(); - this.fill(); - } - - eoFillStroke() { - this.current.element?.setAttributeNS(null, "fill-rule", "evenodd"); - this.fillStroke(); - } - - closeStroke() { - this.closePath(); - this.stroke(); - } - - closeFillStroke() { - this.closePath(); - this.fillStroke(); - } - - closeEOFillStroke() { - this.closePath(); - this.eoFillStroke(); - } - - paintSolidColorImageMask() { - const rect = this.svgFactory.createElement("svg:rect"); - rect.setAttributeNS(null, "x", "0"); - rect.setAttributeNS(null, "y", "0"); - rect.setAttributeNS(null, "width", "1px"); - rect.setAttributeNS(null, "height", "1px"); - rect.setAttributeNS(null, "fill", this.current.fillColor); - - this._ensureTransformGroup().append(rect); - } - - paintImageXObject(objId) { - const imgData = this.getObject(objId); - if (!imgData) { - warn(`Dependent image with object ID ${objId} is not ready yet`); - return; - } - this.paintInlineImageXObject(imgData); - } - - paintInlineImageXObject(imgData, mask) { - const width = imgData.width; - const height = imgData.height; - - const imgSrc = convertImgDataToPng(imgData, this.forceDataSchema, !!mask); - const cliprect = this.svgFactory.createElement("svg:rect"); - cliprect.setAttributeNS(null, "x", "0"); - cliprect.setAttributeNS(null, "y", "0"); - cliprect.setAttributeNS(null, "width", pf(width)); - cliprect.setAttributeNS(null, "height", pf(height)); - this.current.element = cliprect; - this.clip("nonzero"); - - const imgEl = this.svgFactory.createElement("svg:image"); - imgEl.setAttributeNS(XLINK_NS, "xlink:href", imgSrc); - imgEl.setAttributeNS(null, "x", "0"); - imgEl.setAttributeNS(null, "y", pf(-height)); - imgEl.setAttributeNS(null, "width", pf(width) + "px"); - imgEl.setAttributeNS(null, "height", pf(height) + "px"); - imgEl.setAttributeNS( - null, - "transform", - `scale(${pf(1 / width)} ${pf(-1 / height)})` - ); - if (mask) { - mask.append(imgEl); - } else { - this._ensureTransformGroup().append(imgEl); - } - } - - paintImageMaskXObject(img) { - const imgData = this.getObject(img.data, img); - if (imgData.bitmap) { - warn( - "paintImageMaskXObject: ImageBitmap support is not implemented, " + - "ensure that the `isOffscreenCanvasSupported` API parameter is disabled." - ); - return; - } - const current = this.current; - const width = imgData.width; - const height = imgData.height; - const fillColor = current.fillColor; - - current.maskId = `mask${maskCount++}`; - const mask = this.svgFactory.createElement("svg:mask"); - mask.setAttributeNS(null, "id", current.maskId); - - const rect = this.svgFactory.createElement("svg:rect"); - rect.setAttributeNS(null, "x", "0"); - rect.setAttributeNS(null, "y", "0"); - rect.setAttributeNS(null, "width", pf(width)); - rect.setAttributeNS(null, "height", pf(height)); - rect.setAttributeNS(null, "fill", fillColor); - rect.setAttributeNS(null, "mask", `url(#${current.maskId})`); - - this.defs.append(mask); - this._ensureTransformGroup().append(rect); - - this.paintInlineImageXObject(imgData, mask); - } - - paintFormXObjectBegin(matrix, bbox) { - if (Array.isArray(matrix) && matrix.length === 6) { - this.transform( - matrix[0], - matrix[1], - matrix[2], - matrix[3], - matrix[4], - matrix[5] - ); - } - - if (bbox) { - const width = bbox[2] - bbox[0]; - const height = bbox[3] - bbox[1]; - - const cliprect = this.svgFactory.createElement("svg:rect"); - cliprect.setAttributeNS(null, "x", bbox[0]); - cliprect.setAttributeNS(null, "y", bbox[1]); - cliprect.setAttributeNS(null, "width", pf(width)); - cliprect.setAttributeNS(null, "height", pf(height)); - this.current.element = cliprect; - this.clip("nonzero"); - this.endPath(); - } - } - - paintFormXObjectEnd() {} - - /** - * @private - */ - _initialize(viewport) { - const svg = this.svgFactory.create(viewport.width, viewport.height); - - // Create the definitions element. - const definitions = this.svgFactory.createElement("svg:defs"); - svg.append(definitions); - this.defs = definitions; - - // Create the root group element, which acts a container for all other - // groups and applies the viewport transform. - const rootGroup = this.svgFactory.createElement("svg:g"); - rootGroup.setAttributeNS(null, "transform", pm(viewport.transform)); - svg.append(rootGroup); - - // For the construction of the SVG image we are only interested in the - // root group, so we expose it as the entry point of the SVG image for - // the other code in this class. - this.svg = rootGroup; - - return svg; - } - - /** - * @private - */ - _ensureClipGroup() { - if (!this.current.clipGroup) { - const clipGroup = this.svgFactory.createElement("svg:g"); - clipGroup.setAttributeNS(null, "clip-path", this.current.activeClipUrl); - this.svg.append(clipGroup); - this.current.clipGroup = clipGroup; - } - return this.current.clipGroup; - } - - /** - * @private - */ - _ensureTransformGroup() { - if (!this.tgrp) { - this.tgrp = this.svgFactory.createElement("svg:g"); - this.tgrp.setAttributeNS(null, "transform", pm(this.transformMatrix)); - if (this.current.activeClipUrl) { - this._ensureClipGroup().append(this.tgrp); - } else { - this.svg.append(this.tgrp); - } - } - return this.tgrp; - } -} - -export { SVGGraphics }; diff --git a/src/pdf.js b/src/pdf.js index 8d54f67f7..16f18a3c1 100644 --- a/src/pdf.js +++ b/src/pdf.js @@ -50,7 +50,6 @@ import { getDocument, PDFDataRangeTransport, PDFWorker, - SVGGraphics, version, } from "./display/api.js"; import { @@ -119,7 +118,6 @@ export { renderTextLayer, setLayerDimensions, shadow, - SVGGraphics, UnexpectedResponseException, updateTextLayer, Util, diff --git a/test/unit/clitests.json b/test/unit/clitests.json index 94974d54c..666204369 100644 --- a/test/unit/clitests.json +++ b/test/unit/clitests.json @@ -17,7 +17,6 @@ "crypto_spec.js", "custom_spec.js", "default_appearance_spec.js", - "display_svg_spec.js", "display_utils_spec.js", "document_spec.js", "editor_spec.js", diff --git a/test/unit/display_svg_spec.js b/test/unit/display_svg_spec.js deleted file mode 100644 index f20c531e1..000000000 --- a/test/unit/display_svg_spec.js +++ /dev/null @@ -1,163 +0,0 @@ -/* Copyright 2017 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. - */ -/* globals __non_webpack_require__ */ - -import { buildGetDocumentParams } from "./test_utils.js"; -import { getDocument } from "../../src/display/api.js"; -import { isNodeJS } from "../../src/shared/util.js"; -import { SVGGraphics } from "../../src/display/svg.js"; - -const XLINK_NS = "http://www.w3.org/1999/xlink"; - -// withZlib(true, callback); = run test with require('zlib') if possible. -// withZlib(false, callback); = run test without require('zlib').deflateSync. -// The return value of callback is returned as-is. -function withZlib(isZlibRequired, callback) { - if (isZlibRequired) { - // We could try to polyfill zlib in the browser, e.g. using pako. - // For now, only support zlib functionality on Node.js - if (!isNodeJS) { - throw new Error("zlib test can only be run in Node.js"); - } - - return callback(); - } - - if (!isNodeJS) { - // Assume that require('zlib') is unavailable in non-Node. - return callback(); - } - - const zlib = __non_webpack_require__("zlib"); - const deflateSync = zlib.deflateSync; - zlib.deflateSync = disabledDeflateSync; - function disabledDeflateSync() { - throw new Error("zlib.deflateSync is explicitly disabled for testing."); - } - function restoreDeflateSync() { - if (zlib.deflateSync === disabledDeflateSync) { - zlib.deflateSync = deflateSync; - } - } - const promise = callback(); - promise.then(restoreDeflateSync, restoreDeflateSync); - return promise; -} - -describe("SVGGraphics", function () { - let loadingTask; - let page; - - beforeAll(async function () { - loadingTask = getDocument( - buildGetDocumentParams("xobject-image.pdf", { - isOffscreenCanvasSupported: false, - }) - ); - const doc = await loadingTask.promise; - page = await doc.getPage(1); - }); - - afterAll(async function () { - await loadingTask.destroy(); - }); - - describe("paintImageXObject", function () { - function getSVGImage() { - let svgGfx; - return page - .getOperatorList() - .then(function (opList) { - const forceDataSchema = true; - svgGfx = new SVGGraphics(page.commonObjs, page.objs, forceDataSchema); - return svgGfx.loadDependencies(opList); - }) - .then(function () { - let svgImg; - // A mock to steal the svg:image element from paintInlineImageXObject. - const elementContainer = { - append(...elements) { - svgImg = elements.at(-1); - }, - }; - - // This points to the XObject image in xobject-image.pdf. - const xobjectObjId = "img_p0_1"; - if (isNodeJS) { - const { setStubs } = __non_webpack_require__( - "../../examples/node/domstubs.js" - ); - setStubs(global); - } - try { - const imgData = svgGfx.objs.get(xobjectObjId); - svgGfx.paintInlineImageXObject(imgData, elementContainer); - } finally { - if (isNodeJS) { - const { unsetStubs } = __non_webpack_require__( - "../../examples/node/domstubs.js" - ); - unsetStubs(global); - } - } - return svgImg; - }); - } - - it('should fail require("zlib") unless in Node.js', function () { - function testFunc() { - __non_webpack_require__("zlib"); - } - if (isNodeJS) { - // Verifies that the script loader replaces __non_webpack_require__ with - // require. - expect(testFunc.toString()).toMatch(/\srequire\(["']zlib["']\)/); - expect(testFunc).not.toThrow(); - } else { - // require not defined, require('zlib') not a module, etc. - expect(testFunc).toThrow(); - } - }); - - it("should produce a reasonably small svg:image", async function () { - if (!isNodeJS) { - pending("zlib.deflateSync is not supported in non-Node environments."); - } - const svgImg = await withZlib(true, getSVGImage); - expect(svgImg.nodeName).toBe("svg:image"); - expect(svgImg.getAttributeNS(null, "width")).toBe("200px"); - expect(svgImg.getAttributeNS(null, "height")).toBe("100px"); - const imgUrl = svgImg.getAttributeNS(XLINK_NS, "href"); - // forceDataSchema = true, so the generated URL should be a data:-URL. - expect(imgUrl).toMatch(/^data:image\/png;base64,/); - // Test whether the generated image has a reasonable file size. - // I obtained a data URL of size 366 with Node 8.1.3 and zlib 1.2.11. - // Without zlib (uncompressed), the size of the data URL was excessive - // (80246). - expect(imgUrl.length).toBeLessThan(367); - }); - - it("should be able to produce a svg:image without zlib", async function () { - const svgImg = await withZlib(false, getSVGImage); - expect(svgImg.nodeName).toBe("svg:image"); - expect(svgImg.getAttributeNS(null, "width")).toBe("200px"); - expect(svgImg.getAttributeNS(null, "height")).toBe("100px"); - const imgUrl = svgImg.getAttributeNS(XLINK_NS, "href"); - expect(imgUrl).toMatch(/^data:image\/png;base64,/); - // The size of our naively generated PNG file is excessive :( - expect(imgUrl.length).toBe(80246); - }); - }); -}); diff --git a/test/unit/jasmine-boot.js b/test/unit/jasmine-boot.js index 27ebf7a8a..96d48558d 100644 --- a/test/unit/jasmine-boot.js +++ b/test/unit/jasmine-boot.js @@ -58,7 +58,6 @@ async function initializePDFJS(callback) { "pdfjs-test/unit/crypto_spec.js", "pdfjs-test/unit/custom_spec.js", "pdfjs-test/unit/default_appearance_spec.js", - "pdfjs-test/unit/display_svg_spec.js", "pdfjs-test/unit/display_utils_spec.js", "pdfjs-test/unit/document_spec.js", "pdfjs-test/unit/editor_spec.js", diff --git a/test/unit/pdf_spec.js b/test/unit/pdf_spec.js index c2f20eeb4..413b9708d 100644 --- a/test/unit/pdf_spec.js +++ b/test/unit/pdf_spec.js @@ -40,7 +40,6 @@ import { getDocument, PDFDataRangeTransport, PDFWorker, - SVGGraphics, version, } from "../../src/display/api.js"; import { @@ -105,7 +104,6 @@ const expectedAPI = Object.freeze({ renderTextLayer, setLayerDimensions, shadow, - SVGGraphics, UnexpectedResponseException, updateTextLayer, Util, diff --git a/test/unit/unit_test.html b/test/unit/unit_test.html index 222c3c6e3..7d950eb2b 100644 --- a/test/unit/unit_test.html +++ b/test/unit/unit_test.html @@ -22,7 +22,6 @@ "display-network": "../../src/display/network.js", "display-node_stream": "../../src/display/stubs.js", "display-node_utils": "../../src/display/stubs.js", - "display-svg": "../../src/display/stubs.js", "web-alt_text_manager": "../../web/alt_text_manager.js", "web-annotation_editor_params": "../../web/annotation_editor_params.js", diff --git a/tsconfig.json b/tsconfig.json index f549d383c..b2f732b15 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,6 @@ "display-network": ["./src/display/network"], "display-node_stream": ["./src/display/node_stream"], "display-node_utils": ["./src/display/node_utils"], - "display-svg": ["./src/display/svg"], "pdfjs-lib": ["./src/pdf"] } }, diff --git a/web/pdfjs.js b/web/pdfjs.js index 489cd0901..cb9f39d74 100644 --- a/web/pdfjs.js +++ b/web/pdfjs.js @@ -51,7 +51,6 @@ const { renderTextLayer, setLayerDimensions, shadow, - SVGGraphics, UnexpectedResponseException, updateTextLayer, Util, @@ -98,7 +97,6 @@ export { renderTextLayer, setLayerDimensions, shadow, - SVGGraphics, UnexpectedResponseException, updateTextLayer, Util, diff --git a/web/viewer-geckoview.html b/web/viewer-geckoview.html index bf359720b..6f42a2f56 100644 --- a/web/viewer-geckoview.html +++ b/web/viewer-geckoview.html @@ -54,7 +54,6 @@ See https://github.com/adobe-type-tools/cmap-resources "display-network": "../src/display/network.js", "display-node_stream": "../src/display/stubs.js", "display-node_utils": "../src/display/stubs.js", - "display-svg": "../src/display/stubs.js", "web-alt_text_manager": "./stubs-geckoview.js", "web-annotation_editor_params": "./stubs-geckoview.js", diff --git a/web/viewer.html b/web/viewer.html index 327efd365..cd1f6f50e 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -65,7 +65,6 @@ See https://github.com/adobe-type-tools/cmap-resources "display-network": "../src/display/network.js", "display-node_stream": "../src/display/stubs.js", "display-node_utils": "../src/display/stubs.js", - "display-svg": "../src/display/stubs.js", "web-alt_text_manager": "./alt_text_manager.js", "web-annotation_editor_params": "./annotation_editor_params.js",