Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add local caching of "simple" Graphics State (ExtGState) data in PartialEvaluator.{getOperatorList, getTextContent} (issue 2813) #12087

Merged
merged 3 commits into from
Jul 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 129 additions & 37 deletions src/core/evaluator.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,11 @@ import {
import { getTilingPatternIR, Pattern } from "./pattern.js";
import { isPDFFunction, PDFFunctionFactory } from "./function.js";
import { Lexer, Parser } from "./parser.js";
import { LocalColorSpaceCache, LocalImageCache } from "./image_utils.js";
import {
LocalColorSpaceCache,
LocalGStateCache,
LocalImageCache,
} from "./image_utils.js";
import { bidi } from "./bidi.js";
import { ColorSpace } from "./colorspace.js";
import { DecodeStream } from "./stream.js";
Expand Down Expand Up @@ -828,14 +832,18 @@ class PartialEvaluator {
throw reason;
}

setGState(
async setGState({
resources,
gState,
operatorList,
cacheKey,
task,
stateManager,
localColorSpaceCache
) {
localGStateCache,
localColorSpaceCache,
}) {
const gStateRef = gState.objId;
let isSimpleGState = true;
// This array holds the converted/processed state data.
var gStateObj = [];
var gStateKeys = gState.getKeys();
Expand Down Expand Up @@ -881,6 +889,8 @@ class PartialEvaluator {
break;
}
if (isDict(value)) {
isSimpleGState = false;

promise = promise.then(() => {
return this.handleSMask(
value,
Expand Down Expand Up @@ -925,6 +935,10 @@ class PartialEvaluator {
if (gStateObj.length > 0) {
operatorList.addOp(OPS.setGState, [gStateObj]);
}

if (isSimpleGState) {
localGStateCache.set(cacheKey, gStateRef, gStateObj);
}
});
}

Expand Down Expand Up @@ -1245,6 +1259,7 @@ class PartialEvaluator {
let parsingText = false;
const localImageCache = new LocalImageCache();
const localColorSpaceCache = new LocalColorSpaceCache();
const localGStateCache = new LocalGStateCache();

var xobjs = resources.get("XObject") || Dict.empty;
var patterns = resources.get("Pattern") || Dict.empty;
Expand Down Expand Up @@ -1274,7 +1289,8 @@ class PartialEvaluator {
operation = {},
i,
ii,
cs;
cs,
name;
while (!(stop = timeSlotManager.check())) {
// The arguments parsed by read() are used beyond this loop, so we
// cannot reuse the same array on each iteration. Therefore we pass
Expand All @@ -1290,7 +1306,7 @@ class PartialEvaluator {
switch (fn | 0) {
case OPS.paintXObject:
// eagerly compile XForm objects
var name = args[0].name;
name = args[0].name;
if (name) {
const localImage = localImageCache.getByName(name);
if (localImage) {
Expand Down Expand Up @@ -1653,23 +1669,64 @@ class PartialEvaluator {
fn = OPS.shadingFill;
break;
case OPS.setGState:
var dictName = args[0];
var extGState = resources.get("ExtGState");

if (!isDict(extGState) || !extGState.has(dictName.name)) {
break;
name = args[0].name;
if (name) {
const localGStateObj = localGStateCache.getByName(name);
if (localGStateObj) {
if (localGStateObj.length > 0) {
operatorList.addOp(OPS.setGState, [localGStateObj]);
}
args = null;
continue;
}
}

var gState = extGState.get(dictName.name);
next(
self.setGState(
resources,
gState,
operatorList,
task,
stateManager,
localColorSpaceCache
)
new Promise(function (resolveGState, rejectGState) {
if (!name) {
throw new FormatError("GState must be referred to by name.");
}

const extGState = resources.get("ExtGState");
if (!(extGState instanceof Dict)) {
throw new FormatError("ExtGState should be a dictionary.");
}

const gState = extGState.get(name);
// TODO: Attempt to lookup cached GStates by reference as well,
// if and only if there are PDF documents where doing so
// would significantly improve performance.
if (!(gState instanceof Dict)) {
throw new FormatError("GState should be a dictionary.");
}

self
.setGState({
resources,
gState,
operatorList,
cacheKey: name,
task,
stateManager,
localGStateCache,
localColorSpaceCache,
})
.then(resolveGState, rejectGState);
}).catch(function (reason) {
if (reason instanceof AbortException) {
return;
}
if (self.options.ignoreErrors) {
// Error(s) in the ExtGState -- sending unsupported feature
// notification and allow parsing/rendering to continue.
self.handler.send("UnsupportedFeature", {
featureId: UNSUPPORTED_FEATURES.errorExtGState,
});
warn(`getOperatorList - ignoring ExtGState: "${reason}".`);
return;
}
throw reason;
})
);
return;
case OPS.moveTo:
Expand Down Expand Up @@ -1791,6 +1848,7 @@ class PartialEvaluator {
// The xobj is parsed iff it's needed, e.g. if there is a `DO` cmd.
var xobjs = null;
const emptyXObjectCache = new LocalImageCache();
const emptyGStateCache = new LocalGStateCache();

var preprocessor = new EvaluatorPreprocessor(stream, xref, stateManager);

Expand Down Expand Up @@ -2363,25 +2421,59 @@ class PartialEvaluator {
);
return;
case OPS.setGState:
flushTextContentItem();
var dictName = args[0];
var extGState = resources.get("ExtGState");

if (!isDict(extGState) || !isName(dictName)) {
name = args[0].name;
if (name && emptyGStateCache.getByName(name)) {
break;
}
var gState = extGState.get(dictName.name);
if (!isDict(gState)) {
break;
}
var gStateFont = gState.get("Font");
if (gStateFont) {
textState.fontName = null;
textState.fontSize = gStateFont[1];
next(handleSetFont(null, gStateFont[0]));
return;
}
break;

next(
new Promise(function (resolveGState, rejectGState) {
if (!name) {
throw new FormatError("GState must be referred to by name.");
}

const extGState = resources.get("ExtGState");
if (!(extGState instanceof Dict)) {
throw new FormatError("ExtGState should be a dictionary.");
}

const gState = extGState.get(name);
// TODO: Attempt to lookup cached GStates by reference as well,
// if and only if there are PDF documents where doing so
// would significantly improve performance.
if (!(gState instanceof Dict)) {
throw new FormatError("GState should be a dictionary.");
}

const gStateFont = gState.get("Font");
if (!gStateFont) {
emptyGStateCache.set(name, gState.objId, true);

resolveGState();
return;
}
flushTextContentItem();

textState.fontName = null;
textState.fontSize = gStateFont[1];
handleSetFont(null, gStateFont[0]).then(
resolveGState,
rejectGState
);
}).catch(function (reason) {
if (reason instanceof AbortException) {
return;
}
if (self.options.ignoreErrors) {
// Error(s) in the ExtGState -- allow text-extraction to
// continue.
warn(`getTextContent - ignoring ExtGState: "${reason}".`);
return;
}
throw reason;
})
);
return;
} // switch
if (textContent.items.length >= sink.desiredSize) {
// Wait for ready, if we reach highWaterMark.
Expand Down
22 changes: 22 additions & 0 deletions src/core/image_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,27 @@ class LocalFunctionCache extends BaseLocalCache {
}
}

class LocalGStateCache extends BaseLocalCache {
set(name, ref = null, data) {
if (!name) {
throw new Error('LocalGStateCache.set - expected "name" argument.');
}
if (ref) {
if (this._imageCache.has(ref)) {
return;
}
this._nameRefMap.set(name, ref);
this._imageCache.put(ref, data);
return;
}
// name
if (this._imageMap.has(name)) {
return;
}
this._imageMap.set(name, data);
}
}

class GlobalImageCache {
static get NUM_PAGES_THRESHOLD() {
return shadow(this, "NUM_PAGES_THRESHOLD", 2);
Expand Down Expand Up @@ -210,5 +231,6 @@ export {
LocalImageCache,
LocalColorSpaceCache,
LocalFunctionCache,
LocalGStateCache,
GlobalImageCache,
};
44 changes: 28 additions & 16 deletions test/unit/evaluator_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,23 +242,35 @@ describe("evaluator", function () {
);
});
it("should execute if nested commands", function (done) {
const gState = new Dict();
gState.set("LW", 2);
gState.set("CA", 0.5);

const extGState = new Dict();
extGState.set("GS2", gState);

const resources = new ResourcesMock();
resources.ExtGState = extGState;

var stream = new StringStream("/F2 /GS2 gs 5.711 Tf");
runOperatorListCheck(
partialEvaluator,
stream,
new ResourcesMock(),
function (result) {
expect(result.fnArray.length).toEqual(3);
expect(result.fnArray[0]).toEqual(OPS.setGState);
expect(result.fnArray[1]).toEqual(OPS.dependency);
expect(result.fnArray[2]).toEqual(OPS.setFont);
expect(result.argsArray.length).toEqual(3);
expect(result.argsArray[0].length).toEqual(1);
expect(result.argsArray[1].length).toEqual(1);
expect(result.argsArray[2].length).toEqual(2);
done();
}
);
runOperatorListCheck(partialEvaluator, stream, resources, function (
result
) {
expect(result.fnArray.length).toEqual(3);
expect(result.fnArray[0]).toEqual(OPS.setGState);
expect(result.fnArray[1]).toEqual(OPS.dependency);
expect(result.fnArray[2]).toEqual(OPS.setFont);
expect(result.argsArray.length).toEqual(3);
expect(result.argsArray[0]).toEqual([
[
["LW", 2],
["CA", 0.5],
],
]);
expect(result.argsArray[1]).toEqual(["g_font_error"]);
expect(result.argsArray[2]).toEqual(["g_font_error", 5.711]);
done();
});
});
it("should skip if too few arguments", function (done) {
var stream = new StringStream("5 d0");
Expand Down