Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[api-minor] Improve how we disable PDFThumbnailView.setImage for documents with Optional Content #15215

Merged
merged 5 commits into from
Jul 30, 2022
Merged
116 changes: 82 additions & 34 deletions src/display/optional_content_config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,52 +12,85 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { objectFromMap, warn } from "../shared/util.js";

import { objectFromMap, unreachable, warn } from "../shared/util.js";

const INTERNAL = Symbol("INTERNAL");

class OptionalContentGroup {
#visible = true;

constructor(name, intent) {
this.visible = true;
this.name = name;
this.intent = intent;
}

/**
* @type {boolean}
*/
get visible() {
return this.#visible;
}

/**
* @ignore
*/
_setVisible(internal, visible) {
if (internal !== INTERNAL) {
unreachable("Internal method `_setVisible` called.");
}
this.#visible = visible;
}
}

class OptionalContentConfig {
#cachedHasInitialVisibility = true;

#groups = new Map();

#initialVisibility = null;

#order = null;

constructor(data) {
this.name = null;
this.creator = null;
this._order = null;
this._groups = new Map();

if (data === null) {
return;
}
this.name = data.name;
this.creator = data.creator;
this._order = data.order;
this.#order = data.order;
for (const group of data.groups) {
this._groups.set(
this.#groups.set(
group.id,
new OptionalContentGroup(group.name, group.intent)
);
}

if (data.baseState === "OFF") {
for (const group of this._groups) {
group.visible = false;
for (const group of this.#groups.values()) {
group._setVisible(INTERNAL, false);
}
}

for (const on of data.on) {
this._groups.get(on).visible = true;
this.#groups.get(on)._setVisible(INTERNAL, true);
}

for (const off of data.off) {
this._groups.get(off).visible = false;
this.#groups.get(off)._setVisible(INTERNAL, false);
}

// The following code must always run *last* in the constructor.
this.#initialVisibility = new Map();
for (const [id, group] of this.#groups) {
this.#initialVisibility.set(id, group.visible);
}
}

_evaluateVisibilityExpression(array) {
#evaluateVisibilityExpression(array) {
const length = array.length;
if (length < 2) {
return true;
Expand All @@ -67,9 +100,9 @@ class OptionalContentConfig {
const element = array[i];
let state;
if (Array.isArray(element)) {
state = this._evaluateVisibilityExpression(element);
} else if (this._groups.has(element)) {
state = this._groups.get(element).visible;
state = this.#evaluateVisibilityExpression(element);
} else if (this.#groups.has(element)) {
state = this.#groups.get(element).visible;
} else {
warn(`Optional content group not found: ${element}`);
return true;
Expand All @@ -95,65 +128,65 @@ class OptionalContentConfig {
}

isVisible(group) {
if (this._groups.size === 0) {
if (this.#groups.size === 0) {
return true;
}
if (!group) {
warn("Optional content group not defined.");
return true;
}
if (group.type === "OCG") {
if (!this._groups.has(group.id)) {
if (!this.#groups.has(group.id)) {
warn(`Optional content group not found: ${group.id}`);
return true;
}
return this._groups.get(group.id).visible;
return this.#groups.get(group.id).visible;
} else if (group.type === "OCMD") {
// Per the spec, the expression should be preferred if available.
if (group.expression) {
return this._evaluateVisibilityExpression(group.expression);
return this.#evaluateVisibilityExpression(group.expression);
}
if (!group.policy || group.policy === "AnyOn") {
// Default
for (const id of group.ids) {
if (!this._groups.has(id)) {
if (!this.#groups.has(id)) {
warn(`Optional content group not found: ${id}`);
return true;
}
if (this._groups.get(id).visible) {
if (this.#groups.get(id).visible) {
return true;
}
}
return false;
} else if (group.policy === "AllOn") {
for (const id of group.ids) {
if (!this._groups.has(id)) {
if (!this.#groups.has(id)) {
warn(`Optional content group not found: ${id}`);
return true;
}
if (!this._groups.get(id).visible) {
if (!this.#groups.get(id).visible) {
return false;
}
}
return true;
} else if (group.policy === "AnyOff") {
for (const id of group.ids) {
if (!this._groups.has(id)) {
if (!this.#groups.has(id)) {
warn(`Optional content group not found: ${id}`);
return true;
}
if (!this._groups.get(id).visible) {
if (!this.#groups.get(id).visible) {
return true;
}
}
return false;
} else if (group.policy === "AllOff") {
for (const id of group.ids) {
if (!this._groups.has(id)) {
if (!this.#groups.has(id)) {
warn(`Optional content group not found: ${id}`);
return true;
}
if (this._groups.get(id).visible) {
if (this.#groups.get(id).visible) {
return false;
}
}
Expand All @@ -167,29 +200,44 @@ class OptionalContentConfig {
}

setVisibility(id, visible = true) {
if (!this._groups.has(id)) {
if (!this.#groups.has(id)) {
warn(`Optional content group not found: ${id}`);
return;
}
this._groups.get(id).visible = !!visible;
this.#groups.get(id)._setVisible(INTERNAL, !!visible);

this.#cachedHasInitialVisibility = null;
}

get hasInitialVisibility() {
if (this.#cachedHasInitialVisibility !== null) {
return this.#cachedHasInitialVisibility;
}
for (const [id, group] of this.#groups) {
const visible = this.#initialVisibility.get(id);
if (group.visible !== visible) {
return (this.#cachedHasInitialVisibility = false);
}
}
return (this.#cachedHasInitialVisibility = true);
}

getOrder() {
if (!this._groups.size) {
if (!this.#groups.size) {
return null;
}
if (this._order) {
return this._order.slice();
if (this.#order) {
return this.#order.slice();
}
return Array.from(this._groups.keys());
return [...this.#groups.keys()];
}

getGroups() {
return this._groups.size > 0 ? objectFromMap(this._groups) : null;
return this.#groups.size > 0 ? objectFromMap(this.#groups) : null;
}

getGroup(id) {
return this._groups.get(id) || null;
return this.#groups.get(id) || null;
}
}

Expand Down
1 change: 1 addition & 0 deletions web/base_viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -1824,6 +1824,7 @@ class BaseViewer {
return Promise.resolve(null);
}
if (!this._optionalContentConfigPromise) {
console.error("optionalContentConfigPromise: Not initialized yet.");
// Prevent issues if the getter is accessed *before* the `onePageRendered`
// promise has resolved; won't (normally) happen in the default viewer.
return this.pdfDocument.getOptionalContentConfig();
Expand Down
59 changes: 56 additions & 3 deletions web/pdf_page_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ const MAX_CANVAS_PIXELS = compatibilityParams.maxCanvasPixels || 16777216;
class PDFPageView {
#annotationMode = AnnotationMode.ENABLE_FORMS;

#useThumbnailCanvas = true;

/**
* @param {PDFPageViewOptions} options
*/
Expand Down Expand Up @@ -151,7 +153,12 @@ class PDFPageView {
this.renderingState = RenderingStates.INITIAL;
this.resume = null;
this._renderError = null;
this._isStandalone = !this.renderingQueue?.hasViewer();
if (
typeof PDFJSDev === "undefined" ||
PDFJSDev.test("!PRODUCTION || GENERIC")
) {
this._isStandalone = !this.renderingQueue?.hasViewer();
}

this._annotationCanvasMap = null;

Expand All @@ -174,6 +181,26 @@ class PDFPageView {
this.div = div;

container?.append(div);

if (
(typeof PDFJSDev === "undefined" ||
PDFJSDev.test("!PRODUCTION || GENERIC")) &&
this._isStandalone
) {
const { optionalContentConfigPromise } = options;
if (optionalContentConfigPromise) {
// Ensure that the thumbnails always display the *initial* document
// state.
optionalContentConfigPromise.then(optionalContentConfig => {
if (
optionalContentConfigPromise !== this._optionalContentConfigPromise
) {
return;
}
this.#useThumbnailCanvas = optionalContentConfig.hasInitialVisibility;
});
}
}
}

setPdfPage(pdfPage) {
Expand Down Expand Up @@ -359,7 +386,11 @@ class PDFPageView {

this.loadingIconDiv = document.createElement("div");
this.loadingIconDiv.className = "loadingIcon notVisible";
if (this._isStandalone) {
if (
(typeof PDFJSDev === "undefined" ||
PDFJSDev.test("!PRODUCTION || GENERIC")) &&
this._isStandalone
) {
this.toggleLoadingIconSpinner(/* viewVisible = */ true);
}
this.loadingIconDiv.setAttribute("role", "img");
Expand All @@ -376,6 +407,16 @@ class PDFPageView {
}
if (optionalContentConfigPromise instanceof Promise) {
this._optionalContentConfigPromise = optionalContentConfigPromise;

// Ensure that the thumbnails always display the *initial* document state.
optionalContentConfigPromise.then(optionalContentConfig => {
if (
optionalContentConfigPromise !== this._optionalContentConfigPromise
) {
return;
}
this.#useThumbnailCanvas = optionalContentConfig.hasInitialVisibility;
});
}

const totalRotation = (this.rotation + this.pdfPageRotate) % 360;
Expand All @@ -384,7 +425,11 @@ class PDFPageView {
rotation: totalRotation,
});

if (this._isStandalone) {
if (
(typeof PDFJSDev === "undefined" ||
PDFJSDev.test("!PRODUCTION || GENERIC")) &&
this._isStandalone
) {
docStyle.setProperty("--scale-factor", this.viewport.scale);
}

Expand Down Expand Up @@ -1003,6 +1048,14 @@ class PDFPageView {
this.div.removeAttribute("data-page-label");
}
}

/**
* For use by the `PDFThumbnailView.setImage`-method.
* @ignore
*/
get thumbnailCanvas() {
return this.#useThumbnailCanvas ? this.canvas : null;
}
}

export { PDFPageView };
12 changes: 1 addition & 11 deletions web/pdf_thumbnail_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ const THUMBNAIL_WIDTH = 98; // px
* The default value is `null`.
* @property {IPDFLinkService} linkService - The navigation/linking service.
* @property {PDFRenderingQueue} renderingQueue - The rendering queue object.
* @property {function} checkSetImageDisabled
* @property {IL10n} l10n - Localization service.
* @property {Object} [pageColors] - Overwrites background and foreground colors
* with user defined ones in order to improve readability in high contrast
Expand Down Expand Up @@ -88,7 +87,6 @@ class PDFThumbnailView {
optionalContentConfigPromise,
linkService,
renderingQueue,
checkSetImageDisabled,
l10n,
pageColors,
}) {
Expand All @@ -109,11 +107,6 @@ class PDFThumbnailView {
this.renderTask = null;
this.renderingState = RenderingStates.INITIAL;
this.resume = null;
this._checkSetImageDisabled =
checkSetImageDisabled ||
function () {
return false;
};

const pageWidth = this.viewport.width,
pageHeight = this.viewport.height,
Expand Down Expand Up @@ -356,13 +349,10 @@ class PDFThumbnailView {
}

setImage(pageView) {
if (this._checkSetImageDisabled()) {
return;
}
if (this.renderingState !== RenderingStates.INITIAL) {
return;
}
const { canvas, pdfPage } = pageView;
const { thumbnailCanvas: canvas, pdfPage } = pageView;
if (!canvas) {
return;
}
Expand Down
Loading