Skip to content

Commit

Permalink
Implement the new alt text flow (bug 1909604)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
calixteman committed Jul 29, 2024
1 parent 8378c40 commit ed22d93
Show file tree
Hide file tree
Showing 22 changed files with 1,365 additions and 90 deletions.
2 changes: 2 additions & 0 deletions gulpfile.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
];
Expand Down
49 changes: 49 additions & 0 deletions l10n/en-US/viewer.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -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 can’t see the image or when the image doesn’t 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 = Couldn’t 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 }
148 changes: 131 additions & 17 deletions src/display/editor/alt_text.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import { noContextMenu } from "../display_utils.js";

class AltText {
#altText = "";
#altText = null;

#altTextDecorative = false;

Expand All @@ -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) {
Expand All @@ -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";
Expand Down Expand Up @@ -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,
Expand All @@ -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();
}

Expand All @@ -121,25 +203,57 @@ class AltText {
this.#altTextButton?.remove();
this.#altTextButton = null;
this.#altTextTooltip = null;
this.#badge?.remove();
this.#badge = null;
}

async #setState() {
const button = this.#altTextButton;
if (!button) {
return;
}
if (!this.#altText && !this.#altTextDecorative) {
button.classList.remove("done");
this.#altTextTooltip?.remove();
return;

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);
});
}
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");
Expand Down
Loading

0 comments on commit ed22d93

Please sign in to comment.