Avoid to have the text layer mismatching the rendered text with mismatching locales (bug 1869001)
The system locale (used in OffscreenCanvas) can be different from the one guessed by Fluent, consequently, in order to avoid any mismatch, we just use an attached canvas element. The original issue can easily be reproduced locally in adding a lang="ja" in viewer.html (or with an other language for Japanese users).
This commit is contained in:
parent
7873ad98bb
commit
f84f48b5d0
@ -63,6 +63,7 @@ import {
|
|||||||
NodeStandardFontDataFactory,
|
NodeStandardFontDataFactory,
|
||||||
} from "display-node_utils";
|
} from "display-node_utils";
|
||||||
import { CanvasGraphics } from "./canvas.js";
|
import { CanvasGraphics } from "./canvas.js";
|
||||||
|
import { cleanupTextLayer } from "./text_layer.js";
|
||||||
import { GlobalWorkerOptions } from "./worker_options.js";
|
import { GlobalWorkerOptions } from "./worker_options.js";
|
||||||
import { MessageHandler } from "../shared/message_handler.js";
|
import { MessageHandler } from "../shared/message_handler.js";
|
||||||
import { Metadata } from "./metadata.js";
|
import { Metadata } from "./metadata.js";
|
||||||
@ -2481,6 +2482,7 @@ class WorkerTransport {
|
|||||||
this.fontLoader.clear();
|
this.fontLoader.clear();
|
||||||
this.#methodPromises.clear();
|
this.#methodPromises.clear();
|
||||||
this.filterFactory.destroy();
|
this.filterFactory.destroy();
|
||||||
|
cleanupTextLayer();
|
||||||
|
|
||||||
this._networkStream?.cancelAllRequests(
|
this._networkStream?.cancelAllRequests(
|
||||||
new AbortException("Worker was terminated.")
|
new AbortException("Worker was terminated.")
|
||||||
@ -3065,6 +3067,7 @@ class WorkerTransport {
|
|||||||
}
|
}
|
||||||
this.#methodPromises.clear();
|
this.#methodPromises.clear();
|
||||||
this.filterFactory.destroy(/* keepHCM = */ true);
|
this.filterFactory.destroy(/* keepHCM = */ true);
|
||||||
|
cleanupTextLayer();
|
||||||
}
|
}
|
||||||
|
|
||||||
get loadingParams() {
|
get loadingParams() {
|
||||||
|
@ -16,12 +16,7 @@
|
|||||||
/** @typedef {import("./display_utils").PageViewport} PageViewport */
|
/** @typedef {import("./display_utils").PageViewport} PageViewport */
|
||||||
/** @typedef {import("./api").TextContent} TextContent */
|
/** @typedef {import("./api").TextContent} TextContent */
|
||||||
|
|
||||||
import {
|
import { AbortException, PromiseCapability, Util } from "../shared/util.js";
|
||||||
AbortException,
|
|
||||||
FeatureTest,
|
|
||||||
PromiseCapability,
|
|
||||||
Util,
|
|
||||||
} from "../shared/util.js";
|
|
||||||
import { setLayerDimensions } from "./display_utils.js";
|
import { setLayerDimensions } from "./display_utils.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -43,8 +38,6 @@ import { setLayerDimensions } from "./display_utils.js";
|
|||||||
* @property {Array<string>} [textContentItemsStr] - Strings that correspond to
|
* @property {Array<string>} [textContentItemsStr] - Strings that correspond to
|
||||||
* the `str` property of the text items of the textContent input.
|
* the `str` property of the text items of the textContent input.
|
||||||
* This is output and shall initially be set to an empty array.
|
* This is output and shall initially be set to an empty array.
|
||||||
* @property {boolean} [isOffscreenCanvasSupported] true if we can use
|
|
||||||
* OffscreenCanvas to measure string widths.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -60,8 +53,6 @@ import { setLayerDimensions } from "./display_utils.js";
|
|||||||
* This is output and shall initially be set to an empty array.
|
* This is output and shall initially be set to an empty array.
|
||||||
* @property {WeakMap<HTMLElement,Object>} [textDivProperties] - Some properties
|
* @property {WeakMap<HTMLElement,Object>} [textDivProperties] - Some properties
|
||||||
* weakly mapped to the HTML elements used to render the text.
|
* weakly mapped to the HTML elements used to render the text.
|
||||||
* @property {boolean} [isOffscreenCanvasSupported] true if we can use
|
|
||||||
* OffscreenCanvas to measure string widths.
|
|
||||||
* @property {boolean} [mustRotate] true if the text layer must be rotated.
|
* @property {boolean} [mustRotate] true if the text layer must be rotated.
|
||||||
* @property {boolean} [mustRescale] true if the text layer contents must be
|
* @property {boolean} [mustRescale] true if the text layer contents must be
|
||||||
* rescaled.
|
* rescaled.
|
||||||
@ -71,28 +62,43 @@ const MAX_TEXT_DIVS_TO_RENDER = 100000;
|
|||||||
const DEFAULT_FONT_SIZE = 30;
|
const DEFAULT_FONT_SIZE = 30;
|
||||||
const DEFAULT_FONT_ASCENT = 0.8;
|
const DEFAULT_FONT_ASCENT = 0.8;
|
||||||
const ascentCache = new Map();
|
const ascentCache = new Map();
|
||||||
|
let _canvasContext = null;
|
||||||
|
|
||||||
function getCtx(size, isOffscreenCanvasSupported) {
|
function getCtx() {
|
||||||
let ctx;
|
if (!_canvasContext) {
|
||||||
if (isOffscreenCanvasSupported && FeatureTest.isOffscreenCanvasSupported) {
|
// We don't use an OffscreenCanvas here because we use serif/sans serif
|
||||||
ctx = new OffscreenCanvas(size, size).getContext("2d", { alpha: false });
|
// fonts with it and they depends on the locale.
|
||||||
} else {
|
// In Firefox, the <html> element get a lang attribute that depends on what
|
||||||
|
// Fluent returns for the locale and the OffscreenCanvas uses the OS locale.
|
||||||
|
// Those two locales can be different and consequently the used fonts will
|
||||||
|
// be different (see bug 1869001).
|
||||||
|
// Ideally, we should use in the text layer the fonts we've in the pdf (or
|
||||||
|
// their replacements when they aren't embedded) and then we can use an
|
||||||
|
// OffscreenCanvas.
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
canvas.width = canvas.height = size;
|
canvas.className = "hiddenCanvasElement";
|
||||||
ctx = canvas.getContext("2d", { alpha: false });
|
document.body.append(canvas);
|
||||||
|
_canvasContext = canvas.getContext("2d", { alpha: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx;
|
return _canvasContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAscent(fontFamily, isOffscreenCanvasSupported) {
|
function cleanupTextLayer() {
|
||||||
|
_canvasContext?.canvas.remove();
|
||||||
|
_canvasContext = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAscent(fontFamily) {
|
||||||
const cachedAscent = ascentCache.get(fontFamily);
|
const cachedAscent = ascentCache.get(fontFamily);
|
||||||
if (cachedAscent) {
|
if (cachedAscent) {
|
||||||
return cachedAscent;
|
return cachedAscent;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ctx = getCtx(DEFAULT_FONT_SIZE, isOffscreenCanvasSupported);
|
const ctx = getCtx();
|
||||||
|
|
||||||
|
const savedFont = ctx.font;
|
||||||
|
ctx.canvas.width = ctx.canvas.height = DEFAULT_FONT_SIZE;
|
||||||
ctx.font = `${DEFAULT_FONT_SIZE}px ${fontFamily}`;
|
ctx.font = `${DEFAULT_FONT_SIZE}px ${fontFamily}`;
|
||||||
const metrics = ctx.measureText("");
|
const metrics = ctx.measureText("");
|
||||||
|
|
||||||
@ -104,6 +110,7 @@ function getAscent(fontFamily, isOffscreenCanvasSupported) {
|
|||||||
ascentCache.set(fontFamily, ratio);
|
ascentCache.set(fontFamily, ratio);
|
||||||
|
|
||||||
ctx.canvas.width = ctx.canvas.height = 0;
|
ctx.canvas.width = ctx.canvas.height = 0;
|
||||||
|
ctx.font = savedFont;
|
||||||
return ratio;
|
return ratio;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,6 +150,7 @@ function getAscent(fontFamily, isOffscreenCanvasSupported) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx.canvas.width = ctx.canvas.height = 0;
|
ctx.canvas.width = ctx.canvas.height = 0;
|
||||||
|
ctx.font = savedFont;
|
||||||
|
|
||||||
if (ascent) {
|
if (ascent) {
|
||||||
const ratio = ascent / (ascent + descent);
|
const ratio = ascent / (ascent + descent);
|
||||||
@ -176,8 +184,7 @@ function appendText(task, geom, styles) {
|
|||||||
const fontFamily =
|
const fontFamily =
|
||||||
(task._fontInspectorEnabled && style.fontSubstitution) || style.fontFamily;
|
(task._fontInspectorEnabled && style.fontSubstitution) || style.fontFamily;
|
||||||
const fontHeight = Math.hypot(tx[2], tx[3]);
|
const fontHeight = Math.hypot(tx[2], tx[3]);
|
||||||
const fontAscent =
|
const fontAscent = fontHeight * getAscent(fontFamily);
|
||||||
fontHeight * getAscent(fontFamily, task._isOffscreenCanvasSupported);
|
|
||||||
|
|
||||||
let left, top;
|
let left, top;
|
||||||
if (angle === 0) {
|
if (angle === 0) {
|
||||||
@ -308,14 +315,12 @@ class TextLayerRenderTask {
|
|||||||
textDivs,
|
textDivs,
|
||||||
textDivProperties,
|
textDivProperties,
|
||||||
textContentItemsStr,
|
textContentItemsStr,
|
||||||
isOffscreenCanvasSupported,
|
|
||||||
}) {
|
}) {
|
||||||
this._textContentSource = textContentSource;
|
this._textContentSource = textContentSource;
|
||||||
this._isReadableStream = textContentSource instanceof ReadableStream;
|
this._isReadableStream = textContentSource instanceof ReadableStream;
|
||||||
this._container = this._rootContainer = container;
|
this._container = this._rootContainer = container;
|
||||||
this._textDivs = textDivs || [];
|
this._textDivs = textDivs || [];
|
||||||
this._textContentItemsStr = textContentItemsStr || [];
|
this._textContentItemsStr = textContentItemsStr || [];
|
||||||
this._isOffscreenCanvasSupported = isOffscreenCanvasSupported;
|
|
||||||
this._fontInspectorEnabled = !!globalThis.FontInspector?.enabled;
|
this._fontInspectorEnabled = !!globalThis.FontInspector?.enabled;
|
||||||
|
|
||||||
this._reader = null;
|
this._reader = null;
|
||||||
@ -328,7 +333,7 @@ class TextLayerRenderTask {
|
|||||||
div: null,
|
div: null,
|
||||||
scale: viewport.scale * (globalThis.devicePixelRatio || 1),
|
scale: viewport.scale * (globalThis.devicePixelRatio || 1),
|
||||||
properties: null,
|
properties: null,
|
||||||
ctx: getCtx(0, isOffscreenCanvasSupported),
|
ctx: getCtx(),
|
||||||
};
|
};
|
||||||
const { pageWidth, pageHeight, pageX, pageY } = viewport.rawDims;
|
const { pageWidth, pageHeight, pageX, pageY } = viewport.rawDims;
|
||||||
this._transform = [1, 0, 0, -1, -pageX, pageY + pageHeight];
|
this._transform = [1, 0, 0, -1, -pageX, pageY + pageHeight];
|
||||||
@ -474,7 +479,6 @@ function updateTextLayer({
|
|||||||
viewport,
|
viewport,
|
||||||
textDivs,
|
textDivs,
|
||||||
textDivProperties,
|
textDivProperties,
|
||||||
isOffscreenCanvasSupported,
|
|
||||||
mustRotate = true,
|
mustRotate = true,
|
||||||
mustRescale = true,
|
mustRescale = true,
|
||||||
}) {
|
}) {
|
||||||
@ -483,7 +487,7 @@ function updateTextLayer({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mustRescale) {
|
if (mustRescale) {
|
||||||
const ctx = getCtx(0, isOffscreenCanvasSupported);
|
const ctx = getCtx();
|
||||||
const scale = viewport.scale * (globalThis.devicePixelRatio || 1);
|
const scale = viewport.scale * (globalThis.devicePixelRatio || 1);
|
||||||
const params = {
|
const params = {
|
||||||
prevFontSize: null,
|
prevFontSize: null,
|
||||||
@ -501,4 +505,9 @@ function updateTextLayer({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { renderTextLayer, TextLayerRenderTask, updateTextLayer };
|
export {
|
||||||
|
cleanupTextLayer,
|
||||||
|
renderTextLayer,
|
||||||
|
TextLayerRenderTask,
|
||||||
|
updateTextLayer,
|
||||||
|
};
|
||||||
|
@ -441,7 +441,6 @@ const PDFViewerApplication = {
|
|||||||
annotationEditorHighlightColors: AppOptions.get("highlightEditorColors"),
|
annotationEditorHighlightColors: AppOptions.get("highlightEditorColors"),
|
||||||
imageResourcesPath: AppOptions.get("imageResourcesPath"),
|
imageResourcesPath: AppOptions.get("imageResourcesPath"),
|
||||||
enablePrintAutoRotate: AppOptions.get("enablePrintAutoRotate"),
|
enablePrintAutoRotate: AppOptions.get("enablePrintAutoRotate"),
|
||||||
isOffscreenCanvasSupported,
|
|
||||||
maxCanvasPixels: AppOptions.get("maxCanvasPixels"),
|
maxCanvasPixels: AppOptions.get("maxCanvasPixels"),
|
||||||
enablePermissions: AppOptions.get("enablePermissions"),
|
enablePermissions: AppOptions.get("enablePermissions"),
|
||||||
pageColors,
|
pageColors,
|
||||||
|
@ -72,8 +72,6 @@ import { XfaLayerBuilder } from "./xfa_layer_builder.js";
|
|||||||
* The default value is `AnnotationMode.ENABLE_FORMS`.
|
* The default value is `AnnotationMode.ENABLE_FORMS`.
|
||||||
* @property {string} [imageResourcesPath] - Path for image resources, mainly
|
* @property {string} [imageResourcesPath] - Path for image resources, mainly
|
||||||
* for annotation icons. Include trailing slash.
|
* for annotation icons. Include trailing slash.
|
||||||
* @property {boolean} [isOffscreenCanvasSupported] - Allows to use an
|
|
||||||
* OffscreenCanvas if needed.
|
|
||||||
* @property {number} [maxCanvasPixels] - The maximum supported canvas size in
|
* @property {number} [maxCanvasPixels] - The maximum supported canvas size in
|
||||||
* total pixels, i.e. width * height. Use `-1` for no limit, or `0` for
|
* total pixels, i.e. width * height. Use `-1` for no limit, or `0` for
|
||||||
* CSS-only zooming. The default value is 4096 * 4096 (16 mega-pixels).
|
* CSS-only zooming. The default value is 4096 * 4096 (16 mega-pixels).
|
||||||
@ -154,8 +152,6 @@ class PDFPageView {
|
|||||||
this.#annotationMode =
|
this.#annotationMode =
|
||||||
options.annotationMode ?? AnnotationMode.ENABLE_FORMS;
|
options.annotationMode ?? AnnotationMode.ENABLE_FORMS;
|
||||||
this.imageResourcesPath = options.imageResourcesPath || "";
|
this.imageResourcesPath = options.imageResourcesPath || "";
|
||||||
this.isOffscreenCanvasSupported =
|
|
||||||
options.isOffscreenCanvasSupported ?? true;
|
|
||||||
this.maxCanvasPixels = options.maxCanvasPixels ?? MAX_CANVAS_PIXELS;
|
this.maxCanvasPixels = options.maxCanvasPixels ?? MAX_CANVAS_PIXELS;
|
||||||
this.pageColors = options.pageColors || null;
|
this.pageColors = options.pageColors || null;
|
||||||
|
|
||||||
@ -879,7 +875,6 @@ class PDFPageView {
|
|||||||
this.textLayer = new TextLayerBuilder({
|
this.textLayer = new TextLayerBuilder({
|
||||||
highlighter: this._textHighlighter,
|
highlighter: this._textHighlighter,
|
||||||
accessibilityManager: this._accessibilityManager,
|
accessibilityManager: this._accessibilityManager,
|
||||||
isOffscreenCanvasSupported: this.isOffscreenCanvasSupported,
|
|
||||||
enablePermissions:
|
enablePermissions:
|
||||||
this.#textLayerMode === TextLayerMode.ENABLE_PERMISSIONS,
|
this.#textLayerMode === TextLayerMode.ENABLE_PERMISSIONS,
|
||||||
});
|
});
|
||||||
|
@ -46,7 +46,8 @@
|
|||||||
transform: rotate(270deg) translateX(-100%);
|
transform: rotate(270deg) translateX(-100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
#hiddenCopyElement {
|
#hiddenCopyElement,
|
||||||
|
.hiddenCanvasElement {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
@ -115,8 +115,6 @@ function isValidAnnotationEditorMode(mode) {
|
|||||||
* mainly for annotation icons. Include trailing slash.
|
* mainly for annotation icons. Include trailing slash.
|
||||||
* @property {boolean} [enablePrintAutoRotate] - Enables automatic rotation of
|
* @property {boolean} [enablePrintAutoRotate] - Enables automatic rotation of
|
||||||
* landscape pages upon printing. The default is `false`.
|
* landscape pages upon printing. The default is `false`.
|
||||||
* @property {boolean} [isOffscreenCanvasSupported] - Allows to use an
|
|
||||||
* OffscreenCanvas if needed.
|
|
||||||
* @property {number} [maxCanvasPixels] - The maximum supported canvas size in
|
* @property {number} [maxCanvasPixels] - The maximum supported canvas size in
|
||||||
* total pixels, i.e. width * height. Use `-1` for no limit, or `0` for
|
* total pixels, i.e. width * height. Use `-1` for no limit, or `0` for
|
||||||
* CSS-only zooming. The default value is 4096 * 4096 (16 mega-pixels).
|
* CSS-only zooming. The default value is 4096 * 4096 (16 mega-pixels).
|
||||||
@ -287,8 +285,6 @@ class PDFViewer {
|
|||||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
||||||
this.removePageBorders = options.removePageBorders || false;
|
this.removePageBorders = options.removePageBorders || false;
|
||||||
}
|
}
|
||||||
this.isOffscreenCanvasSupported =
|
|
||||||
options.isOffscreenCanvasSupported ?? true;
|
|
||||||
this.maxCanvasPixels = options.maxCanvasPixels;
|
this.maxCanvasPixels = options.maxCanvasPixels;
|
||||||
this.l10n = options.l10n || NullL10n;
|
this.l10n = options.l10n || NullL10n;
|
||||||
this.#enablePermissions = options.enablePermissions || false;
|
this.#enablePermissions = options.enablePermissions || false;
|
||||||
@ -919,7 +915,6 @@ class PDFViewer {
|
|||||||
textLayerMode,
|
textLayerMode,
|
||||||
annotationMode,
|
annotationMode,
|
||||||
imageResourcesPath: this.imageResourcesPath,
|
imageResourcesPath: this.imageResourcesPath,
|
||||||
isOffscreenCanvasSupported: this.isOffscreenCanvasSupported,
|
|
||||||
maxCanvasPixels: this.maxCanvasPixels,
|
maxCanvasPixels: this.maxCanvasPixels,
|
||||||
pageColors: this.pageColors,
|
pageColors: this.pageColors,
|
||||||
l10n: this.l10n,
|
l10n: this.l10n,
|
||||||
|
@ -28,8 +28,6 @@ import { removeNullCharacters } from "./ui_utils.js";
|
|||||||
* @property {TextHighlighter} highlighter - Optional object that will handle
|
* @property {TextHighlighter} highlighter - Optional object that will handle
|
||||||
* highlighting text from the find controller.
|
* highlighting text from the find controller.
|
||||||
* @property {TextAccessibilityManager} [accessibilityManager]
|
* @property {TextAccessibilityManager} [accessibilityManager]
|
||||||
* @property {boolean} [isOffscreenCanvasSupported] - Allows to use an
|
|
||||||
* OffscreenCanvas if needed.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -49,7 +47,6 @@ class TextLayerBuilder {
|
|||||||
constructor({
|
constructor({
|
||||||
highlighter = null,
|
highlighter = null,
|
||||||
accessibilityManager = null,
|
accessibilityManager = null,
|
||||||
isOffscreenCanvasSupported = true,
|
|
||||||
enablePermissions = false,
|
enablePermissions = false,
|
||||||
}) {
|
}) {
|
||||||
this.textContentItemsStr = [];
|
this.textContentItemsStr = [];
|
||||||
@ -59,7 +56,6 @@ class TextLayerBuilder {
|
|||||||
this.textLayerRenderTask = null;
|
this.textLayerRenderTask = null;
|
||||||
this.highlighter = highlighter;
|
this.highlighter = highlighter;
|
||||||
this.accessibilityManager = accessibilityManager;
|
this.accessibilityManager = accessibilityManager;
|
||||||
this.isOffscreenCanvasSupported = isOffscreenCanvasSupported;
|
|
||||||
this.#enablePermissions = enablePermissions === true;
|
this.#enablePermissions = enablePermissions === true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -107,7 +103,6 @@ class TextLayerBuilder {
|
|||||||
viewport,
|
viewport,
|
||||||
textDivs: this.textDivs,
|
textDivs: this.textDivs,
|
||||||
textDivProperties: this.textDivProperties,
|
textDivProperties: this.textDivProperties,
|
||||||
isOffscreenCanvasSupported: this.isOffscreenCanvasSupported,
|
|
||||||
mustRescale,
|
mustRescale,
|
||||||
mustRotate,
|
mustRotate,
|
||||||
});
|
});
|
||||||
@ -129,7 +124,6 @@ class TextLayerBuilder {
|
|||||||
textDivs: this.textDivs,
|
textDivs: this.textDivs,
|
||||||
textDivProperties: this.textDivProperties,
|
textDivProperties: this.textDivProperties,
|
||||||
textContentItemsStr: this.textContentItemsStr,
|
textContentItemsStr: this.textContentItemsStr,
|
||||||
isOffscreenCanvasSupported: this.isOffscreenCanvasSupported,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.textLayerRenderTask.promise;
|
await this.textLayerRenderTask.promise;
|
||||||
|
Loading…
Reference in New Issue
Block a user