Compare commits

..

No commits in common. "e5fbf52405fbb092351ef1cda8f44fbae3a84473" and "a4950c0b719e8194874c2d3fd50c1e29db1a105c" have entirely different histories.

5 changed files with 256 additions and 367 deletions

View File

@ -32,7 +32,6 @@ import {
} from "../shared/util.js";
import { CMapFactory, IdentityCMap } from "./cmap.js";
import { Cmd, Dict, EOF, isName, Name, Ref, RefSet } from "./primitives.js";
import { compileType3Glyph, FontFlags } from "./fonts_utils.js";
import { ErrorFont, Font } from "./fonts.js";
import {
fetchBinaryData,
@ -73,6 +72,7 @@ import { bidi } from "./bidi.js";
import { ColorSpace } from "./colorspace.js";
import { ColorSpaceUtils } from "./colorspace_utils.js";
import { DecodeStream } from "./decode_stream.js";
import { FontFlags } from "./fonts_utils.js";
import { getFontSubstitution } from "./font_substitutions.js";
import { getGlyphsUnicode } from "./glyphlist.js";
import { getMetrics } from "./metrics.js";
@ -180,17 +180,14 @@ function normalizeBlendMode(value, parsingArray = false) {
return "source-over";
}
function addCachedImageOps(
opList,
{ objId, fn, args, optionalContent, hasMask }
) {
if (objId) {
opList.addDependency(objId);
function addLocallyCachedImageOps(opList, data) {
if (data.objId) {
opList.addDependency(data.objId);
}
opList.addImageOps(fn, args, optionalContent, hasMask);
opList.addImageOps(data.fn, data.args, data.optionalContent, data.hasMask);
if (fn === OPS.paintImageMaskXObject && args[0]?.count > 0) {
args[0].count++;
if (data.fn === OPS.paintImageMaskXObject && data.args[0]?.count > 0) {
data.args[0].count++;
}
}
@ -611,12 +608,6 @@ class PartialEvaluator {
const decode = dict.getArray("D", "Decode");
if (this.parsingType3Font) {
// NOTE: Compared to other image resources we don't bother caching
// Type3-glyph image masks, since we've not come across any cases
// where that actually helps.
// In Type3-glyphs image masks are "always" inline resources,
// they're usually fairly small and aren't being re-used either.
imgData = PDFImage.createRawMask({
imgArray,
width: w,
@ -625,21 +616,25 @@ class PartialEvaluator {
inverseDecode: decode?.[0] > 0,
interpolate,
});
args = compileType3Glyph(imgData);
if (args) {
operatorList.addImageOps(OPS.constructPath, args, optionalContent);
return;
imgData.cached = !!cacheKey;
fn = OPS.paintImageMaskXObject;
args = [imgData];
operatorList.addImageOps(fn, args, optionalContent);
if (cacheKey) {
const cacheData = { fn, args, optionalContent };
localImageCache.set(cacheKey, imageRef, cacheData);
if (imageRef) {
this._regionalImageCache.set(
/* name = */ null,
imageRef,
cacheData
);
}
}
warn("Cannot compile Type3 glyph.");
// If compilation failed, or was disabled, fallback to using an inline
// image mask; this case should be extremely rare.
operatorList.addImageOps(
OPS.paintImageMaskXObject,
[imgData],
optionalContent
);
return;
}
@ -747,8 +742,7 @@ class PartialEvaluator {
// If there is no imageMask, create the PDFImage and a lot
// of image processing can be done here.
let objId = `img_${this.idFactory.createObjId()}`,
cacheGlobally = false,
globalCacheData = null;
cacheGlobally = false;
if (this.parsingType3Font) {
objId = `${this.idFactory.getDocId()}_type3_${objId}`;
@ -773,17 +767,15 @@ class PartialEvaluator {
operatorList.addImageOps(fn, args, optionalContent, hasMask);
if (cacheGlobally) {
globalCacheData = {
objId,
fn,
args,
optionalContent,
hasMask,
byteSize: 0, // Temporary entry, to avoid `setData` returning early.
};
if (this.globalImageCache.hasDecodeFailed(imageRef)) {
this.globalImageCache.setData(imageRef, globalCacheData);
this.globalImageCache.setData(imageRef, {
objId,
fn,
args,
optionalContent,
hasMask,
byteSize: 0, // Data is `null`, since decoding failed previously.
});
this._sendImgData(objId, /* imgData = */ null, cacheGlobally);
return;
@ -800,7 +792,14 @@ class PartialEvaluator {
]);
if (localLength) {
this.globalImageCache.setData(imageRef, globalCacheData);
this.globalImageCache.setData(imageRef, {
objId,
fn,
args,
optionalContent,
hasMask,
byteSize: 0, // Temporary entry, to avoid `setData` returning early.
});
this.globalImageCache.addByteSize(imageRef, localLength);
return;
}
@ -849,8 +848,14 @@ class PartialEvaluator {
this._regionalImageCache.set(/* name = */ null, imageRef, cacheData);
if (cacheGlobally) {
assert(globalCacheData, "The global cache-data must be available.");
this.globalImageCache.setData(imageRef, globalCacheData);
this.globalImageCache.setData(imageRef, {
objId,
fn,
args,
optionalContent,
hasMask,
byteSize: 0, // Temporary entry, note `addByteSize` above.
});
}
}
}
@ -1774,7 +1779,7 @@ class PartialEvaluator {
if (isValidName) {
const localImage = localImageCache.getByName(name);
if (localImage) {
addCachedImageOps(operatorList, localImage);
addLocallyCachedImageOps(operatorList, localImage);
args = null;
continue;
}
@ -1788,12 +1793,28 @@ class PartialEvaluator {
let xobj = xobjs.getRaw(name);
if (xobj instanceof Ref) {
const cachedImage =
const localImage =
localImageCache.getByRef(xobj) ||
self._regionalImageCache.getByRef(xobj) ||
self.globalImageCache.getData(xobj, self.pageIndex);
if (cachedImage) {
addCachedImageOps(operatorList, cachedImage);
self._regionalImageCache.getByRef(xobj);
if (localImage) {
addLocallyCachedImageOps(operatorList, localImage);
resolveXObject();
return;
}
const globalImage = self.globalImageCache.getData(
xobj,
self.pageIndex
);
if (globalImage) {
operatorList.addDependency(globalImage.objId);
operatorList.addImageOps(
globalImage.fn,
globalImage.args,
globalImage.optionalContent,
globalImage.hasMask
);
resolveXObject();
return;
}
@ -1886,7 +1907,7 @@ class PartialEvaluator {
if (cacheKey) {
const localImage = localImageCache.getByName(cacheKey);
if (localImage) {
addCachedImageOps(operatorList, localImage);
addLocallyCachedImageOps(operatorList, localImage);
args = null;
continue;
}

View File

@ -13,11 +13,11 @@
* limitations under the License.
*/
import { DrawOPS, info, OPS } from "../shared/util.js";
import { getEncoding, StandardEncoding } from "./encodings.js";
import { getGlyphsUnicode } from "./glyphlist.js";
import { getLookupTableFactory } from "./core_utils.js";
import { getUnicodeForGlyph } from "./unicode.js";
import { info } from "../shared/util.js";
// Accented characters have issues on Windows and Linux. When this flag is
// enabled glyphs that use seac and seac style endchar operators are truncated
@ -207,177 +207,7 @@ const getVerticalPresentationForm = getLookupTableFactory(t => {
t[0xff5d] = 0xfe38; // FULLWIDTH RIGHT CURLY BRACKET
});
// To disable Type3 compilation, set the value to `-1`.
const MAX_SIZE_TO_COMPILE = 1000;
function compileType3Glyph({ data: img, width, height }) {
if (width > MAX_SIZE_TO_COMPILE || height > MAX_SIZE_TO_COMPILE) {
return null;
}
const POINT_TO_PROCESS_LIMIT = 1000;
const POINT_TYPES = new Uint8Array([
0, 2, 4, 0, 1, 0, 5, 4, 8, 10, 0, 8, 0, 2, 1, 0,
]);
const width1 = width + 1;
const points = new Uint8Array(width1 * (height + 1));
let i, j, j0;
// decodes bit-packed mask data
const lineSize = (width + 7) & ~7;
const data = new Uint8Array(lineSize * height);
let pos = 0;
for (const elem of img) {
let mask = 128;
while (mask > 0) {
data[pos++] = elem & mask ? 0 : 255;
mask >>= 1;
}
}
// finding interesting points: every point is located between mask pixels,
// so there will be points of the (width + 1)x(height + 1) grid. Every point
// will have flags assigned based on neighboring mask pixels:
// 4 | 8
// --P--
// 2 | 1
// We are interested only in points with the flags:
// - outside corners: 1, 2, 4, 8;
// - inside corners: 7, 11, 13, 14;
// - and, intersections: 5, 10.
let count = 0;
pos = 0;
if (data[pos] !== 0) {
points[0] = 1;
++count;
}
for (j = 1; j < width; j++) {
if (data[pos] !== data[pos + 1]) {
points[j] = data[pos] ? 2 : 1;
++count;
}
pos++;
}
if (data[pos] !== 0) {
points[j] = 2;
++count;
}
for (i = 1; i < height; i++) {
pos = i * lineSize;
j0 = i * width1;
if (data[pos - lineSize] !== data[pos]) {
points[j0] = data[pos] ? 1 : 8;
++count;
}
// 'sum' is the position of the current pixel configuration in the 'TYPES'
// array (in order 8-1-2-4, so we can use '>>2' to shift the column).
let sum = (data[pos] ? 4 : 0) + (data[pos - lineSize] ? 8 : 0);
for (j = 1; j < width; j++) {
sum =
(sum >> 2) +
(data[pos + 1] ? 4 : 0) +
(data[pos - lineSize + 1] ? 8 : 0);
if (POINT_TYPES[sum]) {
points[j0 + j] = POINT_TYPES[sum];
++count;
}
pos++;
}
if (data[pos - lineSize] !== data[pos]) {
points[j0 + j] = data[pos] ? 2 : 4;
++count;
}
if (count > POINT_TO_PROCESS_LIMIT) {
return null;
}
}
pos = lineSize * (height - 1);
j0 = i * width1;
if (data[pos] !== 0) {
points[j0] = 8;
++count;
}
for (j = 1; j < width; j++) {
if (data[pos] !== data[pos + 1]) {
points[j0 + j] = data[pos] ? 4 : 8;
++count;
}
pos++;
}
if (data[pos] !== 0) {
points[j0 + j] = 4;
++count;
}
if (count > POINT_TO_PROCESS_LIMIT) {
return null;
}
// building outlines
const steps = new Int32Array([0, width1, -1, 0, -width1, 0, 0, 0, 1]);
const pathBuf = [];
// the path shall be painted in [0..1]x[0..1] space
const { a, b, c, d, e, f } = new DOMMatrix()
.scaleSelf(1 / width, -1 / height)
.translateSelf(0, -height);
for (i = 0; count && i <= height; i++) {
let p = i * width1;
const end = p + width;
while (p < end && !points[p]) {
p++;
}
if (p === end) {
continue;
}
let x = p % width1;
let y = i;
pathBuf.push(DrawOPS.moveTo, a * x + c * y + e, b * x + d * y + f);
const p0 = p;
let type = points[p];
do {
const step = steps[type];
do {
p += step;
} while (!points[p]);
const pp = points[p];
if (pp !== 5 && pp !== 10) {
// set new direction
type = pp;
// delete mark
points[p] = 0;
} else {
// type is 5 or 10, ie, a crossing
// set new direction
type = pp & ((0x33 * type) >> 4);
// set new type for "future hit"
points[p] &= (type >> 2) | (type << 2);
}
x = p % width1;
y = (p / width1) | 0;
pathBuf.push(DrawOPS.lineTo, a * x + c * y + e, b * x + d * y + f);
if (!points[p]) {
--count;
}
} while (p0 !== p);
--i;
}
return [
OPS.rawFillPath,
[new Float32Array(pathBuf)],
new Float32Array([0, 0, width, height]),
];
}
export {
compileType3Glyph,
FontFlags,
getVerticalPresentationForm,
MacStandardGlyphOrdering,

View File

@ -13,14 +13,7 @@
* limitations under the License.
*/
import {
DrawOPS,
ImageKind,
OPS,
RenderingIntentFlag,
Util,
warn,
} from "../shared/util.js";
import { ImageKind, OPS, RenderingIntentFlag, warn } from "../shared/util.js";
function addState(parentState, pattern, checkFn, iterateFn, processFn) {
let state = parentState;
@ -477,70 +470,6 @@ addState(
}
);
// This replaces (save, transform, constructPath, restore)
// sequences with |constructPath| operation.
addState(
InitialState,
[OPS.save, OPS.transform, OPS.constructPath, OPS.restore],
context => {
const argsArray = context.argsArray;
const iFirstConstructPath = context.iCurr - 1;
const op = argsArray[iFirstConstructPath][0];
// When stroking the transform has to be applied to the line width too.
// So we can only optimize if the transform is an identity.
if (
op !== OPS.stroke &&
op !== OPS.closeStroke &&
op !== OPS.fillStroke &&
op !== OPS.eoFillStroke &&
op !== OPS.closeFillStroke &&
op !== OPS.closeEOFillStroke
) {
return true;
}
const iFirstTransform = context.iCurr - 2;
const transform = argsArray[iFirstTransform];
return (
transform[0] === 1 &&
transform[1] === 0 &&
transform[2] === 0 &&
transform[3] === 1
);
},
() => false,
(context, i) => {
const { fnArray, argsArray } = context;
const curr = context.iCurr;
const iFirstSave = curr - 3;
const iFirstTransform = curr - 2;
const iFirstConstructPath = curr - 1;
const args = argsArray[iFirstConstructPath];
const transform = argsArray[iFirstTransform];
const [, [buffer], minMax] = args;
Util.scaleMinMax(transform, minMax);
for (let k = 0, kk = buffer.length; k < kk; ) {
switch (buffer[k++]) {
case DrawOPS.moveTo:
case DrawOPS.lineTo:
Util.applyTransformInPlace(buffer.subarray(k), transform);
k += 2;
break;
case DrawOPS.curveTo:
Util.applyTransformToBezierInPlace(buffer.subarray(k), transform);
k += 6;
break;
}
}
// Replace queue items.
fnArray.splice(iFirstSave, 4, OPS.constructPath);
argsArray.splice(iFirstSave, 4, args);
return iFirstSave + 1;
}
);
class NullOptimizer {
constructor(queue) {
this.queue = queue;
@ -770,7 +699,7 @@ class OperatorList {
case OPS.paintInlineImageXObjectGroup:
case OPS.paintImageMaskXObject:
const arg = argsArray[i][0]; // First parameter in imgData.
if (arg.data?.buffer instanceof ArrayBuffer) {
if (!arg.cached && arg.data?.buffer instanceof ArrayBuffer) {
transfers.push(arg.data.buffer);
}
break;

View File

@ -54,6 +54,9 @@ const EXECUTION_TIME = 15; // ms
// Defines the number of steps before checking the execution time.
const EXECUTION_STEPS = 10;
// To disable Type3 compilation, set the value to `-1`.
const MAX_SIZE_TO_COMPILE = 1000;
const FULL_CHUNK_HEIGHT = 16;
// Only used in rescaleAndStroke. The goal is to avoid
@ -296,6 +299,169 @@ function drawImageAtIntegerCoords(
return [scaleX * destW, scaleY * destH];
}
function compileType3Glyph(imgData) {
const { width, height } = imgData;
if (width > MAX_SIZE_TO_COMPILE || height > MAX_SIZE_TO_COMPILE) {
return null;
}
const POINT_TO_PROCESS_LIMIT = 1000;
const POINT_TYPES = new Uint8Array([
0, 2, 4, 0, 1, 0, 5, 4, 8, 10, 0, 8, 0, 2, 1, 0,
]);
const width1 = width + 1;
const points = new Uint8Array(width1 * (height + 1));
let i, j, j0;
// decodes bit-packed mask data
const lineSize = (width + 7) & ~7;
const data = new Uint8Array(lineSize * height);
let pos = 0;
for (const elem of imgData.data) {
let mask = 128;
while (mask > 0) {
data[pos++] = elem & mask ? 0 : 255;
mask >>= 1;
}
}
// finding interesting points: every point is located between mask pixels,
// so there will be points of the (width + 1)x(height + 1) grid. Every point
// will have flags assigned based on neighboring mask pixels:
// 4 | 8
// --P--
// 2 | 1
// We are interested only in points with the flags:
// - outside corners: 1, 2, 4, 8;
// - inside corners: 7, 11, 13, 14;
// - and, intersections: 5, 10.
let count = 0;
pos = 0;
if (data[pos] !== 0) {
points[0] = 1;
++count;
}
for (j = 1; j < width; j++) {
if (data[pos] !== data[pos + 1]) {
points[j] = data[pos] ? 2 : 1;
++count;
}
pos++;
}
if (data[pos] !== 0) {
points[j] = 2;
++count;
}
for (i = 1; i < height; i++) {
pos = i * lineSize;
j0 = i * width1;
if (data[pos - lineSize] !== data[pos]) {
points[j0] = data[pos] ? 1 : 8;
++count;
}
// 'sum' is the position of the current pixel configuration in the 'TYPES'
// array (in order 8-1-2-4, so we can use '>>2' to shift the column).
let sum = (data[pos] ? 4 : 0) + (data[pos - lineSize] ? 8 : 0);
for (j = 1; j < width; j++) {
sum =
(sum >> 2) +
(data[pos + 1] ? 4 : 0) +
(data[pos - lineSize + 1] ? 8 : 0);
if (POINT_TYPES[sum]) {
points[j0 + j] = POINT_TYPES[sum];
++count;
}
pos++;
}
if (data[pos - lineSize] !== data[pos]) {
points[j0 + j] = data[pos] ? 2 : 4;
++count;
}
if (count > POINT_TO_PROCESS_LIMIT) {
return null;
}
}
pos = lineSize * (height - 1);
j0 = i * width1;
if (data[pos] !== 0) {
points[j0] = 8;
++count;
}
for (j = 1; j < width; j++) {
if (data[pos] !== data[pos + 1]) {
points[j0 + j] = data[pos] ? 4 : 8;
++count;
}
pos++;
}
if (data[pos] !== 0) {
points[j0 + j] = 4;
++count;
}
if (count > POINT_TO_PROCESS_LIMIT) {
return null;
}
// building outlines
const steps = new Int32Array([0, width1, -1, 0, -width1, 0, 0, 0, 1]);
const path = new Path2D();
// the path shall be painted in [0..1]x[0..1] space
const { a, b, c, d, e, f } = new DOMMatrix()
.scaleSelf(1 / width, -1 / height)
.translateSelf(0, -height);
for (i = 0; count && i <= height; i++) {
let p = i * width1;
const end = p + width;
while (p < end && !points[p]) {
p++;
}
if (p === end) {
continue;
}
let x = p % width1;
let y = i;
path.moveTo(a * x + c * y + e, b * x + d * y + f);
const p0 = p;
let type = points[p];
do {
const step = steps[type];
do {
p += step;
} while (!points[p]);
const pp = points[p];
if (pp !== 5 && pp !== 10) {
// set new direction
type = pp;
// delete mark
points[p] = 0;
} else {
// type is 5 or 10, ie, a crossing
// set new direction
type = pp & ((0x33 * type) >> 4);
// set new type for "future hit"
points[p] &= (type >> 2) | (type << 2);
}
x = p % width1;
y = (p / width1) | 0;
path.lineTo(a * x + c * y + e, b * x + d * y + f);
if (!points[p]) {
--count;
}
} while (p0 !== p);
--i;
}
return path;
}
class CanvasExtraState {
constructor(width, height) {
// Are soft masks and alpha values shapes or opacities?
@ -655,6 +821,7 @@ class CanvasGraphics {
this.canvasFactory = canvasFactory;
this.filterFactory = filterFactory;
this.groupStack = [];
this.processingType3 = null;
// Patterns are painted relative to the initial page/form transform, see
// PDF spec 8.7.2 NOTE 1.
this.baseTransform = null;
@ -1580,10 +1747,6 @@ class CanvasGraphics {
this.consumePath(path);
}
rawFillPath(path) {
this.ctx.fill(path);
}
// Clipping
clip() {
this.pendingClip = NORMAL_CLIP;
@ -2104,6 +2267,7 @@ class CanvasGraphics {
if (!operatorList) {
warn(`Type3 character "${glyph.operatorListId}" is not available.`);
} else if (this.contentVisible) {
this.processingType3 = glyph;
this.save();
ctx.scale(fontSize, fontSize);
ctx.transform(...fontMatrix);
@ -2118,6 +2282,7 @@ class CanvasGraphics {
current.x += width * textHScale;
}
ctx.restore();
this.processingType3 = null;
}
// Type3 fonts
@ -2538,6 +2703,18 @@ class CanvasGraphics {
img.count = count;
const ctx = this.ctx;
const glyph = this.processingType3;
if (glyph) {
if (glyph.compiled === undefined) {
glyph.compiled = compileType3Glyph(img);
}
if (glyph.compiled) {
ctx.fill(glyph.compiled);
return;
}
}
const mask = this._createMaskCanvas(img);
const maskCanvas = mask.canvas;

View File

@ -339,7 +339,6 @@ const OPS = {
constructPath: 91,
setStrokeTransparent: 92,
setFillTransparent: 93,
rawFillPath: 94,
};
// In order to have a switch statement that is fast (i.e. which use a jump
@ -677,57 +676,6 @@ class Util {
return `#${hexNumbers[r]}${hexNumbers[g]}${hexNumbers[b]}`;
}
// Apply a scaling matrix to some min/max values.
// If a scaling factor is negative then min and max must be
// swapped.
static scaleMinMax(transform, minMax) {
let temp;
if (transform[0]) {
if (transform[0] < 0) {
temp = minMax[0];
minMax[0] = minMax[2];
minMax[2] = temp;
}
minMax[0] *= transform[0];
minMax[2] *= transform[0];
if (transform[3] < 0) {
temp = minMax[1];
minMax[1] = minMax[3];
minMax[3] = temp;
}
minMax[1] *= transform[3];
minMax[3] *= transform[3];
} else {
temp = minMax[0];
minMax[0] = minMax[1];
minMax[1] = temp;
temp = minMax[2];
minMax[2] = minMax[3];
minMax[3] = temp;
if (transform[1] < 0) {
temp = minMax[1];
minMax[1] = minMax[3];
minMax[3] = temp;
}
minMax[1] *= transform[1];
minMax[3] *= transform[1];
if (transform[2] < 0) {
temp = minMax[0];
minMax[0] = minMax[2];
minMax[2] = temp;
}
minMax[0] *= transform[2];
minMax[2] *= transform[2];
}
minMax[0] += transform[4];
minMax[1] += transform[5];
minMax[2] += transform[4];
minMax[3] += transform[5];
}
// Concatenates two transformation matrices together and returns the result.
static transform(m1, m2) {
return [
@ -747,22 +695,6 @@ class Util {
return [xt, yt];
}
static applyTransformInPlace(p, m) {
const [p0, p1] = p;
p[0] = p0 * m[0] + p1 * m[2] + m[4];
p[1] = p0 * m[1] + p1 * m[3] + m[5];
}
// For 2d affine transforms
static applyTransformToBezierInPlace(p, [m0, m1, m2, m3, m4, m5]) {
for (let i = 0; i < 6; i += 2) {
const pI = p[i];
const pI1 = p[i + 1];
p[i] = pI * m0 + pI1 * m2 + m4;
p[i + 1] = pI * m1 + pI1 * m3 + m5;
}
}
static applyInverseTransform(p, m) {
const d = m[0] * m[3] - m[1] * m[2];
const xt = (p[0] * m[3] - p[1] * m[2] + m[2] * m[5] - m[4] * m[3]) / d;