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

Implement the new alt text flow (bug 1909604)

For the Firefox pdf viewer, we want to use AI to guess an alt-text when adding an image to a pdf.
For now the telemtry stuff is not implemented and will come soon.
In order to test it locally:
 - set enableAltText, enableFakeMLManager and enableUpdatedAddImage to true.
or in Firefox:
 - set browser.ml.enable, pdfjs.enableAltText and pdfjs.enableUpdatedAddImage to true.
This commit is contained in:
Calixte Denizet 2024-07-24 09:34:32 +02:00
parent 8378c40f4c
commit ed22d934e5
22 changed files with 1366 additions and 91 deletions

View file

@ -201,6 +201,7 @@ function createWebpackAlias(defines) {
"web-annotation_editor_params": "web/annotation_editor_params.js",
"web-download_manager": "",
"web-external_services": "",
"web-new_alt_text_manager": "web/new_alt_text_manager.js",
"web-null_l10n": "",
"web-pdf_attachment_viewer": "web/pdf_attachment_viewer.js",
"web-pdf_cursor_tools": "web/pdf_cursor_tools.js",
@ -1097,6 +1098,7 @@ function buildComponents(defines, dir) {
"web/images/loading-icon.gif",
"web/images/altText_*.svg",
"web/images/editor-toolbar-*.svg",
"web/images/messageBar_*.svg",
"web/images/toolbarButton-{editorHighlight,menuArrow}.svg",
"web/images/cursor-*.svg",
];

View file

@ -416,3 +416,52 @@ pdfjs-editor-colorpicker-red =
pdfjs-editor-highlight-show-all-button-label = Show all
pdfjs-editor-highlight-show-all-button =
.title = Show all
## New alt-text dialog
## Group note for entire feature: Alternative text (alt text) helps when people can't see the image. This feature includes a tool to create alt text automatically using an AI model that works locally on the user's device to preserve privacy.
# Modal header positioned above a text box where users can edit the alt text.
pdfjs-editor-new-alt-text-dialog-edit-label = Edit alt text (image description)
# Modal header positioned above a text box where users can add the alt text.
pdfjs-editor-new-alt-text-dialog-add-label = Add alt text (image description)
pdfjs-editor-new-alt-text-textarea =
.placeholder = Write your description here…
# This text refers to the alt text box above this description. It offers a definition of alt text.
pdfjs-editor-new-alt-text-description = Short description for people who cant see the image or when the image doesnt load.
# This is a required legal disclaimer that refers to the automatically created text inside the alt text box above this text. It disappears if the text is edited by a human.
pdfjs-editor-new-alt-text-disclaimer = This alt text was created automatically.
pdfjs-editor-new-alt-text-disclaimer-learn-more-url = Learn more
pdfjs-editor-new-alt-text-create-automatically-button-label = Create alt text automatically
pdfjs-editor-new-alt-text-not-now-button = Not now
pdfjs-editor-new-alt-text-error-title = Couldnt create alt text automatically
pdfjs-editor-new-alt-text-error-description = Please write your own alt text or try again later.
pdfjs-editor-new-alt-text-error-close-button = Close
# Variables:
# $totalSize (Number) - the total size (in MB) of the AI model.
# $downloadedSize (Number) - the downloaded size (in MB) of the AI model.
# $percent (Number) - the percentage of the downloaded size.
pdfjs-editor-new-alt-text-ai-model-downloading-progress =
.aria-valuemin = 0
.aria-valuemax = { $totalSize }
.aria-valuenow = { $downloadedSize }
.aria-valuetext = Downloading alt text AI model ({ $downloadedSize } of { $totalSize } MB)
# This is a button that users can click to edit the alt text they have already added.
pdfjs-editor-new-alt-text-added-button-label = Alt text added
# This is a button that users can click to open the alt text editor and add alt text when it is not present.
pdfjs-editor-new-alt-text-missing-button-label = Missing alt text
# This is a button that opens up the alt text modal where users should review the alt text that was automatically generated.
pdfjs-editor-new-alt-text-to-review-button-label = Review alt text
# "Created automatically" is a prefix that will be added to the beginning of any alt text that has been automatically generated. After the colon, the user will see/hear the actual alt text description. If the alt text has been edited by a human, this prefix will not appear.
# Variables:
# $generatedAltText (String) - the generated alt-text.
pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer = Created automatically: { $generatedAltText }

View file

@ -16,7 +16,7 @@
import { noContextMenu } from "../display_utils.js";
class AltText {
#altText = "";
#altText = null;
#altTextDecorative = false;
@ -28,12 +28,21 @@ class AltText {
#altTextWasFromKeyBoard = false;
#badge = null;
#editor = null;
#guessedText = null;
#textWithDisclaimer = null;
#useNewAltTextFlow = false;
static _l10nPromise = null;
constructor(editor) {
this.#editor = editor;
this.#useNewAltTextFlow = editor._uiManager.useNewAltTextFlow;
}
static initialize(l10nPromise) {
@ -43,9 +52,17 @@ class AltText {
async render() {
const altText = (this.#altTextButton = document.createElement("button"));
altText.className = "altText";
const msg = await AltText._l10nPromise.get(
"pdfjs-editor-alt-text-button-label"
);
let msg;
if (this.#useNewAltTextFlow) {
altText.classList.add("new");
msg = await AltText._l10nPromise.get(
"pdfjs-editor-new-alt-text-missing-button-label"
);
} else {
msg = await AltText._l10nPromise.get(
"pdfjs-editor-alt-text-button-label"
);
}
altText.textContent = msg;
altText.setAttribute("aria-label", msg);
altText.tabIndex = "0";
@ -84,9 +101,62 @@ class AltText {
}
isEmpty() {
if (this.#useNewAltTextFlow) {
return this.#altText === null;
}
return !this.#altText && !this.#altTextDecorative;
}
hasData() {
if (this.#useNewAltTextFlow) {
return this.#altText !== null || !!this.#guessedText;
}
return this.isEmpty();
}
get guessedText() {
return this.#guessedText;
}
async setGuessedText(guessedText) {
if (this.#altText !== null) {
// The user provided their own alt text, so we don't want to overwrite it.
return;
}
this.#guessedText = guessedText;
this.#textWithDisclaimer = await AltText._l10nPromise.get(
"pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer"
)({ generatedAltText: guessedText });
this.#setState();
}
toggleAltTextBadge(visibility = false) {
if (!this.#useNewAltTextFlow || this.#altText) {
this.#badge?.remove();
this.#badge = null;
return;
}
if (!this.#badge) {
const badge = (this.#badge = document.createElement("div"));
badge.className = "noAltTextBadge";
this.#editor.div.append(badge);
}
this.#badge.classList.toggle("hidden", !visibility);
}
serialize(isForCopying) {
let altText = this.#altText;
if (!isForCopying && this.#guessedText === altText) {
altText = this.#textWithDisclaimer;
}
return {
altText,
decorative: this.#altTextDecorative,
guessedText: this.#guessedText,
textWithDisclaimer: this.#textWithDisclaimer,
};
}
get data() {
return {
altText: this.#altText,
@ -97,12 +167,24 @@ class AltText {
/**
* Set the alt text data.
*/
set data({ altText, decorative }) {
set data({
altText,
decorative,
guessedText,
textWithDisclaimer,
cancel = false,
}) {
if (guessedText) {
this.#guessedText = guessedText;
this.#textWithDisclaimer = textWithDisclaimer;
}
if (this.#altText === altText && this.#altTextDecorative === decorative) {
return;
}
this.#altText = altText;
this.#altTextDecorative = decorative;
if (!cancel) {
this.#altText = altText;
this.#altTextDecorative = decorative;
}
this.#setState();
}
@ -121,6 +203,8 @@ class AltText {
this.#altTextButton?.remove();
this.#altTextButton = null;
this.#altTextTooltip = null;
this.#badge?.remove();
this.#badge = null;
}
async #setState() {
@ -128,18 +212,48 @@ class AltText {
if (!button) {
return;
}
if (!this.#altText && !this.#altTextDecorative) {
button.classList.remove("done");
this.#altTextTooltip?.remove();
return;
}
button.classList.add("done");
AltText._l10nPromise
.get("pdfjs-editor-alt-text-edit-button-label")
.then(msg => {
button.setAttribute("aria-label", msg);
});
if (this.#useNewAltTextFlow) {
// If we've an alt text, we get an "added".
// If we've a guessed text and the alt text has never been set, we get a
// "to-review" been set.
// Otherwise, we get a "missing".
const type =
(this.#altText && "added") ||
(this.#altText === null && this.guessedText && "to-review") ||
"missing";
button.classList.toggle("done", !!this.#altText);
AltText._l10nPromise
.get(`pdfjs-editor-new-alt-text-${type}-button-label`)
.then(msg => {
button.setAttribute("aria-label", msg);
// We can't just use button.textContent here, because it would remove
// the existing tooltip element.
for (const child of button.childNodes) {
if (child.nodeType === Node.TEXT_NODE) {
child.textContent = msg;
break;
}
}
});
if (!this.#altText) {
this.#altTextTooltip?.remove();
return;
}
} else {
if (!this.#altText && !this.#altTextDecorative) {
button.classList.remove("done");
this.#altTextTooltip?.remove();
return;
}
button.classList.add("done");
AltText._l10nPromise
.get("pdfjs-editor-alt-text-edit-button-label")
.then(msg => {
button.setAttribute("aria-label", msg);
});
}
let tooltip = this.#altTextTooltip;
if (!tooltip) {
this.#altTextTooltip = tooltip = document.createElement("span");

View file

@ -58,8 +58,6 @@ class AnnotationEditor {
#boundFocusout = this.focusout.bind(this);
#editToolbar = null;
#focusedResizerName = "";
#hasBeenClicked = false;
@ -80,6 +78,8 @@ class AnnotationEditor {
#telemetryTimeouts = null;
_editToolbar = null;
_initialOptions = Object.create(null);
_isVisible = true;
@ -210,6 +210,9 @@ class AnnotationEditor {
"pdfjs-editor-alt-text-button-label",
"pdfjs-editor-alt-text-edit-button-label",
"pdfjs-editor-alt-text-decorative-tooltip",
"pdfjs-editor-new-alt-text-added-button-label",
"pdfjs-editor-new-alt-text-missing-button-label",
"pdfjs-editor-new-alt-text-to-review-button-label",
"pdfjs-editor-resizer-label-topLeft",
"pdfjs-editor-resizer-label-topMiddle",
"pdfjs-editor-resizer-label-topRight",
@ -223,6 +226,18 @@ class AnnotationEditor {
l10n.get(str.replaceAll(/([A-Z])/g, c => `-${c.toLowerCase()}`)),
])
);
// The string isn't in the above list because the string has a parameter
// (i.e. the guessed text) and we must pass it to the l10n function to get
// the correct translation.
AnnotationEditor._l10nPromise.set(
"pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer",
l10n.get.bind(
l10n,
"pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer"
)
);
if (options?.strings) {
for (const str of options.strings) {
AnnotationEditor._l10nPromise.set(str, l10n.get(str));
@ -952,6 +967,9 @@ class AnnotationEditor {
this.fixAndSetPosition();
}
/**
* Called when the alt text dialog is closed.
*/
altTextFinish() {
this.#altText?.finish();
}
@ -961,24 +979,24 @@ class AnnotationEditor {
* @returns {Promise<EditorToolbar|null>}
*/
async addEditToolbar() {
if (this.#editToolbar || this.#isInEditMode) {
return this.#editToolbar;
if (this._editToolbar || this.#isInEditMode) {
return this._editToolbar;
}
this.#editToolbar = new EditorToolbar(this);
this.div.append(this.#editToolbar.render());
this._editToolbar = new EditorToolbar(this);
this.div.append(this._editToolbar.render());
if (this.#altText) {
this.#editToolbar.addAltTextButton(await this.#altText.render());
this._editToolbar.addAltTextButton(await this.#altText.render());
}
return this.#editToolbar;
return this._editToolbar;
}
removeEditToolbar() {
if (!this.#editToolbar) {
if (!this._editToolbar) {
return;
}
this.#editToolbar.remove();
this.#editToolbar = null;
this._editToolbar.remove();
this._editToolbar = null;
// We destroy the alt text but we don't null it because we want to be able
// to restore it in case the user undoes the deletion.
@ -1016,8 +1034,24 @@ class AnnotationEditor {
this.#altText.data = data;
}
get guessedAltText() {
return this.#altText?.guessedText;
}
async setGuessedAltText(text) {
await this.#altText?.setGuessedText(text);
}
serializeAltText(isForCopying) {
return this.#altText?.serialize(isForCopying);
}
hasAltText() {
return !this.#altText?.isEmpty();
return !!this.#altText && !this.#altText.isEmpty();
}
hasAltTextData() {
return this.#altText?.hasData() ?? false;
}
/**
@ -1558,18 +1592,19 @@ class AnnotationEditor {
select() {
this.makeResizable();
this.div?.classList.add("selectedEditor");
if (!this.#editToolbar) {
if (!this._editToolbar) {
this.addEditToolbar().then(() => {
if (this.div?.classList.contains("selectedEditor")) {
// The editor can have been unselected while we were waiting for the
// edit toolbar to be created, hence we want to be sure that this
// editor is still selected.
this.#editToolbar?.show();
this._editToolbar?.show();
}
});
return;
}
this.#editToolbar?.show();
this._editToolbar?.show();
this.#altText?.toggleAltTextBadge(false);
}
/**
@ -1585,7 +1620,8 @@ class AnnotationEditor {
preventScroll: true,
});
}
this.#editToolbar?.hide();
this._editToolbar?.hide();
this.#altText?.toggleAltTextBadge(true);
}
/**

View file

@ -36,8 +36,6 @@ class StampEditor extends AnnotationEditor {
#canvas = null;
#hasMLBeenQueried = false;
#observer = null;
#resizeTimeoutId = null;
@ -98,6 +96,14 @@ class StampEditor extends AnnotationEditor {
});
}
/** @inheritdoc */
altTextFinish() {
if (this._uiManager.useNewAltTextFlow) {
this.div.hidden = false;
}
super.altTextFinish();
}
#getBitmapFetched(data, fromId = false) {
if (!data) {
this.remove();
@ -117,7 +123,13 @@ class StampEditor extends AnnotationEditor {
#getBitmapDone() {
this.#bitmapPromise = null;
this._uiManager.enableWaiting(false);
if (this.#canvas) {
if (!this.#canvas) {
return;
}
if (this._uiManager.useNewAltTextFlow && this.#bitmap) {
this._editToolbar.hide();
this._uiManager.editAltText(this, /* firstTime = */ true);
} else {
this.div.focus();
}
}
@ -329,7 +341,9 @@ class StampEditor extends AnnotationEditor {
this._uiManager.enableWaiting(false);
const canvas = (this.#canvas = document.createElement("canvas"));
div.append(canvas);
div.hidden = false;
if (!this._uiManager.useNewAltTextFlow) {
div.hidden = false;
}
this.#drawBitmap(width, height);
this.#createObserver();
if (!this.#hasBeenAddedInUndoStack) {
@ -348,6 +362,77 @@ class StampEditor extends AnnotationEditor {
}
}
copyCanvas(maxDimension, createImageData = false) {
const { width: bitmapWidth, height: bitmapHeight } = this.#bitmap;
const canvas = document.createElement("canvas");
let bitmap = this.#bitmap;
let width = bitmapWidth,
height = bitmapHeight;
if (bitmapWidth > maxDimension || bitmapHeight > maxDimension) {
const ratio = Math.min(
maxDimension / bitmapWidth,
maxDimension / bitmapHeight
);
width = Math.floor(bitmapWidth * ratio);
height = Math.floor(bitmapHeight * ratio);
if (!this.#isSvg) {
bitmap = this.#scaleBitmap(width, height);
}
}
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d", {
willReadFrequently: true,
});
ctx.filter = this._uiManager.hcmFilter;
if (createImageData && this._uiManager.hcmFilter !== "none") {
const offscreen = new OffscreenCanvas(width, height);
const offscreenCtx = offscreen.getContext("2d", {
willReadFrequently: true,
});
offscreenCtx.drawImage(
bitmap,
0,
0,
bitmap.width,
bitmap.height,
0,
0,
width,
height
);
const data = offscreenCtx.getImageData(0, 0, width, height).data;
ctx.drawImage(offscreen, 0, 0);
return { canvas, imageData: { width, height, data } };
}
ctx.drawImage(
bitmap,
0,
0,
bitmap.width,
bitmap.height,
0,
0,
width,
height
);
let imageData = null;
if (createImageData) {
imageData = {
width,
height,
data: ctx.getImageData(0, 0, width, height).data,
};
}
return { canvas, imageData };
}
/**
* When the dimensions of the div change the inner canvas must
* renew its dimensions, hence it must redraw its own contents.
@ -425,43 +510,6 @@ class StampEditor extends AnnotationEditor {
return bitmap;
}
async #mlGuessAltText(bitmap, width, height) {
if (this.#hasMLBeenQueried) {
return;
}
this.#hasMLBeenQueried = true;
const isMLEnabled = await this._uiManager.isMLEnabledFor("altText");
if (!isMLEnabled || this.hasAltText()) {
return;
}
const offscreen = new OffscreenCanvas(width, height);
const ctx = offscreen.getContext("2d", { willReadFrequently: true });
ctx.drawImage(
bitmap,
0,
0,
bitmap.width,
bitmap.height,
0,
0,
width,
height
);
const response = await this._uiManager.mlGuess({
service: "moz-image-to-text",
request: {
data: ctx.getImageData(0, 0, width, height).data,
width,
height,
channels: 4,
},
});
const altText = response?.output || "";
if (this.parent && altText && !this.hasAltText()) {
this.altTextData = { altText, decorative: false };
}
}
#drawBitmap(width, height) {
width = Math.ceil(width);
height = Math.ceil(height);
@ -475,8 +523,6 @@ class StampEditor extends AnnotationEditor {
? this.#bitmap
: this.#scaleBitmap(width, height);
this.#mlGuessAltText(bitmap, width, height);
const ctx = canvas.getContext("2d");
ctx.filter = this._uiManager.hcmFilter;
ctx.drawImage(
@ -616,11 +662,11 @@ class StampEditor extends AnnotationEditor {
// of this annotation and the clipboard doesn't support ImageBitmaps,
// hence we serialize the bitmap to a data url.
serialized.bitmapUrl = this.#serializeBitmap(/* toUrl = */ true);
serialized.accessibilityData = this.altTextData;
serialized.accessibilityData = this.serializeAltText(true);
return serialized;
}
const { decorative, altText } = this.altTextData;
const { decorative, altText } = this.serializeAltText(false);
if (!decorative && altText) {
serialized.accessibilityData = { type: "Figure", alt: altText };
}

View file

@ -851,6 +851,10 @@ class AnnotationEditorUIManager {
}
}
hasMLManager() {
return !!this.#mlManager;
}
async mlGuess(data) {
return this.#mlManager?.guess(data) || null;
}
@ -859,6 +863,10 @@ class AnnotationEditorUIManager {
return !!(await this.#mlManager?.isEnabledFor(name));
}
get mlManager() {
return this.#mlManager;
}
get useNewAltTextFlow() {
return this.#enableUpdatedAddImage;
}
@ -912,8 +920,8 @@ class AnnotationEditorUIManager {
this.#mainHighlightColorPicker = colorPicker;
}
editAltText(editor) {
this.#altTextManager?.editAltText(this, editor);
editAltText(editor, firstTime = false) {
this.#altTextManager?.editAltText(this, editor, firstTime);
}
switchToMode(mode, callback) {

View file

@ -46,6 +46,8 @@
--editorFreeHighlight-editing-cursor: url(images/cursor-editorFreeHighlight.svg)
1 18,
pointer;
--new-alt-text-warning-image: url(images/altText_warning.svg);
}
/* The following class is used to hide an element but keep it available to
@ -76,6 +78,12 @@
}
}
#viewerContainer.pdfPresentationMode:fullscreen {
.noAltTextBadge {
display: none !important;
}
}
@media (min-resolution: 1.1dppx) {
:root {
--editorFreeText-editing-cursor: url(images/cursor-editorFreeText.svg) 0 16,
@ -222,12 +230,18 @@
--editor-toolbar-vert-offset: 6px;
--editor-toolbar-height: 28px;
--editor-toolbar-padding: 2px;
--alt-text-done-color: #2ac3a2;
--alt-text-warning-color: #0090ed;
--alt-text-hover-done-color: var(--alt-text-done-color);
--alt-text-hover-warning-color: var(--alt-text-warning-color);
@media (prefers-color-scheme: dark) {
--editor-toolbar-bg-color: #2b2a33;
--editor-toolbar-fg-color: #fbfbfe;
--editor-toolbar-hover-bg-color: #52525e;
--editor-toolbar-focus-outline-color: #0df;
--alt-text-done-color: #54ffbd;
--alt-text-warning-color: #80ebff;
}
@media screen and (forced-colors: active) {
@ -241,6 +255,10 @@
var(--editor-toolbar-hover-border-color);
--editor-toolbar-focus-outline-color: ButtonBorder;
--editor-toolbar-shadow: none;
--alt-text-done-color: var(--editor-toolbar-fg-color);
--alt-text-warning-color: var(--editor-toolbar-fg-color);
--alt-text-hover-done-color: var(--editor-toolbar-hover-fg-color);
--alt-text-hover-warning-color: var(--editor-toolbar-hover-fg-color);
}
display: flex;
@ -400,6 +418,31 @@
mask-image: var(--alt-text-done-image);
}
&.new {
&::before {
width: 16px;
height: 16px;
mask-image: var(--new-alt-text-warning-image);
background-color: var(--alt-text-warning-color);
mask-size: cover;
}
&:hover::before {
background-color: var(--alt-text-hover-warning-color);
}
&.done {
&::before {
mask-image: var(--alt-text-done-image);
background-color: var(--alt-text-done-color);
}
&:hover::before {
background-color: var(--alt-text-hover-done-color);
}
}
}
.tooltip {
display: none;
@ -519,6 +562,50 @@
top: 0;
left: 0;
}
.noAltTextBadge {
--no-alt-text-badge-border-color: #f0f0f4;
--no-alt-text-badge-bg-color: #cfcfd8;
--no-alt-text-badge-fg-color: #5b5b66;
@media (prefers-color-scheme: dark) {
--no-alt-text-badge-border-color: #52525e;
--no-alt-text-badge-bg-color: #fbfbfe;
--no-alt-text-badge-fg-color: #15141a;
}
@media screen and (forced-colors: active) {
--no-alt-text-badge-border-color: ButtonText;
--no-alt-text-badge-bg-color: ButtonFace;
--no-alt-text-badge-fg-color: ButtonText;
}
position: absolute;
inset-inline-end: 5px;
inset-block-end: 5px;
display: inline-flex;
width: 32px;
height: 32px;
padding: 3px;
justify-content: center;
align-items: center;
pointer-events: none;
z-index: 1;
border-radius: 2px;
border: 1px solid var(--no-alt-text-badge-border-color);
background: var(--no-alt-text-badge-bg-color);
&::before {
content: "";
display: inline-block;
width: 16px;
height: 16px;
mask-image: var(--new-alt-text-warning-image);
mask-size: cover;
background-color: var(--no-alt-text-badge-fg-color);
}
}
}
.annotationEditorLayer {
@ -767,6 +854,177 @@
}
}
.dialog.newAltText {
--new-alt-text-ai-disclaimer-icon: url(images/altText_disclaimer.svg);
--new-alt-text-spinner-icon: url(images/altText_spinner.svg);
width: 80%;
max-width: 570px;
min-width: 300px;
padding: 0;
&.noAi {
#newAltTextDisclaimer,
#newAltTextCreateAutomatically {
display: none !important;
}
}
&.aiInstalling {
#newAltTextCreateAutomatically {
display: none !important;
}
#newAltTextDownloadModel {
display: flex !important;
}
}
&.error {
#newAltTextNotNow {
display: none !important;
}
#newAltTextCancel {
display: inline-block !important;
}
}
&:not(.error) #newAltTextError {
display: none !important;
}
#newAltTextContainer {
display: flex;
width: auto;
padding: 16px;
flex-direction: column;
justify-content: flex-end;
align-items: flex-start;
gap: 12px;
flex: 0 1 auto;
#mainContent {
display: flex;
justify-content: flex-end;
align-items: flex-start;
gap: 12px;
align-self: stretch;
flex: 1 1 auto;
#descriptionAndSettings {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 16px;
flex: 1 0 0;
align-self: stretch;
}
#descriptionInstruction {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
align-self: stretch;
flex: 1 1 auto;
#newAltTextDescriptionContainer {
width: 100%;
height: 70px;
position: relative;
textarea {
width: 100%;
height: 100%;
padding: 8px;
&::placeholder {
color: var(--text-secondary-color);
}
}
.altTextSpinner {
display: none;
position: absolute;
width: 16px;
height: 16px;
inset-inline-start: 8px;
inset-block-start: 8px;
mask-size: cover;
background-color: var(--text-secondary-color);
pointer-events: none;
}
&.loading {
textarea::placeholder {
color: transparent;
}
.altTextSpinner {
display: inline-block;
mask-image: var(--new-alt-text-spinner-icon);
}
}
}
#newAltTextDescription {
font-size: 11px;
}
#newAltTextDisclaimer {
display: flex;
align-items: center;
gap: 4px;
align-self: stretch;
flex-wrap: wrap;
font-size: 11px;
&::before {
content: "";
display: inline-block;
width: 16px;
height: 16px;
mask-image: var(--new-alt-text-ai-disclaimer-icon);
mask-size: cover;
background-color: var(--text-secondary-color);
}
}
}
#newAltTextDownloadModel {
display: flex;
align-items: center;
gap: 4px;
align-self: stretch;
&::before {
content: "";
display: inline-block;
width: 16px;
height: 16px;
mask-image: var(--new-alt-text-spinner-icon);
mask-size: cover;
background-color: var(--text-secondary-color);
}
}
#newAltTextImagePreview {
width: 180px;
aspect-ratio: 1;
display: flex;
justify-content: center;
align-items: center;
flex: 0 0 auto;
> canvas {
max-width: 100%;
max-height: 100%;
}
}
}
}
}
.colorPicker {
--hover-outline-color: #0250bb;
--selected-outline-color: #0060df;

View file

@ -64,6 +64,7 @@ import { AltTextManager } from "web-alt_text_manager";
import { AnnotationEditorParams } from "web-annotation_editor_params";
import { CaretBrowsingMode } from "./caret_browsing.js";
import { DownloadManager } from "web-download_manager";
import { NewAltTextManager } from "web-new_alt_text_manager";
import { OverlayManager } from "./overlay_manager.js";
import { PasswordPrompt } from "./password_prompt.js";
import { PDFAttachmentViewer } from "web-pdf_attachment_viewer";
@ -205,6 +206,14 @@ const PDFViewerApplication = {
if (mode) {
document.documentElement.classList.add(mode);
}
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
if (AppOptions.get("enableFakeMLManager")) {
this.mlManager =
MLManager.getFakeMLManager?.({
enableGuessAltText: AppOptions.get("enableGuessAltText"),
}) || null;
}
}
} else if (AppOptions.get("enableAltText")) {
// We want to load the image-to-text AI engine as soon as possible.
this.mlManager = new MLManager({
@ -433,14 +442,21 @@ const PDFViewerApplication = {
foreground: AppOptions.get("pageColorsForeground"),
}
: null;
const altTextManager = appConfig.altTextDialog
? new AltTextManager(
appConfig.altTextDialog,
container,
this.overlayManager,
eventBus
)
: null;
let altTextManager;
if (AppOptions.get("enableUpdatedAddImage")) {
altTextManager = appConfig.newAltTextDialog
? new NewAltTextManager(appConfig.newAltTextDialog, this.overlayManager)
: null;
} else {
altTextManager = appConfig.altTextDialog
? new AltTextManager(
appConfig.altTextDialog,
container,
this.overlayManager,
eventBus
)
: null;
}
const enableHWA = AppOptions.get("enableHWA");
const pdfViewer = new PDFViewer({

View file

@ -469,6 +469,11 @@ if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
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 = {

View file

@ -22,6 +22,8 @@
--hover-filter: brightness(0.9);
--focus-ring-color: #0060df;
--focus-ring-outline: 2px solid var(--focus-ring-color);
--link-fg-color: #0060df;
--link-hover-fg-color: #0250bb;
--textarea-border-color: #8f8f9d;
--textarea-bg-color: white;
@ -53,6 +55,8 @@
--text-secondary-color: #cfcfd8;
--focus-ring-color: #0df;
--hover-filter: brightness(1.4);
--link-fg-color: #0df;
--link-hover-fg-color: #80ebff;
--textarea-bg-color: #42414d;
@ -73,6 +77,8 @@
--text-secondary-color: CanvasText;
--hover-filter: none;
--focus-ring-color: ButtonBorder;
--link-fg-color: LinkText;
--link-hover-fg-color: LinkText;
--textarea-border-color: ButtonBorder;
--textarea-bg-color: Field;
@ -112,6 +118,28 @@
outline-offset: 2px;
}
.title {
display: flex;
width: auto;
flex-direction: column;
justify-content: flex-end;
align-items: flex-start;
gap: 12px;
> span {
font-size: 13px;
font-style: normal;
font-weight: 590;
line-height: 150%; /* 19.5px */
}
}
.dialogButtonsGroup {
display: flex;
gap: 12px;
align-self: flex-end;
}
.radio {
display: flex;
flex-direction: column;
@ -159,7 +187,7 @@
}
}
button {
button:not(:is(.toggle-button, .closeButton)) {
border-radius: 4px;
border: 1px solid;
font: menu;
@ -199,6 +227,14 @@
}
}
a {
color: var(--link-fg-color);
&:hover {
color: var(--link-hover-fg-color);
}
}
textarea {
font: inherit;
padding: 8px;
@ -220,5 +256,148 @@
opacity: 0.4;
}
}
.messageBar {
--message-bar-warning-icon: url(images/messageBar_warning.svg);
--closing-button-icon: url(images/messageBar_closingButton.svg);
--message-bar-bg-color: #ffebcd;
--message-bar-fg-color: #15141a;
--message-bar-border-color: rgb(0 0 0 / 0.08);
--message-bar-icon-color: #cd411e;
--message-bar-close-button-border-radius: 4px;
--message-bar-close-button-border: none;
--message-bar-close-button-color: var(--text-primary-color);
--message-bar-close-button-hover-bg-color: rgb(21 20 26 / 0.14);
--message-bar-close-button-active-bg-color: rgb(21 20 26 / 0.21);
--message-bar-close-button-focus-bg-color: rgb(21 20 26 / 0.07);
--message-bar-close-button-color-hover: var(--text-primary-color);
@media (prefers-color-scheme: dark) {
--message-bar-bg-color: #5a3100;
--message-bar-fg-color: #fbfbfe;
--message-bar-border-color: rgb(255 255 255 / 0.08);
--message-bar-icon-color: #e49c49;
--message-bar-close-button-hover-bg-color: rgb(251 251 254 / 0.14);
--message-bar-close-button-active-bg-color: rgb(251 251 254 / 0.21);
--message-bar-close-button-focus-bg-color: rgb(251 251 254 / 0.07);
}
@media screen and (forced-colors: active) {
--message-bar-bg-color: HighlightText;
--message-bar-fg-color: CanvasText;
--message-bar-border-color: CanvasText;
--message-bar-icon-color: CanvasText;
--message-bar-close-button-color: ButtonText;
--message-bar-close-button-border: 1px solid ButtonText;
--message-bar-close-button-hover-bg-color: ButtonText;
--message-bar-close-button-active-bg-color: ButtonText;
--message-bar-close-button-focus-bg-color: ButtonText;
--message-bar-close-button-color-hover: HighlightText;
}
display: flex;
position: relative;
padding: 12px 8px 12px 0;
flex-direction: column;
justify-content: center;
align-items: flex-start;
gap: 8px;
align-self: stretch;
border-radius: 4px;
border: 1px solid var(--message-bar-border-color);
background: var(--message-bar-bg-color);
> div {
display: flex;
padding-inline-start: 16px;
align-items: flex-start;
gap: 8px;
align-self: stretch;
&::before {
content: "";
display: inline-block;
width: 16px;
height: 16px;
mask-image: var(--message-bar-warning-icon);
mask-size: cover;
background-color: var(--message-bar-icon-color);
}
> div {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
flex: 1 0 0;
.title {
font-size: 13px;
font-weight: 590;
}
.description {
font-size: 13px;
}
}
}
.closeButton {
position: absolute;
width: 32px;
height: 32px;
inset-inline-end: 8px;
inset-block-start: 8px;
background: none;
border-radius: var(--message-bar-close-button-border-radius);
border: var(--message-bar-close-button-border);
&::before {
content: "";
display: inline-block;
width: 16px;
height: 16px;
mask-image: var(--closing-button-icon);
mask-size: cover;
background-color: var(--message-bar-close-button-color);
}
&:is(:hover, :active, :focus)::before {
background-color: var(--message-bar-close-button-color-hover);
}
&:hover {
background-color: var(--message-bar-close-button-hover-bg-color);
}
&:active {
background-color: var(--message-bar-close-button-active-bg-color);
}
&:focus {
background-color: var(--message-bar-close-button-focus-bg-color);
}
> span {
display: inline-block;
width: 0;
height: 0;
overflow: hidden;
}
}
}
.toggler {
display: flex;
align-items: center;
gap: 8px;
align-self: stretch;
> .togglerLabel {
user-select: none;
}
}
}
}

View file

@ -310,6 +310,8 @@ class FirefoxScripting {
class MLManager {
#enabled = null;
#ready = null;
eventBus = null;
constructor(options) {
@ -320,6 +322,10 @@ class MLManager {
return !!(await this.#enabled?.get(name));
}
isReady(name) {
return this.#ready?.has(name) ?? false;
}
deleteModel(service) {
return FirefoxCom.requestAsync("mlDelete", service);
}
@ -338,14 +344,33 @@ class MLManager {
this.altTextLearnMoreUrl = altTextLearnMoreUrl;
}
async toggleService(name, enabled) {
if (name !== "altText") {
return;
}
if (enabled) {
await this.#loadAltTextEngine(false);
} else {
this.#enabled?.delete(name);
this.#ready?.delete(name);
}
}
async #loadAltTextEngine(listenToProgress) {
if (this.#enabled?.has("altText")) {
// We already have a promise for the "altText" service.
return;
}
this.#ready ||= new Set();
const promise = FirefoxCom.requestAsync("loadAIEngine", {
service: "moz-image-to-text",
listenToProgress,
}).then(ok => {
if (ok) {
this.#ready.add("altText");
}
return ok;
});
(this.#enabled ||= new Map()).set("altText", promise);
if (listenToProgress) {

View file

@ -56,9 +56,47 @@ class MLManager {
return null;
}
async guess() {
isReady(_name) {
return false;
}
guess(_data) {}
toggleService(_name, _enabled) {}
static getFakeMLManager(options) {
return new FakeMLManager(options);
}
}
class FakeMLManager {
constructor({ enableGuessAltText }) {
this.enableGuessAltText = enableGuessAltText;
}
async isEnabledFor(_name) {
return this.enableGuessAltText;
}
async deleteModel(_service) {
return null;
}
isReady(_name) {
return true;
}
guess({ request: { data } }) {
return new Promise(resolve => {
setTimeout(() => {
resolve(data ? { output: "Fake alt text" } : { error: true });
}, 3000);
});
}
toggleService(_name, enabled) {
this.enableGuessAltText = enabled;
}
}
export { ExternalServices, initCom, MLManager, Preferences };

View file

@ -0,0 +1,3 @@
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.49073 1.3015L3.30873 2.1505C3.29349 2.22246 3.25769 2.28844 3.20568 2.34045C3.15368 2.39246 3.08769 2.42826 3.01573 2.4435L2.16673 2.6255C1.76473 2.7125 1.76473 3.2865 2.16673 3.3725L3.01573 3.5555C3.08769 3.57074 3.15368 3.60654 3.20568 3.65855C3.25769 3.71056 3.29349 3.77654 3.30873 3.8485L3.49073 4.6975C3.57773 5.0995 4.15173 5.0995 4.23773 4.6975L4.42073 3.8485C4.43598 3.77654 4.47177 3.71056 4.52378 3.65855C4.57579 3.60654 4.64178 3.57074 4.71373 3.5555L5.56173 3.3725C5.96373 3.2855 5.96373 2.7115 5.56173 2.6255L4.71273 2.4435C4.64083 2.42814 4.57491 2.3923 4.52292 2.34031C4.47093 2.28832 4.43509 2.2224 4.41973 2.1505L4.23773 1.3015C4.15073 0.8995 3.57673 0.8995 3.49073 1.3015ZM10.8647 13.9995C10.4853 14.0056 10.1158 13.8782 9.82067 13.6397C9.52553 13.4013 9.32347 13.0667 9.24973 12.6945L8.89273 11.0275C8.83676 10.7687 8.70738 10.5316 8.52009 10.3445C8.3328 10.1574 8.09554 10.0282 7.83673 9.9725L6.16973 9.6155C5.38873 9.4465 4.86473 8.7975 4.86473 7.9995C4.86473 7.2015 5.38873 6.5525 6.16973 6.3845L7.83673 6.0275C8.09551 5.97135 8.33267 5.84193 8.51992 5.65468C8.70716 5.46744 8.83658 5.23028 8.89273 4.9715L9.25073 3.3045C9.41773 2.5235 10.0667 1.9995 10.8647 1.9995C11.6627 1.9995 12.3117 2.5235 12.4797 3.3045L12.8367 4.9715C12.9507 5.4995 13.3647 5.9135 13.8927 6.0265L15.5597 6.3835C16.3407 6.5525 16.8647 7.2015 16.8647 7.9995C16.8647 8.7975 16.3407 9.4465 15.5597 9.6145L13.8927 9.9715C13.6337 10.0275 13.3963 10.157 13.209 10.3445C13.0217 10.5319 12.8925 10.7694 12.8367 11.0285L12.4787 12.6945C12.4054 13.0667 12.2036 13.4014 11.9086 13.6399C11.6135 13.8784 11.2441 14.0057 10.8647 13.9995ZM10.8647 3.2495C10.7667 3.2495 10.5337 3.2795 10.4727 3.5655L10.1147 5.2335C10.0081 5.72777 9.76116 6.18082 9.40361 6.53837C9.04606 6.89593 8.59301 7.14283 8.09873 7.2495L6.43173 7.6065C6.14573 7.6685 6.11473 7.9015 6.11473 7.9995C6.11473 8.0975 6.14573 8.3305 6.43173 8.3925L8.09873 8.7495C8.59301 8.85617 9.04606 9.10307 9.40361 9.46062C9.76116 9.81817 10.0081 10.2712 10.1147 10.7655L10.4727 12.4335C10.5337 12.7195 10.7667 12.7495 10.8647 12.7495C10.9627 12.7495 11.1957 12.7195 11.2567 12.4335L11.6147 10.7665C11.7212 10.272 11.9681 9.81878 12.3256 9.46103C12.6832 9.10329 13.1363 8.85624 13.6307 8.7495L15.2977 8.3925C15.5837 8.3305 15.6147 8.0975 15.6147 7.9995C15.6147 7.9015 15.5837 7.6685 15.2977 7.6065L13.6307 7.2495C13.1365 7.14283 12.6834 6.89593 12.3259 6.53837C11.9683 6.18082 11.7214 5.72777 11.6147 5.2335L11.2567 3.5655C11.1957 3.2795 10.9627 3.2495 10.8647 3.2495ZM3.30873 12.1505L3.49073 11.3015C3.57673 10.8995 4.15073 10.8995 4.23773 11.3015L4.41973 12.1505C4.43509 12.2224 4.47093 12.2883 4.52292 12.3403C4.57491 12.3923 4.64083 12.4281 4.71273 12.4435L5.56173 12.6255C5.96373 12.7115 5.96373 13.2855 5.56173 13.3725L4.71273 13.5545C4.64083 13.5699 4.57491 13.6057 4.52292 13.6577C4.47093 13.7097 4.43509 13.7756 4.41973 13.8475L4.23773 14.6965C4.15173 15.0985 3.57773 15.0985 3.49073 14.6965L3.30873 13.8475C3.29337 13.7756 3.25754 13.7097 3.20555 13.6577C3.15356 13.6057 3.08764 13.5699 3.01573 13.5545L2.16673 13.3725C1.76473 13.2865 1.76473 12.7125 2.16673 12.6255L3.01573 12.4435C3.08769 12.4283 3.15368 12.3925 3.20568 12.3405C3.25769 12.2884 3.29349 12.2225 3.30873 12.1505Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View file

@ -0,0 +1,16 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg id="loading-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" width="12" height="12">
<style>
@keyframes loadingSVGRotate {
from { rotate: 0; } to { rotate: 360deg }
}
#loading-svg {
animation: loadingSVGRotate 1.2s linear infinite;
transform-origin: 50% 50%;
}
</style>
<path d="M8.9 3.8c-.2-.2-.1-.5.1-.7.2-.1.6-.1.7.2.5.7.8 1.6.8 2.5 0 2.5-2 4.5-4.5 4.5l0 1.5c0 .2-.2.3-.3.1l-2-1.9 0-.4 1.9-1.9c.2-.2.4-.1.4.1l0 1.5c1.9 0 3.5-1.6 3.5-3.5 0-.7-.2-1.4-.6-2z"/>
<path d="M3.1 8.2c.2.2.1.5-.1.7-.2.1-.6.1-.7-.2-.5-.7-.8-1.6-.8-2.5 0-2.5 2-4.5 4.5-4.5L6 .2c0-.2.2-.3.3-.1l2 1.9 0 .4-2 2c-.1.1-.3 0-.3-.2l0-1.5c-1.9 0-3.5 1.6-3.5 3.5 0 .7.2 1.4.6 2z"/>
</svg>

After

Width:  |  Height:  |  Size: 926 B

View file

@ -0,0 +1,3 @@
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.78182 2.63903C8.58882 2.28803 8.25782 2.25003 8.12482 2.25003C7.99019 2.24847 7.85771 2.28393 7.74185 2.35253C7.62599 2.42113 7.5312 2.52023 7.46782 2.63903L1.97082 12.639C1.90673 12.7528 1.87406 12.8816 1.87617 13.0122C1.87828 13.1427 1.91509 13.2704 1.98282 13.382C2.04798 13.4951 2.14207 13.5888 2.25543 13.6535C2.36879 13.7182 2.49732 13.7515 2.62782 13.75H13.6218C13.7523 13.7515 13.8809 13.7182 13.9942 13.6535C14.1076 13.5888 14.2017 13.4951 14.2668 13.382C14.3346 13.2704 14.3714 13.1427 14.3735 13.0122C14.3756 12.8816 14.3429 12.7528 14.2788 12.639L8.78182 2.63903ZM6.37282 2.03703C6.75182 1.34603 7.43882 1.00003 8.12482 1.00003C8.48341 0.997985 8.83583 1.09326 9.14454 1.2757C9.45325 1.45814 9.70668 1.72092 9.87782 2.03603L15.3748 12.036C16.1078 13.369 15.1438 15 13.6228 15H2.62782C1.10682 15 0.141823 13.37 0.875823 12.037L6.37282 2.03703ZM8.74982 9.06203C8.74982 9.22779 8.68397 9.38676 8.56676 9.50397C8.44955 9.62118 8.29058 9.68703 8.12482 9.68703C7.95906 9.68703 7.80009 9.62118 7.68288 9.50397C7.56566 9.38676 7.49982 9.22779 7.49982 9.06203V5.62503C7.49982 5.45927 7.56566 5.3003 7.68288 5.18309C7.80009 5.06588 7.95906 5.00003 8.12482 5.00003C8.29058 5.00003 8.44955 5.06588 8.56676 5.18309C8.68397 5.3003 8.74982 5.45927 8.74982 5.62503V9.06203ZM7.74982 12L7.49982 11.75V11L7.74982 10.75H8.49982L8.74982 11V11.75L8.49982 12H7.74982Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.85822 8.84922L4.85322 11.8542C4.75891 11.9453 4.63261 11.9957 4.50151 11.9946C4.37042 11.9934 4.24501 11.9408 4.15231 11.8481C4.0596 11.7554 4.00702 11.63 4.00588 11.4989C4.00474 11.3678 4.05514 11.2415 4.14622 11.1472L7.15122 8.14222V7.85922L4.14622 4.85322C4.05514 4.75891 4.00474 4.63261 4.00588 4.50151C4.00702 4.37042 4.0596 4.24501 4.15231 4.15231C4.24501 4.0596 4.37042 4.00702 4.50151 4.00588C4.63261 4.00474 4.75891 4.05514 4.85322 4.14622L7.85822 7.15122H8.14122L11.1462 4.14622C11.2405 4.05514 11.3668 4.00474 11.4979 4.00588C11.629 4.00702 11.7544 4.0596 11.8471 4.15231C11.9398 4.24501 11.9924 4.37042 11.9936 4.50151C11.9947 4.63261 11.9443 4.75891 11.8532 4.85322L8.84822 7.85922V8.14222L11.8532 11.1472C11.9443 11.2415 11.9947 11.3678 11.9936 11.4989C11.9924 11.63 11.9398 11.7554 11.8471 11.8481C11.7544 11.9408 11.629 11.9934 11.4979 11.9946C11.3668 11.9957 11.2405 11.9453 11.1462 11.8542L8.14122 8.84922L8.14222 8.85022L7.85822 8.84922Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.8748 12.037L9.37782 2.037C8.99682 1.346 8.31082 1 7.62482 1C6.93882 1 6.25282 1.346 5.87282 2.037L0.375823 12.037C-0.358177 13.37 0.606823 15 2.12782 15H13.1228C14.6428 15 15.6078 13.37 14.8748 12.037ZM8.24982 11.75L7.99982 12H7.24982L6.99982 11.75V11L7.24982 10.75H7.99982L8.24982 11V11.75ZM8.24982 9.062C8.24982 9.22776 8.18398 9.38673 8.06677 9.50394C7.94955 9.62115 7.79058 9.687 7.62482 9.687C7.45906 9.687 7.30009 9.62115 7.18288 9.50394C7.06567 9.38673 6.99982 9.22776 6.99982 9.062V5.625C6.99982 5.45924 7.06567 5.30027 7.18288 5.18306C7.30009 5.06585 7.45906 5 7.62482 5C7.79058 5 7.94955 5.06585 8.06677 5.18306C8.18398 5.30027 8.24982 5.45924 8.24982 5.625V9.062Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 811 B

399
web/new_alt_text_manager.js Normal file
View file

@ -0,0 +1,399 @@
/* Copyright 2024 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.
*/
class NewAltTextManager {
#boundCancel = this.#cancel.bind(this);
#createAutomaticallyButton;
#currentEditor = null;
#cancelButton;
#descriptionContainer;
#dialog;
#disclaimer;
#firstTime = false;
#guessedAltText;
#isEditing = null;
#imagePreview;
#imageData;
#isAILoading = false;
#wasAILoading = false;
#learnMore;
#notNowButton;
#overlayManager;
#textarea;
#title;
#uiManager;
#previousAltText = null;
#telemetryData = null;
constructor(
{
descriptionContainer,
dialog,
imagePreview,
cancelButton,
disclaimer,
notNowButton,
saveButton,
textarea,
learnMore,
errorCloseButton,
createAutomaticallyButton,
title,
},
overlayManager
) {
this.#cancelButton = cancelButton;
this.#createAutomaticallyButton = createAutomaticallyButton;
this.#descriptionContainer = descriptionContainer;
this.#dialog = dialog;
this.#disclaimer = disclaimer;
this.#notNowButton = notNowButton;
this.#imagePreview = imagePreview;
this.#textarea = textarea;
this.#learnMore = learnMore;
this.#title = title;
this.#overlayManager = overlayManager;
dialog.addEventListener("close", this.#close.bind(this));
dialog.addEventListener("contextmenu", event => {
if (event.target !== this.#textarea) {
event.preventDefault();
}
});
cancelButton.addEventListener("click", this.#boundCancel);
notNowButton.addEventListener("click", this.#boundCancel);
saveButton.addEventListener("click", this.#save.bind(this));
errorCloseButton.addEventListener("click", () => {
this.#toggleError(false);
});
createAutomaticallyButton.addEventListener("click", async () => {
const checked =
createAutomaticallyButton.getAttribute("aria-pressed") !== "true";
if (this.#uiManager) {
this.#uiManager.setPreference("enableGuessAltText", checked);
await this.#uiManager.mlManager.toggleService("altText", checked);
}
this.#toggleGuessAltText(checked, /* isInitial = */ false);
});
textarea.addEventListener("focus", () => {
this.#wasAILoading = this.#isAILoading;
this.#toggleLoading(false);
});
textarea.addEventListener("blur", () => {
if (textarea.value) {
return;
}
this.#toggleLoading(this.#wasAILoading);
});
textarea.addEventListener("input", () => {
this.#toggleTitle();
this.#toggleDisclaimer();
});
this.#overlayManager.register(dialog);
}
#toggleLoading(value) {
if (!this.#uiManager || this.#isAILoading === value) {
return;
}
this.#isAILoading = value;
this.#descriptionContainer.classList.toggle("loading", value);
}
#toggleError(value) {
if (!this.#uiManager) {
return;
}
this.#dialog.classList.toggle("error", value);
}
#toggleTitle() {
const isEditing = this.#isAILoading || !!this.#textarea.value;
if (this.#isEditing === isEditing) {
return;
}
this.#isEditing = isEditing;
this.#title.setAttribute(
"data-l10n-id",
`pdfjs-editor-new-alt-text-dialog-${isEditing ? "edit" : "add"}-label`
);
}
async #toggleGuessAltText(value, isInitial = false) {
if (!this.#uiManager) {
return;
}
this.#dialog.classList.toggle("aiDisabled", !value);
this.#createAutomaticallyButton.setAttribute("aria-pressed", value);
if (value) {
const { altTextLearnMoreUrl } = this.#uiManager.mlManager;
if (altTextLearnMoreUrl) {
this.#learnMore.href = altTextLearnMoreUrl;
}
this.#mlGuessAltText(isInitial);
} else {
this.#toggleLoading(false);
this.#isAILoading = false;
this.#toggleTitle();
this.#toggleDisclaimer();
}
}
#toggleNotNow() {
this.#notNowButton.classList.toggle("hidden", !this.#firstTime);
this.#cancelButton.classList.toggle("hidden", this.#firstTime);
}
#toggleAI(value) {
this.#dialog.classList.toggle("noAi", !value);
this.#toggleTitle();
}
#toggleDisclaimer(value = null) {
if (!this.#uiManager) {
return;
}
const hidden =
value === null
? !this.#guessedAltText || this.#guessedAltText !== this.#textarea.value
: !value;
this.#disclaimer.classList.toggle("hidden", hidden);
}
async #mlGuessAltText(isInitial) {
if (this.#isAILoading) {
// We're still loading the previous guess.
return;
}
if (this.#textarea.value) {
// The user has already set an alt text.
return;
}
if (isInitial && this.#previousAltText !== null) {
// The user has already set an alt text (empty or not).
return;
}
this.#guessedAltText = this.#currentEditor.guessedAltText;
if (this.#previousAltText === null && this.#guessedAltText) {
// We have a guessed alt text and the user didn't change it.
this.#addAltText(this.#guessedAltText);
this.#toggleDisclaimer();
this.#toggleTitle();
return;
}
this.#toggleLoading(true);
this.#toggleTitle();
this.#toggleDisclaimer(true);
let hasError = false;
try {
const { width, height, data } = this.#imageData;
// Take a reference on the current editor, as it can be set to null (if
// the dialog is closed before the end of the guess).
// But in case we've an alt-text, we want to set it on the editor.
const editor = this.#currentEditor;
// When calling #mlGuessAltText we don't wait for it, so we must take care
// that the alt text dialog can have been closed before the response is.
const response = await this.#uiManager.mlGuess({
service: "moz-image-to-text",
request: {
data,
width,
height,
channels: data.length / (width * height),
},
});
if (!response || response.error || !response.output) {
throw new Error("No valid response from the AI service.");
}
const altText = (this.#guessedAltText = response.output);
await editor.setGuessedAltText(altText);
this.#wasAILoading = this.#isAILoading;
if (this.#isAILoading) {
this.#addAltText(altText);
}
} catch (e) {
console.error(e);
hasError = true;
}
this.#toggleLoading(false);
if (hasError && this.#uiManager) {
this.#toggleError(true);
this.#toggleTitle();
this.#toggleDisclaimer();
}
}
#addAltText(altText) {
if (!this.#uiManager || this.#textarea.value) {
return;
}
this.#textarea.value = altText;
}
async editAltText(uiManager, editor, firstTime) {
if (this.#currentEditor || !editor) {
return;
}
if (firstTime && editor.hasAltTextData()) {
editor.altTextFinish();
return;
}
this.#firstTime = firstTime;
let { mlManager } = uiManager;
if (!mlManager?.isReady("altText")) {
mlManager = null;
}
const isAltTextEnabledPromise = mlManager?.isEnabledFor("altText");
this.#currentEditor = editor;
this.#uiManager = uiManager;
this.#uiManager.removeEditListeners();
({ altText: this.#previousAltText } = editor.altTextData);
this.#textarea.value = this.#previousAltText ?? "";
// TODO: get this value from Firefox
// (https://bugzilla.mozilla.org/show_bug.cgi?id=1908184)
const AI_MAX_IMAGE_DIMENSION = 224;
// The max dimension of the preview in the dialog is 180px, so we keep 224px
// and rescale it thanks to css.
let canvas;
if (mlManager) {
({ canvas, imageData: this.#imageData } = editor.copyCanvas(
AI_MAX_IMAGE_DIMENSION,
/* createImageData = */ true
));
this.#toggleGuessAltText(
await isAltTextEnabledPromise,
/* isInitial = */ true
);
} else {
({ canvas } = editor.copyCanvas(
AI_MAX_IMAGE_DIMENSION,
/* createImageData = */ false
));
}
canvas.setAttribute("role", "presentation");
this.#imagePreview.append(canvas);
this.#toggleNotNow();
this.#toggleAI(!!mlManager);
this.#toggleError(false);
try {
await this.#overlayManager.open(this.#dialog);
} catch (ex) {
this.#close();
throw ex;
}
}
#cancel() {
this.#currentEditor.altTextData = {
cancel: true,
};
this.#finish();
}
#finish() {
if (this.#overlayManager.active === this.#dialog) {
this.#overlayManager.close(this.#dialog);
}
}
#close() {
const canvas = this.#imagePreview.firstChild;
canvas.remove();
canvas.width = canvas.height = 0;
this.#imageData = null;
this.#currentEditor._reportTelemetry(
this.#telemetryData || {
action: "alt_text_cancel",
}
);
this.#telemetryData = null;
this.#toggleLoading(false);
this.#uiManager?.addEditListeners();
this.#currentEditor.altTextFinish();
this.#uiManager?.setSelected(this.#currentEditor);
this.#currentEditor = null;
this.#uiManager = null;
}
#save() {
const altText = this.#textarea.value.trim();
this.#currentEditor.altTextData = {
altText,
decorative: false,
};
this.#telemetryData = {
action: "alt_text_save",
alt_text_description: !!altText,
alt_text_edit:
!!this.#previousAltText && this.#previousAltText !== altText,
alt_text_decorative: false,
alt_text_altered:
this.#guessedAltText && this.#guessedAltText !== altText,
};
this.#finish();
}
destroy() {
this.#uiManager = null; // Avoid re-adding the edit listeners.
this.#finish();
}
}
export { NewAltTextManager };

View file

@ -15,6 +15,7 @@
const AltTextManager = null;
const AnnotationEditorParams = null;
const NewAltTextManager = null;
const PDFAttachmentViewer = null;
const PDFCursorTools = null;
const PDFDocumentProperties = null;
@ -29,6 +30,7 @@ const SecondaryToolbar = null;
export {
AltTextManager,
AnnotationEditorParams,
NewAltTextManager,
PDFAttachmentViewer,
PDFCursorTools,
PDFDocumentProperties,

View file

@ -68,6 +68,7 @@ See https://github.com/adobe-type-tools/cmap-resources
"web-annotation_editor_params": "./stubs-geckoview.js",
"web-download_manager": "./download_manager.js",
"web-external_services": "./genericcom.js",
"web-new_alt_text_manager": "./stubs-geckoview.js",
"web-null_l10n": "./genericl10n.js",
"web-pdf_attachment_viewer": "./stubs-geckoview.js",
"web-pdf_cursor_tools": "./stubs-geckoview.js",

View file

@ -71,6 +71,7 @@ See https://github.com/adobe-type-tools/cmap-resources
"web-annotation_editor_params": "./annotation_editor_params.js",
"web-download_manager": "./download_manager.js",
"web-external_services": "./genericcom.js",
"web-new_alt_text_manager": "./new_alt_text_manager.js",
"web-null_l10n": "./genericl10n.js",
"web-pdf_attachment_viewer": "./pdf_attachment_viewer.js",
"web-pdf_cursor_tools": "./pdf_cursor_tools.js",
@ -550,6 +551,48 @@ See https://github.com/adobe-type-tools/cmap-resources
</div>
</div>
</dialog>
<dialog class="dialog newAltText" id="newAltTextDialog" aria-labelledby="newAltTextTitle" aria-describedby="newAltTextDescription" tabindex="0">
<div id="newAltTextContainer" class="mainContainer">
<div class="title">
<span id="newAltTextTitle" data-l10n-id="pdfjs-editor-new-alt-text-dialog-edit-label" role="sectionhead" tabindex="0">Edit alt text (image description)</span>
</div>
<div id="mainContent">
<div id="descriptionAndSettings">
<div id="descriptionInstruction">
<div id="newAltTextDescriptionContainer">
<div class="altTextSpinner" role="status" aria-live="polite"></div>
<textarea id="newAltTextDescriptionTextarea" placeholder="Write your description here…" aria-labelledby="descriptionAreaLabel" data-l10n-id="pdfjs-editor-new-alt-text-textarea" tabindex="0"></textarea>
</div>
<span id="newAltTextDescription" role="note" data-l10n-id="pdfjs-editor-new-alt-text-description">Short description for people who cant see the image or when the image doesnt load.</span>
<div id="newAltTextDisclaimer" role="note"><span data-l10n-id="pdfjs-editor-new-alt-text-disclaimer">This alt text was created automatically.</span> <a href="https://support.mozilla.org/en-US/kb/pdf-alt-text" target="_blank" rel="noopener noreferrer" id="newAltTextLearnMore" data-l10n-id="pdfjs-editor-new-alt-text-disclaimer-learn-more-url" tabindex="0">Learn more</a></div>
</div>
<div id="newAltTextCreateAutomatically" class="toggler">
<button id="newAltTextCreateAutomaticallyButton" class="toggle-button" aria-pressed="true" tabindex="0"></button>
<label for="newAltTextCreateAutomaticallyButton" class="togglerLabel" data-l10n-id="pdfjs-editor-new-alt-text-create-automatically-button-label">Create alt text automatically</label>
</div>
<div id="newAltTextDownloadModel" class="hidden">
<span id="newAltTextDownloadModelDescription" data-l10n-id="pdfjs-editor-new-alt-text-ai-model-downloading-progress" data-l10n-args='{ "totalSize": 0, "downloadedSize": 0 }'>Downloading alt text AI model (0 of 0 MB)</span>
</div>
</div>
<div id="newAltTextImagePreview"></div>
</div>
<div id="newAltTextError" class="messageBar">
<div>
<div>
<span class="title" data-l10n-id="pdfjs-editor-new-alt-text-error-title">Couldnt create alt text automatically</span>
<span class="description" data-l10n-id="pdfjs-editor-new-alt-text-error-description">Please write your own alt text or try again later.</span>
</div>
<button id="newAltTextCloseButton" class="closeButton" tabindex="0" title="Close"><span data-l10n-id="pdfjs-editor-new-alt-text-error-close-button">Close</span></button>
</div>
</div>
<div id="newAltTextButtons" class="dialogButtonsGroup">
<button id="newAltTextCancel" type="button" class="secondaryButton hidden" tabindex="0"><span data-l10n-id="pdfjs-editor-alt-text-cancel-button">Cancel</span></button>
<button id="newAltTextNotNow" type="button" class="secondaryButton" tabindex="0"><span data-l10n-id="pdfjs-editor-new-alt-text-not-now-button">Not now</span></button>
<button id="newAltTextSave" type="button" class="primaryButton" tabindex="0"><span data-l10n-id="pdfjs-editor-alt-text-save-button">Save</span></button>
</div>
</div>
</dialog>
<!--#if !MOZCENTRAL-->
<dialog id="printServiceDialog" style="min-width: 200px;">
<div class="row">

View file

@ -163,6 +163,32 @@ function getViewerConfiguration() {
cancelButton: document.getElementById("altTextCancel"),
saveButton: document.getElementById("altTextSave"),
},
newAltTextDialog: {
dialog: document.getElementById("newAltTextDialog"),
title: document.getElementById("newAltTextTitle"),
descriptionContainer: document.getElementById(
"newAltTextDescriptionContainer"
),
textarea: document.getElementById("newAltTextDescriptionTextarea"),
disclaimer: document.getElementById("newAltTextDisclaimer"),
learnMore: document.getElementById("newAltTextLearnMore"),
imagePreview: document.getElementById("newAltTextImagePreview"),
createAutomatically: document.getElementById(
"newAltTextCreateAutomatically"
),
createAutomaticallyButton: document.getElementById(
"newAltTextCreateAutomaticallyButton"
),
downloadModel: document.getElementById("newAltTextDownloadModel"),
downloadModelDescription: document.getElementById(
"newAltTextDownloadModelDescription"
),
error: document.getElementById("newAltTextError"),
errorCloseButton: document.getElementById("newAltTextCloseButton"),
cancelButton: document.getElementById("newAltTextCancel"),
notNowButton: document.getElementById("newAltTextNotNow"),
saveButton: document.getElementById("newAltTextSave"),
},
annotationEditorParams: {
editorFreeTextFontSize: document.getElementById("editorFreeTextFontSize"),
editorFreeTextColor: document.getElementById("editorFreeTextColor"),