From f2d9d9b58c89046ac32bd388c7fd01cce18062d0 Mon Sep 17 00:00:00 2001 From: adrianoracano Date: Tue, 21 Jul 2026 00:58:05 +0200 Subject: [PATCH 1/2] dark mode ok, soft mode useless, need to set it by default --- l10n/en-US/viewer.ftl | 6 +++++ web/app.js | 52 ++++++++++++++++++++++++++++++++++++++++ web/app_options.js | 5 ++++ web/pdf_viewer.css | 10 ++++++++ web/secondary_toolbar.js | 5 ++++ web/viewer.css | 5 ++++ web/viewer.html | 12 ++++++++++ web/viewer.js | 1 + 8 files changed, 96 insertions(+) diff --git a/l10n/en-US/viewer.ftl b/l10n/en-US/viewer.ftl index 46f376891c5d0..240ed4864701a 100644 --- a/l10n/en-US/viewer.ftl +++ b/l10n/en-US/viewer.ftl @@ -104,6 +104,12 @@ pdfjs-spread-even-button = .title = Join page spreads starting with even-numbered pages pdfjs-spread-even-button-label = Even Spreads +## Page Dark Mode + +pdfjs-page-dark-mode-button = + .title = Page Dark Mode +pdfjs-page-dark-mode-button-label = Page Dark Mode + ## Document properties dialog pdfjs-document-properties-button = diff --git a/web/app.js b/web/app.js index fdf2e20946d02..5a4edb8a6717b 100644 --- a/web/app.js +++ b/web/app.js @@ -630,6 +630,9 @@ const PDFViewerApplication = { renderingQueue.setThumbnailViewer(this.pdfThumbnailViewer); } + // Apply the saved page dark mode preference. + this._applyPageDarkMode(AppOptions.get("pageDarkMode")); + // The browsing history is only enabled when the viewer is standalone, // i.e. not when it is embedded in a web page. if (!this.isViewerEmbedded && !AppOptions.get("disableHistory")) { @@ -2175,6 +2178,45 @@ const PDFViewerApplication = { // in the 'rotationchanging' event handler. }, + _togglePageDarkMode() { + const newMode = (AppOptions.get("pageDarkMode") + 1) % 3; + AppOptions.set("pageDarkMode", newMode); + }, + + _applyPageDarkMode(mode) { + const filterMap = { + 0: "none", + 1: "brightness(0.85) contrast(0.95)", + 2: "invert(0.88) hue-rotate(180deg)", + }; + const viewer = this.pdfViewer?.viewer; + if (viewer) { + viewer.style.setProperty( + "--page-dark-mode-filter", + filterMap[mode] || "none" + ); + } + // Update thumbnail viewer too + const thumbnailContainer = this.pdfThumbnailViewer?.container; + if (thumbnailContainer) { + thumbnailContainer.style.setProperty( + "--page-dark-mode-filter", + filterMap[mode] || "none" + ); + } + // Update button label + const button = this.appConfig?.secondaryToolbar?.pageDarkModeButton; + if (button) { + const labelMap = { 0: "Off", 1: "Soft", 2: "Full" }; + const label = labelMap[mode] || "Off"; + button.setAttribute("data-page-dark-mode", mode); + const span = button.querySelector("span"); + if (span) { + span.textContent = `Page Dark Mode: ${label}`; + } + } + }, + requestPresentationMode() { this.pdfPresentationMode?.request(); }, @@ -2245,6 +2287,16 @@ const PDFViewerApplication = { ); eventBus.on("rotatecw", this.rotatePages.bind(this, 90), opts); eventBus.on("rotateccw", this.rotatePages.bind(this, -90), opts); + eventBus.on( + "togglepagedarkmode", + this._togglePageDarkMode.bind(this), + opts + ); + eventBus.on( + "pagedarkmode", + evt => this._applyPageDarkMode(evt.value), + opts + ); eventBus.on( "optionalcontentconfig", evt => (pdfViewer.optionalContentConfigPromise = evt.promise), diff --git a/web/app_options.js b/web/app_options.js index cb20c0f45e405..23420aad514eb 100644 --- a/web/app_options.js +++ b/web/app_options.js @@ -363,6 +363,11 @@ const defaultOptions = { value: 500, // ms kind: OptionKind.VIEWER, }, + pageDarkMode: { + /** @type {number} */ + value: 0, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE + OptionKind.EVENT_DISPATCH, + }, forcePageColors: { /** @type {boolean} */ value: false, diff --git a/web/pdf_viewer.css b/web/pdf_viewer.css index e787cc8e15d4e..b6d34d346093d 100644 --- a/web/pdf_viewer.css +++ b/web/pdf_viewer.css @@ -89,6 +89,8 @@ --hcm-highlight-filter: none; --hcm-highlight-selected-filter: none; + --page-dark-mode-filter: none; + @media screen and (forced-colors: active) { --hcm-highlight-filter: invert(100%); } @@ -111,6 +113,7 @@ width: 100%; height: 100%; contain: content; + filter: var(--page-dark-mode-filter); .structTree { contain: strict; @@ -255,3 +258,10 @@ margin: 0 auto; border: 2px solid transparent; } + +/* Page dark mode filter for thumbnail images. + The CSS variable --page-dark-mode-filter is set on the thumbnails container + by the app and inherited here. */ +.thumbnailsView .thumbnailImageContainer img { + filter: var(--page-dark-mode-filter); +} diff --git a/web/secondary_toolbar.js b/web/secondary_toolbar.js index c2048f7ea4a53..17eb0c3a8de02 100644 --- a/web/secondary_toolbar.js +++ b/web/secondary_toolbar.js @@ -145,6 +145,11 @@ class SecondaryToolbar { eventName: "imagealttextsettings", close: true, }, + { + element: options.pageDarkModeButton, + eventName: "togglepagedarkmode", + close: false, + }, { element: options.documentPropertiesButton, eventName: "documentproperties", diff --git a/web/viewer.css b/web/viewer.css index d607e356afef8..1833c9beb84d7 100644 --- a/web/viewer.css +++ b/web/viewer.css @@ -126,6 +126,7 @@ --toolbarButton-editorStamp-icon ); --secondaryToolbarButton-documentProperties-icon: url(images/secondaryToolbarButton-documentProperties.svg); + --secondaryToolbarButton-pageDarkMode-icon: url(images/secondaryToolbarButton-pageDarkMode.svg); --editorParams-stampAddImage-icon: url(images/toolbarButton-zoomIn.svg); --comment-edit-button-icon: url(images/comment-editButton.svg); } @@ -1061,6 +1062,10 @@ dialog :link { #documentProperties::before { mask-image: var(--secondaryToolbarButton-documentProperties-icon); } + + #pageDarkMode::before { + mask-image: var(--secondaryToolbarButton-pageDarkMode-icon); + } } } diff --git a/web/viewer.html b/web/viewer.html index d02f476986819..412a479379094 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -856,6 +856,18 @@
+ + +
+