mirror of
https://github.com/mozilla/pdf.js.git
synced 2025-04-20 15:18:08 +02:00
[Editor] Add a way to extract the outlines of a union of rectangles
The goal is to be able to get these outlines to fill the shape corresponding to a text selection in order to highlight some text contents. The outlines will be used either to show selected/hovered highlights.
This commit is contained in:
parent
d8424a43ba
commit
31d9b9f574
11 changed files with 777 additions and 12 deletions
39
test/draw_layer_test.css
Normal file
39
test/draw_layer_test.css
Normal file
|
@ -0,0 +1,39 @@
|
|||
/* Copyright 2023 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.
|
||||
*/
|
||||
|
||||
/* Used in 'highlight' tests */
|
||||
|
||||
.highlight {
|
||||
position: absolute;
|
||||
mix-blend-mode: multiply;
|
||||
fill-rule: evenodd;
|
||||
}
|
||||
|
||||
.highlightOutline {
|
||||
position: absolute;
|
||||
mix-blend-mode: normal;
|
||||
fill-rule: evenodd;
|
||||
fill: none;
|
||||
|
||||
.mainOutline {
|
||||
stroke: white;
|
||||
stroke-width: 4px;
|
||||
}
|
||||
|
||||
.secondaryOutline {
|
||||
stroke: blue;
|
||||
stroke-width: 2px;
|
||||
}
|
||||
}
|
132
test/driver.js
132
test/driver.js
|
@ -17,8 +17,10 @@
|
|||
const {
|
||||
AnnotationLayer,
|
||||
AnnotationMode,
|
||||
DrawLayer,
|
||||
getDocument,
|
||||
GlobalWorkerOptions,
|
||||
Outliner,
|
||||
PixelsPerInch,
|
||||
PromiseCapability,
|
||||
renderTextLayer,
|
||||
|
@ -181,6 +183,11 @@ class Rasterize {
|
|||
return shadow(this, "textStylePromise", loadStyles(styles));
|
||||
}
|
||||
|
||||
static get drawLayerStylePromise() {
|
||||
const styles = [VIEWER_CSS, "./draw_layer_test.css"];
|
||||
return shadow(this, "drawLayerStylePromise", loadStyles(styles));
|
||||
}
|
||||
|
||||
static get xfaStylePromise() {
|
||||
const styles = [VIEWER_CSS, "./xfa_layer_builder_overrides.css"];
|
||||
return shadow(this, "xfaStylePromise", loadStyles(styles));
|
||||
|
@ -292,6 +299,7 @@ class Rasterize {
|
|||
});
|
||||
|
||||
await task.promise;
|
||||
|
||||
svg.append(foreignObject);
|
||||
|
||||
await writeSVG(svg, ctx);
|
||||
|
@ -300,6 +308,93 @@ class Rasterize {
|
|||
}
|
||||
}
|
||||
|
||||
static async highlightLayer(ctx, viewport, textContent) {
|
||||
try {
|
||||
const { svg, foreignObject, style, div } = this.createContainer(viewport);
|
||||
const dummyParent = document.createElement("div");
|
||||
|
||||
// Items are transformed to have 1px font size.
|
||||
svg.setAttribute("font-size", 1);
|
||||
|
||||
const [common, overrides] = await this.drawLayerStylePromise;
|
||||
style.textContent =
|
||||
`${common}\n${overrides}` +
|
||||
`:root { --scale-factor: ${viewport.scale} }`;
|
||||
|
||||
// Rendering text layer as HTML.
|
||||
const task = renderTextLayer({
|
||||
textContentSource: textContent,
|
||||
container: dummyParent,
|
||||
viewport,
|
||||
});
|
||||
|
||||
await task.promise;
|
||||
|
||||
const { _pageWidth, _pageHeight, _textContentSource, _textDivs } = task;
|
||||
const boxes = [];
|
||||
let posRegex;
|
||||
for (
|
||||
let i = 0, j = 0, ii = _textContentSource.items.length;
|
||||
i < ii;
|
||||
i++
|
||||
) {
|
||||
const { width, height, type } = _textContentSource.items[i];
|
||||
if (type) {
|
||||
continue;
|
||||
}
|
||||
const { top, left } = _textDivs[j++].style;
|
||||
let x = parseFloat(left) / 100;
|
||||
let y = parseFloat(top) / 100;
|
||||
if (isNaN(x)) {
|
||||
posRegex ||= /^calc\(var\(--scale-factor\)\*(.*)px\)$/;
|
||||
// The element is tagged so we've to extract the position from the
|
||||
// string, e.g. `calc(var(--scale-factor)*66.32px)`.
|
||||
let match = left.match(posRegex);
|
||||
if (match) {
|
||||
x = parseFloat(match[1]) / _pageWidth;
|
||||
}
|
||||
|
||||
match = top.match(posRegex);
|
||||
if (match) {
|
||||
y = parseFloat(match[1]) / _pageHeight;
|
||||
}
|
||||
}
|
||||
if (width === 0 || height === 0) {
|
||||
continue;
|
||||
}
|
||||
boxes.push({
|
||||
x,
|
||||
y,
|
||||
width: width / _pageWidth,
|
||||
height: height / _pageHeight,
|
||||
});
|
||||
}
|
||||
// We set the borderWidth to 0.001 to slighly increase the size of the
|
||||
// boxes so that they can be merged together.
|
||||
const outliner = new Outliner(boxes, /* borderWidth = */ 0.001);
|
||||
// We set the borderWidth to 0.0025 in order to have an outline which is
|
||||
// slightly bigger than the highlight itself.
|
||||
// We must add an inner margin to avoid to have a partial outline.
|
||||
const outlinerForOutline = new Outliner(
|
||||
boxes,
|
||||
/* borderWidth = */ 0.0025,
|
||||
/* innerMargin = */ 0.001
|
||||
);
|
||||
const drawLayer = new DrawLayer({ pageIndex: 0 });
|
||||
drawLayer.setParent(div);
|
||||
drawLayer.highlight(outliner.getOutlines(), "orange", 0.4);
|
||||
drawLayer.highlightOutline(outlinerForOutline.getOutlines());
|
||||
|
||||
svg.append(foreignObject);
|
||||
|
||||
await writeSVG(svg, ctx);
|
||||
|
||||
drawLayer.destroy();
|
||||
} catch (reason) {
|
||||
throw new Error(`Rasterize.textLayer: "${reason?.message}".`);
|
||||
}
|
||||
}
|
||||
|
||||
static async xfaLayer(
|
||||
ctx,
|
||||
viewport,
|
||||
|
@ -737,7 +832,7 @@ class Driver {
|
|||
|
||||
let textLayerCanvas, annotationLayerCanvas, annotationLayerContext;
|
||||
let initPromise;
|
||||
if (task.type === "text") {
|
||||
if (task.type === "text" || task.type === "highlight") {
|
||||
// Using a dummy canvas for PDF context drawing operations
|
||||
textLayerCanvas = this.textLayerCanvas;
|
||||
if (!textLayerCanvas) {
|
||||
|
@ -761,11 +856,17 @@ class Driver {
|
|||
disableNormalization: true,
|
||||
})
|
||||
.then(function (textContent) {
|
||||
return Rasterize.textLayer(
|
||||
textLayerContext,
|
||||
viewport,
|
||||
textContent
|
||||
);
|
||||
return task.type === "text"
|
||||
? Rasterize.textLayer(
|
||||
textLayerContext,
|
||||
viewport,
|
||||
textContent
|
||||
)
|
||||
: Rasterize.highlightLayer(
|
||||
textLayerContext,
|
||||
viewport,
|
||||
textContent
|
||||
);
|
||||
});
|
||||
} else {
|
||||
textLayerCanvas = null;
|
||||
|
@ -840,12 +941,19 @@ class Driver {
|
|||
const completeRender = error => {
|
||||
// if text layer is present, compose it on top of the page
|
||||
if (textLayerCanvas) {
|
||||
ctx.save();
|
||||
ctx.globalCompositeOperation = "screen";
|
||||
ctx.fillStyle = "rgb(128, 255, 128)"; // making it green
|
||||
ctx.fillRect(0, 0, pixelWidth, pixelHeight);
|
||||
ctx.restore();
|
||||
ctx.drawImage(textLayerCanvas, 0, 0);
|
||||
if (task.type === "text") {
|
||||
ctx.save();
|
||||
ctx.globalCompositeOperation = "screen";
|
||||
ctx.fillStyle = "rgb(128, 255, 128)"; // making it green
|
||||
ctx.fillRect(0, 0, pixelWidth, pixelHeight);
|
||||
ctx.restore();
|
||||
ctx.drawImage(textLayerCanvas, 0, 0);
|
||||
} else if (task.type === "highlight") {
|
||||
ctx.save();
|
||||
ctx.globalCompositeOperation = "multiply";
|
||||
ctx.drawImage(textLayerCanvas, 0, 0);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
// If we have annotation layer, compose it on top of the page.
|
||||
if (annotationLayerCanvas) {
|
||||
|
|
|
@ -670,6 +670,7 @@ function checkRefTestResults(browser, id, results) {
|
|||
switch (task.type) {
|
||||
case "eq":
|
||||
case "text":
|
||||
case "highlight":
|
||||
checkEq(task, results, browser, session.masterMode);
|
||||
break;
|
||||
case "fbf":
|
||||
|
|
|
@ -8214,5 +8214,19 @@
|
|||
"rounds": 1,
|
||||
"link": true,
|
||||
"type": "eq"
|
||||
},
|
||||
{
|
||||
"id": "tracemonkey-highlight",
|
||||
"file": "pdfs/tracemonkey.pdf",
|
||||
"md5": "9a192d8b1a7dc652a19835f6f08098bd",
|
||||
"rounds": 1,
|
||||
"type": "highlight"
|
||||
},
|
||||
{
|
||||
"id": "160F-2019-highlight",
|
||||
"file": "pdfs/160F-2019.pdf",
|
||||
"md5": "71591f11ee717e12887f529c84d5ae89",
|
||||
"rounds": 1,
|
||||
"type": "highlight"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -63,7 +63,9 @@ import {
|
|||
import { AnnotationEditorLayer } from "../../src/display/editor/annotation_editor_layer.js";
|
||||
import { AnnotationEditorUIManager } from "../../src/display/editor/tools.js";
|
||||
import { AnnotationLayer } from "../../src/display/annotation_layer.js";
|
||||
import { DrawLayer } from "../../src/display/draw_layer.js";
|
||||
import { GlobalWorkerOptions } from "../../src/display/worker_options.js";
|
||||
import { Outliner } from "../../src/display/editor/outliner.js";
|
||||
import { XfaLayer } from "../../src/display/xfa_layer.js";
|
||||
|
||||
const expectedAPI = Object.freeze({
|
||||
|
@ -78,6 +80,7 @@ const expectedAPI = Object.freeze({
|
|||
CMapCompressionType,
|
||||
createValidAbsoluteUrl,
|
||||
DOMSVGFactory,
|
||||
DrawLayer,
|
||||
FeatureTest,
|
||||
fetchData,
|
||||
getDocument,
|
||||
|
@ -93,6 +96,7 @@ const expectedAPI = Object.freeze({
|
|||
noContextMenu,
|
||||
normalizeUnicode,
|
||||
OPS,
|
||||
Outliner,
|
||||
PasswordResponses,
|
||||
PDFDataRangeTransport,
|
||||
PDFDateString,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue