1
0
Fork 0
mirror of https://github.com/mozilla/pdf.js.git synced 2025-04-19 06:38:07 +02:00
pdf.js/web/app_options.js
Calixte Denizet 2f828c7bf4 [Editor] (WIP) Add a new tool in order to add an handwritten signature to a pdf (bug 1942343)
This patch is adding some code in order to extract a drawing as curves from an image.
The algorithm is basically the following:
 - reduce the dimensions
 - make it gray
 - apply a bilateral filter in order to add some blurryness while keeping the edges
 - compute the histogram
 - guess what's the background color which should contain a large majority of the pixels
 - make a binary image
 - extract the contours in using the Suzuki algorithm
 - apply the Douglas-Peucker algorithm in order to reduce the number of points

The algorithm is improvable but it should work pretty well if there's a clear difference between
the background and the drawing.
In a v2 we could use a ML model in order to improve the extraction.

There's few changes related to the UI in order to make the tool usable, but they're very basic
for the moment.
2025-01-29 21:52:14 +01:00

650 lines
17 KiB
JavaScript

/* Copyright 2018 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.
*/
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
// eslint-disable-next-line no-var
var compatParams = new Map();
if (
typeof PDFJSDev !== "undefined" &&
PDFJSDev.test("LIB") &&
typeof navigator === "undefined"
) {
globalThis.navigator = Object.create(null);
}
const userAgent = navigator.userAgent || "";
const platform = navigator.platform || "";
const maxTouchPoints = navigator.maxTouchPoints || 1;
const isAndroid = /Android/.test(userAgent);
const isIOS =
/\b(iPad|iPhone|iPod)(?=;)/.test(userAgent) ||
(platform === "MacIntel" && maxTouchPoints > 1);
// Limit canvas size to 5 mega-pixels on mobile.
// Support: Android, iOS
(function () {
if (isIOS || isAndroid) {
compatParams.set("maxCanvasPixels", 5242880);
}
})();
// Don't use system fonts on Android (issue 18210).
// Support: Android
(function () {
if (isAndroid) {
compatParams.set("useSystemFonts", false);
}
})();
}
const OptionKind = {
BROWSER: 0x01,
VIEWER: 0x02,
API: 0x04,
WORKER: 0x08,
EVENT_DISPATCH: 0x10,
PREFERENCE: 0x80,
};
// Should only be used with options that allow multiple types.
const Type = {
BOOLEAN: 0x01,
NUMBER: 0x02,
OBJECT: 0x04,
STRING: 0x08,
UNDEFINED: 0x10,
};
/**
* NOTE: These options are used to generate the `default_preferences.json` file,
* see `OptionKind.PREFERENCE`, hence the values below must use only
* primitive types and cannot rely on any imported types.
*/
const defaultOptions = {
allowedGlobalEvents: {
/** @type {Object} */
value: null,
kind: OptionKind.BROWSER,
},
canvasMaxAreaInBytes: {
/** @type {number} */
value: -1,
kind: OptionKind.BROWSER + OptionKind.API,
},
isInAutomation: {
/** @type {boolean} */
value: false,
kind: OptionKind.BROWSER,
},
localeProperties: {
/** @type {Object} */
value:
typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")
? { lang: navigator.language || "en-US" }
: null,
kind: OptionKind.BROWSER,
},
nimbusDataStr: {
/** @type {string} */
value: "",
kind: OptionKind.BROWSER,
},
supportsCaretBrowsingMode: {
/** @type {boolean} */
value: false,
kind: OptionKind.BROWSER,
},
supportsDocumentFonts: {
/** @type {boolean} */
value: true,
kind: OptionKind.BROWSER,
},
supportsIntegratedFind: {
/** @type {boolean} */
value: false,
kind: OptionKind.BROWSER,
},
supportsMouseWheelZoomCtrlKey: {
/** @type {boolean} */
value: true,
kind: OptionKind.BROWSER,
},
supportsMouseWheelZoomMetaKey: {
/** @type {boolean} */
value: true,
kind: OptionKind.BROWSER,
},
supportsPinchToZoom: {
/** @type {boolean} */
value: true,
kind: OptionKind.BROWSER,
},
toolbarDensity: {
/** @type {number} */
value: 0, // 0 = "normal", 1 = "compact", 2 = "touch"
kind: OptionKind.BROWSER + OptionKind.EVENT_DISPATCH,
},
altTextLearnMoreUrl: {
/** @type {string} */
value:
typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")
? "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/pdf-alt-text"
: "",
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
annotationEditorMode: {
/** @type {number} */
value: 0,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
annotationMode: {
/** @type {number} */
value: 2,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
cursorToolOnLoad: {
/** @type {number} */
value: 0,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
debuggerSrc: {
/** @type {string} */
value: "./debugger.mjs",
kind: OptionKind.VIEWER,
},
defaultZoomDelay: {
/** @type {number} */
value: 400,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
defaultZoomValue: {
/** @type {string} */
value: "",
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
disableHistory: {
/** @type {boolean} */
value: false,
kind: OptionKind.VIEWER,
},
disablePageLabels: {
/** @type {boolean} */
value: false,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
enableAltText: {
/** @type {boolean} */
value: false,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
enableAltTextModelDownload: {
/** @type {boolean} */
value: true,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE + OptionKind.EVENT_DISPATCH,
},
enableGuessAltText: {
/** @type {boolean} */
value: true,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE + OptionKind.EVENT_DISPATCH,
},
enableHighlightFloatingButton: {
// We'll probably want to make some experiments before enabling this
// in Firefox release, but it has to be temporary.
// TODO: remove it when unnecessary.
/** @type {boolean} */
value: typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING"),
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
enableNewAltTextWhenAddingImage: {
/** @type {boolean} */
value: true,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
enablePermissions: {
/** @type {boolean} */
value: false,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
enablePrintAutoRotate: {
/** @type {boolean} */
value: true,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
enableScripting: {
/** @type {boolean} */
value: typeof PDFJSDev === "undefined" || !PDFJSDev.test("CHROME"),
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
enableSignatureEditor: {
/** @type {boolean} */
value: typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING"),
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
enableUpdatedAddImage: {
// We'll probably want to make some experiments before enabling this
// in Firefox release, but it has to be temporary.
// TODO: remove it when unnecessary.
/** @type {boolean} */
value: false,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
externalLinkRel: {
/** @type {string} */
value: "noopener noreferrer nofollow",
kind: OptionKind.VIEWER,
},
externalLinkTarget: {
/** @type {number} */
value: 0,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
highlightEditorColors: {
/** @type {string} */
value: "yellow=#FFFF98,green=#53FFBC,blue=#80EBFF,pink=#FFCBE6,red=#FF4F5F",
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
historyUpdateUrl: {
/** @type {boolean} */
value: false,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
ignoreDestinationZoom: {
/** @type {boolean} */
value: false,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
imageResourcesPath: {
/** @type {string} */
value:
typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")
? "resource://pdf.js/web/images/"
: "./images/",
kind: OptionKind.VIEWER,
},
maxCanvasPixels: {
/** @type {number} */
value: 2 ** 25,
kind: OptionKind.VIEWER,
},
forcePageColors: {
/** @type {boolean} */
value: false,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
pageColorsBackground: {
/** @type {string} */
value: "Canvas",
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
pageColorsForeground: {
/** @type {string} */
value: "CanvasText",
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
pdfBugEnabled: {
/** @type {boolean} */
value: typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING"),
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
printResolution: {
/** @type {number} */
value: 150,
kind: OptionKind.VIEWER,
},
sidebarViewOnLoad: {
/** @type {number} */
value: -1,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
scrollModeOnLoad: {
/** @type {number} */
value: -1,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
spreadModeOnLoad: {
/** @type {number} */
value: -1,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
textLayerMode: {
/** @type {number} */
value: 1,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
viewOnLoad: {
/** @type {boolean} */
value: 0,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
cMapPacked: {
/** @type {boolean} */
value: true,
kind: OptionKind.API,
},
cMapUrl: {
/** @type {string} */
value:
// eslint-disable-next-line no-nested-ternary
typeof PDFJSDev === "undefined"
? "../external/bcmaps/"
: PDFJSDev.test("MOZCENTRAL")
? "resource://pdf.js/web/cmaps/"
: "../web/cmaps/",
kind: OptionKind.API,
},
disableAutoFetch: {
/** @type {boolean} */
value: false,
kind: OptionKind.API + OptionKind.PREFERENCE,
},
disableFontFace: {
/** @type {boolean} */
value: false,
kind: OptionKind.API + OptionKind.PREFERENCE,
},
disableRange: {
/** @type {boolean} */
value: false,
kind: OptionKind.API + OptionKind.PREFERENCE,
},
disableStream: {
/** @type {boolean} */
value: false,
kind: OptionKind.API + OptionKind.PREFERENCE,
},
docBaseUrl: {
/** @type {string} */
value: typeof PDFJSDev === "undefined" ? document.URL.split("#", 1)[0] : "",
kind: OptionKind.API,
},
enableHWA: {
/** @type {boolean} */
value: typeof PDFJSDev !== "undefined" && !PDFJSDev.test("MOZCENTRAL"),
kind: OptionKind.API + OptionKind.VIEWER + OptionKind.PREFERENCE,
},
enableXfa: {
/** @type {boolean} */
value: true,
kind: OptionKind.API + OptionKind.PREFERENCE,
},
fontExtraProperties: {
/** @type {boolean} */
value: false,
kind: OptionKind.API,
},
isEvalSupported: {
/** @type {boolean} */
value: true,
kind: OptionKind.API,
},
isOffscreenCanvasSupported: {
/** @type {boolean} */
value: true,
kind: OptionKind.API,
},
maxImageSize: {
/** @type {number} */
value: -1,
kind: OptionKind.API,
},
pdfBug: {
/** @type {boolean} */
value: false,
kind: OptionKind.API,
},
standardFontDataUrl: {
/** @type {string} */
value:
// eslint-disable-next-line no-nested-ternary
typeof PDFJSDev === "undefined"
? "../external/standard_fonts/"
: PDFJSDev.test("MOZCENTRAL")
? "resource://pdf.js/web/standard_fonts/"
: "../web/standard_fonts/",
kind: OptionKind.API,
},
useSystemFonts: {
// On Android, there is almost no chance to have the font we want so we
// don't use the system fonts in this case (bug 1882613).
/** @type {boolean|undefined} */
value: (
typeof PDFJSDev === "undefined"
? window.isGECKOVIEW
: PDFJSDev.test("GECKOVIEW")
)
? false
: undefined,
kind: OptionKind.API,
type: Type.BOOLEAN + Type.UNDEFINED,
},
verbosity: {
/** @type {number} */
value: 1,
kind: OptionKind.API,
},
workerPort: {
/** @type {Object} */
value: null,
kind: OptionKind.WORKER,
},
workerSrc: {
/** @type {string} */
value:
// eslint-disable-next-line no-nested-ternary
typeof PDFJSDev === "undefined"
? "../src/pdf.worker.js"
: PDFJSDev.test("MOZCENTRAL")
? "resource://pdf.js/build/pdf.worker.mjs"
: "../build/pdf.worker.mjs",
kind: OptionKind.WORKER,
},
};
if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
defaultOptions.defaultUrl = {
/** @type {string} */
value:
typeof PDFJSDev !== "undefined" && PDFJSDev.test("CHROME")
? ""
: "compressed.tracemonkey-pldi-09.pdf",
kind: OptionKind.VIEWER,
};
defaultOptions.sandboxBundleSrc = {
/** @type {string} */
value:
typeof PDFJSDev === "undefined"
? "../build/dev-sandbox/pdf.sandbox.mjs"
: "../build/pdf.sandbox.mjs",
kind: OptionKind.VIEWER,
};
defaultOptions.viewerCssTheme = {
/** @type {number} */
value: typeof PDFJSDev !== "undefined" && PDFJSDev.test("CHROME") ? 2 : 0,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
};
defaultOptions.enableFakeMLManager = {
/** @type {boolean} */
value: true,
kind: OptionKind.VIEWER,
};
}
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
defaultOptions.disablePreferences = {
/** @type {boolean} */
value: typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING"),
kind: OptionKind.VIEWER,
};
} else if (PDFJSDev.test("CHROME")) {
defaultOptions.disableTelemetry = {
/** @type {boolean} */
value: false,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
};
}
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING || LIB")) {
// Ensure that the `defaultOptions` are correctly specified.
for (const name in defaultOptions) {
const { value, kind, type } = defaultOptions[name];
if (kind & OptionKind.PREFERENCE) {
if (kind === OptionKind.PREFERENCE) {
throw new Error(`Cannot use only "PREFERENCE" kind: ${name}`);
}
if (kind & OptionKind.BROWSER) {
throw new Error(`Cannot mix "PREFERENCE" and "BROWSER" kind: ${name}`);
}
if (type !== undefined) {
throw new Error(
`Cannot have \`type\`-field for "PREFERENCE" kind: ${name}`
);
}
if (typeof compatParams === "object" && compatParams.has(name)) {
throw new Error(
`Should not have compatibility-value for "PREFERENCE" kind: ${name}`
);
}
// Only "simple" preference-values are allowed.
if (
typeof value !== "boolean" &&
typeof value !== "string" &&
!Number.isInteger(value)
) {
throw new Error(`Invalid value for "PREFERENCE" kind: ${name}`);
}
} else if (kind & OptionKind.BROWSER) {
if (type !== undefined) {
throw new Error(
`Cannot have \`type\`-field for "BROWSER" kind: ${name}`
);
}
if (typeof compatParams === "object" && compatParams.has(name)) {
throw new Error(
`Should not have compatibility-value for "BROWSER" kind: ${name}`
);
}
if (value === undefined) {
throw new Error(`Invalid value for "BROWSER" kind: ${name}`);
}
}
}
}
class AppOptions {
static eventBus;
static #opts = new Map();
static {
// Initialize all the user-options.
for (const name in defaultOptions) {
this.#opts.set(name, defaultOptions[name].value);
}
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
// Apply any compatibility-values to the user-options.
for (const [name, value] of compatParams) {
this.#opts.set(name, value);
}
this._hasInvokedSet = false;
this._checkDisablePreferences = () => {
if (this.get("disablePreferences")) {
// Give custom implementations of the default viewer a simpler way to
// opt-out of having the `Preferences` override existing `AppOptions`.
return true;
}
if (this._hasInvokedSet) {
console.warn(
"The Preferences may override manually set AppOptions; " +
'please use the "disablePreferences"-option to prevent that.'
);
}
return false;
};
}
}
constructor() {
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
throw new Error("Cannot initialize AppOptions.");
}
}
static get(name) {
return this.#opts.get(name);
}
static getAll(kind = null, defaultOnly = false) {
const options = Object.create(null);
for (const name in defaultOptions) {
const defaultOpt = defaultOptions[name];
if (kind && !(kind & defaultOpt.kind)) {
continue;
}
options[name] = !defaultOnly ? this.#opts.get(name) : defaultOpt.value;
}
return options;
}
static set(name, value) {
this.setAll({ [name]: value });
}
static setAll(options, prefs = false) {
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
this._hasInvokedSet ||= true;
}
let events;
for (const name in options) {
const defaultOpt = defaultOptions[name],
userOpt = options[name];
if (
!defaultOpt ||
!(
typeof userOpt === typeof defaultOpt.value ||
Type[(typeof userOpt).toUpperCase()] & defaultOpt.type
)
) {
continue;
}
const { kind } = defaultOpt;
if (
prefs &&
!(kind & OptionKind.BROWSER || kind & OptionKind.PREFERENCE)
) {
continue;
}
if (this.eventBus && kind & OptionKind.EVENT_DISPATCH) {
(events ||= new Map()).set(name, userOpt);
}
this.#opts.set(name, userOpt);
}
if (events) {
for (const [name, value] of events) {
this.eventBus.dispatch(name.toLowerCase(), { source: this, value });
}
}
}
}
export { AppOptions, OptionKind };