diff --git a/addon/chrome/content/fonts/Roboto-Black.ttf b/addon/chrome/content/fonts/Roboto-Black.ttf new file mode 100644 index 0000000..58fa175 Binary files /dev/null and b/addon/chrome/content/fonts/Roboto-Black.ttf differ diff --git a/addon/chrome/content/fonts/Roboto-Bold.ttf b/addon/chrome/content/fonts/Roboto-Bold.ttf new file mode 100644 index 0000000..e64db79 Binary files /dev/null and b/addon/chrome/content/fonts/Roboto-Bold.ttf differ diff --git a/addon/chrome/content/fonts/Roboto-Light.ttf b/addon/chrome/content/fonts/Roboto-Light.ttf new file mode 100644 index 0000000..a7e0284 Binary files /dev/null and b/addon/chrome/content/fonts/Roboto-Light.ttf differ diff --git a/addon/chrome/content/fonts/Roboto-Medium.ttf b/addon/chrome/content/fonts/Roboto-Medium.ttf new file mode 100644 index 0000000..0707e15 Binary files /dev/null and b/addon/chrome/content/fonts/Roboto-Medium.ttf differ diff --git a/addon/chrome/content/fonts/Roboto-Regular.ttf b/addon/chrome/content/fonts/Roboto-Regular.ttf new file mode 100644 index 0000000..2d116d9 Binary files /dev/null and b/addon/chrome/content/fonts/Roboto-Regular.ttf differ diff --git a/addon/chrome/content/fonts/Roboto-Thin.ttf b/addon/chrome/content/fonts/Roboto-Thin.ttf new file mode 100644 index 0000000..ab68508 Binary files /dev/null and b/addon/chrome/content/fonts/Roboto-Thin.ttf differ diff --git a/addon/chrome/content/preferences.css b/addon/chrome/content/preferences.css new file mode 100644 index 0000000..ba377a0 --- /dev/null +++ b/addon/chrome/content/preferences.css @@ -0,0 +1,55 @@ +@font-face { + font-family: "Roboto"; + src: url("fonts/Roboto-Regular.ttf") format("truetype"); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: "Roboto"; + src: url("fonts/Roboto-Bold.ttf") format("truetype"); + font-weight: 700; + font-style: normal; +} + +@font-face { + font-family: "Roboto"; + src: url("fonts/Roboto-Black.ttf") format("truetype"); + font-weight: 900; + font-style: normal; +} + +@font-face { + font-family: "Roboto"; + src: url("fonts/Roboto-Light.ttf") format("truetype"); + font-weight: 300; + font-style: normal; +} + +@font-face { + font-family: "Roboto"; + src: url("fonts/Roboto-Thin.ttf") format("truetype"); + font-weight: 100; + font-style: normal; +} + +@font-face { + font-family: "Roboto"; + src: url("fonts/Roboto-Medium.ttf") format("truetype"); + font-weight: 500; + font-style: normal; +} + +#__addonRef__-parsingPreviewWarning { + padding-top: 8px; +} + +#__addonRef__-parsingPreviewWarning img { + width: 20px; + height: 20px; + padding: 6px; +} + +#__addonRef__-pluginInfo { + padding-top: 28px; +} diff --git a/addon/chrome/content/preferences.xhtml b/addon/chrome/content/preferences.xhtml index 9d32ca8..02fa2a7 100644 --- a/addon/chrome/content/preferences.xhtml +++ b/addon/chrome/content/preferences.xhtml @@ -29,22 +29,50 @@ > - + + + + + + + + + + + Zotero is a free, easy-to-use tool to help you collect, organize, + annotate, cite, and share research. + + + + + - + { + init(); + }, 1000); +} + +main(); + +let initialized = false; + +function init() { + if (initialized) { + return; + } + const parsingContainer = document.querySelector( + `#${config.addonRef}-parsing`, + ); + if (!parsingContainer) { + return; + } + initialized = true; + document + .querySelector("#prefs-navigation") + ?.removeEventListener("select", init); + + parsingContainer.addEventListener("input", (event) => { + updatePreview(); + }); + updatePreview(); +} + +const PREVIEW_STRING = + "Zotero is a free, easy-to-use tool to help you collect, organize, annotate, cite, and share research."; +const BIONIC_GROUPS = [ + { startIdx: 0, endIdx: 3, isBold: true }, + { startIdx: 3, endIdx: 7, isBold: false }, + { startIdx: 7, endIdx: 8, isBold: true }, + { startIdx: 8, endIdx: 10, isBold: false }, + { startIdx: 10, endIdx: 11, isBold: true }, + { startIdx: 11, endIdx: 12, isBold: false }, + { startIdx: 12, endIdx: 14, isBold: true }, + { startIdx: 14, endIdx: 18, isBold: false }, + { startIdx: 18, endIdx: 20, isBold: true }, + { startIdx: 20, endIdx: 23, isBold: false }, + { startIdx: 23, endIdx: 24, isBold: true }, + { startIdx: 24, endIdx: 26, isBold: false }, + { startIdx: 26, endIdx: 27, isBold: true }, + { startIdx: 27, endIdx: 30, isBold: false }, + { startIdx: 30, endIdx: 32, isBold: true }, + { startIdx: 32, endIdx: 35, isBold: false }, + { startIdx: 35, endIdx: 36, isBold: true }, + { startIdx: 36, endIdx: 38, isBold: false }, + { startIdx: 38, endIdx: 40, isBold: true }, + { startIdx: 40, endIdx: 43, isBold: false }, + { startIdx: 43, endIdx: 44, isBold: true }, + { startIdx: 44, endIdx: 47, isBold: false }, + { startIdx: 47, endIdx: 51, isBold: true }, + { startIdx: 51, endIdx: 56, isBold: false }, + { startIdx: 56, endIdx: 60, isBold: true }, + { startIdx: 60, endIdx: 66, isBold: false }, + { startIdx: 66, endIdx: 70, isBold: true }, + { startIdx: 70, endIdx: 76, isBold: false }, + { startIdx: 76, endIdx: 78, isBold: true }, + { startIdx: 78, endIdx: 82, isBold: false }, + { startIdx: 82, endIdx: 83, isBold: true }, + { startIdx: 83, endIdx: 86, isBold: false }, + { startIdx: 86, endIdx: 89, isBold: true }, + { startIdx: 89, endIdx: 92, isBold: false }, + { startIdx: 92, endIdx: 96, isBold: true }, + { startIdx: 96, endIdx: 101, isBold: false }, +]; + +function updatePreview() { + const previewContainer = document.querySelector( + `#${config.addonRef}-parsingPreview`, + ); + + if (!previewContainer) { + return; + } + + const fontData = computeFont({ + alpha: 1, + font: 'normal normal 14px "Roboto", sans-serif', + opacityContrast: + parseInt( + ( + document.querySelector( + `#${config.addonRef}-opacityContrast`, + ) as HTMLInputElement + )?.value, + ) || 1, + weightContrast: + parseInt( + ( + document.querySelector( + `#${config.addonRef}-weightContrast`, + ) as HTMLInputElement + )?.value, + ) || 3, + weightOffset: + parseInt( + ( + document.querySelector( + `#${config.addonRef}-weightOffset`, + ) as HTMLInputElement + )?.value, + ) || 0, + }); + + const parsingOffset = + parseInt( + ( + document.querySelector( + `#${config.addonRef}-parsingOffset`, + ) as HTMLInputElement + )?.value, + ) || 0; + + previewContainer.innerHTML = ""; + + for (let i = 0; i < BIONIC_GROUPS.length; i++) { + const group = BIONIC_GROUPS[i]; + let startIdx = group.startIdx; + let endIdx = group.endIdx; + if (parsingOffset) { + const nextGroup = BIONIC_GROUPS[i + 1]; + const prevGroup = BIONIC_GROUPS[i - 1]; + if (group.isBold && nextGroup) { + endIdx = Math.max( + // Can grow until the next group ends + Math.min(endIdx + parsingOffset, nextGroup.endIdx), + startIdx + 1, + ); + } else if (!group.isBold && prevGroup) { + startIdx = Math.min( + // Can shrink until the previous group starts + Math.max(startIdx + parsingOffset, prevGroup.startIdx + 1), + endIdx, + ); + } + } + + if (startIdx >= endIdx) { + continue; + } + + const span = document.createElement("span"); + span.textContent = PREVIEW_STRING.slice(startIdx, endIdx); + span.style.font = group.isBold ? fontData.bold.font : fontData.light.font; + if (!group.isBold) { + span.style.opacity = String(fontData.light.alpha); + } + previewContainer.appendChild(span); + } +} diff --git a/src/reader/pdf.ts b/src/reader/pdf.ts index f5f839b..a3088ca 100644 --- a/src/reader/pdf.ts +++ b/src/reader/pdf.ts @@ -1,5 +1,7 @@ /* eslint-disable no-restricted-globals */ import { wait } from "zotero-plugin-toolkit"; +import { computeFont } from "../utils/font"; + import type { PDFPage, CanvasGraphics, @@ -95,43 +97,30 @@ function patchCanvasGraphicsShowText( if (!window.__BIONIC_READER_ENABLED) { return original_showText.apply(this, [glyphs]); } - const contrast = window.__BIONIC_PARSING_CONTRAST || 1; - const contrastRatio = 1 - (contrast - 1) * 0.15; - const savedFont = this.ctx.font; - const savedOpacity = this.ctx.globalAlpha; - // Compute font weight - let nextBold = ""; - if (savedFont.includes("black") || savedFont.includes("900")) { - // Cannot be bolder than 900 - nextBold = ""; - } else if (savedFont.includes("bold")) { - nextBold = "black"; - } else { - nextBold = "bold"; - } - let nextLight = ""; - if (!nextBold) { - nextLight = "light"; - } + const opacityContrast = window.__BIONIC_OPACITY_CONTRAST || 1; - const italic = savedFont.includes("italic") ? "italic" : "normal"; + const weightContrast = window.__BIONIC_WEIGHT_CONTRAST || 1; + const weightOffset = window.__BIONIC_WEIGHT_OFFSET || 0; - const fontParams = savedFont.split(" "); - _log("Showing text", glyphs, savedFont); - const fontSizeIndex = fontParams.findIndex((font) => font.includes("px")); + const savedFont = this.ctx.font; + const savedOpacity = this.ctx.globalAlpha; - const baseFont = fontParams.slice(fontSizeIndex).join(" "); - const bolderFont = `${italic} ${nextBold || "normal"} ${baseFont}`; - const lighterFont = `${italic} ${nextLight || "normal"} ${baseFont}`; + const { bold, light } = computeFont({ + font: savedFont, + alpha: savedOpacity, + opacityContrast, + weightContrast, + weightOffset, + }); const newGlyphData = computeBionicGlyphs(glyphs); - for (const { glyphs: newG, bold } of newGlyphData) { - this.ctx.font = bold ? bolderFont : lighterFont; + for (const { glyphs: newG, isBold } of newGlyphData) { + this.ctx.font = isBold ? bold.font : light.font; // If use greater contrast is enabled, set text opacity to less than 1 - if (contrast > 1 && !bold) { - this.ctx.globalAlpha = savedOpacity * contrastRatio; + if (opacityContrast > 1 && !isBold) { + this.ctx.globalAlpha = bold.alpha; } original_showText.apply(this, [newG]); this.ctx.font = savedFont; @@ -152,7 +141,7 @@ function computeBionicGlyphs(glyphs: Glyph[]) { let word = ""; const newGlyphData: { glyphs: Glyph[]; - bold: boolean; + isBold: boolean; }[] = []; const parsingOffset = window.__BIONIC_PARSING_OFFSET || 0; @@ -204,7 +193,7 @@ function computeBionicGlyphs(glyphs: Glyph[]) { // If the word has not started and we encounter a space, the word has not started newGlyphData.push({ glyphs: glyphs.slice(i, i + 1), - bold: false, + isBold: false, }); continue; } @@ -225,7 +214,7 @@ function computeBionicGlyphs(glyphs: Glyph[]) { if (wordEndIdx === wordStartIdx || !CONVERTIBLE_REGEX.test(word)) { newGlyphData.push({ glyphs: glyphs.slice(wordStartIdx, wordEndIdx + 1), - bold: false, + isBold: false, }); wordStartIdx = NaN; wordEndIdx = NaN; @@ -265,13 +254,13 @@ function computeBionicGlyphs(glyphs: Glyph[]) { newGlyphData.push({ glyphs: glyphs.slice(wordStartIdx, wordStartIdx + boldNumber), - bold: true, + isBold: true, }); if (wordStartIdx + boldNumber <= wordEndIdx) { newGlyphData.push({ glyphs: glyphs.slice(wordStartIdx + boldNumber, wordEndIdx + 1), - bold: false, + isBold: false, }); } @@ -284,7 +273,7 @@ function computeBionicGlyphs(glyphs: Glyph[]) { if (!Number.isNaN(wordStartIdx)) { newGlyphData.push({ glyphs: glyphs.slice(wordStartIdx, wordStartIdx + glyphs.length), - bold: false, + isBold: false, }); } return newGlyphData; diff --git a/src/utils/font.ts b/src/utils/font.ts new file mode 100644 index 0000000..5882baf --- /dev/null +++ b/src/utils/font.ts @@ -0,0 +1,66 @@ +export { computeFont }; + +function computeFont(options: { + font: string; + alpha: number; + opacityContrast: number; + weightContrast: number; + weightOffset: number; +}) { + const { font, alpha, opacityContrast, weightContrast, weightOffset } = + options; + const computedData = { + bold: { + font: "", + alpha: alpha, + }, + light: { + font: "", + alpha: alpha, + }, + }; + + const fontParams = font.split(" "); + + const fontSizeIndex = fontParams.findIndex((font) => font.includes("px")); + + const baseFont = fontParams.slice(fontSizeIndex).join(" "); + + // Compute alpha + const opacityRatio = 1 - (opacityContrast - 1) * 0.15; + if (opacityContrast > 1) { + computedData.light.alpha *= opacityRatio; + } + + let baseWeight = 400; + if (font.includes("black")) { + baseWeight = 900; + } else if (font.includes("bold")) { + baseWeight = 700; + } else { + baseWeight = parseInt(fontParams[fontSizeIndex - 1]) || 400; + } + + baseWeight += weightOffset * 100; + + let boldWeight = baseWeight + 100 * weightContrast; + let lightWeight = baseWeight; + + if (boldWeight > 900) { + const diff = boldWeight - 900; + boldWeight -= diff; + lightWeight -= diff; + } + + if (lightWeight < 100) { + const diff = 100 - lightWeight; + lightWeight += diff; + boldWeight += diff; + } + + const italic = font.includes("italic") ? "italic" : "normal"; + + computedData.bold.font = `${italic} ${boldWeight} ${baseFont}`; + computedData.light.font = `${italic} ${lightWeight} ${baseFont}`; + return computedData; +} diff --git a/zotero-plugin.config.ts b/zotero-plugin.config.ts index 4cf26f4..fe18913 100644 --- a/zotero-plugin.config.ts +++ b/zotero-plugin.config.ts @@ -42,6 +42,15 @@ export default defineConfig({ target: "firefox115", outdir: "build/addon/chrome/content/scripts/reader", }, + { + entryPoints: ["src/preferences/index.ts"], + define: { + __env__: `"${process.env.NODE_ENV}"`, + }, + bundle: true, + target: "firefox115", + outfile: "build/addon/chrome/content/scripts/preferences.js", + }, ], },