Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
calixteman committed Nov 20, 2023
1 parent 3459615 commit f0ee554
Show file tree
Hide file tree
Showing 27 changed files with 3,564 additions and 122 deletions.
2,002 changes: 2,002 additions & 0 deletions 0001-WIP.patch

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions l10n/en-US/viewer.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -339,10 +339,13 @@ pdfjs-editor-stamp-add-image-button-label = Add image
pdfjs-free-text =
.aria-label = Text Editor
pdfjs-free-text-default-content = Start typing…
pdfjs-comment-default-content=Type your comment here…
pdfjs-ink =
.aria-label = Draw Editor
pdfjs-ink-canvas =
.aria-label = User-created image
pdfjs-comment =
.aria-label = Comment Editor
## Alt-text dialog

Expand Down
6 changes: 0 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 3 additions & 20 deletions src/display/annotation_layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
import {
DOMSVGFactory,
getFilenameFromUrl,
lighten,
PDFDateString,
setLayerDimensions,
} from "./display_utils.js";
Expand Down Expand Up @@ -2132,26 +2133,8 @@ class PopupElement {
popup.className = "popup";

if (this.#color) {
const baseColor = (popup.style.outlineColor = Util.makeHexColor(
...this.#color
));
if (
(typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) ||
CSS.supports("background-color", "color-mix(in srgb, red 30%, white)")
) {
popup.style.backgroundColor = `color-mix(in srgb, ${baseColor} 30%, white)`;
} else {
// color-mix isn't supported in some browsers hence this version.
// See https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/color-mix#browser_compatibility
// TODO: Use color-mix when it's supported everywhere.
// Enlighten the color.
const BACKGROUND_ENLIGHT = 0.7;
popup.style.backgroundColor = Util.makeHexColor(
...this.#color.map(c =>
Math.floor(BACKGROUND_ENLIGHT * (255 - c) + c)
)
);
}
popup.style.outlineColor = Util.makeHexColor(...this.#color);
popup.style.backgroundColor = lighten(this.#color, 30);
}

const header = document.createElement("span");
Expand Down
21 changes: 21 additions & 0 deletions src/display/display_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -1003,6 +1003,26 @@ function setLayerDimensions(
}
}

function lighten(color, percent = 30) {
if (
(typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) ||
CSS.supports("background-color", "color-mix(in srgb, red 30%, white)")
) {
const baseColor =
typeof color === "string" ? color : Util.makeHexColor(...color);
return `color-mix(in srgb, ${baseColor} ${percent}%, white)`;
}
// color-mix isn't supported in some browsers hence this version.
// See https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/color-mix#browser_compatibility
// TODO: Use color-mix when it's supported everywhere.
// Enlighten the color.
const BACKGROUND_ENLIGHT = 1 - percent / 100;
const baseColor = typeof color === "string" ? getRGB(color) : color;
return Util.makeHexColor(
...baseColor.map(c => Math.floor(BACKGROUND_ENLIGHT * (255 - c) + c))
);
}

export {
deprecated,
DOMCanvasFactory,
Expand All @@ -1021,6 +1041,7 @@ export {
isDataScheme,
isPdfFile,
isValidFetchUrl,
lighten,
noContextMenu,
PageViewport,
PDFDateString,
Expand Down
203 changes: 203 additions & 0 deletions src/display/draw_layer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
/* 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.
*/

import { DOMSVGFactory } from "./display_utils.js";
import { shadow } from "../shared/util.js";

/**
* Manage the SVGs drawn on top of the page canvas.
* It's important to have them directly on top of the canvas because we want to
* be able to use mix-blend-mode for some of them.
*/
class DrawLayer {
#parent = null;

#id = 0;

#mapping = new Map();

constructor({ pageIndex }) {
this.pageIndex = pageIndex;
}

setParent(parent) {
if (!this.#parent) {
this.#parent = parent;
return;
}

if (this.#parent !== parent) {
if (this.#mapping.size > 0) {
for (const root of this.#mapping.values()) {
root.remove();
parent.append(root);
}
}
this.#parent = parent;
}
}

static get _svgFactory() {
return shadow(this, "_svgFactory", new DOMSVGFactory());
}

static #setBox(element, { x, y, width, height }) {
const { style } = element;
style.top = `${100 * y}%`;
style.left = `${100 * x}%`;
style.width = `${100 * width}%`;
style.height = `${100 * height}%`;
}

#createSVG(box) {
const svg = DrawLayer._svgFactory.createElement("svg");
this.#parent.append(svg);
svg.setAttribute("preserveAspectRatio", "none");
svg.setAttribute("viewBox", "0 0 1 1");
DrawLayer.#setBox(svg, box);

return svg;
}

highlight({ outlines, box }, color, opacity) {
const id = this.#id++;
const root = this.#createSVG(box);
root.classList.add("highlight");
const defs = DrawLayer._svgFactory.createElement("defs");
root.append(defs);
const path = DrawLayer._svgFactory.createElement("path");
defs.append(path);
const pathId = `path_p${this.pageIndex}_${id}`;
path.setAttribute("id", pathId);
path.setAttribute(
"d",
DrawLayer.#extractPathFromHighlightOutlines(outlines)
);

// Create the clipping path for the editor div.
const clipPath = DrawLayer._svgFactory.createElement("clipPath");
defs.append(clipPath);
const clipPathId = `clip_${pathId}`;
clipPath.setAttribute("id", clipPathId);
clipPath.setAttribute("clipPathUnits", "objectBoundingBox");
const clipPathUse = DrawLayer._svgFactory.createElement("use");
clipPath.append(clipPathUse);
clipPathUse.setAttribute("href", `#${pathId}`);
clipPathUse.classList.add("clip");

const use = DrawLayer._svgFactory.createElement("use");
root.append(use);
root.setAttribute("fill", color);
root.setAttribute("fill-opacity", opacity);
use.setAttribute("href", `#${pathId}`);

this.#mapping.set(id, root);

return { id, clipPathId: `url(#${clipPathId})` };
}

highlightOutline({ outlines, box }) {
// We cannot draw the outline directly in the SVG for highlights because
// it composes with its parent with mix-blend-mode: multiply.
// But the outline has a different mix-blend-mode, so we need to draw it in
// its own SVG.
const id = this.#id++;
const root = this.#createSVG(box);
root.classList.add("highlightOutline");
const defs = DrawLayer._svgFactory.createElement("defs");
root.append(defs);
const path = DrawLayer._svgFactory.createElement("path");
defs.append(path);
const pathId = `path_p${this.pageIndex}_${id}`;
path.setAttribute("id", pathId);
path.setAttribute(
"d",
DrawLayer.#extractPathFromHighlightOutlines(outlines)
);
path.setAttribute("vector-effect", "non-scaling-stroke");

const use1 = DrawLayer._svgFactory.createElement("use");
root.append(use1);
use1.setAttribute("href", `#${pathId}`);
const use2 = use1.cloneNode();
root.append(use2);
use1.classList.add("mainOutline");
use2.classList.add("secondaryOutline");

this.#mapping.set(id, root);

return id;
}

static #extractPathFromHighlightOutlines(polygons) {
const buffer = [];
for (const polygon of polygons) {
let [prevX, prevY] = polygon;
buffer.push(`M${prevX} ${prevY}`);
for (let i = 2; i < polygon.length; i += 2) {
const x = polygon[i];
const y = polygon[i + 1];
if (x === prevX) {
buffer.push(`V${y}`);
prevY = y;
} else if (y === prevY) {
buffer.push(`H${x}`);
prevX = x;
}
}
buffer.push("Z");
}
return buffer.join(" ");
}

updateBox(id, box) {
DrawLayer.#setBox(this.#mapping.get(id), box);
}

rotate(id, angle) {
this.#mapping.get(id).setAttribute("data-main-rotation", angle);
}

changeColor(id, color) {
this.#mapping.get(id).setAttribute("fill", color);
}

changeOpacity(id, opacity) {
this.#mapping.get(id).setAttribute("fill-opacity", opacity);
}

addClass(id, className) {
this.#mapping.get(id).classList.add(className);
}

removeClass(id, className) {
this.#mapping.get(id).classList.remove(className);
}

remove(id) {
this.#mapping.get(id).remove();
this.#mapping.delete(id);
}

destroy() {
this.#parent = null;
for (const root of this.#mapping.values()) {
root.remove();
}
this.#mapping.clear();
}
}

export { DrawLayer };
Loading

0 comments on commit f0ee554

Please sign in to comment.