PDF to Text browser smoke
Running…
diff --git a/js/i18n.d.ts b/js/i18n.d.ts
new file mode 100644
index 0000000..4eccda4
--- /dev/null
+++ b/js/i18n.d.ts
@@ -0,0 +1,2 @@
+export function t(key: string): string;
+export function initializeI18n(): void;
diff --git a/js/i18n.js b/js/i18n.js
index 09f9365..9542849 100644
--- a/js/i18n.js
+++ b/js/i18n.js
@@ -8,6 +8,7 @@ import { imageResizeLocales } from "./locales/image-resize.js";
import { imageCompressorLocales } from "./locales/image-compressor.js";
import { imageMetadataLocales } from "./locales/image-metadata.js";
import { imageToTextLocales } from "./locales/image-to-text.js";
+import { pdfToTextLocales } from "./locales/pdf-to-text.js";
import { privacyHubLocales } from "./locales/privacy-hub.js";
import { metadataUxLocales } from "./locales/metadata-ux.js";
@@ -15,13 +16,14 @@ const STORAGE_KEY = "secure-tools-language";
const baseTranslations = { en, ko, ja, es, de, fr };
export const translations = Object.fromEntries(Object.entries(baseTranslations).map(([language, catalog]) => [language, {
...catalog,
- metadata: { ...catalog.metadata, imageResize: imageResizeLocales[language].metadata, imageCompressor: imageCompressorLocales[language].metadata, imageMetadata: imageMetadataLocales[language].metadata, imageToText: imageToTextLocales[language].metadata, privacyCategory: privacyHubLocales[language].metadata },
- tools: { ...catalog.tools, imageMetadata: imageMetadataLocales[language].toolName, imageToText: imageToTextLocales[language].toolName, categoryDescriptions: { ...catalog.tools.categoryDescriptions, privacy: privacyHubLocales[language].categoryDescription } },
- categories: { ...catalog.categories, image: { ...catalog.categories.image, metadata: imageMetadataLocales[language].categoryDescription, toText: imageToTextLocales[language].categoryDescription } },
+ metadata: { ...catalog.metadata, imageResize: imageResizeLocales[language].metadata, imageCompressor: imageCompressorLocales[language].metadata, imageMetadata: imageMetadataLocales[language].metadata, imageToText: imageToTextLocales[language].metadata, pdfToText: pdfToTextLocales[language].metadata, privacyCategory: privacyHubLocales[language].metadata },
+ tools: { ...catalog.tools, imageMetadata: imageMetadataLocales[language].toolName, imageToText: imageToTextLocales[language].toolName, pdfToText: pdfToTextLocales[language].toolName, categoryDescriptions: { ...catalog.tools.categoryDescriptions, privacy: privacyHubLocales[language].categoryDescription } },
+ categories: { ...catalog.categories, pdf: { ...catalog.categories.pdf, toText: pdfToTextLocales[language].categoryDescription }, image: { ...catalog.categories.image, metadata: imageMetadataLocales[language].categoryDescription, toText: imageToTextLocales[language].categoryDescription }, scan: { ...catalog.categories.scan, pdfToText: pdfToTextLocales[language].categoryDescription } },
imageResize: imageResizeLocales[language].copy,
imageCompressor: imageCompressorLocales[language].copy,
imageMetadata: { ...imageMetadataLocales[language].copy, source: { ...imageMetadataLocales[language].copy.source, ...metadataUxLocales[language].image.source }, inspector: { ...imageMetadataLocales[language].copy.inspector, ...metadataUxLocales[language].image.inspector }, clean: { ...imageMetadataLocales[language].copy.clean, ...metadataUxLocales[language].image.clean }, policy: metadataUxLocales[language].image.policy },
imageToText: imageToTextLocales[language].copy,
+ pdfToText: pdfToTextLocales[language].copy,
pdfMetadata: { ...catalog.pdfMetadata, source: { ...catalog.pdfMetadata.source, ...metadataUxLocales[language].pdf.source }, inspector: { ...catalog.pdfMetadata.inspector, ...metadataUxLocales[language].pdf.inspector }, actions: { ...catalog.pdfMetadata.actions, ...metadataUxLocales[language].pdf.actions }, custom: metadataUxLocales[language].pdf.custom, errors: { ...catalog.pdfMetadata.errors, ...metadataUxLocales[language].pdf.errors } },
privacyHub: privacyHubLocales[language].copy,
}]));
diff --git a/js/locales/pdf-to-text.js b/js/locales/pdf-to-text.js
new file mode 100644
index 0000000..2e7caa7
--- /dev/null
+++ b/js/locales/pdf-to-text.js
@@ -0,0 +1,65 @@
+const copy = {
+ eyebrow: "Local PDF OCR", title: "PDF to Text", description: "Recognize editable text from every page or a selected page range without uploading your PDF.",
+ drop: { title: "Add one PDF", description: "Drop a PDF here or use the picker.", choose: "Choose PDF", localTitle: "Processed locally.", localBody: "The PDF, rendered pages, and recognized text stay on this device.", privacyLink: "How privacy works" },
+ source: { title: "Source PDF", empty: "No PDF selected yet.", selectedLabel: "Selected source PDF", meta: "{pages} pages · {size}", replace: "Replace PDF", remove: "Remove PDF" },
+ warning: { large: "Large PDFs can take a while. Keep this tab open while local OCR runs." },
+ settings: { title: "Recognition", pages: "Pages", all: "All pages", selected: "Selected pages", range: "Page range", rangePlaceholder: "1, 3-5", rangeHelp: "Use commas and ranges, such as 1, 3-5.", language: "Text language", english: "English", korean: "Korean", combined: "English + Korean", recognize: "Recognize text", cancel: "Cancel recognition", progress: "Recognition progress" },
+ result: { title: "Recognized text", description: "Review and edit each page before copying or downloading it.", page: "Page {page}", pageLabel: "Editable recognized text for page {page}", copyPage: "Copy page", copyAll: "Copy all", download: "Download TXT" },
+ status: { preparing: "Preparing the PDF locally…", ready: "PDF ready. Choose pages and a language to begin.", cancelled: "Recognition cancelled. The PDF remains ready.", success: "Recognition complete. You can edit every page.", copied: "All recognized text copied.", pageCopied: "Page {page} copied.", downloaded: "Downloaded {name}." },
+ progress: { starting: "Starting local recognition…", loading: "Loading the PDF locally…", "rendering-page": "Rendering page {page} of {total}…", "recognizing-page": "Recognizing page {page} of {total}… {percent}%", complete: "Finishing recognition…" },
+ errors: { oneFile: "Choose exactly one PDF.", UNSUPPORTED_PDF: "Choose a valid PDF file.", UNREADABLE_PDF: "This PDF could not be read.", ENCRYPTED_PDF: "Password-protected PDFs are not supported.", PDF_HAS_NO_PAGES: "This PDF has no pages.", PAGE_RANGE_REQUIRED: "Enter at least one page.", PAGE_RANGE_INVALID: "Enter pages as numbers and ranges, such as 1, 3-5.", PAGE_RANGE_REVERSED: "A page range cannot run backwards.", PAGE_OUT_OF_RANGE: "One or more pages are outside this PDF.", PDF_OCR_PAGE_DUPLICATE: "Choose each page only once.", PDF_OCR_RENDER_DIMENSION_EXCEEDED: "A page is too large to render safely.", PDF_OCR_RENDER_PIXELS_EXCEEDED: "A page is too large to render safely.", PDF_OCR_FAILED: "Recognition failed. Try again or choose another PDF.", PDF_LIBRARY_UNAVAILABLE: "The local PDF reader is unavailable.", copy: "The text could not be copied. Select it manually and copy it.", download: "The text file could not be downloaded." },
+};
+const localized = {
+ en: { title: "PDF to Text", description: copy.description, toolName: "PDF to Text OCR", category: "Extract editable text from PDF pages with local English and Korean OCR." },
+ ko: { title: "PDF → 텍스트", description: "PDF를 업로드하지 않고 전체 또는 선택한 페이지에서 편집 가능한 텍스트를 인식합니다.", toolName: "PDF 텍스트 OCR", category: "로컬 영어·한국어 OCR로 PDF 페이지에서 편집 가능한 텍스트를 추출합니다." },
+ ja: { title: "PDFからテキスト", description: "PDFをアップロードせず、全ページまたは選択ページから編集可能なテキストを認識します。", toolName: "PDFテキストOCR", category: "ローカルの英語・韓国語OCRでPDFページから編集可能なテキストを抽出します。" },
+ es: { title: "PDF a texto", description: "Reconoce texto editable de todas las páginas o de una selección sin subir el PDF.", toolName: "OCR de PDF a texto", category: "Extrae texto editable de páginas PDF con OCR local en inglés y coreano." },
+ de: { title: "PDF zu Text", description: "Erkennt bearbeitbaren Text auf allen oder ausgewählten Seiten, ohne die PDF hochzuladen.", toolName: "PDF-zu-Text-OCR", category: "Extrahiert bearbeitbaren Text aus PDF-Seiten mit lokaler englischer und koreanischer OCR." },
+ fr: { title: "PDF en texte", description: "Reconnaît du texte modifiable dans toutes les pages ou une sélection sans téléverser le PDF.", toolName: "OCR PDF en texte", category: "Extrait du texte modifiable des pages PDF par OCR local anglais et coréen." },
+};
+const overrides = {
+ ko: {
+ eyebrow: "로컬 PDF OCR", drop: { title: "PDF 한 개 추가", description: "PDF를 놓거나 파일 선택기를 사용하세요.", choose: "PDF 선택", localTitle: "로컬에서 처리됩니다.", localBody: "PDF, 렌더링한 페이지, 인식된 텍스트는 이 기기에만 남습니다.", privacyLink: "개인정보 보호 방식" },
+ source: { title: "원본 PDF", empty: "선택한 PDF가 없습니다.", selectedLabel: "선택한 원본 PDF", meta: "{pages}페이지 · {size}", replace: "PDF 교체", remove: "PDF 제거" }, warning: { large: "큰 PDF는 시간이 걸릴 수 있습니다. 로컬 OCR이 실행되는 동안 이 탭을 열어 두세요." },
+ settings: { title: "텍스트 인식", pages: "페이지", all: "모든 페이지", selected: "선택한 페이지", range: "페이지 범위", rangePlaceholder: "1, 3-5", rangeHelp: "1, 3-5처럼 쉼표와 범위를 사용하세요.", language: "텍스트 언어", english: "영어", korean: "한국어", combined: "영어 + 한국어", recognize: "텍스트 인식", cancel: "인식 취소", progress: "인식 진행률" },
+ result: { title: "인식된 텍스트", description: "복사하거나 다운로드하기 전에 각 페이지를 확인하고 편집하세요.", page: "{page}페이지", pageLabel: "{page}페이지의 편집 가능한 인식 텍스트", copyPage: "페이지 복사", copyAll: "모두 복사", download: "TXT 다운로드" },
+ status: { preparing: "PDF를 로컬에서 준비하는 중…", ready: "PDF가 준비되었습니다. 페이지와 언어를 선택하세요.", cancelled: "인식을 취소했습니다. PDF는 준비 상태로 유지됩니다.", success: "인식이 완료되었습니다. 각 페이지를 편집할 수 있습니다.", copied: "인식된 텍스트를 모두 복사했습니다.", pageCopied: "{page}페이지를 복사했습니다.", downloaded: "{name}을 다운로드했습니다." },
+ progress: { starting: "로컬 인식을 시작하는 중…", loading: "PDF를 로컬에서 불러오는 중…", "rendering-page": "{total}페이지 중 {page}페이지를 렌더링하는 중…", "recognizing-page": "{total}페이지 중 {page}페이지를 인식하는 중… {percent}%", complete: "인식을 마무리하는 중…" },
+ },
+ ja: {
+ eyebrow: "ローカルPDF OCR", drop: { title: "PDFを1件追加", description: "PDFをドロップするか、ファイル選択を使用します。", choose: "PDFを選択", localTitle: "ローカル処理。", localBody: "PDF、描画ページ、認識テキストは端末外へ送信されません。", privacyLink: "プライバシーの仕組み" },
+ source: { title: "元のPDF", empty: "PDFが未選択です。", selectedLabel: "選択した元のPDF", meta: "{pages}ページ · {size}", replace: "PDFを置換", remove: "PDFを削除" }, warning: { large: "大きなPDFは時間がかかることがあります。ローカルOCR中はこのタブを開いたままにしてください。" },
+ settings: { title: "文字認識", pages: "ページ", all: "全ページ", selected: "選択したページ", range: "ページ範囲", rangePlaceholder: "1, 3-5", rangeHelp: "1, 3-5のようにカンマと範囲を使用します。", language: "テキスト言語", english: "英語", korean: "韓国語", combined: "英語 + 韓国語", recognize: "テキストを認識", cancel: "認識をキャンセル", progress: "認識の進行状況" },
+ result: { title: "認識したテキスト", description: "コピーまたはダウンロード前に各ページを確認・編集できます。", page: "ページ{page}", pageLabel: "ページ{page}の編集可能な認識テキスト", copyPage: "ページをコピー", copyAll: "すべてコピー", download: "TXTをダウンロード" },
+ status: { preparing: "PDFをローカルで準備中…", ready: "PDFの準備ができました。ページと言語を選択してください。", cancelled: "認識をキャンセルしました。PDFは準備済みです。", success: "認識が完了しました。各ページを編集できます。", copied: "認識テキストをすべてコピーしました。", pageCopied: "ページ{page}をコピーしました。", downloaded: "{name}をダウンロードしました。" },
+ progress: { starting: "ローカル認識を開始中…", loading: "PDFをローカルで読み込み中…", "rendering-page": "{total}ページ中{page}ページを描画中…", "recognizing-page": "{total}ページ中{page}ページを認識中… {percent}%", complete: "認識を完了中…" },
+ },
+ es: {
+ eyebrow: "OCR local de PDF", drop: { title: "Añadir un PDF", description: "Suelta un PDF aquí o usa el selector.", choose: "Elegir PDF", localTitle: "Procesamiento local.", localBody: "El PDF, las páginas renderizadas y el texto reconocido permanecen en este dispositivo.", privacyLink: "Cómo funciona la privacidad" },
+ source: { title: "PDF de origen", empty: "No hay ningún PDF seleccionado.", selectedLabel: "PDF de origen seleccionado", meta: "{pages} páginas · {size}", replace: "Reemplazar PDF", remove: "Quitar PDF" }, warning: { large: "Los PDF grandes pueden tardar. Mantén esta pestaña abierta mientras se ejecuta el OCR local." },
+ settings: { title: "Reconocimiento", pages: "Páginas", all: "Todas las páginas", selected: "Páginas seleccionadas", range: "Rango de páginas", rangePlaceholder: "1, 3-5", rangeHelp: "Usa comas y rangos, por ejemplo 1, 3-5.", language: "Idioma del texto", english: "Inglés", korean: "Coreano", combined: "Inglés + coreano", recognize: "Reconocer texto", cancel: "Cancelar reconocimiento", progress: "Progreso del reconocimiento" },
+ result: { title: "Texto reconocido", description: "Revisa y edita cada página antes de copiarla o descargarla.", page: "Página {page}", pageLabel: "Texto reconocido editable de la página {page}", copyPage: "Copiar página", copyAll: "Copiar todo", download: "Descargar TXT" },
+ status: { preparing: "Preparando el PDF localmente…", ready: "PDF listo. Elige páginas e idioma para comenzar.", cancelled: "Reconocimiento cancelado. El PDF sigue listo.", success: "Reconocimiento terminado. Puedes editar cada página.", copied: "Se copió todo el texto reconocido.", pageCopied: "Página {page} copiada.", downloaded: "Se descargó {name}." },
+ progress: { starting: "Iniciando el reconocimiento local…", loading: "Cargando el PDF localmente…", "rendering-page": "Renderizando página {page} de {total}…", "recognizing-page": "Reconociendo página {page} de {total}… {percent}%", complete: "Finalizando el reconocimiento…" },
+ },
+ de: {
+ eyebrow: "Lokale PDF-OCR", drop: { title: "Eine PDF hinzufügen", description: "PDF hier ablegen oder Dateiauswahl verwenden.", choose: "PDF auswählen", localTitle: "Lokale Verarbeitung.", localBody: "PDF, gerenderte Seiten und erkannter Text bleiben auf diesem Gerät.", privacyLink: "So funktioniert der Datenschutz" },
+ source: { title: "Quell-PDF", empty: "Keine PDF ausgewählt.", selectedLabel: "Ausgewählte Quell-PDF", meta: "{pages} Seiten · {size}", replace: "PDF ersetzen", remove: "PDF entfernen" }, warning: { large: "Große PDFs können länger dauern. Lassen Sie diesen Tab während der lokalen OCR geöffnet." },
+ settings: { title: "Erkennung", pages: "Seiten", all: "Alle Seiten", selected: "Ausgewählte Seiten", range: "Seitenbereich", rangePlaceholder: "1, 3-5", rangeHelp: "Kommas und Bereiche verwenden, zum Beispiel 1, 3-5.", language: "Textsprache", english: "Englisch", korean: "Koreanisch", combined: "Englisch + Koreanisch", recognize: "Text erkennen", cancel: "Erkennung abbrechen", progress: "Erkennungsfortschritt" },
+ result: { title: "Erkannter Text", description: "Jede Seite vor dem Kopieren oder Herunterladen prüfen und bearbeiten.", page: "Seite {page}", pageLabel: "Bearbeitbarer erkannter Text für Seite {page}", copyPage: "Seite kopieren", copyAll: "Alles kopieren", download: "TXT herunterladen" },
+ status: { preparing: "PDF wird lokal vorbereitet…", ready: "PDF bereit. Seiten und Sprache auswählen.", cancelled: "Erkennung abgebrochen. Die PDF bleibt bereit.", success: "Erkennung abgeschlossen. Jede Seite kann bearbeitet werden.", copied: "Gesamten erkannten Text kopiert.", pageCopied: "Seite {page} kopiert.", downloaded: "{name} wurde heruntergeladen." },
+ progress: { starting: "Lokale Erkennung wird gestartet…", loading: "PDF wird lokal geladen…", "rendering-page": "Seite {page} von {total} wird gerendert…", "recognizing-page": "Seite {page} von {total} wird erkannt… {percent}%", complete: "Erkennung wird abgeschlossen…" },
+ },
+ fr: {
+ eyebrow: "OCR PDF local", drop: { title: "Ajouter un PDF", description: "Déposez un PDF ici ou utilisez le sélecteur.", choose: "Choisir un PDF", localTitle: "Traitement local.", localBody: "Le PDF, les pages rendues et le texte reconnu restent sur cet appareil.", privacyLink: "Fonctionnement de la confidentialité" },
+ source: { title: "PDF source", empty: "Aucun PDF sélectionné.", selectedLabel: "PDF source sélectionné", meta: "{pages} pages · {size}", replace: "Remplacer le PDF", remove: "Retirer le PDF" }, warning: { large: "Les PDF volumineux peuvent prendre du temps. Gardez cet onglet ouvert pendant l’OCR local." },
+ settings: { title: "Reconnaissance", pages: "Pages", all: "Toutes les pages", selected: "Pages sélectionnées", range: "Plage de pages", rangePlaceholder: "1, 3-5", rangeHelp: "Utilisez des virgules et des plages, par exemple 1, 3-5.", language: "Langue du texte", english: "Anglais", korean: "Coréen", combined: "Anglais + coréen", recognize: "Reconnaître le texte", cancel: "Annuler la reconnaissance", progress: "Progression de la reconnaissance" },
+ result: { title: "Texte reconnu", description: "Vérifiez et modifiez chaque page avant de la copier ou de la télécharger.", page: "Page {page}", pageLabel: "Texte reconnu modifiable de la page {page}", copyPage: "Copier la page", copyAll: "Tout copier", download: "Télécharger le TXT" },
+ status: { preparing: "Préparation locale du PDF…", ready: "PDF prêt. Choisissez les pages et la langue.", cancelled: "Reconnaissance annulée. Le PDF reste prêt.", success: "Reconnaissance terminée. Chaque page est modifiable.", copied: "Tout le texte reconnu a été copié.", pageCopied: "Page {page} copiée.", downloaded: "{name} téléchargé." },
+ progress: { starting: "Démarrage de la reconnaissance locale…", loading: "Chargement local du PDF…", "rendering-page": "Rendu de la page {page} sur {total}…", "recognizing-page": "Reconnaissance de la page {page} sur {total}… {percent}%", complete: "Finalisation de la reconnaissance…" },
+ },
+};
+export const pdfToTextLocales = Object.fromEntries(Object.entries(localized).map(([language, value]) => [language, {
+ metadata: { title: `${value.toolName} — Secure Tools`, description: value.description }, toolName: value.toolName,
+ categoryDescription: value.category, copy: Object.fromEntries(Object.entries({ ...copy, title: value.title, description: value.description }).map(([key, item]) => [key, item && typeof item === "object" ? { ...item, ...(overrides[language]?.[key] || {}) } : (overrides[language]?.[key] || item)])),
+}]));
diff --git a/scripts/site-routes.mjs b/scripts/site-routes.mjs
index 4465863..025ffe9 100644
--- a/scripts/site-routes.mjs
+++ b/scripts/site-routes.mjs
@@ -11,6 +11,7 @@ export const canonicalPages = [
{ source: "tools/pdf/organize/index.html", route: "/pdf/organize/" },
{ source: "tools/pdf/to-images/index.html", route: "/pdf/to-images/" },
{ source: "tools/pdf/metadata/index.html", route: "/pdf/metadata/" },
+ { source: "tools/pdf/to-text/index.html", route: "/pdf/to-text/", legacy: false },
{ source: "tools/image/index.html", route: "/image/" },
{ source: "tools/image/converter/index.html", route: "/image/converter/" },
{ source: "tools/image/resize/index.html", route: "/image/resize/" },
@@ -23,7 +24,7 @@ export const canonicalPages = [
export const legacyRedirects = [
...canonicalPages
- .filter(({ source }) => source.startsWith("tools/"))
+ .filter(({ source, legacy }) => source.startsWith("tools/") && legacy !== false)
.map(({ route }) => ({ from: `/tools${route}`, to: route })),
{ from: "/tools/privacy/", to: "/privacy/" },
{ from: "/tools/image-to-pdf/", to: "/pdf/images-to-pdf/" },
diff --git a/scripts/typescript-modules.mjs b/scripts/typescript-modules.mjs
index 6951c96..2205fbb 100644
--- a/scripts/typescript-modules.mjs
+++ b/scripts/typescript-modules.mjs
@@ -1,4 +1,19 @@
export const compiledBrowserModules = Object.freeze([
+ Object.freeze({
+ source: "tools/pdf/to-text/app.ts",
+ compiled: "tools/pdf/to-text/app.js",
+ public: "pdf/to-text/app.js",
+ }),
+ Object.freeze({
+ source: "tools/pdf/to-text/controller.ts",
+ compiled: "tools/pdf/to-text/controller.js",
+ public: "pdf/to-text/controller.js",
+ }),
+ Object.freeze({
+ source: "tools/pdf/to-text/output.ts",
+ compiled: "tools/pdf/to-text/output.js",
+ public: "pdf/to-text/output.js",
+ }),
Object.freeze({
source: "tools/shared/pdf-ocr.ts",
compiled: "tools/shared/pdf-ocr.js",
diff --git a/sitemap.xml b/sitemap.xml
index 574798e..e0fc784 100644
--- a/sitemap.xml
+++ b/sitemap.xml
@@ -10,6 +10,7 @@
Running…