2014-05-20 08:53:40 +09:00
|
|
|
/* 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.
|
|
|
|
*/
|
2017-07-06 22:08:37 +09:00
|
|
|
/* globals __non_webpack_require__ */
|
2019-03-24 05:55:37 +09:00
|
|
|
/* eslint no-var: error */
|
2014-05-20 08:53:40 +09:00
|
|
|
|
2017-04-02 21:25:33 +09:00
|
|
|
import {
|
2018-01-06 21:10:58 +09:00
|
|
|
createObjectURL, FONT_IDENTITY_MATRIX, IDENTITY_MATRIX, ImageKind, isNum, OPS,
|
2018-08-29 06:42:07 +09:00
|
|
|
TextRenderingMode, Util, warn
|
2017-04-02 21:25:33 +09:00
|
|
|
} from '../shared/util';
|
2019-02-23 23:41:02 +09:00
|
|
|
import { DOMSVGFactory } from './display_utils';
|
2018-01-06 21:10:58 +09:00
|
|
|
import isNodeJS from '../shared/is_node';
|
2017-04-02 21:25:33 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
let SVGGraphics = function() {
|
2017-04-02 21:25:33 +09:00
|
|
|
throw new Error('Not implemented: SVGGraphics');
|
|
|
|
};
|
|
|
|
|
2018-01-18 02:20:00 +09:00
|
|
|
if (typeof PDFJSDev === 'undefined' || PDFJSDev.test('GENERIC')) {
|
2015-11-22 01:32:47 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
const SVG_DEFAULTS = {
|
2014-08-15 06:13:15 +09:00
|
|
|
fontStyle: 'normal',
|
|
|
|
fontWeight: 'normal',
|
Fix inconsistent spacing and trailing commas in objects in remaining `src/` files, so we can enable the `comma-dangle` and `object-curly-spacing` ESLint rules later on
http://eslint.org/docs/rules/comma-dangle
http://eslint.org/docs/rules/object-curly-spacing
Given that we currently have quite inconsistent object formatting, fixing this in *one* big patch probably wouldn't be feasible (since I cannot imagine anyone wanting to review that); hence I've opted to try and do this piecewise instead.
Please note: This patch was created automatically, using the ESLint `--fix` command line option. In a couple of places this caused lines to become too long, and I've fixed those manually; please refer to the interdiff below for the only hand-edits in this patch.
```diff
diff --git a/src/display/canvas.js b/src/display/canvas.js
index 5739f6f2..4216b2d2 100644
--- a/src/display/canvas.js
+++ b/src/display/canvas.js
@@ -2071,7 +2071,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
var map = [];
for (var i = 0, ii = positions.length; i < ii; i += 2) {
map.push({ transform: [scaleX, 0, 0, scaleY, positions[i],
- positions[i + 1]], x: 0, y: 0, w: width, h: height, });
+ positions[i + 1]], x: 0, y: 0, w: width, h: height, });
}
this.paintInlineImageXObjectGroup(imgData, map);
},
diff --git a/src/display/svg.js b/src/display/svg.js
index 9eb05dfa..2aa21482 100644
--- a/src/display/svg.js
+++ b/src/display/svg.js
@@ -458,7 +458,11 @@ SVGGraphics = (function SVGGraphicsClosure() {
for (var x = 0; x < fnArrayLen; x++) {
var fnId = fnArray[x];
- opList.push({ 'fnId': fnId, 'fn': REVOPS[fnId], 'args': argsArray[x], });
+ opList.push({
+ 'fnId': fnId,
+ 'fn': REVOPS[fnId],
+ 'args': argsArray[x],
+ });
}
return opListToTree(opList);
},
```
2017-06-02 18:26:37 +09:00
|
|
|
fillColor: '#000000',
|
2014-08-15 06:13:15 +09:00
|
|
|
};
|
2019-03-24 05:55:37 +09:00
|
|
|
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'];
|
2014-08-15 06:13:15 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
const convertImgDataToPng = (function() {
|
|
|
|
const PNG_HEADER =
|
2014-08-10 05:53:05 +09:00
|
|
|
new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
2019-03-24 05:55:37 +09:00
|
|
|
const CHUNK_WRAPPER_SIZE = 12;
|
2014-08-10 05:53:05 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
const crcTable = new Int32Array(256);
|
|
|
|
for (let i = 0; i < 256; i++) {
|
|
|
|
let c = i;
|
|
|
|
for (let h = 0; h < 8; h++) {
|
2014-08-08 01:30:48 +09:00
|
|
|
if (c & 1) {
|
|
|
|
c = 0xedB88320 ^ ((c >> 1) & 0x7fffffff);
|
|
|
|
} else {
|
|
|
|
c = (c >> 1) & 0x7fffffff;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
crcTable[i] = c;
|
|
|
|
}
|
|
|
|
|
|
|
|
function crc32(data, start, end) {
|
2019-03-24 05:55:37 +09:00
|
|
|
let crc = -1;
|
|
|
|
for (let i = start; i < end; i++) {
|
|
|
|
const a = (crc ^ data[i]) & 0xff;
|
|
|
|
const b = crcTable[a];
|
2014-08-08 01:30:48 +09:00
|
|
|
crc = (crc >>> 8) ^ b;
|
|
|
|
}
|
|
|
|
return crc ^ -1;
|
|
|
|
}
|
|
|
|
|
2014-08-10 05:53:05 +09:00
|
|
|
function writePngChunk(type, body, data, offset) {
|
2019-03-24 05:55:37 +09:00
|
|
|
let p = offset;
|
|
|
|
const len = body.length;
|
2014-08-08 01:30:48 +09:00
|
|
|
|
2014-08-10 05:53:05 +09:00
|
|
|
data[p] = len >> 24 & 0xff;
|
|
|
|
data[p + 1] = len >> 16 & 0xff;
|
|
|
|
data[p + 2] = len >> 8 & 0xff;
|
|
|
|
data[p + 3] = len & 0xff;
|
|
|
|
p += 4;
|
2014-08-08 01:30:48 +09:00
|
|
|
|
2014-08-10 05:53:05 +09:00
|
|
|
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;
|
2014-08-08 01:30:48 +09:00
|
|
|
|
2014-08-10 05:53:05 +09:00
|
|
|
data.set(body, p);
|
|
|
|
p += body.length;
|
2014-08-08 01:30:48 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
const crc = crc32(data, offset + 4, p);
|
2014-08-10 05:53:05 +09:00
|
|
|
data[p] = crc >> 24 & 0xff;
|
|
|
|
data[p + 1] = crc >> 16 & 0xff;
|
|
|
|
data[p + 2] = crc >> 8 & 0xff;
|
|
|
|
data[p + 3] = crc & 0xff;
|
2014-08-08 01:30:48 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
function adler32(data, start, end) {
|
2019-03-24 05:55:37 +09:00
|
|
|
let a = 1;
|
|
|
|
let b = 0;
|
|
|
|
for (let i = start; i < end; ++i) {
|
2014-08-08 01:30:48 +09:00
|
|
|
a = (a + (data[i] & 0xff)) % 65521;
|
|
|
|
b = (b + a) % 65521;
|
|
|
|
}
|
|
|
|
return (b << 16) | a;
|
|
|
|
}
|
|
|
|
|
2017-07-05 02:13:34 +09:00
|
|
|
/**
|
|
|
|
* @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) {
|
2017-07-06 22:08:37 +09:00
|
|
|
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.
|
2019-03-24 05:55:37 +09:00
|
|
|
let input;
|
2017-07-06 22:08:37 +09:00
|
|
|
// eslint-disable-next-line no-undef
|
|
|
|
if (parseInt(process.versions.node) >= 8) {
|
|
|
|
input = literals;
|
|
|
|
} else {
|
|
|
|
// eslint-disable-next-line no-undef
|
|
|
|
input = new Buffer(literals);
|
|
|
|
}
|
2019-03-24 05:55:37 +09:00
|
|
|
const output = __non_webpack_require__('zlib')
|
2017-07-06 22:08:37 +09:00
|
|
|
.deflateSync(input, { level: 9, });
|
|
|
|
return output instanceof Uint8Array ? output : new Uint8Array(output);
|
|
|
|
} catch (e) {
|
|
|
|
warn('Not compressing PNG because zlib.deflateSync is unavailable: ' + e);
|
|
|
|
}
|
|
|
|
|
2017-07-05 02:13:34 +09:00
|
|
|
return deflateSyncUncompressed(literals);
|
|
|
|
}
|
|
|
|
|
|
|
|
// An implementation of DEFLATE with compression level 0 (Z_NO_COMPRESSION).
|
|
|
|
function deflateSyncUncompressed(literals) {
|
2019-03-24 05:55:37 +09:00
|
|
|
let len = literals.length;
|
|
|
|
const maxBlockLength = 0xFFFF;
|
2017-07-05 02:13:34 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
const deflateBlocks = Math.ceil(len / maxBlockLength);
|
|
|
|
const idat = new Uint8Array(2 + len + deflateBlocks * 5 + 4);
|
|
|
|
let pi = 0;
|
2017-07-05 02:13:34 +09:00
|
|
|
idat[pi++] = 0x78; // compression method and flags
|
|
|
|
idat[pi++] = 0x9c; // flags
|
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
let pos = 0;
|
2017-07-05 02:13:34 +09:00
|
|
|
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;
|
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
const adler = adler32(literals, 0, literals.length); // checksum
|
2017-07-05 02:13:34 +09:00
|
|
|
idat[pi++] = adler >> 24 & 0xff;
|
|
|
|
idat[pi++] = adler >> 16 & 0xff;
|
|
|
|
idat[pi++] = adler >> 8 & 0xff;
|
|
|
|
idat[pi++] = adler & 0xff;
|
|
|
|
return idat;
|
|
|
|
}
|
|
|
|
|
2017-11-29 04:24:27 +09:00
|
|
|
function encode(imgData, kind, forceDataSchema, isMask) {
|
2019-03-24 05:55:37 +09:00
|
|
|
const width = imgData.width;
|
|
|
|
const height = imgData.height;
|
|
|
|
let bitDepth, colorType, lineSize;
|
|
|
|
const bytes = imgData.data;
|
2014-08-14 03:31:21 +09:00
|
|
|
|
2014-08-08 01:30:48 +09:00
|
|
|
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');
|
|
|
|
}
|
|
|
|
|
2014-08-10 05:53:05 +09:00
|
|
|
// prefix every row with predictor 0
|
2019-03-24 05:55:37 +09:00
|
|
|
const literals = new Uint8Array((1 + lineSize) * height);
|
|
|
|
let offsetLiterals = 0, offsetBytes = 0;
|
|
|
|
for (let y = 0; y < height; ++y) {
|
2014-08-10 05:53:05 +09:00
|
|
|
literals[offsetLiterals++] = 0; // no prediction
|
2014-08-08 01:30:48 +09:00
|
|
|
literals.set(bytes.subarray(offsetBytes, offsetBytes + lineSize),
|
|
|
|
offsetLiterals);
|
|
|
|
offsetBytes += lineSize;
|
|
|
|
offsetLiterals += lineSize;
|
|
|
|
}
|
2014-08-10 05:53:05 +09:00
|
|
|
|
2017-11-29 04:24:27 +09:00
|
|
|
if (kind === ImageKind.GRAYSCALE_1BPP && isMask) {
|
|
|
|
// inverting for image masks
|
2014-08-10 05:53:05 +09:00
|
|
|
offsetLiterals = 0;
|
2019-03-24 05:55:37 +09:00
|
|
|
for (let y = 0; y < height; y++) {
|
2014-08-10 05:53:05 +09:00
|
|
|
offsetLiterals++; // skipping predictor
|
2019-03-24 05:55:37 +09:00
|
|
|
for (let i = 0; i < lineSize; i++) {
|
2014-08-10 05:53:05 +09:00
|
|
|
literals[offsetLiterals++] ^= 0xFF;
|
|
|
|
}
|
2014-08-08 01:30:48 +09:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
const ihdr = new Uint8Array([
|
2014-08-08 01:30:48 +09:00
|
|
|
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
|
|
|
|
]);
|
2019-03-24 05:55:37 +09:00
|
|
|
const idat = deflateSync(literals);
|
2014-08-08 01:30:48 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
// 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;
|
2014-08-10 05:53:05 +09:00
|
|
|
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);
|
2014-08-08 01:30:48 +09:00
|
|
|
|
2016-03-29 04:49:22 +09:00
|
|
|
return createObjectURL(data, 'image/png', forceDataSchema);
|
2014-08-08 01:30:48 +09:00
|
|
|
}
|
|
|
|
|
2017-11-29 04:24:27 +09:00
|
|
|
return function convertImgDataToPng(imgData, forceDataSchema, isMask) {
|
2019-03-24 05:55:37 +09:00
|
|
|
const kind = (imgData.kind === undefined ?
|
|
|
|
ImageKind.GRAYSCALE_1BPP : imgData.kind);
|
2017-11-29 04:24:27 +09:00
|
|
|
return encode(imgData, kind, forceDataSchema, isMask);
|
2014-08-08 01:30:48 +09:00
|
|
|
};
|
|
|
|
})();
|
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
class SVGExtraState {
|
|
|
|
constructor() {
|
2014-05-20 08:53:40 +09:00
|
|
|
this.fontSizeScale = 1;
|
2014-08-15 06:13:15 +09:00
|
|
|
this.fontWeight = SVG_DEFAULTS.fontWeight;
|
2014-08-12 01:18:32 +09:00
|
|
|
this.fontSize = 0;
|
2014-07-04 01:03:31 +09:00
|
|
|
|
2014-05-20 08:53:40 +09:00
|
|
|
this.textMatrix = IDENTITY_MATRIX;
|
|
|
|
this.fontMatrix = FONT_IDENTITY_MATRIX;
|
|
|
|
this.leading = 0;
|
2018-08-29 06:42:07 +09:00
|
|
|
this.textRenderingMode = TextRenderingMode.FILL;
|
2019-03-06 05:54:25 +09:00
|
|
|
this.textMatrixScale = 1;
|
2014-08-14 03:31:21 +09:00
|
|
|
|
2014-05-20 08:53:40 +09:00
|
|
|
// Current point (in user coordinates)
|
|
|
|
this.x = 0;
|
|
|
|
this.y = 0;
|
2014-08-14 03:31:21 +09:00
|
|
|
|
2014-05-20 08:53:40 +09:00
|
|
|
// Start of text line (in text coordinates)
|
|
|
|
this.lineX = 0;
|
|
|
|
this.lineY = 0;
|
2014-08-14 03:31:21 +09:00
|
|
|
|
2014-05-20 08:53:40 +09:00
|
|
|
// Character and word spacing
|
|
|
|
this.charSpacing = 0;
|
|
|
|
this.wordSpacing = 0;
|
|
|
|
this.textHScale = 1;
|
|
|
|
this.textRise = 0;
|
2014-08-14 03:31:21 +09:00
|
|
|
|
|
|
|
// Default foreground and background colors
|
2014-08-15 06:13:15 +09:00
|
|
|
this.fillColor = SVG_DEFAULTS.fillColor;
|
2014-05-20 08:53:40 +09:00
|
|
|
this.strokeColor = '#000000';
|
|
|
|
|
|
|
|
this.fillAlpha = 1;
|
|
|
|
this.strokeAlpha = 1;
|
|
|
|
this.lineWidth = 1;
|
|
|
|
this.lineJoin = '';
|
|
|
|
this.lineCap = '';
|
|
|
|
this.miterLimit = 0;
|
2015-02-03 00:12:52 +09:00
|
|
|
|
2014-07-04 01:07:05 +09:00
|
|
|
this.dashArray = [];
|
|
|
|
this.dashPhase = 0;
|
2014-05-20 08:53:40 +09:00
|
|
|
|
|
|
|
this.dependencies = [];
|
2014-07-04 00:59:04 +09:00
|
|
|
|
|
|
|
// Clipping
|
2016-10-18 06:09:24 +09:00
|
|
|
this.activeClipUrl = null;
|
|
|
|
this.clipGroup = null;
|
2014-08-08 01:30:48 +09:00
|
|
|
|
|
|
|
this.maskId = '';
|
2014-05-20 08:53:40 +09:00
|
|
|
}
|
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
clone() {
|
|
|
|
return Object.create(this);
|
|
|
|
}
|
2014-08-12 01:18:32 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
setCurrentPoint(x, y) {
|
|
|
|
this.x = x;
|
|
|
|
this.y = y;
|
2014-08-12 01:18:32 +09:00
|
|
|
}
|
2019-03-24 05:55:37 +09:00
|
|
|
}
|
2014-08-12 01:18:32 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
// eslint-disable-next-line no-inner-declarations
|
|
|
|
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[opTree.length - 1].items;
|
|
|
|
continue;
|
2014-05-20 08:53:40 +09:00
|
|
|
}
|
2019-03-24 05:55:37 +09:00
|
|
|
|
|
|
|
if (opListElement.fn === 'restore') {
|
|
|
|
opTree = tmp.pop();
|
|
|
|
} else {
|
|
|
|
opTree.push(opListElement);
|
2014-08-12 01:18:32 +09:00
|
|
|
}
|
|
|
|
}
|
2019-03-24 05:55:37 +09:00
|
|
|
return opTree;
|
|
|
|
}
|
2014-05-20 08:53:40 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
/**
|
|
|
|
* Format a float number as a string.
|
|
|
|
*
|
|
|
|
* @param value {number} - The float number to format.
|
|
|
|
* @returns {string}
|
|
|
|
*/
|
|
|
|
// eslint-disable-next-line no-inner-declarations
|
|
|
|
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}
|
|
|
|
*/
|
|
|
|
// eslint-disable-next-line no-inner-declarations
|
|
|
|
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 '';
|
2014-08-12 01:18:32 +09:00
|
|
|
}
|
2019-03-24 05:55:37 +09:00
|
|
|
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])})`;
|
2014-05-20 08:53:40 +09:00
|
|
|
}
|
|
|
|
}
|
2019-03-24 05:55:37 +09:00
|
|
|
return `matrix(${pf(m[0])} ${pf(m[1])} ${pf(m[2])} ${pf(m[3])} ${pf(m[4])} ` +
|
|
|
|
`${pf(m[5])})`;
|
|
|
|
}
|
2014-05-20 08:53:40 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
// 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;
|
|
|
|
|
|
|
|
SVGGraphics = class SVGGraphics {
|
|
|
|
constructor(commonObjs, objs, forceDataSchema) {
|
2017-07-24 07:09:18 +09:00
|
|
|
this.svgFactory = new DOMSVGFactory();
|
|
|
|
|
2014-05-20 08:53:40 +09:00
|
|
|
this.current = new SVGExtraState();
|
|
|
|
this.transformMatrix = IDENTITY_MATRIX; // Graphics state matrix
|
|
|
|
this.transformStack = [];
|
|
|
|
this.extraStack = [];
|
|
|
|
this.commonObjs = commonObjs;
|
2014-06-25 05:24:00 +09:00
|
|
|
this.objs = objs;
|
Move svg:clipPath generation from clip to endPath
In the PDF from issue 8527, the clip operator (W) shows up before a path
is defined. The current SVG backend however expects a path to exist
before generating a `<svg:clipPath>` element.
In the example, the path was defined after the clip, followed by a
endPath operator (n).
So this commit fixes the bug by moving the path generation logic from
clip to endPath.
Our canvas backend appears to use similar logic:
`CanvasGraphics_endPath` calls `consumePath`, which in turn draws the
clip and resets the `pendingClip` state. The canvas backend calls
`consumePath` from multiple other places, so we probably need to check
whether doing so is also necessary for the SVG backend.
I scanned our corpus of PDF files in test/pdfs, and found that in every
instance (except for one), the "W" PDF operator (clip) is immediately
followed by "n" (endPath). The new test from this commit (clippath.pdf)
starts with "W", followed by a path definition and then "n".
# Commands used to find some of the clipping commands:
grep -ra '^W$' -C7 | less -S
grep -ra '^W ' -C7 | less -S
grep -ra ' W$' -C7 | less -S
test/pdfs/issue6413.pdf is the only file where "W" (a tline 55) is not
followed by "n". In fact, the "W" is the last operation of a series of
XObject painting operations, and removing it does not have any effect
on the rendered PDF (confirmed by looking at the output of PDF.js's
canvas backend, and ImageMagick's convert command).
2017-06-19 19:40:48 +09:00
|
|
|
this.pendingClip = null;
|
2014-05-20 08:53:40 +09:00
|
|
|
this.pendingEOFill = false;
|
2014-08-15 05:11:27 +09:00
|
|
|
|
|
|
|
this.embedFonts = false;
|
2016-01-28 02:04:13 +09:00
|
|
|
this.embeddedFonts = Object.create(null);
|
2014-08-15 05:11:27 +09:00
|
|
|
this.cssStyle = null;
|
2016-03-29 04:49:22 +09:00
|
|
|
this.forceDataSchema = !!forceDataSchema;
|
2019-04-06 23:52:29 +09:00
|
|
|
|
|
|
|
// 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;
|
|
|
|
}
|
2014-05-20 08:53:40 +09:00
|
|
|
}
|
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
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();
|
|
|
|
}
|
2014-05-20 08:53:40 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
loadDependencies(operatorList) {
|
|
|
|
const fnArray = operatorList.fnArray;
|
|
|
|
const argsArray = operatorList.argsArray;
|
2014-05-20 08:53:40 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
for (let i = 0, ii = fnArray.length; i < ii; i++) {
|
|
|
|
if (fnArray[i] !== OPS.dependency) {
|
|
|
|
continue;
|
2014-05-20 08:53:40 +09:00
|
|
|
}
|
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
for (const obj of argsArray[i]) {
|
|
|
|
const objsPool = obj.startsWith('g_') ? this.commonObjs : this.objs;
|
|
|
|
const promise = new Promise((resolve) => {
|
|
|
|
objsPool.get(obj, resolve);
|
Fix inconsistent spacing and trailing commas in objects in remaining `src/` files, so we can enable the `comma-dangle` and `object-curly-spacing` ESLint rules later on
http://eslint.org/docs/rules/comma-dangle
http://eslint.org/docs/rules/object-curly-spacing
Given that we currently have quite inconsistent object formatting, fixing this in *one* big patch probably wouldn't be feasible (since I cannot imagine anyone wanting to review that); hence I've opted to try and do this piecewise instead.
Please note: This patch was created automatically, using the ESLint `--fix` command line option. In a couple of places this caused lines to become too long, and I've fixed those manually; please refer to the interdiff below for the only hand-edits in this patch.
```diff
diff --git a/src/display/canvas.js b/src/display/canvas.js
index 5739f6f2..4216b2d2 100644
--- a/src/display/canvas.js
+++ b/src/display/canvas.js
@@ -2071,7 +2071,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
var map = [];
for (var i = 0, ii = positions.length; i < ii; i += 2) {
map.push({ transform: [scaleX, 0, 0, scaleY, positions[i],
- positions[i + 1]], x: 0, y: 0, w: width, h: height, });
+ positions[i + 1]], x: 0, y: 0, w: width, h: height, });
}
this.paintInlineImageXObjectGroup(imgData, map);
},
diff --git a/src/display/svg.js b/src/display/svg.js
index 9eb05dfa..2aa21482 100644
--- a/src/display/svg.js
+++ b/src/display/svg.js
@@ -458,7 +458,11 @@ SVGGraphics = (function SVGGraphicsClosure() {
for (var x = 0; x < fnArrayLen; x++) {
var fnId = fnArray[x];
- opList.push({ 'fnId': fnId, 'fn': REVOPS[fnId], 'args': argsArray[x], });
+ opList.push({
+ 'fnId': fnId,
+ 'fn': REVOPS[fnId],
+ 'args': argsArray[x],
+ });
}
return opListToTree(opList);
},
```
2017-06-02 18:26:37 +09:00
|
|
|
});
|
2019-03-24 05:55:37 +09:00
|
|
|
this.current.dependencies.push(promise);
|
2014-05-20 08:53:40 +09:00
|
|
|
}
|
2019-03-24 05:55:37 +09:00
|
|
|
}
|
|
|
|
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) {
|
2019-04-06 23:52:29 +09:00
|
|
|
const operatorIdMapping = this._operatorIdMapping;
|
2019-03-24 05:55:37 +09:00
|
|
|
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': fnId,
|
2019-04-06 23:52:29 +09:00
|
|
|
'fn': operatorIdMapping[fnId],
|
2019-03-24 05:55:37 +09:00
|
|
|
'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;
|
2019-03-24 07:05:12 +09:00
|
|
|
case OPS.setRenderingIntent:
|
|
|
|
this.setRenderingIntent(args[0]);
|
|
|
|
break;
|
|
|
|
case OPS.setFlatness:
|
|
|
|
this.setFlatness(args[0]);
|
|
|
|
break;
|
2019-03-24 05:55:37 +09:00
|
|
|
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.paintJpegXObject:
|
|
|
|
this.paintJpegXObject(args[0], args[1], args[2]);
|
|
|
|
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;
|
2014-05-20 08:53:40 +09:00
|
|
|
}
|
2019-03-24 05:55:37 +09:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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.sqrt(a * a + b * b);
|
|
|
|
|
|
|
|
current.x = current.lineX = 0;
|
|
|
|
current.y = current.lineY = 0;
|
|
|
|
|
|
|
|
current.xcoords = [];
|
|
|
|
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.appendChild(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 = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
moveText(x, y) {
|
|
|
|
const current = this.current;
|
|
|
|
current.x = current.lineX += x;
|
|
|
|
current.y = current.lineY += y;
|
|
|
|
|
|
|
|
current.xcoords = [];
|
|
|
|
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 charSpacing = current.charSpacing;
|
|
|
|
const wordSpacing = current.wordSpacing;
|
|
|
|
const fontDirection = current.fontDirection;
|
|
|
|
const textHScale = current.textHScale * fontDirection;
|
|
|
|
const vertical = font.vertical;
|
|
|
|
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 (isNum(glyph)) {
|
|
|
|
x += -glyph * fontSize * 0.001;
|
|
|
|
continue;
|
2014-05-20 08:53:40 +09:00
|
|
|
}
|
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
const width = glyph.width;
|
|
|
|
const character = glyph.fontChar;
|
|
|
|
const spacing = (glyph.isSpace ? wordSpacing : 0) + charSpacing;
|
|
|
|
const charWidth = width * widthAdvanceScale + spacing * fontDirection;
|
2014-05-20 08:53:40 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
if (!glyph.isInFont && !font.missingFile) {
|
2017-08-17 06:35:39 +09:00
|
|
|
x += charWidth;
|
2019-03-24 05:55:37 +09:00
|
|
|
// 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.
|
|
|
|
continue;
|
2014-05-20 08:53:40 +09:00
|
|
|
}
|
2019-03-24 05:55:37 +09:00
|
|
|
current.xcoords.push(current.x + x * textHScale);
|
|
|
|
current.tspan.textContent += character;
|
|
|
|
x += charWidth;
|
|
|
|
}
|
|
|
|
if (vertical) {
|
|
|
|
current.y -= x * textHScale;
|
|
|
|
} else {
|
|
|
|
current.x += x * textHScale;
|
|
|
|
}
|
2014-05-20 08:53:40 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
current.tspan.setAttributeNS(null, 'x',
|
|
|
|
current.xcoords.map(pf).join(' '));
|
|
|
|
current.tspan.setAttributeNS(null, 'y', pf(-current.y));
|
|
|
|
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);
|
2014-08-15 06:13:15 +09:00
|
|
|
}
|
2019-03-24 05:55:37 +09:00
|
|
|
if (current.fillAlpha < 1) {
|
|
|
|
current.tspan.setAttributeNS(null, 'fill-opacity', current.fillAlpha);
|
2014-08-15 06:13:15 +09:00
|
|
|
}
|
2019-03-24 05:55:37 +09:00
|
|
|
} 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');
|
|
|
|
}
|
2018-08-29 06:42:07 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
if (fillStrokeMode === TextRenderingMode.STROKE ||
|
|
|
|
fillStrokeMode === TextRenderingMode.FILL_STROKE) {
|
|
|
|
const lineWidthScale = 1 / (current.textMatrixScale || 1);
|
|
|
|
this._setStrokeAttributes(current.tspan, lineWidthScale);
|
|
|
|
}
|
2018-08-29 06:42:07 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
// 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;
|
|
|
|
}
|
2018-08-29 06:42:07 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
current.txtElement.setAttributeNS(null, 'transform',
|
|
|
|
`${pm(textMatrix)} scale(1, -1)`);
|
|
|
|
current.txtElement.setAttributeNS(XML_NS, 'xml:space', 'preserve');
|
|
|
|
current.txtElement.appendChild(current.tspan);
|
|
|
|
current.txtgrp.appendChild(current.txtElement);
|
2014-05-20 08:53:40 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
this._ensureTransformGroup().appendChild(current.txtElement);
|
|
|
|
}
|
2017-08-20 00:21:08 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
setLeadingMoveText(x, y) {
|
|
|
|
this.setLeading(-y);
|
|
|
|
this.moveText(x, y);
|
|
|
|
}
|
|
|
|
|
|
|
|
addFontStyle(fontObj) {
|
|
|
|
if (!this.cssStyle) {
|
|
|
|
this.cssStyle = this.svgFactory.createElement('svg:style');
|
|
|
|
this.cssStyle.setAttributeNS(null, 'type', 'text/css');
|
|
|
|
this.defs.appendChild(this.cssStyle);
|
|
|
|
}
|
2014-08-15 05:11:27 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
const url = createObjectURL(fontObj.data, fontObj.mimetype,
|
2016-03-29 04:49:22 +09:00
|
|
|
this.forceDataSchema);
|
2019-03-24 05:55:37 +09:00
|
|
|
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.data &&
|
|
|
|
!this.embeddedFonts[fontObj.loadedName]) {
|
|
|
|
this.addFontStyle(fontObj);
|
|
|
|
this.embeddedFonts[fontObj.loadedName] = fontObj;
|
|
|
|
}
|
2014-08-15 05:11:27 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
current.fontMatrix = (fontObj.fontMatrix ?
|
|
|
|
fontObj.fontMatrix : FONT_IDENTITY_MATRIX);
|
2014-05-20 08:53:40 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
const bold = fontObj.black ? (fontObj.bold ? 'bolder' : 'bold') :
|
2014-05-20 08:53:40 +09:00
|
|
|
(fontObj.bold ? 'bold' : 'normal');
|
2019-03-24 05:55:37 +09:00
|
|
|
const italic = fontObj.italic ? 'italic' : 'normal';
|
2014-05-20 08:53:40 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
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 = [];
|
|
|
|
}
|
2014-05-20 08:53:40 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
endText() {
|
|
|
|
const current = this.current;
|
|
|
|
if ((current.textRenderingMode & TextRenderingMode.ADD_TO_PATH_FLAG) &&
|
|
|
|
current.txtElement && 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.makeCssRgb(r, g, b);
|
|
|
|
}
|
|
|
|
|
|
|
|
setFillAlpha(fillAlpha) {
|
|
|
|
this.current.fillAlpha = fillAlpha;
|
|
|
|
}
|
|
|
|
|
|
|
|
setFillRGBColor(r, g, b) {
|
|
|
|
this.current.fillColor = Util.makeCssRgb(r, g, b);
|
|
|
|
this.current.tspan = this.svgFactory.createElement('svg:tspan');
|
|
|
|
this.current.xcoords = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
setStrokeColorN(args) {
|
|
|
|
this.current.strokeColor = this._makeColorN_Pattern(args);
|
|
|
|
}
|
|
|
|
|
|
|
|
setFillColorN(args) {
|
|
|
|
this.current.fillColor = this._makeColorN_Pattern(args);
|
|
|
|
}
|
|
|
|
|
|
|
|
shadingFill(args) {
|
|
|
|
const width = this.viewport.width;
|
|
|
|
const height = this.viewport.height;
|
|
|
|
const inv = Util.inverseTransform(this.transformMatrix);
|
|
|
|
const bl = Util.applyTransform([0, 0], inv);
|
|
|
|
const br = Util.applyTransform([0, height], inv);
|
|
|
|
const ul = Util.applyTransform([width, 0], inv);
|
|
|
|
const ur = Util.applyTransform([width, height], inv);
|
|
|
|
const x0 = Math.min(bl[0], br[0], ul[0], ur[0]);
|
|
|
|
const y0 = Math.min(bl[1], br[1], ul[1], ur[1]);
|
|
|
|
const x1 = Math.max(bl[0], br[0], ul[0], ur[0]);
|
|
|
|
const y1 = Math.max(bl[1], br[1], ul[1], ur[1]);
|
|
|
|
|
|
|
|
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));
|
|
|
|
this._ensureTransformGroup().appendChild(rect);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
_makeColorN_Pattern(args) {
|
|
|
|
if (args[0] === 'TilingPattern') {
|
2019-06-14 14:59:12 +09:00
|
|
|
return this._makeTilingPattern(args);
|
2019-03-24 05:55:37 +09:00
|
|
|
}
|
|
|
|
return this._makeShadingPattern(args);
|
|
|
|
}
|
|
|
|
|
2019-06-14 14:59:12 +09:00
|
|
|
/**
|
|
|
|
* @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] = Util.applyTransform([x0, y0], matrix);
|
|
|
|
const [tx1, ty1] = 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.makeCssRgb(...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.appendChild(bbox.childNodes[0]);
|
|
|
|
this.defs.appendChild(tiling);
|
|
|
|
return `url(#${tilingId})`;
|
|
|
|
}
|
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
/**
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
_makeShadingPattern(args) {
|
|
|
|
switch (args[0]) {
|
|
|
|
case 'RadialAxial':
|
|
|
|
const shadingId = `shading${shadingCount++}`;
|
|
|
|
const colorStops = args[2];
|
|
|
|
let gradient;
|
|
|
|
|
|
|
|
switch (args[1]) {
|
|
|
|
case 'axial':
|
|
|
|
const point0 = args[3];
|
|
|
|
const point1 = args[4];
|
|
|
|
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[3];
|
|
|
|
const circlePoint = args[4];
|
|
|
|
const focalRadius = args[5];
|
|
|
|
const circleRadius = args[6];
|
|
|
|
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);
|
2014-05-20 08:53:40 +09:00
|
|
|
break;
|
2019-03-24 05:55:37 +09:00
|
|
|
default:
|
|
|
|
throw new Error(`Unknown RadialAxial type: ${args[1]}`);
|
2014-05-20 08:53:40 +09:00
|
|
|
}
|
2019-03-24 05:55:37 +09:00
|
|
|
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.appendChild(stop);
|
|
|
|
}
|
|
|
|
this.defs.appendChild(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]}`);
|
|
|
|
}
|
|
|
|
}
|
2014-07-25 21:29:10 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
setDash(dashArray, dashPhase) {
|
|
|
|
this.current.dashArray = dashArray;
|
|
|
|
this.current.dashPhase = dashPhase;
|
|
|
|
}
|
2019-02-27 03:00:35 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
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:
|
|
|
|
x = args[j + 2];
|
|
|
|
y = args[j + 3];
|
|
|
|
d.push('C', pf(x), pf(y), pf(args[j]), pf(args[j + 1]),
|
|
|
|
pf(args[j + 2]), pf(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;
|
2019-02-27 03:00:35 +09:00
|
|
|
}
|
2019-03-24 05:55:37 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
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().appendChild(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;
|
2019-02-27 03:00:35 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
if (!this.pendingClip) {
|
|
|
|
return;
|
|
|
|
}
|
2019-05-29 00:37:15 +09:00
|
|
|
if (!current.element) {
|
|
|
|
this.pendingClip = null;
|
|
|
|
return;
|
|
|
|
}
|
2019-03-24 05:55:37 +09:00
|
|
|
|
|
|
|
// 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.appendChild(clipElement);
|
|
|
|
this.defs.appendChild(clipPath);
|
|
|
|
|
|
|
|
if (current.activeClipUrl) {
|
|
|
|
// The previous clipping group content can go out of order -- resetting
|
|
|
|
// cached clipGroups.
|
|
|
|
current.clipGroup = null;
|
|
|
|
this.extraStack.forEach(function(prev) {
|
|
|
|
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`;
|
2019-02-27 03:00:35 +09:00
|
|
|
current.path.setAttributeNS(null, 'd', d);
|
2019-03-24 05:55:37 +09:00
|
|
|
}
|
|
|
|
}
|
2016-10-13 04:22:35 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
setLeading(leading) {
|
|
|
|
this.current.leading = -leading;
|
|
|
|
}
|
2014-05-20 08:53:40 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
setTextRise(textRise) {
|
|
|
|
this.current.textRise = textRise;
|
|
|
|
}
|
2019-02-27 03:00:35 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
setTextRenderingMode(textRenderingMode) {
|
|
|
|
this.current.textRenderingMode = textRenderingMode;
|
|
|
|
}
|
|
|
|
|
|
|
|
setHScale(scale) {
|
|
|
|
this.current.textHScale = scale / 100;
|
|
|
|
}
|
|
|
|
|
2019-03-24 07:05:12 +09:00
|
|
|
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.
|
|
|
|
}
|
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
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;
|
2019-03-24 07:05:12 +09:00
|
|
|
case 'RI':
|
|
|
|
this.setRenderingIntent(value);
|
|
|
|
break;
|
|
|
|
case 'FL':
|
|
|
|
this.setFlatness(value);
|
|
|
|
break;
|
2019-03-24 05:55:37 +09:00
|
|
|
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;
|
2016-10-18 06:09:24 +09:00
|
|
|
}
|
2019-03-24 05:55:37 +09:00
|
|
|
}
|
|
|
|
}
|
2016-10-18 06:09:24 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
}
|
2014-07-04 00:59:04 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
stroke() {
|
|
|
|
const current = this.current;
|
|
|
|
if (current.element) {
|
|
|
|
this._setStrokeAttributes(current.element);
|
|
|
|
current.element.setAttributeNS(null, 'fill', 'none');
|
|
|
|
this.endPath();
|
|
|
|
}
|
|
|
|
}
|
Move svg:clipPath generation from clip to endPath
In the PDF from issue 8527, the clip operator (W) shows up before a path
is defined. The current SVG backend however expects a path to exist
before generating a `<svg:clipPath>` element.
In the example, the path was defined after the clip, followed by a
endPath operator (n).
So this commit fixes the bug by moving the path generation logic from
clip to endPath.
Our canvas backend appears to use similar logic:
`CanvasGraphics_endPath` calls `consumePath`, which in turn draws the
clip and resets the `pendingClip` state. The canvas backend calls
`consumePath` from multiple other places, so we probably need to check
whether doing so is also necessary for the SVG backend.
I scanned our corpus of PDF files in test/pdfs, and found that in every
instance (except for one), the "W" PDF operator (clip) is immediately
followed by "n" (endPath). The new test from this commit (clippath.pdf)
starts with "W", followed by a path definition and then "n".
# Commands used to find some of the clipping commands:
grep -ra '^W$' -C7 | less -S
grep -ra '^W ' -C7 | less -S
grep -ra ' W$' -C7 | less -S
test/pdfs/issue6413.pdf is the only file where "W" (a tline 55) is not
followed by "n". In fact, the "W" is the last operation of a series of
XObject painting operations, and removing it does not have any effect
on the rendered PDF (confirmed by looking at the output of PDF.js's
canvas backend, and ImageMagick's convert command).
2017-06-19 19:40:48 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
/**
|
|
|
|
* @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');
|
|
|
|
}
|
2014-05-20 08:53:40 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
eoFill() {
|
|
|
|
if (this.current.element) {
|
|
|
|
this.current.element.setAttributeNS(null, 'fill-rule', 'evenodd');
|
|
|
|
}
|
|
|
|
this.fill();
|
|
|
|
}
|
2014-05-20 08:53:40 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
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();
|
|
|
|
}
|
2014-05-20 08:53:40 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
eoFillStroke() {
|
|
|
|
if (this.current.element) {
|
|
|
|
this.current.element.setAttributeNS(null, 'fill-rule', 'evenodd');
|
|
|
|
}
|
|
|
|
this.fillStroke();
|
|
|
|
}
|
2018-08-29 06:42:07 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
closeStroke() {
|
|
|
|
this.closePath();
|
|
|
|
this.stroke();
|
|
|
|
}
|
2014-05-20 08:53:40 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
closeFillStroke() {
|
|
|
|
this.closePath();
|
|
|
|
this.fillStroke();
|
|
|
|
}
|
2014-05-20 08:53:40 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
closeEOFillStroke() {
|
|
|
|
this.closePath();
|
|
|
|
this.eoFillStroke();
|
|
|
|
}
|
2014-05-20 08:53:40 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
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);
|
2017-06-26 05:28:46 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
this._ensureTransformGroup().appendChild(rect);
|
|
|
|
}
|
2017-11-15 01:35:39 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
paintJpegXObject(objId, w, h) {
|
|
|
|
const imgObj = this.objs.get(objId);
|
|
|
|
const imgEl = this.svgFactory.createElement('svg:image');
|
|
|
|
imgEl.setAttributeNS(XLINK_NS, 'xlink:href', imgObj.src);
|
|
|
|
imgEl.setAttributeNS(null, 'width', pf(w));
|
|
|
|
imgEl.setAttributeNS(null, 'height', pf(h));
|
|
|
|
imgEl.setAttributeNS(null, 'x', '0');
|
|
|
|
imgEl.setAttributeNS(null, 'y', pf(-h));
|
|
|
|
imgEl.setAttributeNS(null, 'transform',
|
|
|
|
`scale(${pf(1 / w)} ${pf(-1 / h)})`);
|
|
|
|
|
|
|
|
this._ensureTransformGroup().appendChild(imgEl);
|
|
|
|
}
|
2018-04-18 01:20:29 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
paintImageXObject(objId) {
|
|
|
|
const imgData = this.objs.get(objId);
|
|
|
|
if (!imgData) {
|
|
|
|
warn(`Dependent image with object ID ${objId} is not ready yet`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.paintInlineImageXObject(imgData);
|
|
|
|
}
|
2014-08-08 01:30:48 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
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.appendChild(imgEl);
|
|
|
|
} else {
|
2016-10-13 04:22:35 +09:00
|
|
|
this._ensureTransformGroup().appendChild(imgEl);
|
2019-03-24 05:55:37 +09:00
|
|
|
}
|
|
|
|
}
|
2014-08-08 01:30:48 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
paintImageMaskXObject(imgData) {
|
|
|
|
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.appendChild(mask);
|
|
|
|
this._ensureTransformGroup().appendChild(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]);
|
2014-08-12 01:18:32 +09:00
|
|
|
cliprect.setAttributeNS(null, 'width', pf(width));
|
|
|
|
cliprect.setAttributeNS(null, 'height', pf(height));
|
2016-10-13 04:22:35 +09:00
|
|
|
this.current.element = cliprect;
|
2014-08-08 01:30:48 +09:00
|
|
|
this.clip('nonzero');
|
2019-03-24 05:55:37 +09:00
|
|
|
this.endPath();
|
|
|
|
}
|
|
|
|
}
|
2014-08-15 05:54:38 +09:00
|
|
|
|
2019-03-24 05:55:37 +09:00
|
|
|
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.appendChild(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.appendChild(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.appendChild(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().appendChild(this.tgrp);
|
|
|
|
} else {
|
|
|
|
this.svg.appendChild(this.tgrp);
|
2016-10-13 04:22:35 +09:00
|
|
|
}
|
2019-03-24 05:55:37 +09:00
|
|
|
}
|
|
|
|
return this.tgrp;
|
|
|
|
}
|
|
|
|
};
|
2014-08-14 03:59:28 +09:00
|
|
|
|
2016-10-15 00:57:53 +09:00
|
|
|
}
|
2017-04-02 21:25:33 +09:00
|
|
|
|
|
|
|
export {
|
|
|
|
SVGGraphics,
|
|
|
|
};
|