diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index 0f59633cb..c72625c7c 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -38,6 +38,7 @@ import { warn, } from "../shared/util.js"; import { + applyOpacity, changeLightness, PDFDateString, setLayerDimensions, @@ -232,15 +233,8 @@ class AnnotationElement { if (!this.data.color) { return null; } - const [r, g, b] = this.data.color; - const opacity = this.data.opacity ?? 1; - const oppositeOpacity = 255 * (1 - opacity); - - return changeLightness( - Math.min(r + oppositeOpacity, 255), - Math.min(g + oppositeOpacity, 255), - Math.min(b + oppositeOpacity, 255) - ); + const [r, g, b] = applyOpacity(...this.data.color, this.data.opacity); + return changeLightness(r, g, b); } _normalizePoint(point) { diff --git a/src/display/display_utils.js b/src/display/display_utils.js index 17ec261da..0cb65303f 100644 --- a/src/display/display_utils.js +++ b/src/display/display_utils.js @@ -770,7 +770,24 @@ const SupportedImageMimeTypes = [ "image/x-icon", ]; -function changeLightness(r, g, b, lumCallback = l => (1 + Math.sqrt(l)) / 2) { +class ColorScheme { + static get isDarkMode() { + return shadow( + this, + "isDarkMode", + !!window?.matchMedia?.("(prefers-color-scheme: dark)").matches + ); + } +} + +function changeLightness( + r, + g, + b, + lumCallback = ColorScheme.isDarkMode + ? l => (1 - Math.sqrt(1 - l)) / 2 + : l => (1 + Math.sqrt(l)) / 2 +) { r /= 255; g /= 255; b /= 255; @@ -803,8 +820,19 @@ function changeLightness(r, g, b, lumCallback = l => (1 + Math.sqrt(l)) / 2) { return `hsl(${h}, ${s}%, ${newL}%)`; } +function applyOpacity(r, g, b, opacity) { + opacity = Math.min(Math.max(opacity ?? 1, 0), 1); + const white = 255 * (1 - opacity); + r = Math.round(r * opacity + white); + g = Math.round(g * opacity + white); + b = Math.round(b * opacity + white); + return [r, g, b]; +} + export { + applyOpacity, changeLightness, + ColorScheme, deprecated, fetchData, getColorValues, diff --git a/src/display/editor/comment.js b/src/display/editor/comment.js index 37ed503f2..f71892321 100644 --- a/src/display/editor/comment.js +++ b/src/display/editor/comment.js @@ -58,6 +58,10 @@ class Comment { : position[0]) }% - var(--comment-button-dim))`; style.top = `calc(${100 * position[1]}% - var(--comment-button-dim))`; + const color = this.#editor.commentButtonColor; + if (color) { + style.backgroundColor = color; + } } return this.#render(button); diff --git a/src/display/editor/editor.js b/src/display/editor/editor.js index ee895a905..19d34cf97 100644 --- a/src/display/editor/editor.js +++ b/src/display/editor/editor.js @@ -22,13 +22,18 @@ import { ColorManager, KeyboardManager, } from "./tools.js"; +import { + applyOpacity, + changeLightness, + noContextMenu, + stopEvent, +} from "../display_utils.js"; import { FeatureTest, MathClamp, shadow, unreachable, } from "../../shared/util.js"; -import { noContextMenu, stopEvent } from "../display_utils.js"; import { AltText } from "./alt_text.js"; import { Comment } from "./comment.js"; import { EditorToolbar } from "./toolbar.js"; @@ -1857,6 +1862,17 @@ class AnnotationEditor { return this._uiManager.direction === "ltr" ? [1, 0] : [0, 0]; } + get commentButtonColor() { + if (!this.color) { + return null; + } + let [r, g, b] = AnnotationEditor._colorManager.convert( + this._uiManager.getNonHCMColor(this.color) + ); + [r, g, b] = applyOpacity(r, g, b, this.opacity); + return changeLightness(r, g, b); + } + /** * onkeydown callback. * @param {KeyboardEvent} event diff --git a/src/display/editor/freetext.js b/src/display/editor/freetext.js index 18b49d3e8..bc47d21c2 100644 --- a/src/display/editor/freetext.js +++ b/src/display/editor/freetext.js @@ -35,8 +35,6 @@ const EOL_PATTERN = /\r\n?|\n/g; * Basic text editor in order to create a FreeTex annotation. */ class FreeTextEditor extends AnnotationEditor { - #color; - #content = ""; #editorDivId = `${this.id}-editor`; @@ -129,7 +127,7 @@ class FreeTextEditor extends AnnotationEditor { constructor(params) { super({ ...params, name: "freeTextEditor" }); - this.#color = + this.color = params.color || FreeTextEditor._defaultColor || AnnotationEditor._defaultLineColor; @@ -201,7 +199,7 @@ class FreeTextEditor extends AnnotationEditor { get propertiesToUpdate() { return [ [AnnotationEditorParamsType.FREETEXT_SIZE, this.#fontSize], - [AnnotationEditorParamsType.FREETEXT_COLOR, this.#color], + [AnnotationEditorParamsType.FREETEXT_COLOR, this.color], ]; } @@ -215,10 +213,6 @@ class FreeTextEditor extends AnnotationEditor { return AnnotationEditorParamsType.FREETEXT_COLOR; } - get colorValue() { - return this.#color; - } - /** * Update the font size and make this action as undoable. * @param {number} fontSize @@ -248,10 +242,10 @@ class FreeTextEditor extends AnnotationEditor { */ #updateColor(color) { const setColor = col => { - this.#color = this.editorDiv.style.color = col; + this.color = this.editorDiv.style.color = col; this._colorPicker?.update(col); }; - const savedColor = this.#color; + const savedColor = this.color; this.addCommands({ cmd: setColor.bind(this, color), undo: setColor.bind(this, savedColor), @@ -581,7 +575,7 @@ class FreeTextEditor extends AnnotationEditor { const { style } = this.editorDiv; style.fontSize = `calc(${this.#fontSize}px * var(--total-scale-factor))`; - style.color = this.#color; + style.color = this.color; this.div.append(this.editorDiv); @@ -819,7 +813,7 @@ class FreeTextEditor extends AnnotationEditor { } const editor = await super.deserialize(data, parent, uiManager); editor.#fontSize = data.fontSize; - editor.#color = Util.makeHexColor(...data.color); + editor.color = Util.makeHexColor(...data.color); editor.#content = FreeTextEditor.#deserializeContent(data.value); editor._initialData = initialData; if (data.comment) { @@ -841,9 +835,7 @@ class FreeTextEditor extends AnnotationEditor { const rect = this.getPDFRect(); const color = AnnotationEditor._colorManager.convert( - this.isAttachedToDOM - ? getComputedStyle(this.editorDiv).color - : this.#color + this.isAttachedToDOM ? getComputedStyle(this.editorDiv).color : this.color ); const serialized = { @@ -895,7 +887,7 @@ class FreeTextEditor extends AnnotationEditor { } const { style } = content; style.fontSize = `calc(${this.#fontSize}px * var(--total-scale-factor))`; - style.color = this.#color; + style.color = this.color; content.replaceChildren(); for (const line of this.#content.split("\n")) { diff --git a/src/display/editor/highlight.js b/src/display/editor/highlight.js index 090edf5aa..53f43c8c0 100644 --- a/src/display/editor/highlight.js +++ b/src/display/editor/highlight.js @@ -64,8 +64,6 @@ class HighlightEditor extends AnnotationEditor { #lastPoint = null; - #opacity; - #outlineId = null; #text = ""; @@ -108,7 +106,7 @@ class HighlightEditor extends AnnotationEditor { super({ ...params, name: "highlightEditor" }); this.color = params.color || HighlightEditor._defaultColor; this.#thickness = params.thickness || HighlightEditor._defaultThickness; - this.#opacity = params.opacity || HighlightEditor._defaultOpacity; + this.opacity = params.opacity || HighlightEditor._defaultOpacity; this.#boxes = params.boxes || null; this.#methodOfCreation = params.methodOfCreation || ""; this.#text = params.text || ""; @@ -361,7 +359,7 @@ class HighlightEditor extends AnnotationEditor { #updateColor(color) { const setColorAndOpacity = (col, opa) => { this.color = col; - this.#opacity = opa; + this.opacity = opa; this.parent?.drawLayer.updateProperties(this.#id, { root: { fill: col, @@ -371,7 +369,7 @@ class HighlightEditor extends AnnotationEditor { this.#colorPicker?.updateColor(col); }; const savedColor = this.color; - const savedOpacity = this.#opacity; + const savedOpacity = this.opacity; this.addCommands({ cmd: setColorAndOpacity.bind( this, @@ -549,7 +547,7 @@ class HighlightEditor extends AnnotationEditor { root: { viewBox: "0 0 1 1", fill: this.color, - "fill-opacity": this.#opacity, + "fill-opacity": this.opacity, }, rootClass: { highlight: true, @@ -951,7 +949,7 @@ class HighlightEditor extends AnnotationEditor { const editor = await super.deserialize(data, parent, uiManager); editor.color = Util.makeHexColor(...color); - editor.#opacity = opacity || 1; + editor.opacity = opacity || 1; if (inkLists) { editor.#thickness = data.thickness; } @@ -1046,7 +1044,7 @@ class HighlightEditor extends AnnotationEditor { const serialized = { annotationType: AnnotationEditorType.HIGHLIGHT, color, - opacity: this.#opacity, + opacity: this.opacity, thickness: this.#thickness, quadPoints: this.#serializeBoxes(), outlines: this.#serializeOutlines(rect), diff --git a/src/display/editor/ink.js b/src/display/editor/ink.js index 58afba998..83823f179 100644 --- a/src/display/editor/ink.js +++ b/src/display/editor/ink.js @@ -193,10 +193,14 @@ class InkEditor extends DrawingEditor { return AnnotationEditorParamsType.INK_COLOR; } - get colorValue() { + get color() { return this._drawingOptions.stroke; } + get opacity() { + return this._drawingOptions["stroke-opacity"]; + } + /** @inheritdoc */ onScaleChanging() { if (!this.parent) { diff --git a/src/display/editor/stamp.js b/src/display/editor/stamp.js index cf5f50d73..a8a615a4a 100644 --- a/src/display/editor/stamp.js +++ b/src/display/editor/stamp.js @@ -15,6 +15,7 @@ import { AnnotationEditorType, AnnotationPrefix } from "../../shared/util.js"; import { + ColorScheme, OutputScale, PixelsPerInch, SupportedImageMimeTypes, @@ -535,7 +536,7 @@ class StampEditor extends AnnotationEditor { black = "#cfcfd8"; if (this._uiManager.hcmFilter !== "none") { black = "black"; - } else if (window.matchMedia?.("(prefers-color-scheme: dark)").matches) { + } else if (ColorScheme.isDarkMode) { white = "#8f8f9d"; black = "#42414d"; } diff --git a/src/pdf.js b/src/pdf.js index 7b6954eb6..54bf98079 100644 --- a/src/pdf.js +++ b/src/pdf.js @@ -45,13 +45,7 @@ import { VerbosityLevel, } from "./shared/util.js"; import { - build, - getDocument, - PDFDataRangeTransport, - PDFWorker, - version, -} from "./display/api.js"; -import { + applyOpacity, changeLightness, fetchData, getFilenameFromUrl, @@ -69,6 +63,13 @@ import { stopEvent, SupportedImageMimeTypes, } from "./display/display_utils.js"; +import { + build, + getDocument, + PDFDataRangeTransport, + PDFWorker, + version, +} from "./display/api.js"; import { AnnotationEditorLayer } from "./display/editor/annotation_editor_layer.js"; import { AnnotationEditorUIManager } from "./display/editor/tools.js"; import { AnnotationLayer } from "./display/annotation_layer.js"; @@ -98,6 +99,7 @@ globalThis.pdfjsLib = { AnnotationLayer, AnnotationMode, AnnotationType, + applyOpacity, build, changeLightness, ColorPicker, @@ -154,6 +156,7 @@ export { AnnotationLayer, AnnotationMode, AnnotationType, + applyOpacity, build, changeLightness, ColorPicker, diff --git a/test/unit/display_utils_spec.js b/test/unit/display_utils_spec.js index 34975e554..cb26189d0 100644 --- a/test/unit/display_utils_spec.js +++ b/test/unit/display_utils_spec.js @@ -14,6 +14,7 @@ */ import { + applyOpacity, changeLightness, getFilenameFromUrl, getPdfFilenameFromUrl, @@ -324,4 +325,21 @@ describe("display_utils", function () { ); }); }); + + describe("applyOpacity", function () { + it("Check that the opacity is applied correctly", function () { + if (isNodeJS) { + pending("OffscreenCanvas is not supported in Node.js."); + } + const canvas = new OffscreenCanvas(1, 1); + const ctx = canvas.getContext("2d"); + ctx.fillStyle = "white"; + ctx.fillRect(0, 0, 1, 1); + ctx.fillStyle = "rgb(123, 45, 67)"; + ctx.globalAlpha = 0.8; + ctx.fillRect(0, 0, 1, 1); + const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data; + expect(applyOpacity(123, 45, 67, ctx.globalAlpha)).toEqual([r, g, b]); + }); + }); }); diff --git a/test/unit/pdf_spec.js b/test/unit/pdf_spec.js index 45ae6a192..8451ec7b3 100644 --- a/test/unit/pdf_spec.js +++ b/test/unit/pdf_spec.js @@ -36,13 +36,7 @@ import { VerbosityLevel, } from "../../src/shared/util.js"; import { - build, - getDocument, - PDFDataRangeTransport, - PDFWorker, - version, -} from "../../src/display/api.js"; -import { + applyOpacity, changeLightness, fetchData, getFilenameFromUrl, @@ -60,6 +54,13 @@ import { stopEvent, SupportedImageMimeTypes, } from "../../src/display/display_utils.js"; +import { + build, + getDocument, + PDFDataRangeTransport, + PDFWorker, + version, +} from "../../src/display/api.js"; 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"; @@ -82,6 +83,7 @@ const expectedAPI = Object.freeze({ AnnotationLayer, AnnotationMode, AnnotationType, + applyOpacity, build, changeLightness, ColorPicker, diff --git a/web/comment_manager.css b/web/comment_manager.css index 3870819a8..fb19a2b84 100644 --- a/web/comment_manager.css +++ b/web/comment_manager.css @@ -257,6 +257,7 @@ :is(.annotationLayer, .annotationEditorLayer) { .annotationCommentButton { + color-scheme: light dark; --comment-button-bg: light-dark(white, #1c1b22); --comment-button-fg: light-dark(#5b5b66, #fbfbfe); --comment-button-active-bg: light-dark(#0041a4, #a6ecf4); diff --git a/web/pdfjs.js b/web/pdfjs.js index 13bf17bb7..c1d0aa59d 100644 --- a/web/pdfjs.js +++ b/web/pdfjs.js @@ -22,6 +22,7 @@ const { AnnotationLayer, AnnotationMode, AnnotationType, + applyOpacity, build, changeLightness, ColorPicker, @@ -78,6 +79,7 @@ export { AnnotationLayer, AnnotationMode, AnnotationType, + applyOpacity, build, changeLightness, ColorPicker,