mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-04-09 23:04:02 +02:00
And a specific view for inspecting font information and the text layer on top of the canvas.
252 lines
7.3 KiB
JavaScript
252 lines
7.3 KiB
JavaScript
/* Copyright 2026 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.
|
|
*/
|
|
|
|
const FONT_HIGHLIGHT_COLOR_KEY = "debugger.fontHighlightColor";
|
|
const DEFAULT_FONT_HIGHLIGHT_COLOR = "#0070c1";
|
|
|
|
// Maps MIME types to file extensions used when downloading fonts.
|
|
const MIMETYPE_TO_EXTENSION = new Map([
|
|
["font/opentype", "otf"],
|
|
["font/otf", "otf"],
|
|
["font/woff", "woff"],
|
|
["font/woff2", "woff2"],
|
|
["application/x-font-ttf", "ttf"],
|
|
["font/truetype", "ttf"],
|
|
["font/ttf", "ttf"],
|
|
["application/x-font-type1", "pfb"],
|
|
]);
|
|
|
|
// Maps MIME types reported by FontFaceObject to human-readable font format
|
|
// names.
|
|
const MIMETYPE_TO_FORMAT = new Map([
|
|
["font/opentype", "OpenType"],
|
|
["font/otf", "OpenType"],
|
|
["font/woff", "WOFF"],
|
|
["font/woff2", "WOFF2"],
|
|
["application/x-font-ttf", "TrueType"],
|
|
["font/truetype", "TrueType"],
|
|
["font/ttf", "TrueType"],
|
|
["application/x-font-type1", "Type1"],
|
|
]);
|
|
|
|
class FontView {
|
|
// Persistent map of all fonts seen since the document was opened,
|
|
// keyed by loadedName (= the PDF resource name / CSS font-family).
|
|
// Never cleared on page navigation so fonts cached in commonObjs
|
|
// (which only trigger fontAdded once per document) are always available.
|
|
#fontMap = new Map();
|
|
|
|
#container;
|
|
|
|
#list = (() => {
|
|
const ul = document.createElement("ul");
|
|
ul.className = "font-list";
|
|
return ul;
|
|
})();
|
|
|
|
#onSelect;
|
|
|
|
#selectedName = null;
|
|
|
|
#downloadBtn;
|
|
|
|
constructor(containerEl, { onSelect } = {}) {
|
|
this.#container = containerEl;
|
|
this.#onSelect = onSelect;
|
|
|
|
this.#container.append(this.#buildToolbar(), this.#list);
|
|
}
|
|
|
|
#buildToolbar() {
|
|
const toolbar = document.createElement("div");
|
|
toolbar.className = "font-toolbar";
|
|
|
|
// Color picker button + hidden input.
|
|
const colorInput = document.createElement("input");
|
|
colorInput.type = "color";
|
|
colorInput.hidden = true;
|
|
|
|
const colorSwatch = document.createElement("span");
|
|
colorSwatch.className = "font-color-swatch";
|
|
|
|
const colorBtn = document.createElement("button");
|
|
colorBtn.className = "font-color-button";
|
|
colorBtn.title = "Highlight color";
|
|
colorBtn.append(colorSwatch);
|
|
|
|
const applyColor = color => {
|
|
colorInput.value = color;
|
|
document.documentElement.style.setProperty(
|
|
"--font-highlight-color",
|
|
color
|
|
);
|
|
};
|
|
applyColor(
|
|
localStorage.getItem(FONT_HIGHLIGHT_COLOR_KEY) ??
|
|
DEFAULT_FONT_HIGHLIGHT_COLOR
|
|
);
|
|
|
|
colorBtn.addEventListener("click", () => colorInput.click());
|
|
colorInput.addEventListener("input", () => {
|
|
applyColor(colorInput.value);
|
|
localStorage.setItem(FONT_HIGHLIGHT_COLOR_KEY, colorInput.value);
|
|
});
|
|
|
|
// Download button — enabled only when a font with data is selected.
|
|
const downloadBtn = (this.#downloadBtn = document.createElement("button"));
|
|
downloadBtn.className = "font-download-button";
|
|
downloadBtn.title = "Download selected font";
|
|
downloadBtn.disabled = true;
|
|
downloadBtn.addEventListener("click", () => {
|
|
const font = this.#fontMap.get(this.#selectedName);
|
|
if (!font?.data) {
|
|
return;
|
|
}
|
|
const ext = MIMETYPE_TO_EXTENSION.get(font.mimetype) ?? "font";
|
|
const name = (font.name || font.loadedName).replaceAll(
|
|
/[^a-z0-9_-]/gi,
|
|
"_"
|
|
);
|
|
const blob = new Blob([font.data], { type: font.mimetype });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = `${name}.${ext}`;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
});
|
|
|
|
toolbar.append(colorBtn, colorInput, downloadBtn);
|
|
return toolbar;
|
|
}
|
|
|
|
get element() {
|
|
return this.#container;
|
|
}
|
|
|
|
// Called by FontInspector.fontAdded whenever a font face is bound.
|
|
fontAdded(font) {
|
|
this.#fontMap.set(font.loadedName, font);
|
|
}
|
|
|
|
// Show the subset of known fonts that appear in the given op list.
|
|
// Uses setFont ops to determine which fonts are actually used on the page.
|
|
showForOpList({ fnArray, argsArray }, OPS) {
|
|
const usedNames = new Set();
|
|
for (let i = 0, len = fnArray.length; i < len; i++) {
|
|
if (fnArray[i] === OPS.setFont) {
|
|
usedNames.add(argsArray[i][0]);
|
|
}
|
|
}
|
|
|
|
const fonts = [];
|
|
for (const name of usedNames) {
|
|
const font = this.#fontMap.get(name);
|
|
if (font) {
|
|
fonts.push(font);
|
|
}
|
|
}
|
|
this.#render(fonts);
|
|
}
|
|
|
|
clear() {
|
|
this.#selectedName = null;
|
|
this.#downloadBtn.disabled = true;
|
|
this.#list.replaceChildren();
|
|
}
|
|
|
|
#render(fonts) {
|
|
if (fonts.length === 0) {
|
|
const li = document.createElement("li");
|
|
li.className = "font-empty";
|
|
li.textContent = "No fonts on this page.";
|
|
this.#list.replaceChildren(li);
|
|
return;
|
|
}
|
|
|
|
const frag = document.createDocumentFragment();
|
|
for (const font of fonts) {
|
|
const li = document.createElement("li");
|
|
li.className = "font-item";
|
|
li.dataset.loadedName = font.loadedName;
|
|
if (font.loadedName === this.#selectedName) {
|
|
li.classList.add("selected");
|
|
}
|
|
li.addEventListener("click", () => {
|
|
const next =
|
|
font.loadedName === this.#selectedName ? null : font.loadedName;
|
|
this.#selectedName = next;
|
|
for (const item of this.#list.querySelectorAll(".font-item")) {
|
|
item.classList.toggle("selected", item.dataset.loadedName === next);
|
|
}
|
|
const selectedFont = next ? this.#fontMap.get(next) : null;
|
|
this.#downloadBtn.disabled = !selectedFont?.data;
|
|
this.#onSelect?.(next);
|
|
});
|
|
|
|
const nameEl = document.createElement("div");
|
|
nameEl.className = "font-name";
|
|
nameEl.textContent = font.name || font.loadedName;
|
|
li.append(nameEl);
|
|
|
|
const tags = [];
|
|
const fmt = MIMETYPE_TO_FORMAT.get(font.mimetype);
|
|
if (fmt) {
|
|
tags.push(fmt);
|
|
}
|
|
if (font.isType3Font) {
|
|
tags.push("Type3");
|
|
}
|
|
if (font.bold) {
|
|
tags.push("Bold");
|
|
}
|
|
if (font.italic) {
|
|
tags.push("Italic");
|
|
}
|
|
if (font.vertical) {
|
|
tags.push("Vertical");
|
|
}
|
|
if (font.disableFontFace) {
|
|
tags.push("System");
|
|
}
|
|
if (font.missingFile) {
|
|
tags.push("Missing");
|
|
}
|
|
|
|
if (tags.length) {
|
|
const tagsEl = document.createElement("div");
|
|
tagsEl.className = "font-tags";
|
|
for (const tag of tags) {
|
|
const span = document.createElement("span");
|
|
span.className = "font-tag";
|
|
span.textContent = tag;
|
|
tagsEl.append(span);
|
|
}
|
|
li.append(tagsEl);
|
|
}
|
|
|
|
const loadedEl = document.createElement("div");
|
|
loadedEl.className = "font-loaded-name";
|
|
loadedEl.textContent = font.loadedName;
|
|
li.append(loadedEl);
|
|
|
|
frag.append(li);
|
|
}
|
|
this.#list.replaceChildren(frag);
|
|
}
|
|
}
|
|
|
|
export { FontView };
|