Move the rasterization logic into one single class

This refactoring ensures that we can get rid of the closures and
encapsulate the logic in a nicer way with e.g., getters for the style
promises.
This commit is contained in:
Tim van der Meij 2021-12-05 14:57:01 +01:00
parent 33dc0628a0
commit 03506f25c0
No known key found for this signature in database
GPG Key ID: 8C3FD2925A5F2762

View File

@ -23,6 +23,7 @@ const {
GlobalWorkerOptions, GlobalWorkerOptions,
PixelsPerInch, PixelsPerInch,
renderTextLayer, renderTextLayer,
shadow,
XfaLayer, XfaLayer,
} = pdfjsLib; } = pdfjsLib;
const { SimpleLinkService } = pdfjsViewer; const { SimpleLinkService } = pdfjsViewer;
@ -37,26 +38,25 @@ const RENDER_TASK_ON_CONTINUE_DELAY = 5; // ms
const SVG_NS = "http://www.w3.org/2000/svg"; const SVG_NS = "http://www.w3.org/2000/svg";
function loadStyles(styles) { function loadStyles(styles) {
styles = Object.values(styles); const promises = [];
if (styles.every(style => style.promise)) {
return Promise.all(styles.map(style => style.promise)); for (const file of styles) {
promises.push(
new Promise(function (resolve, reject) {
const xhr = new XMLHttpRequest();
xhr.open("GET", file);
xhr.onload = function () {
resolve(xhr.responseText);
};
xhr.onerror = function (e) {
reject(new Error(`Error fetching style (${file}): ${e}`));
};
xhr.send(null);
})
);
} }
for (const style of styles) { return Promise.all(promises);
style.promise = new Promise(function (resolve, reject) {
const xhr = new XMLHttpRequest();
xhr.open("GET", style.file);
xhr.onload = function () {
resolve(xhr.responseText);
};
xhr.onerror = function (e) {
reject(new Error(`Error fetching style (${style.file}): ${e}`));
};
xhr.send(null);
});
}
return Promise.all(styles.map(style => style.promise));
} }
function writeSVG(svgElement, ctx) { function writeSVG(svgElement, ctx) {
@ -144,98 +144,38 @@ async function resolveImages(node, silentErrors = false) {
await Promise.all(loadedPromises); await Promise.all(loadedPromises);
} }
/** class Rasterize {
* @class
*/
const rasterizeTextLayer = (function rasterizeTextLayerClosure() {
const styles = {
common: {
file: "./text_layer_test.css",
promise: null,
},
};
// eslint-disable-next-line no-shadow
async function rasterizeTextLayer(
ctx,
viewport,
textContent,
enhanceTextSelection
) {
try {
// Building SVG with size of the viewport.
const svg = document.createElementNS(SVG_NS, "svg:svg");
svg.setAttribute("width", viewport.width + "px");
svg.setAttribute("height", viewport.height + "px");
// items are transformed to have 1px font size
svg.setAttribute("font-size", 1);
// Adding element to host our HTML (style + text layer div).
const foreignObject = document.createElementNS(
SVG_NS,
"svg:foreignObject"
);
foreignObject.setAttribute("x", "0");
foreignObject.setAttribute("y", "0");
foreignObject.setAttribute("width", viewport.width + "px");
foreignObject.setAttribute("height", viewport.height + "px");
const style = document.createElement("style");
const stylePromise = loadStyles(styles);
foreignObject.appendChild(style);
const div = document.createElement("div");
div.className = "textLayer";
foreignObject.appendChild(div);
const [cssRules] = await stylePromise;
style.textContent = cssRules;
// Rendering text layer as HTML.
const task = renderTextLayer({
textContent,
container: div,
viewport,
enhanceTextSelection,
});
await task.promise;
task.expandTextDivs(true);
svg.appendChild(foreignObject);
await writeSVG(svg, ctx);
} catch (reason) {
throw new Error(`rasterizeTextLayer: "${reason?.message}".`);
}
}
return rasterizeTextLayer;
})();
/**
* @class
*/
const rasterizeAnnotationLayer = (function rasterizeAnnotationLayerClosure() {
/** /**
* For the reference tests, the entire annotation layer must be visible. To * For the reference tests, the full content of the various layers must be
* achieve this, we load the common styles as used by the viewer and extend * visible. To achieve this, we load the common styles as used by the viewer
* them with a set of overrides to make all elements visible. * and extend them with a set of overrides to make all elements visible.
* *
* Note that we cannot simply use `@import` to import the common styles in * Note that we cannot simply use `@import` to import the common styles in
* the overrides file because the browser does not resolve that when the * the overrides file because the browser does not resolve that when the
* styles are inserted via XHR. Therefore, we load and combine them here. * styles are inserted via XHR. Therefore, we load and combine them here.
*/ */
const styles = { static get annotationStylePromise() {
common: { const styles = [
file: "../web/annotation_layer_builder.css", "../web/annotation_layer_builder.css",
promise: null, "./annotation_layer_builder_overrides.css",
}, ];
overrides: { return shadow(this, "annotationStylePromise", loadStyles(styles));
file: "./annotation_layer_builder_overrides.css", }
promise: null,
},
};
// eslint-disable-next-line no-shadow static get textStylePromise() {
async function rasterizeAnnotationLayer( const styles = ["./text_layer_test.css"];
return shadow(this, "textStylePromise", loadStyles(styles));
}
static get xfaStylePromise() {
const styles = [
"../web/xfa_layer_builder.css",
"./xfa_layer_builder_overrides.css",
];
return shadow(this, "xfaStylePromise", loadStyles(styles));
}
static async annotationLayer(
ctx, ctx,
viewport, viewport,
annotations, annotations,
@ -260,13 +200,12 @@ const rasterizeAnnotationLayer = (function rasterizeAnnotationLayerClosure() {
foreignObject.setAttribute("width", viewport.width + "px"); foreignObject.setAttribute("width", viewport.width + "px");
foreignObject.setAttribute("height", viewport.height + "px"); foreignObject.setAttribute("height", viewport.height + "px");
const style = document.createElement("style"); const style = document.createElement("style");
const stylePromise = loadStyles(styles);
foreignObject.appendChild(style); foreignObject.appendChild(style);
const div = document.createElement("div"); const div = document.createElement("div");
div.className = "annotationLayer"; div.className = "annotationLayer";
// Rendering annotation layer as HTML. // Rendering annotation layer as HTML.
const [common, overrides] = await stylePromise; const [common, overrides] = await this.annotationStylePromise;
style.textContent = common + "\n" + overrides; style.textContent = common + "\n" + overrides;
const annotation_viewport = viewport.clone({ dontFlip: true }); const annotation_viewport = viewport.clone({ dontFlip: true });
@ -293,30 +232,56 @@ const rasterizeAnnotationLayer = (function rasterizeAnnotationLayerClosure() {
await writeSVG(svg, ctx); await writeSVG(svg, ctx);
} catch (reason) { } catch (reason) {
throw new Error(`rasterizeAnnotationLayer: "${reason?.message}".`); throw new Error(`Rasterize.annotationLayer: "${reason?.message}".`);
} }
} }
return rasterizeAnnotationLayer; static async textLayer(ctx, viewport, textContent, enhanceTextSelection) {
})(); try {
// Building SVG with size of the viewport.
const svg = document.createElementNS(SVG_NS, "svg:svg");
svg.setAttribute("width", viewport.width + "px");
svg.setAttribute("height", viewport.height + "px");
// items are transformed to have 1px font size
svg.setAttribute("font-size", 1);
/** // Adding element to host our HTML (style + text layer div).
* @class const foreignObject = document.createElementNS(
*/ SVG_NS,
const rasterizeXfaLayer = (function rasterizeXfaLayerClosure() { "svg:foreignObject"
const styles = { );
common: { foreignObject.setAttribute("x", "0");
file: "../web/xfa_layer_builder.css", foreignObject.setAttribute("y", "0");
promise: null, foreignObject.setAttribute("width", viewport.width + "px");
}, foreignObject.setAttribute("height", viewport.height + "px");
overrides: { const style = document.createElement("style");
file: "./xfa_layer_builder_overrides.css", foreignObject.appendChild(style);
promise: null, const div = document.createElement("div");
}, div.className = "textLayer";
}; foreignObject.appendChild(div);
// eslint-disable-next-line no-shadow const [cssRules] = await this.textStylePromise;
async function rasterizeXfaLayer( style.textContent = cssRules;
// Rendering text layer as HTML.
const task = renderTextLayer({
textContent,
container: div,
viewport,
enhanceTextSelection,
});
await task.promise;
task.expandTextDivs(true);
svg.appendChild(foreignObject);
await writeSVG(svg, ctx);
} catch (reason) {
throw new Error(`Rasterize.textLayer: "${reason?.message}".`);
}
}
static async xfaLayer(
ctx, ctx,
viewport, viewport,
xfa, xfa,
@ -338,12 +303,11 @@ const rasterizeXfaLayer = (function rasterizeXfaLayerClosure() {
foreignObject.setAttribute("width", viewport.width + "px"); foreignObject.setAttribute("width", viewport.width + "px");
foreignObject.setAttribute("height", viewport.height + "px"); foreignObject.setAttribute("height", viewport.height + "px");
const style = document.createElement("style"); const style = document.createElement("style");
const stylePromise = loadStyles(styles);
foreignObject.appendChild(style); foreignObject.appendChild(style);
const div = document.createElement("div"); const div = document.createElement("div");
foreignObject.appendChild(div); foreignObject.appendChild(div);
const [common, overrides] = await stylePromise; const [common, overrides] = await this.xfaStylePromise;
style.textContent = fontRules + "\n" + common + "\n" + overrides; style.textContent = fontRules + "\n" + common + "\n" + overrides;
XfaLayer.render({ XfaLayer.render({
@ -362,12 +326,10 @@ const rasterizeXfaLayer = (function rasterizeXfaLayerClosure() {
await writeSVG(svg, ctx); await writeSVG(svg, ctx);
} catch (reason) { } catch (reason) {
throw new Error(`rasterizeXfaLayer: "${reason?.message}".`); throw new Error(`Rasterize.xfaLayer: "${reason?.message}".`);
} }
} }
}
return rasterizeXfaLayer;
})();
/** /**
* @typedef {Object} DriverOptions * @typedef {Object} DriverOptions
@ -713,7 +675,7 @@ class Driver {
includeMarkedContent: true, includeMarkedContent: true,
}) })
.then(function (textContent) { .then(function (textContent) {
return rasterizeTextLayer( return Rasterize.textLayer(
textLayerContext, textLayerContext,
viewport, viewport,
textContent, textContent,
@ -754,7 +716,7 @@ class Driver {
annotationCanvasMap = new Map(); annotationCanvasMap = new Map();
} else { } else {
initPromise = page.getXfa().then(function (xfa) { initPromise = page.getXfa().then(function (xfa) {
return rasterizeXfaLayer( return Rasterize.xfaLayer(
annotationLayerContext, annotationLayerContext,
viewport, viewport,
xfa, xfa,
@ -817,7 +779,7 @@ class Driver {
} }
return renderTask.promise.then(function () { return renderTask.promise.then(function () {
if (annotationCanvasMap) { if (annotationCanvasMap) {
rasterizeAnnotationLayer( Rasterize.annotationLayer(
annotationLayerContext, annotationLayerContext,
viewport, viewport,
data, data,