1
0
Fork 0
mirror of https://github.com/mozilla/pdf.js.git synced 2025-04-19 22:58:07 +02:00

[Editor] Add a new editor to highlight some text in a pdf (bug 1866119)

This patch is first big step for the new highlight feature.
Few patches will follow in order to conform to the specs UX/UI gave us.
This commit is contained in:
Calixte Denizet 2023-11-22 19:02:42 +01:00
parent 4bf7ff2027
commit 1ea6293923
19 changed files with 897 additions and 56 deletions

View file

@ -21,10 +21,12 @@
/** @typedef {import("../../../web/interfaces").IL10n} IL10n */
// eslint-disable-next-line max-len
/** @typedef {import("../annotation_layer.js").AnnotationLayer} AnnotationLayer */
/** @typedef {import("../draw_layer.js").DrawLayer} DrawLayer */
import { AnnotationEditorType, FeatureTest } from "../../shared/util.js";
import { AnnotationEditor } from "./editor.js";
import { FreeTextEditor } from "./freetext.js";
import { HighlightEditor } from "./highlight.js";
import { InkEditor } from "./ink.js";
import { setLayerDimensions } from "../display_utils.js";
import { StampEditor } from "./stamp.js";
@ -39,6 +41,8 @@ import { StampEditor } from "./stamp.js";
* @property {number} pageIndex
* @property {IL10n} l10n
* @property {AnnotationLayer} [annotationLayer]
* @property {HTMLDivElement} [textLayer]
* @property {DrawLayer} drawLayer
* @property {PageViewport} viewport
*/
@ -59,10 +63,14 @@ class AnnotationEditorLayer {
#boundPointerup = this.pointerup.bind(this);
#boundPointerUpAfterSelection = this.pointerUpAfterSelection.bind(this);
#boundPointerdown = this.pointerdown.bind(this);
#editorFocusTimeoutId = null;
#boundSelectionStart = this.selectionStart.bind(this);
#editors = new Map();
#hadPointerDown = false;
@ -71,12 +79,14 @@ class AnnotationEditorLayer {
#isDisabling = false;
#textLayer = null;
#uiManager;
static _initialized = false;
static #editorTypes = new Map(
[FreeTextEditor, InkEditor, StampEditor].map(type => [
[FreeTextEditor, InkEditor, StampEditor, HighlightEditor].map(type => [
type._editorType,
type,
])
@ -91,6 +101,8 @@ class AnnotationEditorLayer {
div,
accessibilityManager,
annotationLayer,
drawLayer,
textLayer,
viewport,
l10n,
}) {
@ -109,6 +121,8 @@ class AnnotationEditorLayer {
this.#accessibilityManager = accessibilityManager;
this.#annotationLayer = annotationLayer;
this.viewport = viewport;
this.#textLayer = textLayer;
this.drawLayer = drawLayer;
this.#uiManager.addLayer(this);
}
@ -131,12 +145,24 @@ class AnnotationEditorLayer {
*/
updateMode(mode = this.#uiManager.getMode()) {
this.#cleanup();
if (mode === AnnotationEditorType.INK) {
// We always want to an ink editor ready to draw in.
this.addInkEditorIfNeeded(false);
this.disableClick();
} else {
this.enableClick();
switch (mode) {
case AnnotationEditorType.INK:
// We always want to have an ink editor ready to draw in.
this.addInkEditorIfNeeded(false);
this.disableTextSelection();
this.togglePointerEvents(true);
this.disableClick();
break;
case AnnotationEditorType.HIGHLIGHT:
this.enableTextSelection();
this.togglePointerEvents(false);
this.disableClick();
break;
default:
this.disableTextSelection();
this.togglePointerEvents(true);
this.enableClick();
}
if (mode !== AnnotationEditorType.NONE) {
@ -272,6 +298,7 @@ class AnnotationEditorLayer {
for (const editorType of AnnotationEditorLayer.#editorTypes.values()) {
classList.remove(`${editorType._type}Editing`);
}
this.disableTextSelection();
this.#isDisabling = false;
}
@ -293,6 +320,18 @@ class AnnotationEditorLayer {
this.#uiManager.setActiveEditor(editor);
}
enableTextSelection() {
if (this.#textLayer?.div) {
document.addEventListener("selectstart", this.#boundSelectionStart);
}
}
disableTextSelection() {
if (this.#textLayer?.div) {
document.removeEventListener("selectstart", this.#boundSelectionStart);
}
}
enableClick() {
this.div.addEventListener("pointerdown", this.#boundPointerdown);
this.div.addEventListener("pointerup", this.#boundPointerup);
@ -458,18 +497,24 @@ class AnnotationEditorLayer {
return this.#uiManager.getId();
}
get #currentEditorType() {
return AnnotationEditorLayer.#editorTypes.get(this.#uiManager.getMode());
}
/**
* Create a new editor
* @param {Object} params
* @returns {AnnotationEditor}
*/
#createNewEditor(params) {
const editorType = AnnotationEditorLayer.#editorTypes.get(
this.#uiManager.getMode()
);
const editorType = this.#currentEditorType;
return editorType ? new editorType.prototype.constructor(params) : null;
}
canCreateNewEmptyEditor() {
return this.#currentEditorType?.canCreateNewEmptyEditor();
}
/**
* Paste some content into a new editor.
* @param {number} mode
@ -512,9 +557,10 @@ class AnnotationEditorLayer {
* Create and add a new editor.
* @param {PointerEvent} event
* @param {boolean} isCentered
* @param [Object] data
* @returns {AnnotationEditor}
*/
#createAndAddNewEditor(event, isCentered) {
#createAndAddNewEditor(event, isCentered, data = {}) {
const id = this.getNextId();
const editor = this.#createNewEditor({
parent: this,
@ -523,6 +569,7 @@ class AnnotationEditorLayer {
y: event.offsetY,
uiManager: this.#uiManager,
isCentered,
...data,
});
if (editor) {
this.add(editor);
@ -589,6 +636,98 @@ class AnnotationEditorLayer {
this.#uiManager.unselect(editor);
}
/**
* SelectionChange callback.
* @param {Event} _event
*/
selectionStart(_event) {
this.#textLayer?.div.addEventListener(
"pointerup",
this.#boundPointerUpAfterSelection,
{ once: true }
);
}
/**
* Called when the user releases the mouse button after having selected
* some text.
* @param {PointerEvent} event
*/
pointerUpAfterSelection(event) {
const selection = document.getSelection();
if (selection.rangeCount === 0) {
return;
}
const range = selection.getRangeAt(0);
if (range.collapsed) {
return;
}
if (!this.#textLayer?.div.contains(range.commonAncestorContainer)) {
return;
}
const {
x: layerX,
y: layerY,
width: parentWidth,
height: parentHeight,
} = this.#textLayer.div.getBoundingClientRect();
const bboxes = range.getClientRects();
// We must rotate the boxes because we want to have them in the non-rotated
// page coordinates.
let rotator;
switch (this.viewport.rotation) {
case 90:
rotator = (x, y, w, h) => ({
x: (y - layerY) / parentHeight,
y: 1 - (x + w - layerX) / parentWidth,
width: h / parentHeight,
height: w / parentWidth,
});
break;
case 180:
rotator = (x, y, w, h) => ({
x: 1 - (x + w - layerX) / parentWidth,
y: 1 - (y + h - layerY) / parentHeight,
width: w / parentWidth,
height: h / parentHeight,
});
break;
case 270:
rotator = (x, y, w, h) => ({
x: 1 - (y + h - layerY) / parentHeight,
y: (x - layerX) / parentWidth,
width: h / parentHeight,
height: w / parentWidth,
});
break;
default:
rotator = (x, y, w, h) => ({
x: (x - layerX) / parentWidth,
y: (y - layerY) / parentHeight,
width: w / parentWidth,
height: h / parentHeight,
});
break;
}
const boxes = [];
for (const { x, y, width, height } of bboxes) {
if (width === 0 || height === 0) {
continue;
}
boxes.push(rotator(x, y, width, height));
}
if (boxes.length !== 0) {
this.#createAndAddNewEditor(event, false, {
boxes,
});
}
selection.empty();
}
/**
* Pointerup callback.
* @param {PointerEvent} event
@ -631,6 +770,9 @@ class AnnotationEditorLayer {
* @param {PointerEvent} event
*/
pointerdown(event) {
if (this.#uiManager.getMode() === AnnotationEditorType.HIGHLIGHT) {
this.enableTextSelection();
}
if (this.#hadPointerDown) {
// It's possible to have a second pointerdown event before a pointerup one
// when the user puts a finger on a touchscreen and then add a second one
@ -734,8 +876,15 @@ class AnnotationEditorLayer {
// the viewport.
this.#uiManager.commitOrRemove();
const oldRotation = this.viewport.rotation;
const rotation = viewport.rotation;
this.viewport = viewport;
setLayerDimensions(this.div, { rotation: viewport.rotation });
setLayerDimensions(this.div, { rotation });
if (oldRotation !== rotation) {
for (const editor of this.#editors.values()) {
editor.rotate(rotation);
}
}
this.updateMode();
}

View file

@ -507,7 +507,11 @@ class AnnotationEditor {
}
}
fixAndSetPosition() {
/**
* Fix the position of the editor in order to keep it inside its parent page.
* @param {number} [rotation] - the rotation of the page.
*/
fixAndSetPosition(rotation = this.rotation) {
const [pageWidth, pageHeight] = this.pageDimensions;
let { x, y, width, height } = this;
width *= pageWidth;
@ -515,7 +519,7 @@ class AnnotationEditor {
x *= pageWidth;
y *= pageHeight;
switch (this.rotation) {
switch (rotation) {
case 0:
x = Math.max(0, Math.min(pageWidth - width, x));
y = Math.max(0, Math.min(pageHeight - height, y));
@ -1125,14 +1129,28 @@ class AnnotationEditor {
this.#hasBeenClicked = true;
this.#setUpDragSession(event);
}
#setUpDragSession(event) {
if (!this._isDraggable) {
if (this._isDraggable) {
this.#setUpDragSession(event);
return;
}
this.#selectOnPointerEvent(event);
}
#selectOnPointerEvent(event) {
const { isMac } = FeatureTest.platform;
if (
(event.ctrlKey && !isMac) ||
event.shiftKey ||
(event.metaKey && isMac)
) {
this.parent.toggleSelected(this);
} else {
this.parent.setSelected(this);
}
}
#setUpDragSession(event) {
const isSelected = this._uiManager.isSelected(this);
this._uiManager.setUpDragSession();
@ -1163,16 +1181,7 @@ class AnnotationEditor {
this.#hasBeenClicked = false;
if (!this._uiManager.endDragSession()) {
const { isMac } = FeatureTest.platform;
if (
(event.ctrlKey && !isMac) ||
event.shiftKey ||
(event.metaKey && isMac)
) {
this.parent.toggleSelected(this);
} else {
this.parent.setSelected(this);
}
this.#selectOnPointerEvent(event);
}
};
window.addEventListener("pointerup", pointerUpCallback);
@ -1204,8 +1213,11 @@ class AnnotationEditor {
/**
* Convert the current rect into a page one.
* @param {number} tx - x-translation in screen coordinates.
* @param {number} ty - y-translation in screen coordinates.
* @param {number} [rotation] - the rotation of the page.
*/
getRect(tx, ty) {
getRect(tx, ty, rotation = this.rotation) {
const scale = this.parentScale;
const [pageWidth, pageHeight] = this.pageDimensions;
const [pageX, pageY] = this.pageTranslation;
@ -1216,7 +1228,7 @@ class AnnotationEditor {
const width = this.width * pageWidth;
const height = this.height * pageHeight;
switch (this.rotation) {
switch (rotation) {
case 0:
return [
x + shiftX + pageX,
@ -1332,6 +1344,12 @@ class AnnotationEditor {
this.div?.addEventListener("focusout", this.#boundFocusout);
}
/**
* Rotate the editor.
* @param {number} angle
*/
rotate(_angle) {}
/**
* Serialize the editor.
* The result of the serialization will be used to construct a
@ -1426,6 +1444,10 @@ class AnnotationEditor {
}
}
get toolbarPosition() {
return null;
}
/**
* onkeydown callback.
* @param {KeyboardEvent} event
@ -1669,6 +1691,10 @@ class AnnotationEditor {
static get MIN_SIZE() {
return 16;
}
static canCreateNewEmptyEditor() {
return true;
}
}
// This class is used to fake an editor which has been deleted.

View file

@ -0,0 +1,454 @@
/* Copyright 2022 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 {
AnnotationEditorParamsType,
AnnotationEditorType,
Util,
} from "../../shared/util.js";
import { AnnotationEditor } from "./editor.js";
import { bindEvents } from "./tools.js";
import { Outliner } from "./outliner.js";
/**
* Basic draw editor in order to generate an Highlight annotation.
*/
class HighlightEditor extends AnnotationEditor {
#boxes;
#clipPathId = null;
#color;
#focusOutlines = null;
#highlightDiv = null;
#highlightOutlines = null;
#id = null;
#lastPoint = null;
#opacity;
#outlineId = null;
static _defaultColor = "#FFF066";
static _defaultOpacity = 0.4;
static _l10nPromise;
static _type = "highlight";
static _editorType = AnnotationEditorType.HIGHLIGHT;
constructor(params) {
super({ ...params, name: "highlightEditor" });
this.#color = params.color || HighlightEditor._defaultColor;
this.#opacity = params.opacity || HighlightEditor._defaultOpacity;
this.#boxes = params.boxes || null;
this._isDraggable = false;
this.#createOutlines();
this.#addToDrawLayer();
this.rotate(this.rotation);
}
#createOutlines() {
const outliner = new Outliner(this.#boxes, /* borderWidth = */ 0.001);
this.#highlightOutlines = outliner.getOutlines();
({
x: this.x,
y: this.y,
width: this.width,
height: this.height,
} = this.#highlightOutlines.box);
const outlinerForOutline = new Outliner(
this.#boxes,
/* borderWidth = */ 0.0025,
/* innerMargin = */ 0.001,
this._uiManager.direction === "ltr"
);
this.#focusOutlines = outlinerForOutline.getOutlines();
// The last point is in the pages coordinate system.
const { lastPoint } = this.#focusOutlines.box;
this.#lastPoint = [
(lastPoint[0] - this.x) / this.width,
(lastPoint[1] - this.y) / this.height,
];
}
static initialize(l10n) {
AnnotationEditor.initialize(l10n);
}
static updateDefaultParams(type, value) {
switch (type) {
case AnnotationEditorParamsType.HIGHLIGHT_COLOR:
HighlightEditor._defaultColor = value;
break;
case AnnotationEditorParamsType.HIGHLIGHT_OPACITY:
HighlightEditor._defaultOpacity = value / 100;
break;
}
}
/** @inheritdoc */
get toolbarPosition() {
return this.#lastPoint;
}
/** @inheritdoc */
updateParams(type, value) {
switch (type) {
case AnnotationEditorParamsType.HIGHLIGHT_COLOR:
this.#updateColor(value);
break;
case AnnotationEditorParamsType.HIGHLIGHT_OPACITY:
this.#updateOpacity(value);
break;
}
}
static get defaultPropertiesToUpdate() {
return [
[
AnnotationEditorParamsType.HIGHLIGHT_COLOR,
HighlightEditor._defaultColor,
],
[
AnnotationEditorParamsType.HIGHLIGHT_OPACITY,
Math.round(HighlightEditor._defaultOpacity * 100),
],
];
}
/** @inheritdoc */
get propertiesToUpdate() {
return [
[
AnnotationEditorParamsType.HIGHLIGHT_COLOR,
this.#color || HighlightEditor._defaultColor,
],
[
AnnotationEditorParamsType.HIGHLIGHT_OPACITY,
Math.round(100 * (this.#opacity ?? HighlightEditor._defaultOpacity)),
],
];
}
/**
* Update the color and make this action undoable.
* @param {string} color
*/
#updateColor(color) {
const savedColor = this.color;
this.addCommands({
cmd: () => {
this.#color = color;
this.parent.drawLayer.changeColor(this.#id, color);
},
undo: () => {
this.#color = savedColor;
this.parent.drawLayer.changeColor(this.#id, savedColor);
},
mustExec: true,
type: AnnotationEditorParamsType.HIGHLIGHT_COLOR,
overwriteIfSameType: true,
keepUndo: true,
});
}
/**
* Update the opacity and make this action undoable.
* @param {number} opacity
*/
#updateOpacity(opacity) {
opacity /= 100;
const savedOpacity = this.#opacity;
this.addCommands({
cmd: () => {
this.#opacity = opacity;
this.parent.drawLayer.changeOpacity(this.#id, opacity);
},
undo: () => {
this.#opacity = savedOpacity;
this.parent.drawLayer.changeOpacity(this.#id, savedOpacity);
},
mustExec: true,
type: AnnotationEditorParamsType.HIGHLIGHT_OPACITY,
overwriteIfSameType: true,
keepUndo: true,
});
}
/** @inheritdoc */
fixAndSetPosition() {
return super.fixAndSetPosition(0);
}
/** @inheritdoc */
getRect(tx, ty) {
return super.getRect(tx, ty, 0);
}
/** @inheritdoc */
onceAdded() {
this.parent.addUndoableEditor(this);
this.div.focus();
}
/** @inheritdoc */
remove() {
super.remove();
this.#cleanDrawLayer();
}
/** @inheritdoc */
rebuild() {
if (!this.parent) {
return;
}
super.rebuild();
if (this.div === null) {
return;
}
this.#addToDrawLayer();
if (!this.isAttachedToDOM) {
// At some point this editor was removed and we're rebuilting it,
// hence we must add it to its parent.
this.parent.add(this);
}
}
setParent(parent) {
if (this.parent && !parent) {
this.#cleanDrawLayer();
} else if (parent) {
this.#addToDrawLayer(parent);
}
super.setParent(parent);
}
#cleanDrawLayer() {
if (this.#id === null || !this.parent) {
return;
}
this.parent.drawLayer.remove(this.#id);
this.#id = null;
this.parent.drawLayer.remove(this.#outlineId);
this.#outlineId = null;
}
#addToDrawLayer(parent = this.parent) {
if (this.#id !== null) {
return;
}
({ id: this.#id, clipPathId: this.#clipPathId } =
parent.drawLayer.highlight(
this.#highlightOutlines,
this.#color,
this.#opacity
));
if (this.#highlightDiv) {
this.#highlightDiv.style.clipPath = this.#clipPathId;
}
this.#outlineId = parent.drawLayer.highlightOutline(this.#focusOutlines);
}
static #rotateBbox({ x, y, width, height }, angle) {
switch (angle) {
case 90:
return {
x: 1 - y - height,
y: x,
width: height,
height: width,
};
case 180:
return {
x: 1 - x - width,
y: 1 - y - height,
width,
height,
};
case 270:
return {
x: y,
y: 1 - x - width,
width: height,
height: width,
};
}
return {
x,
y,
width,
height,
};
}
/** @inheritdoc */
rotate(angle) {
const { drawLayer } = this.parent;
drawLayer.rotate(this.#id, angle);
drawLayer.rotate(this.#outlineId, angle);
drawLayer.updateBox(this.#id, HighlightEditor.#rotateBbox(this, angle));
drawLayer.updateBox(
this.#outlineId,
HighlightEditor.#rotateBbox(this.#focusOutlines.box, angle)
);
}
/** @inheritdoc */
render() {
if (this.div) {
return this.div;
}
const div = super.render();
const highlightDiv = (this.#highlightDiv = document.createElement("div"));
div.append(highlightDiv);
highlightDiv.className = "internal";
highlightDiv.style.clipPath = this.#clipPathId;
const [parentWidth, parentHeight] = this.parentDimensions;
this.setDims(this.width * parentWidth, this.height * parentHeight);
bindEvents(this, this.#highlightDiv, ["pointerover", "pointerleave"]);
this.enableEditing();
return div;
}
pointerover() {
this.parent.drawLayer.addClass(this.#outlineId, "hovered");
}
pointerleave() {
this.parent.drawLayer.removeClass(this.#outlineId, "hovered");
}
/** @inheritdoc */
select() {
super.select();
this.parent?.drawLayer.removeClass(this.#outlineId, "hovered");
this.parent?.drawLayer.addClass(this.#outlineId, "selected");
}
/** @inheritdoc */
unselect() {
super.unselect();
this.parent?.drawLayer.removeClass(this.#outlineId, "selected");
}
#serializeBoxes() {
const [pageWidth, pageHeight] = this.pageDimensions;
const boxes = this.#boxes;
const quadPoints = new Array(boxes.length * 8);
let i = 0;
for (const { x, y, width, height } of boxes) {
const sx = x * pageWidth;
const sy = (1 - y - height) * pageHeight;
// The specifications say that the rectangle should start from the bottom
// left corner and go counter-clockwise.
// But when opening the file in Adobe Acrobat it appears that this isn't
// correct hence the 4th and 6th numbers are just swapped.
quadPoints[i] = quadPoints[i + 4] = sx;
quadPoints[i + 1] = quadPoints[i + 3] = sy;
quadPoints[i + 2] = quadPoints[i + 6] = sx + width * pageWidth;
quadPoints[i + 5] = quadPoints[i + 7] = sy + height * pageHeight;
i += 8;
}
return quadPoints;
}
#serializeOutlines() {
const [pageWidth, pageHeight] = this.pageDimensions;
const width = this.width * pageWidth;
const height = this.height * pageHeight;
const tx = this.x * pageWidth;
const ty = (1 - this.y - this.height) * pageHeight;
const outlines = [];
for (const outline of this.#highlightOutlines.outlines) {
const points = new Array(outline.length);
for (let i = 0; i < outline.length; i += 2) {
points[i] = tx + outline[i] * width;
points[i + 1] = ty + (1 - outline[i + 1]) * height;
}
outlines.push(points);
}
return outlines;
}
/** @inheritdoc */
static deserialize(data, parent, uiManager) {
const editor = super.deserialize(data, parent, uiManager);
const { rect, color, quadPoints } = data;
editor.#color = Util.makeHexColor(...color);
editor.#opacity = data.opacity;
const [pageWidth, pageHeight] = editor.pageDimensions;
editor.width = (rect[2] - rect[0]) / pageWidth;
editor.height = (rect[3] - rect[1]) / pageHeight;
const boxes = (editor.#boxes = []);
for (let i = 0; i < quadPoints.length; i += 8) {
boxes.push({
x: quadPoints[4] / pageWidth,
y: 1 - quadPoints[i + 5] / pageHeight,
width: (quadPoints[i + 2] - quadPoints[i]) / pageWidth,
height: (quadPoints[i + 5] - quadPoints[i + 1]) / pageHeight,
});
}
editor.#createOutlines();
return editor;
}
/** @inheritdoc */
serialize(isForCopying = false) {
// It doesn't make sense to copy/paste a highlight annotation.
if (this.isEmpty() || isForCopying) {
return null;
}
const rect = this.getRect(0, 0);
const color = AnnotationEditor._colorManager.convert(this.#color);
return {
annotationType: AnnotationEditorType.HIGHLIGHT,
color,
opacity: this.#opacity,
quadPoints: this.#serializeBoxes(),
outlines: this.#serializeOutlines(),
pageIndex: this.pageIndex,
rect,
rotation: 0,
structTreeParentId: this._structTreeParentId,
};
}
static canCreateNewEmptyEditor() {
return false;
}
}
export { HighlightEditor };

View file

@ -36,6 +36,19 @@ class EditorToolbar {
buttons.className = "buttons";
editToolbar.append(buttons);
const position = this.#editor.toolbarPosition;
if (position) {
const { style } = editToolbar;
const x =
this.#editor._uiManager.direction === "ltr"
? 1 - position[0]
: position[0];
style.insetInlineEnd = `${100 * x}%`;
style.top = `calc(${
100 * position[1]
}% + var(--editor-toolbar-vert-offset))`;
}
this.#addDeleteButton();
return editToolbar;

View file

@ -1217,7 +1217,9 @@ class AnnotationEditorUIManager {
}
addNewEditorFromKeyboard() {
this.currentLayer.addNewEditor();
if (this.currentLayer.canCreateNewEmptyEditor()) {
this.currentLayer.addNewEditor();
}
}
/**

View file

@ -86,6 +86,8 @@ const AnnotationEditorParamsType = {
INK_COLOR: 21,
INK_THICKNESS: 22,
INK_OPACITY: 23,
HIGHLIGHT_COLOR: 31,
HIGHLIGHT_OPACITY: 32,
};
// Permission flags from Table 22, Section 7.6.3.2 of the PDF specification.