From 649aec046d2967c7b3332146ee312281f76456a5 Mon Sep 17 00:00:00 2001 From: abdullahalam123 Date: Wed, 3 Dec 2025 23:13:14 +0530 Subject: [PATCH] feat(ocr,form-creator): Add comprehensive font support and TypeScript type definitions - Add @pdf-lib/fontkit dependency for enhanced font rendering capabilities - Create font-mappings.ts configuration with language-to-font-family mappings for 100+ languages - Implement font-loader.ts utility for dynamic font loading from CDN sources - Add TypeScript type definitions for form-creator, OCR, and general application types - Create types/index.ts as centralized type exports - Remove hidden-on-touch CSS class and update shortcuts button styling for better accessibility - Update OCR text layer rendering to support multilingual font families - Enhance form-creator with improved font handling for international text - Update txt-to-pdf with font support for diverse character sets - Migrate fileHandler to support new font loading workflow - Update main.ts and ui.ts to integrate new type system and font utilities - Update form-creator.html page with enhanced font configuration UI --- index.html | 2 +- package-lock.json | 14 +- package.json | 1 + src/css/styles.css | 14 -- src/js/config/font-mappings.ts | 189 ++++++++++++++++ src/js/handlers/fileHandler.ts | 8 +- src/js/logic/form-creator.ts | 235 +++++++++++++++----- src/js/logic/ocr-pdf.ts | 148 +++++++++---- src/js/logic/txt-to-pdf.ts | 388 ++++++++++++++++++++++++--------- src/js/main.ts | 51 +++-- src/js/types/form-creator.ts | 40 ++++ src/js/types/index.ts | 2 + src/js/types/ocr.ts | 10 + src/js/ui.ts | 57 ++++- src/js/utils/font-loader.ts | 281 ++++++++++++++++++++++++ src/pages/form-creator.html | 21 ++ 16 files changed, 1220 insertions(+), 241 deletions(-) create mode 100644 src/js/config/font-mappings.ts create mode 100644 src/js/types/form-creator.ts create mode 100644 src/js/types/index.ts create mode 100644 src/js/types/ocr.ts create mode 100644 src/js/utils/font-loader.ts diff --git a/index.html b/index.html index f63bbc8..3dcd988 100644 --- a/index.html +++ b/index.html @@ -280,7 +280,7 @@ diff --git a/package-lock.json b/package-lock.json index ac62519..5721625 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "bento-pdf", - "version": "1.7.3", + "version": "1.10.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bento-pdf", - "version": "1.7.3", + "version": "1.10.0", "license": "Apache-2.0", "dependencies": { "@fontsource/cedarville-cursive": "^5.2.7", @@ -17,6 +17,7 @@ "@fontsource/lato": "^5.2.7", "@fontsource/merriweather": "^5.2.11", "@neslinesli93/qpdf-wasm": "^0.3.0", + "@pdf-lib/fontkit": "^1.1.1", "@tailwindcss/vite": "^4.1.15", "archiver": "^7.0.1", "blob-stream": "^0.1.3", @@ -3088,6 +3089,15 @@ "node": ">= 8" } }, + "node_modules/@pdf-lib/fontkit": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@pdf-lib/fontkit/-/fontkit-1.1.1.tgz", + "integrity": "sha512-KjMd7grNapIWS/Dm0gvfHEilSyAmeLvrEGVcqLGi0VYebuqqzTbgF29efCx7tvx+IEbG3zQciRSWl3GkUSvjZg==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.6" + } + }, "node_modules/@pdf-lib/standard-fonts": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz", diff --git a/package.json b/package.json index 37e6cd1..89bbb90 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@fontsource/lato": "^5.2.7", "@fontsource/merriweather": "^5.2.11", "@neslinesli93/qpdf-wasm": "^0.3.0", + "@pdf-lib/fontkit": "^1.1.1", "@tailwindcss/vite": "^4.1.15", "archiver": "^7.0.1", "blob-stream": "^0.1.3", diff --git a/src/css/styles.css b/src/css/styles.css index f8de956..cb8ee6b 100644 --- a/src/css/styles.css +++ b/src/css/styles.css @@ -568,20 +568,6 @@ button:disabled, background: #6b7280; } -/* Hide elements on touch devices */ -@media (pointer: coarse) { - .hidden-on-touch { - display: none !important; - } -} - -/* Also hide on very small screens just in case */ -@media (max-width: 640px) { - .hidden-on-touch { - display: none !important; - } -} - /* Scroll to Top Button Visibility */ #scroll-to-top-btn.visible { opacity: 1; diff --git a/src/js/config/font-mappings.ts b/src/js/config/font-mappings.ts new file mode 100644 index 0000000..c6c0c31 --- /dev/null +++ b/src/js/config/font-mappings.ts @@ -0,0 +1,189 @@ +/** + * Font mappings for OCR text layer rendering + * Maps Tesseract language codes to appropriate Noto Sans font families and their CDN URLs + */ + +export const languageToFontFamily: Record = { + // CJK Languages + jpn: 'Noto Sans JP', + chi_sim: 'Noto Sans SC', + chi_tra: 'Noto Sans TC', + kor: 'Noto Sans KR', + + // Arabic Script + ara: 'Noto Sans Arabic', + fas: 'Noto Sans Arabic', + urd: 'Noto Sans Arabic', + pus: 'Noto Sans Arabic', + kur: 'Noto Sans Arabic', + + // Devanagari Script + hin: 'Noto Sans Devanagari', + mar: 'Noto Sans Devanagari', + san: 'Noto Sans Devanagari', + nep: 'Noto Sans Devanagari', + + // Bengali Script + ben: 'Noto Sans Bengali', + asm: 'Noto Sans Bengali', + + // Tamil Script + tam: 'Noto Sans Tamil', + + // Telugu Script + tel: 'Noto Sans Telugu', + + // Kannada Script + kan: 'Noto Sans Kannada', + + // Malayalam Script + mal: 'Noto Sans Malayalam', + + // Gujarati Script + guj: 'Noto Sans Gujarati', + + // Gurmukhi Script (Punjabi) + pan: 'Noto Sans Gurmukhi', + + // Oriya Script + ori: 'Noto Sans Oriya', + + // Sinhala Script + sin: 'Noto Sans Sinhala', + + // Thai Script + tha: 'Noto Sans Thai', + + // Lao Script + lao: 'Noto Sans Lao', + + // Khmer Script + khm: 'Noto Sans Khmer', + + // Myanmar Script + mya: 'Noto Sans Myanmar', + + // Tibetan Script + bod: 'Noto Serif Tibetan', + + // Georgian Script + kat: 'Noto Sans Georgian', + kat_old: 'Noto Sans Georgian', + + // Armenian Script + hye: 'Noto Sans Armenian', + + // Hebrew Script + heb: 'Noto Sans Hebrew', + yid: 'Noto Sans Hebrew', + + // Ethiopic Script + amh: 'Noto Sans Ethiopic', + tir: 'Noto Sans Ethiopic', + + // Cherokee Script + chr: 'Noto Sans Cherokee', + + // Syriac Script + syr: 'Noto Sans Syriac', + + // Cyrillic Script (Noto Sans includes Cyrillic) + bel: 'Noto Sans', + bul: 'Noto Sans', + mkd: 'Noto Sans', + rus: 'Noto Sans', + srp: 'Noto Sans', + srp_latn: 'Noto Sans', + ukr: 'Noto Sans', + kaz: 'Noto Sans', + kir: 'Noto Sans', + tgk: 'Noto Sans', + uzb: 'Noto Sans', + uzb_cyrl: 'Noto Sans', + aze_cyrl: 'Noto Sans', + + // Latin Script (covered by base Noto Sans) + afr: 'Noto Sans', + aze: 'Noto Sans', + bos: 'Noto Sans', + cat: 'Noto Sans', + ceb: 'Noto Sans', + ces: 'Noto Sans', + cym: 'Noto Sans', + dan: 'Noto Sans', + deu: 'Noto Sans', + ell: 'Noto Sans', + eng: 'Noto Sans', + enm: 'Noto Sans', + epo: 'Noto Sans', + est: 'Noto Sans', + eus: 'Noto Sans', + fin: 'Noto Sans', + fra: 'Noto Sans', + frk: 'Noto Sans', + frm: 'Noto Sans', + gle: 'Noto Sans', + glg: 'Noto Sans', + grc: 'Noto Sans', + hat: 'Noto Sans', + hrv: 'Noto Sans', + hun: 'Noto Sans', + iku: 'Noto Sans', + ind: 'Noto Sans', + isl: 'Noto Sans', + ita: 'Noto Sans', + ita_old: 'Noto Sans', + jav: 'Noto Sans', + lat: 'Noto Sans', + lav: 'Noto Sans', + lit: 'Noto Sans', + mlt: 'Noto Sans', + msa: 'Noto Sans', + nld: 'Noto Sans', + nor: 'Noto Sans', + pol: 'Noto Sans', + por: 'Noto Sans', + ron: 'Noto Sans', + slk: 'Noto Sans', + slv: 'Noto Sans', + spa: 'Noto Sans', + spa_old: 'Noto Sans', + sqi: 'Noto Sans', + swa: 'Noto Sans', + swe: 'Noto Sans', + tgl: 'Noto Sans', + tur: 'Noto Sans', + vie: 'Noto Sans', + dzo: 'Noto Sans', + uig: 'Noto Sans', +}; + +export const fontFamilyToUrl: Record = { + 'Noto Sans JP': 'https://raw.githack.com/googlefonts/noto-cjk/main/Sans/OTF/Japanese/NotoSansCJKjp-Regular.otf', + 'Noto Sans SC': 'https://raw.githack.com/googlefonts/noto-cjk/main/Sans/OTF/SimplifiedChinese/NotoSansCJKsc-Regular.otf', + 'Noto Sans TC': 'https://raw.githack.com/googlefonts/noto-cjk/main/Sans/OTF/TraditionalChinese/NotoSansCJKtc-Regular.otf', + 'Noto Sans KR': 'https://raw.githack.com/googlefonts/noto-cjk/main/Sans/OTF/Korean/NotoSansCJKkr-Regular.otf', + 'Noto Sans Arabic': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansArabic/NotoSansArabic-Regular.ttf', + 'Noto Sans Devanagari': 'https://raw.githack.com/googlefonts/noto-fonts/main/unhinted/ttf/NotoSansDevanagari/NotoSansDevanagari-Regular.ttf', + 'Noto Sans Bengali': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansBengali/NotoSansBengali-Regular.ttf', + 'Noto Sans Gujarati': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansGujarati/NotoSansGujarati-Regular.ttf', + 'Noto Sans Kannada': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansKannada/NotoSansKannada-Regular.ttf', + 'Noto Sans Malayalam': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansMalayalam/NotoSansMalayalam-Regular.ttf', + 'Noto Sans Oriya': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansOriya/NotoSansOriya-Regular.ttf', + 'Noto Sans Gurmukhi': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansGurmukhi/NotoSansGurmukhi-Regular.ttf', + 'Noto Sans Tamil': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansTamil/NotoSansTamil-Regular.ttf', + 'Noto Sans Telugu': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansTelugu/NotoSansTelugu-Regular.ttf', + 'Noto Sans Sinhala': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansSinhala/NotoSansSinhala-Regular.ttf', + 'Noto Sans Thai': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansThai/NotoSansThai-Regular.ttf', + 'Noto Sans Khmer': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansKhmer/NotoSansKhmer-Regular.ttf', + 'Noto Sans Lao': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansLao/NotoSansLao-Regular.ttf', + 'Noto Sans Myanmar': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansMyanmar/NotoSansMyanmar-Regular.ttf', + 'Noto Sans Hebrew': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansHebrew/NotoSansHebrew-Regular.ttf', + 'Noto Sans Georgian': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansGeorgian/NotoSansGeorgian-Regular.ttf', + 'Noto Sans Ethiopic': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansEthiopic/NotoSansEthiopic-Regular.ttf', + 'Noto Serif Tibetan': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSerifTibetan/NotoSerifTibetan-Regular.ttf', + 'Noto Sans Cherokee': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansCherokee/NotoSansCherokee-Regular.ttf', + 'Noto Sans Armenian': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansArmenian/NotoSansArmenian-Regular.ttf', + 'Noto Sans Syriac': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansSyriac/NotoSansSyriac-Regular.ttf', + 'Noto Sans': 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSans/NotoSans-Regular.ttf', +}; \ No newline at end of file diff --git a/src/js/handlers/fileHandler.ts b/src/js/handlers/fileHandler.ts index 445a1aa..34fef03 100644 --- a/src/js/handlers/fileHandler.ts +++ b/src/js/handlers/fileHandler.ts @@ -473,24 +473,24 @@ async function handleSinglePdfUpload(toolId, file) { addBtn.onclick = () => { const fieldWrapper = document.createElement('div'); - fieldWrapper.className = 'flex items-center gap-2 custom-field-wrapper'; + fieldWrapper.className = 'flex flex-col sm:flex-row items-stretch sm:items-center gap-2 custom-field-wrapper'; const keyInput = document.createElement('input'); keyInput.type = 'text'; keyInput.placeholder = 'Key (e.g., Department)'; keyInput.className = - 'custom-meta-key w-1/3 bg-gray-800 border border-gray-600 text-white rounded-lg p-2'; + 'custom-meta-key w-full sm:w-1/3 bg-gray-800 border border-gray-600 text-white rounded-lg p-2'; const valueInput = document.createElement('input'); valueInput.type = 'text'; valueInput.placeholder = 'Value (e.g., Marketing)'; valueInput.className = - 'custom-meta-value flex-grow bg-gray-800 border border-gray-600 text-white rounded-lg p-2'; + 'custom-meta-value w-full sm:flex-grow bg-gray-800 border border-gray-600 text-white rounded-lg p-2'; const removeBtn = document.createElement('button'); removeBtn.type = 'button'; removeBtn.className = - 'btn p-2 text-red-500 hover:bg-gray-700 rounded-full'; + 'btn p-2 text-red-500 hover:bg-gray-700 rounded-full self-center sm:self-auto'; removeBtn.innerHTML = ''; removeBtn.addEventListener('click', () => fieldWrapper.remove()); diff --git a/src/js/logic/form-creator.ts b/src/js/logic/form-creator.ts index c7d5629..db5808c 100644 --- a/src/js/logic/form-creator.ts +++ b/src/js/logic/form-creator.ts @@ -8,50 +8,14 @@ import 'pdfjs-dist/web/pdf_viewer.css' // Initialize PDF.js worker pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString() -interface FormField { - id: string - type: 'text' | 'checkbox' | 'radio' | 'dropdown' | 'optionlist' | 'button' | 'signature' | 'date' | 'image' - x: number - y: number - width: number - height: number - name: string - defaultValue: string - fontSize: number - alignment: 'left' | 'center' | 'right' - textColor: string - required: boolean - readOnly: boolean - tooltip: string - combCells: number - maxLength: number - options?: string[] - checked?: boolean - exportValue?: string - groupName?: string - label?: string - pageIndex: number - action?: 'none' | 'reset' | 'print' | 'url' | 'js' | 'showHide' - actionUrl?: string - jsScript?: string - targetFieldName?: string - visibilityAction?: 'show' | 'hide' | 'toggle' - dateFormat?: string - multiline?: boolean -} +import { FormField, PageData } from '../types/index.js' -interface PageData { - index: number - width: number - height: number - pdfPageData?: string -} let fields: FormField[] = [] let selectedField: FormField | null = null let fieldCounter = 0 -let existingFieldNames: Set = new Set() -let existingRadioGroups: Set = new Set() +let existingFieldNames: Set = new Set() +let existingRadioGroups: Set = new Set() let draggedElement: HTMLElement | null = null let offsetX = 0 let offsetY = 0 @@ -59,7 +23,7 @@ let offsetY = 0 let pages: PageData[] = [] let currentPageIndex = 0 let uploadedPdfDoc: PDFDocument | null = null -let uploadedPdfjsDoc: any = null +let uploadedPdfjsDoc: any = null let pageSize: { width: number; height: number } = { width: 612, height: 792 } let currentScale = 1.333 let pdfViewerOffset = { x: 0, y: 0 } @@ -99,6 +63,132 @@ const addPageBtn = document.getElementById('addPageBtn') as HTMLButtonElement const resetBtn = document.getElementById('resetBtn') as HTMLButtonElement const downloadBtn = document.getElementById('downloadBtn') as HTMLButtonElement const backToToolsBtn = document.getElementById('back-to-tools') as HTMLButtonElement | null +const gotoPageInput = document.getElementById('gotoPageInput') as HTMLInputElement +const gotoPageBtn = document.getElementById('gotoPageBtn') as HTMLButtonElement + +const gridVInput = document.getElementById('gridVInput') as HTMLInputElement +const gridHInput = document.getElementById('gridHInput') as HTMLInputElement +const toggleGridBtn = document.getElementById('toggleGridBtn') as HTMLButtonElement +const enableGridCheckbox = document.getElementById('enableGridCheckbox') as HTMLInputElement +let gridV = 2 +let gridH = 2 +let gridAlwaysVisible = false +let gridEnabled = true + +if (gridVInput && gridHInput) { + gridVInput.value = '2' + gridHInput.value = '2' + + const updateGrid = () => { + let v = parseInt(gridVInput.value) || 2 + let h = parseInt(gridHInput.value) || 2 + + if (v < 2) { v = 2; gridVInput.value = '2' } + if (h < 2) { h = 2; gridHInput.value = '2' } + if (v > 14) { v = 14; gridVInput.value = '14' } + if (h > 14) { h = 14; gridHInput.value = '14' } + + gridV = v + gridH = h + + if (gridAlwaysVisible && gridEnabled) { + renderGrid() + } + } + + gridVInput.addEventListener('input', updateGrid) + gridHInput.addEventListener('input', updateGrid) +} + +if (enableGridCheckbox) { + enableGridCheckbox.addEventListener('change', (e) => { + gridEnabled = (e.target as HTMLInputElement).checked + + if (!gridEnabled) { + removeGrid() + if (gridVInput) gridVInput.disabled = true + if (gridHInput) gridHInput.disabled = true + if (toggleGridBtn) toggleGridBtn.disabled = true + } else { + if (gridVInput) gridVInput.disabled = false + if (gridHInput) gridHInput.disabled = false + if (toggleGridBtn) toggleGridBtn.disabled = false + if (gridAlwaysVisible) renderGrid() + } + }) +} + +if (toggleGridBtn) { + toggleGridBtn.addEventListener('click', () => { + gridAlwaysVisible = !gridAlwaysVisible + + if (gridAlwaysVisible) { + toggleGridBtn.classList.add('bg-indigo-600') + toggleGridBtn.classList.remove('bg-gray-600') + if (gridEnabled) renderGrid() + } else { + toggleGridBtn.classList.remove('bg-indigo-600') + toggleGridBtn.classList.add('bg-gray-600') + removeGrid() + } + }) +} + +function renderGrid() { + const existingGrid = document.getElementById('pdfGrid') + if (existingGrid) existingGrid.remove() + + const gridContainer = document.createElement('div') + gridContainer.id = 'pdfGrid' + gridContainer.className = 'absolute inset-0 pointer-events-none' + gridContainer.style.zIndex = '1' + + if (gridV > 0) { + const stepX = canvas.offsetWidth / gridV + for (let i = 0; i <= gridV; i++) { + const line = document.createElement('div') + line.className = 'absolute top-0 bottom-0 border-l-2 border-indigo-500 opacity-60' + line.style.left = (i * stepX) + 'px' + gridContainer.appendChild(line) + } + } + + if (gridH > 0) { + const stepY = canvas.offsetHeight / gridH + for (let i = 0; i <= gridH; i++) { + const line = document.createElement('div') + line.className = 'absolute left-0 right-0 border-t-2 border-indigo-500 opacity-60' + line.style.top = (i * stepY) + 'px' + gridContainer.appendChild(line) + } + } + + canvas.insertBefore(gridContainer, canvas.firstChild) +} + +function removeGrid() { + const existingGrid = document.getElementById('pdfGrid') + if (existingGrid) existingGrid.remove() +} + +if (gotoPageBtn && gotoPageInput) { + gotoPageBtn.addEventListener('click', () => { + const pageNum = parseInt(gotoPageInput.value) + if (!isNaN(pageNum) && pageNum >= 1 && pageNum <= pages.length) { + currentPageIndex = pageNum - 1 + renderCanvas() + updatePageNavigation() + } else { + alert(`Please enter a valid page number between 1 and ${pages.length}`) + } + }) + + gotoPageInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + gotoPageBtn.click() + } + }) +} // Tool item interactions const toolItems = document.querySelectorAll('.tool-item') @@ -109,10 +199,13 @@ toolItems.forEach(item => { e.dataTransfer.effectAllowed = 'copy' const type = (item as HTMLElement).dataset.type || 'text' e.dataTransfer.setData('text/plain', type) + if (gridEnabled) renderGrid() } }) - // Click to select tool for placement + item.addEventListener('dragend', () => { + if (!gridAlwaysVisible && gridEnabled) removeGrid() + }) item.addEventListener('click', () => { const type = (item as HTMLElement).dataset.type || 'text' @@ -191,8 +284,9 @@ canvas.addEventListener('dragover', (e) => { canvas.addEventListener('drop', (e) => { e.preventDefault() + if (!gridAlwaysVisible) removeGrid() const rect = canvas.getBoundingClientRect() - const x = e.clientX - rect.left - 75 // Center the field on drop point + const x = e.clientX - rect.left - 75 const y = e.clientY - rect.top - 15 const type = e.dataTransfer?.getData('text/plain') || 'text' createField(type as any, x, y) @@ -246,7 +340,9 @@ function createField(type: FormField['type'], x: number, y: number): void { visibilityAction: type === 'button' ? 'toggle' : undefined, dateFormat: type === 'date' ? 'mm/dd/yyyy' : undefined, pageIndex: currentPageIndex, - multiline: type === 'text' ? false : undefined + multiline: type === 'text' ? false : undefined, + borderColor: '#000000', + hideBorder: false } fields.push(field) @@ -263,6 +359,7 @@ function renderField(field: FormField): void { fieldWrapper.style.top = field.y + 'px' fieldWrapper.style.width = field.width + 'px' fieldWrapper.style.overflow = 'visible' + fieldWrapper.style.zIndex = '10' // Ensure fields are above grid and PDF // Create label - hidden by default, shown on group hover or selection const label = document.createElement('div') @@ -398,6 +495,7 @@ function renderField(field: FormField): void { offsetX = e.clientX - rect.left - field.x offsetY = e.clientY - rect.top - field.y selectField(field) + if (gridEnabled) renderGrid() e.preventDefault() }) @@ -559,9 +657,9 @@ document.addEventListener('mouseup', () => { draggedElement = null resizing = false resizeField = null + if (!gridAlwaysVisible) removeGrid() }) -// Touch move for dragging and resizing document.addEventListener('touchmove', (e) => { const touch = e.touches[0] if (resizing && resizeField) { @@ -866,6 +964,14 @@ function showProperties(field: FormField): void { +
+ + +
+
+ + +
@@ -963,6 +1069,17 @@ function showProperties(field: FormField): void { field.readOnly = (e.target as HTMLInputElement).checked }) + const propBorderColor = document.getElementById('propBorderColor') as HTMLInputElement + const propHideBorder = document.getElementById('propHideBorder') as HTMLInputElement + + propBorderColor.addEventListener('input', (e) => { + field.borderColor = (e.target as HTMLInputElement).value + }) + + propHideBorder.addEventListener('change', (e) => { + field.hideBorder = (e.target as HTMLInputElement).checked + }) + deleteBtn.addEventListener('click', () => { deleteField(field) }) @@ -1424,14 +1541,15 @@ downloadBtn.addEventListener('click', async () => { if (field.type === 'text') { const textField = form.createTextField(field.name) const rgbColor = hexToRgb(field.textColor) + const borderRgb = hexToRgb(field.borderColor || '#000000') textField.addToPage(pdfPage, { x: x, y: y, width: width, height: height, - borderWidth: 1, - borderColor: rgb(0, 0, 0), + borderWidth: field.hideBorder ? 0 : 1, + borderColor: rgb(borderRgb.r, borderRgb.g, borderRgb.b), backgroundColor: rgb(1, 1, 1), textColor: rgb(rgbColor.r, rgbColor.g, rgbColor.b), }) @@ -1474,13 +1592,14 @@ downloadBtn.addEventListener('click', async () => { } else if (field.type === 'checkbox') { const checkBox = form.createCheckBox(field.name) + const borderRgb = hexToRgb(field.borderColor || '#000000') checkBox.addToPage(pdfPage, { x: x, y: y, width: width, height: height, - borderWidth: 1, - borderColor: rgb(0, 0, 0), + borderWidth: field.hideBorder ? 0 : 1, + borderColor: rgb(borderRgb.r, borderRgb.g, borderRgb.b), backgroundColor: rgb(1, 1, 1), }) if (field.checked) checkBox.check() @@ -1512,13 +1631,14 @@ downloadBtn.addEventListener('click', async () => { } } + const borderRgb = hexToRgb(field.borderColor || '#000000') radioGroup.addOptionToPage(field.exportValue || 'Yes', pdfPage as any, { x: x, y: y, width: width, height: height, - borderWidth: 1, - borderColor: rgb(0, 0, 0), + borderWidth: field.hideBorder ? 0 : 1, + borderColor: rgb(borderRgb.r, borderRgb.g, borderRgb.b), backgroundColor: rgb(1, 1, 1), }) if (field.checked) radioGroup.select(field.exportValue || 'Yes') @@ -1532,13 +1652,14 @@ downloadBtn.addEventListener('click', async () => { } else if (field.type === 'dropdown') { const dropdown = form.createDropdown(field.name) + const borderRgb = hexToRgb(field.borderColor || '#000000') dropdown.addToPage(pdfPage, { x: x, y: y, width: width, height: height, - borderWidth: 1, - borderColor: rgb(0, 0, 0), + borderWidth: field.hideBorder ? 0 : 1, + borderColor: rgb(borderRgb.r, borderRgb.g, borderRgb.b), backgroundColor: rgb(1, 1, 1), // Light blue not supported in standard PDF appearance easily without streams }) if (field.options) dropdown.setOptions(field.options) @@ -1561,13 +1682,14 @@ downloadBtn.addEventListener('click', async () => { } else if (field.type === 'optionlist') { const optionList = form.createOptionList(field.name) + const borderRgb = hexToRgb(field.borderColor || '#000000') optionList.addToPage(pdfPage, { x: x, y: y, width: width, height: height, - borderWidth: 1, - borderColor: rgb(0, 0, 0), + borderWidth: field.hideBorder ? 0 : 1, + borderColor: rgb(borderRgb.r, borderRgb.g, borderRgb.b), backgroundColor: rgb(1, 1, 1), }) if (field.options) optionList.setOptions(field.options) @@ -1590,13 +1712,14 @@ downloadBtn.addEventListener('click', async () => { } else if (field.type === 'button') { const button = form.createButton(field.name) + const borderRgb = hexToRgb(field.borderColor || '#000000') button.addToPage(field.label || 'Button', pdfPage, { x: x, y: y, width: width, height: height, - borderWidth: 1, - borderColor: rgb(0, 0, 0), + borderWidth: field.hideBorder ? 0 : 1, + borderColor: rgb(borderRgb.r, borderRgb.g, borderRgb.b), backgroundColor: rgb(0.8, 0.8, 0.8), // Light gray }) diff --git a/src/js/logic/ocr-pdf.ts b/src/js/logic/ocr-pdf.ts index 303b09f..d77fcbd 100644 --- a/src/js/logic/ocr-pdf.ts +++ b/src/js/logic/ocr-pdf.ts @@ -4,20 +4,25 @@ import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/he import { state } from '../state.js'; import Tesseract from 'tesseract.js'; import { PDFDocument as PDFLibDocument, StandardFonts, rgb } from 'pdf-lib'; +import fontkit from '@pdf-lib/fontkit'; import { icons, createIcons } from 'lucide'; import * as pdfjsLib from 'pdfjs-dist'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString(); +import type { Word } from '../types/index.js'; -let searchablePdfBytes: any = null; +let searchablePdfBytes: Uint8Array | null = null; -function sanitizeTextForWinAnsi(text: string): string { - // Remove invisible Unicode control characters (like Left-to-Right Mark U+200E) - return text - .replace(/[\u0000-\u001F\u007F-\u009F\u200E\u200F\u202A-\u202E\uFEFF]/g, '') - .replace(/[^\u0020-\u007E\u00A0-\u00FF]/g, ''); -} +import { getFontForLanguage } from '../utils/font-loader.js'; + + +// function sanitizeTextForWinAnsi(text: string): string { +// // Remove invisible Unicode control characters (like Left-to-Right Mark U+200E) +// return text +// .replace(/[\u0000-\u001F\u007F-\u009F\u200E\u200F\u202A-\u202E\uFEFF]/g, '') +// .replace(/[^\u0020-\u007E\u00A0-\u00FF]/g, ''); +// } function parseHOCR(hocrText: string) { const parser = new DOMParser(); @@ -55,7 +60,7 @@ function parseHOCR(hocrText: string) { return words; } -function binarizeCanvas(ctx: any) { +function binarizeCanvas(ctx: CanvasRenderingContext2D) { const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height); const data = imageData.data; for (let i = 0; i < data.length; i += 4) { @@ -68,7 +73,7 @@ function binarizeCanvas(ctx: any) { ctx.putImageData(imageData, 0, 0); } -function updateProgress(status: any, progress: any) { +function updateProgress(status: string, progress: number) { const progressBar = document.getElementById('progress-bar'); const progressStatus = document.getElementById('progress-status'); const progressLog = document.getElementById('progress-log'); @@ -88,12 +93,13 @@ async function runOCR() { const selectedLangs = Array.from( document.querySelectorAll('.lang-checkbox:checked') ).map((cb) => (cb as HTMLInputElement).value); - // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message - const scale = parseFloat(document.getElementById('ocr-resolution').value); - // @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message - const binarize = document.getElementById('ocr-binarize').checked; - // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message - const whitelist = document.getElementById('ocr-whitelist').value; + const scale = parseFloat( + (document.getElementById('ocr-resolution') as HTMLSelectElement).value + ); + const binarize = (document.getElementById('ocr-binarize') as HTMLInputElement) + .checked; + const whitelist = (document.getElementById('ocr-whitelist') as HTMLInputElement) + .value; if (selectedLangs.length === 0) { showAlert( @@ -109,12 +115,13 @@ async function runOCR() { try { const worker = await Tesseract.createWorker(langString, 1, { - logger: (m: any) => updateProgress(m.status, m.progress || 0), + logger: (m: { status: string; progress: number }) => + updateProgress(m.status, m.progress || 0), }); - // Enable hOCR output await worker.setParameters({ tessjs_create_hocr: '1', + tessedit_pageseg_mode: Tesseract.PSM.AUTO, }); await worker.setParameters({ @@ -125,7 +132,48 @@ async function runOCR() { await readFileAsArrayBuffer(state.files[0]) ).promise; const newPdfDoc = await PDFLibDocument.create(); - const font = await newPdfDoc.embedFont(StandardFonts.Helvetica); + + newPdfDoc.registerFontkit(fontkit); + + updateProgress('Loading fonts...', 0); + + // Prioritize non-Latin languages for font selection if multiple are selected + const cjkLangs = ['jpn', 'chi_sim', 'chi_tra', 'kor']; + const indicLangs = ['hin', 'ben', 'guj', 'kan', 'mal', 'ori', 'pan', 'tam', 'tel', 'sin']; + const priorityLangs = [...cjkLangs, ...indicLangs, 'ara', 'rus', 'ukr']; + + const primaryLang = selectedLangs.find(l => priorityLangs.includes(l)) || selectedLangs[0] || 'eng'; + + const hasCJK = selectedLangs.some(l => cjkLangs.includes(l)); + const hasIndic = selectedLangs.some(l => indicLangs.includes(l)); + const hasLatin = selectedLangs.some(l => !priorityLangs.includes(l)) || selectedLangs.includes('eng'); + const isIndicPlusLatin = hasIndic && hasLatin && !hasCJK; + + let primaryFont; + let latinFont; + + try { + let fontBytes; + if (isIndicPlusLatin) { + const [scriptFontBytes, latinFontBytes] = await Promise.all([ + getFontForLanguage(primaryLang), + getFontForLanguage('eng') + ]); + primaryFont = await newPdfDoc.embedFont(scriptFontBytes, { subset: false }); + latinFont = await newPdfDoc.embedFont(latinFontBytes, { subset: false }); + } else { + // For CJK or single-script, use one font + fontBytes = await getFontForLanguage(primaryLang); + primaryFont = await newPdfDoc.embedFont(fontBytes, { subset: false }); + latinFont = primaryFont; + } + } catch (e) { + console.error('Font loading failed, falling back to Helvetica', e); + primaryFont = await newPdfDoc.embedFont(StandardFonts.Helvetica); + latinFont = primaryFont; + showAlert('Font Warning', 'Could not load the specific font for this language. Some characters may not appear correctly.'); + } + let fullText = ''; for (let i = 1; i <= pdf.numPages; i++) { @@ -135,10 +183,12 @@ async function runOCR() { ); const page = await pdf.getPage(i); const viewport = page.getViewport({ scale }); + const canvas = document.createElement('canvas'); canvas.width = viewport.width; canvas.height = viewport.height; const context = canvas.getContext('2d'); + await page.render({ canvasContext: context, viewport, canvas }).promise; if (binarize) { @@ -155,8 +205,8 @@ async function runOCR() { const pngImageBytes = await new Promise((resolve) => canvas.toBlob((blob) => { const reader = new FileReader(); - // @ts-expect-error TS(2769) FIXME: No overload matches this call. - reader.onload = () => resolve(new Uint8Array(reader.result)); + reader.onload = () => + resolve(new Uint8Array(reader.result as ArrayBuffer)); reader.readAsArrayBuffer(blob); }, 'image/png') ); @@ -172,22 +222,37 @@ async function runOCR() { if (data.hocr) { const words = parseHOCR(data.hocr); - words.forEach((word: any) => { + words.forEach((word: Word) => { const { x0, y0, x1, y1 } = word.bbox; - // Sanitize the text to remove characters WinAnsi cannot encode - const text = sanitizeTextForWinAnsi(word.text); + const text = word.text.replace(/[\u0000-\u001F\u007F-\u009F\u200E\u200F\u202A-\u202E\uFEFF]/g, ''); - // Skip words that become empty after sanitization if (!text.trim()) return; + const hasNonLatin = /[^\u0000-\u007F]/.test(text); + const font = hasNonLatin ? primaryFont : latinFont; + + if (!font) { + console.warn(`Font not available for text: "${text}"`); + return; + } + const bboxWidth = x1 - x0; const bboxHeight = y1 - y0; + if (bboxWidth <= 0 || bboxHeight <= 0) { + return; + } + let fontSize = bboxHeight * 0.9; - let textWidth = font.widthOfTextAtSize(text, fontSize); - while (textWidth > bboxWidth && fontSize > 1) { - fontSize -= 0.5; - textWidth = font.widthOfTextAtSize(text, fontSize); + try { + let textWidth = font.widthOfTextAtSize(text, fontSize); + while (textWidth > bboxWidth && fontSize > 1) { + fontSize -= 0.5; + textWidth = font.widthOfTextAtSize(text, fontSize); + } + } catch (error) { + console.warn(`Could not calculate text width for "${text}":`, error); + return; } try { @@ -200,12 +265,12 @@ async function runOCR() { opacity: 0, }); } catch (error) { - // If drawing fails despite sanitization, log and skip this word console.warn(`Could not draw text "${text}":`, error); } }); } + fullText += data.text + '\n\n'; } @@ -216,23 +281,27 @@ async function runOCR() { document.getElementById('ocr-results').classList.remove('hidden'); createIcons({ icons }); - // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message - document.getElementById('ocr-text-output').value = fullText.trim(); + ( + document.getElementById('ocr-text-output') as HTMLTextAreaElement + ).value = fullText.trim(); document .getElementById('download-searchable-pdf') .addEventListener('click', () => { - downloadFile( - new Blob([searchablePdfBytes], { type: 'application/pdf' }), - 'searchable.pdf' - ); + if (searchablePdfBytes) { + downloadFile( + new Blob([searchablePdfBytes as BlobPart], { type: 'application/pdf' }), + 'searchable.pdf' + ); + } }); // CHANGE: The copy button logic is updated to be safer. document.getElementById('copy-text-btn').addEventListener('click', (e) => { const button = e.currentTarget as HTMLButtonElement; - // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... - const textToCopy = document.getElementById('ocr-text-output').value; + const textToCopy = ( + document.getElementById('ocr-text-output') as HTMLTextAreaElement + ).value; navigator.clipboard.writeText(textToCopy).then(() => { button.textContent = ''; // Clear the button safely @@ -259,8 +328,9 @@ async function runOCR() { document .getElementById('download-txt-btn') .addEventListener('click', () => { - // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message - const textToSave = document.getElementById('ocr-text-output').value; + const textToSave = ( + document.getElementById('ocr-text-output') as HTMLTextAreaElement + ).value; const blob = new Blob([textToSave], { type: 'text/plain' }); downloadFile(blob, 'ocr-text.txt'); }); diff --git a/src/js/logic/txt-to-pdf.ts b/src/js/logic/txt-to-pdf.ts index 5245335..299cc55 100644 --- a/src/js/logic/txt-to-pdf.ts +++ b/src/js/logic/txt-to-pdf.ts @@ -2,6 +2,9 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; import { downloadFile, hexToRgb } from '../utils/helpers.js'; import { state } from '../state.js'; import JSZip from 'jszip'; +import { getFontForLanguage, getLanguageForChar } from '../utils/font-loader.js'; +import { languageToFontFamily } from '../config/font-mappings.js'; +import fontkit from '@pdf-lib/fontkit'; import { PDFDocument as PDFLibDocument, @@ -10,69 +13,46 @@ import { PageSizes, } from 'pdf-lib'; -function sanitizeTextForPdf(text: string): string { - return text - .split('') - .map((char) => { - const code = char.charCodeAt(0); - - if (code === 0x20 || code === 0x09 || code === 0x0A) { - return char; - } - - if ((code >= 0x00 && code <= 0x1F) || (code >= 0x7F && code <= 0x9F)) { - return ' '; - } - - if (code < 0x20 || (code > 0x7E && code < 0xA0)) { - return ' '; - } - - const replacements: { [key: number]: string } = { - 0x2018: "'", - 0x2019: "'", - 0x201C: '"', - 0x201D: '"', - 0x2013: '-', - 0x2014: '--', - 0x2026: '...', - 0x00A0: ' ', - }; - - if (replacements[code]) { - return replacements[code]; - } - - try { - if (code <= 0xFF) { - return char; - } - return '?'; - } catch { - return '?'; - } - }) - .join('') - .replace(/[ \t]+/g, ' ') - .replace(/\r\n/g, '\n') - .replace(/\r/g, '\n') - .split('\n') - .map((line) => line.trimEnd()) - .join('\n'); -} - async function createPdfFromText( text: string, - fontFamilyKey: string, + selectedLanguages: string[], fontSize: number, pageSizeKey: string, - colorHex: string + colorHex: string, + orientation: string, + customWidth?: number, + customHeight?: number ): Promise { - const sanitizedText = sanitizeTextForPdf(text); - const pdfDoc = await PDFLibDocument.create(); - const font = await pdfDoc.embedFont(StandardFonts[fontFamilyKey]); - const pageSize = PageSizes[pageSizeKey]; + pdfDoc.registerFontkit(fontkit); + + console.log(`User selected languages: ${selectedLanguages.join(', ')}`); + + const fontMap = new Map(); + const fallbackFont = await pdfDoc.embedFont(StandardFonts.Helvetica); + + if (!selectedLanguages.includes('eng')) { + selectedLanguages.push('eng'); + } + + for (const lang of selectedLanguages) { + try { + const fontBytes = await getFontForLanguage(lang); + const font = await pdfDoc.embedFont(fontBytes, { subset: false }); + fontMap.set(lang, font); + } catch (e) { + console.warn(`Failed to load font for ${lang}, using fallback`, e); + fontMap.set(lang, fallbackFont); + } + } + + let pageSize = pageSizeKey === 'Custom' + ? [customWidth || 595, customHeight || 842] as [number, number] + : PageSizes[pageSizeKey]; + + if (orientation === 'landscape') { + pageSize = [pageSize[1], pageSize[0]] as [number, number]; + } const margin = 72; const textColor = hexToRgb(colorHex); @@ -82,43 +62,130 @@ async function createPdfFromText( const lineHeight = fontSize * 1.3; let y = height - margin; - const paragraphs = sanitizedText.split('\n'); + const paragraphs = text.split('\n'); + for (const paragraph of paragraphs) { + if (paragraph.trim() === '') { + y -= lineHeight; + if (y < margin) { + page = pdfDoc.addPage(pageSize); + y = page.getHeight() - margin; + } + continue; + } + const words = paragraph.split(' '); - let currentLine = ''; + let currentLineWords: { text: string; font: any }[] = []; + let currentLineWidth = 0; + for (const word of words) { - const testLine = - currentLine.length > 0 ? `${currentLine} ${word}` : word; - if (font.widthOfTextAtSize(testLine, fontSize) <= textWidth) { - currentLine = testLine; + let wordLang = 'eng'; + + for (const char of word) { + const charLang = getLanguageForChar(char); + + if (charLang === 'chi_sim') { + if (selectedLanguages.includes('jpn')) wordLang = 'jpn'; + else if (selectedLanguages.includes('kor')) wordLang = 'kor'; + else if (selectedLanguages.includes('chi_tra')) wordLang = 'chi_tra'; + else if (selectedLanguages.includes('chi_sim')) wordLang = 'chi_sim'; + } else if (selectedLanguages.includes(charLang)) { + wordLang = charLang; + } + + if (wordLang !== 'eng') break; + } + + const font = fontMap.get(wordLang) || fontMap.get('eng') || fallbackFont; + + let wordWidth = 0; + try { + wordWidth = font.widthOfTextAtSize(word, fontSize); + } catch (e) { + console.warn(`Width calculation failed for "${word}"`, e); + wordWidth = word.length * fontSize * 0.5; + } + + let spaceWidth = 0; + if (currentLineWords.length > 0) { + try { + spaceWidth = font.widthOfTextAtSize(' ', fontSize); + } catch { + spaceWidth = fontSize * 0.25; + } + } + + if (currentLineWidth + spaceWidth + wordWidth <= textWidth) { + currentLineWords.push({ text: word, font }); + currentLineWidth += spaceWidth + wordWidth; } else { + // Draw current line if (y < margin + lineHeight) { page = pdfDoc.addPage(pageSize); y = page.getHeight() - margin; } - page.drawText(currentLine, { - x: margin, - y, - font, - size: fontSize, - color: rgb(textColor.r, textColor.g, textColor.b), - }); + + let currentX = margin; + for (let i = 0; i < currentLineWords.length; i++) { + const w = currentLineWords[i]; + try { + page.drawText(w.text, { + x: currentX, + y, + font: w.font, + size: fontSize, + color: rgb(textColor.r, textColor.g, textColor.b), + }); + + const wWidth = w.font.widthOfTextAtSize(w.text, fontSize); + currentX += wWidth; + + if (i < currentLineWords.length - 1) { + const sWidth = w.font.widthOfTextAtSize(' ', fontSize); + currentX += sWidth; + } + } catch (e) { + console.warn(`Failed to draw word: "${w.text}"`, e); + } + } + y -= lineHeight; - currentLine = word; + + currentLineWords = [{ text: word, font }]; + currentLineWidth = wordWidth; } } - if (currentLine.length > 0) { + + if (currentLineWords.length > 0) { if (y < margin + lineHeight) { page = pdfDoc.addPage(pageSize); y = page.getHeight() - margin; } - page.drawText(currentLine, { - x: margin, - y, - font, - size: fontSize, - color: rgb(textColor.r, textColor.g, textColor.b), - }); + + let currentX = margin; + for (let i = 0; i < currentLineWords.length; i++) { + const w = currentLineWords[i]; + try { + page.drawText(w.text, { + x: currentX, + y, + font: w.font, + size: fontSize, + color: rgb(textColor.r, textColor.g, textColor.b), + }); + + const wWidth = w.font.widthOfTextAtSize(w.text, fontSize); + currentX += wWidth; + + if (i < currentLineWords.length - 1) { + const sWidth = w.font.widthOfTextAtSize(' ', fontSize); + currentX += sWidth; + } + } catch (e) { + console.warn(`Failed to draw word: "${w.text}"`, e); + } + } + y -= lineHeight; } } @@ -134,6 +201,99 @@ export async function setupTxtToPdfTool() { if (!uploadBtn || !textBtn || !uploadPanel || !textPanel) return; + const langContainer = document.getElementById('language-list-container'); + const dropdownBtn = document.getElementById('lang-dropdown-btn'); + const dropdownContent = document.getElementById('lang-dropdown-content'); + const dropdownText = document.getElementById('lang-dropdown-text'); + const searchInput = document.getElementById('lang-search'); + + if (langContainer && langContainer.children.length === 0) { + const allLanguages = Object.keys(languageToFontFamily).sort().map(code => { + let name = code; + try { + const displayNames = new Intl.DisplayNames(['en'], { type: 'language' }); + name = displayNames.of(code) || code; + } catch (e) { + console.warn(`Failed to get language name for ${code}`, e); + } + return { code, name: `${name} (${code})` }; + }); + + const renderLanguages = (filter = '') => { + langContainer.innerHTML = ''; + const lowerFilter = filter.toLowerCase(); + + allLanguages.forEach(lang => { + if (lang.name.toLowerCase().includes(lowerFilter) || lang.code.toLowerCase().includes(lowerFilter)) { + const wrapper = document.createElement('div'); + wrapper.className = 'flex items-center hover:bg-gray-700 p-1 rounded'; + + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.value = lang.code; + checkbox.id = `lang-${lang.code}`; + checkbox.className = 'w-4 h-4 text-indigo-600 bg-gray-600 border-gray-500 rounded focus:ring-indigo-500 ring-offset-gray-800'; + if (lang.code === 'eng') checkbox.checked = true; + + const label = document.createElement('label'); + label.htmlFor = `lang-${lang.code}`; + label.className = 'ml-2 text-sm font-medium text-gray-300 w-full cursor-pointer'; + label.textContent = lang.name; + + checkbox.addEventListener('change', updateButtonText); + + wrapper.appendChild(checkbox); + wrapper.appendChild(label); + langContainer.appendChild(wrapper); + } + }); + }; + + renderLanguages(); + + if (searchInput) { + searchInput.addEventListener('input', (e) => { + const filter = (e.target as HTMLInputElement).value.toLowerCase(); + const items = langContainer.children; + for (let i = 0; i < items.length; i++) { + const item = items[i] as HTMLElement; + const text = item.textContent?.toLowerCase() || ''; + if (text.includes(filter)) { + item.classList.remove('hidden'); + } else { + item.classList.add('hidden'); + } + } + }); + } + + if (dropdownBtn && dropdownContent) { + dropdownBtn.addEventListener('click', (e) => { + e.stopPropagation(); + dropdownContent.classList.toggle('hidden'); + }); + + document.addEventListener('click', (e) => { + if (!dropdownBtn.contains(e.target as Node) && !dropdownContent.contains(e.target as Node)) { + dropdownContent.classList.add('hidden'); + } + }); + } + + function updateButtonText() { + const checkboxes = langContainer?.querySelectorAll('input[type="checkbox"]:checked'); + const count = checkboxes?.length || 0; + if (count === 0) { + if (dropdownText) dropdownText.textContent = 'Select Languages'; + } else if (count === 1) { + const text = checkboxes[0].nextElementSibling.textContent; + if (dropdownText) dropdownText.textContent = text || '1 Language Selected'; + } else { + if (dropdownText) dropdownText.textContent = `${count} Languages Selected`; + } + } + } + const switchToUpload = () => { uploadPanel.classList.remove('hidden'); textPanel.classList.add('hidden'); @@ -155,6 +315,19 @@ export async function setupTxtToPdfTool() { uploadBtn.addEventListener('click', switchToUpload); textBtn.addEventListener('click', switchToText); + const pageSizeSelect = document.getElementById('page-size') as HTMLSelectElement; + const customSizeContainer = document.getElementById('custom-size-container'); + + if (pageSizeSelect && customSizeContainer) { + pageSizeSelect.addEventListener('change', () => { + if (pageSizeSelect.value === 'Custom') { + customSizeContainer.classList.remove('hidden'); + } else { + customSizeContainer.classList.add('hidden'); + } + }); + } + const processBtn = document.getElementById('process-btn'); if (processBtn) { processBtn.onclick = txtToPdf; @@ -167,25 +340,41 @@ export async function txtToPdf() { showLoader('Creating PDF...'); try { - // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message - const fontFamilyKey = document.getElementById('font-family').value; - // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message - const fontSize = parseInt(document.getElementById('font-size').value) || 12; - // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message - const pageSizeKey = document.getElementById('page-size').value; - // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message - const colorHex = document.getElementById('text-color').value; + const selectedLanguages: string[] = []; + const langContainer = document.getElementById('language-list-container'); + if (langContainer) { + const checkboxes = langContainer.querySelectorAll('input[type="checkbox"]:checked'); + checkboxes.forEach((cb) => { + selectedLanguages.push((cb as HTMLInputElement).value); + }); + } + if (selectedLanguages.length === 0) selectedLanguages.push('eng'); // Fallback + + const fontSize = parseInt((document.getElementById('font-size') as HTMLInputElement)?.value) || 12; + const pageSizeKey = (document.getElementById('page-size') as HTMLSelectElement)?.value; + const orientation = (document.getElementById('page-orientation') as HTMLSelectElement)?.value || 'portrait'; + const colorHex = (document.getElementById('text-color') as HTMLInputElement)?.value; + + let customWidth: number | undefined; + let customHeight: number | undefined; + if (pageSizeKey === 'Custom') { + customWidth = parseInt((document.getElementById('custom-width') as HTMLInputElement)?.value) || 595; + customHeight = parseInt((document.getElementById('custom-height') as HTMLInputElement)?.value) || 842; + } if (isUploadMode && state.files.length > 0) { if (state.files.length === 1) { const file = state.files[0]; - const text = await file.text(); + const text = (await file.text()).normalize('NFC'); const pdfBytes = await createPdfFromText( text, - fontFamilyKey, + selectedLanguages, fontSize, pageSizeKey, - colorHex + colorHex, + orientation, + customWidth, + customHeight ); const baseName = file.name.replace(/\.txt$/i, ''); downloadFile( @@ -197,13 +386,16 @@ export async function txtToPdf() { const zip = new JSZip(); for (const file of state.files) { - const text = await file.text(); + const text = (await file.text()).normalize('NFC'); const pdfBytes = await createPdfFromText( text, - fontFamilyKey, + selectedLanguages, fontSize, pageSizeKey, - colorHex + colorHex, + orientation, + customWidth, + customHeight ); const baseName = file.name.replace(/\.txt$/i, ''); zip.file(`${baseName}.pdf`, pdfBytes); @@ -213,8 +405,7 @@ export async function txtToPdf() { downloadFile(zipBlob, 'text-to-pdf.zip'); } } else { - // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message - const text = document.getElementById('text-input').value; + const text = ((document.getElementById('text-input') as HTMLTextAreaElement)?.value || '').normalize('NFC'); if (!text.trim()) { showAlert('Input Required', 'Please enter some text to convert.'); hideLoader(); @@ -223,10 +414,13 @@ export async function txtToPdf() { const pdfBytes = await createPdfFromText( text, - fontFamilyKey, + selectedLanguages, fontSize, pageSizeKey, - colorHex + colorHex, + orientation, + customWidth, + customHeight ); downloadFile( new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), diff --git a/src/js/main.ts b/src/js/main.ts index 97a0faa..fe54d03 100644 --- a/src/js/main.ts +++ b/src/js/main.ts @@ -140,15 +140,20 @@ const init = () => { hideBrandingSections(); } - // Hide shortcuts button on touch devices - const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0; - if (isTouchDevice) { - const shortcutsBtn = document.getElementById('open-shortcuts-btn'); - if (shortcutsBtn) { - shortcutsBtn.style.display = 'none'; - } - } + // Hide shortcuts buttons on mobile devices (Android/iOS) + // exclude iPad -> users can connect keyboard and use shortcuts + const isMobile = /Android|iPhone|iPod/i.test(navigator.userAgent); + const keyboardShortcutBtn = document.getElementById('shortcut'); + const shortcutSettingsBtn = document.getElementById('open-shortcuts-btn'); + if (isMobile) { + keyboardShortcutBtn.style.display = 'none'; + shortcutSettingsBtn.style.display = 'none'; + } else { + keyboardShortcutBtn.textContent = navigator.userAgent.toUpperCase().includes('MAC') + ? '⌘ + K' + : 'Ctrl + K'; + } dom.toolGrid.textContent = ''; @@ -205,6 +210,22 @@ const init = () => { const searchBar = document.getElementById('search-bar'); const categoryGroups = dom.toolGrid.querySelectorAll('.category-group'); + + const fuzzyMatch = (searchTerm: string, targetText: string): boolean => { + if (!searchTerm) return true; + + let searchIndex = 0; + let targetIndex = 0; + + while (searchIndex < searchTerm.length && targetIndex < targetText.length) { + if (searchTerm[searchIndex] === targetText[targetIndex]) { + searchIndex++; + } + targetIndex++; + } + + return searchIndex === searchTerm.length; + }; searchBar.addEventListener('input', () => { // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message @@ -218,8 +239,9 @@ const init = () => { const toolName = card.querySelector('h3').textContent.toLowerCase(); const toolSubtitle = card.querySelector('p')?.textContent.toLowerCase() || ''; + const isMatch = - toolName.includes(searchTerm) || toolSubtitle.includes(searchTerm); + fuzzyMatch(searchTerm, toolName) || fuzzyMatch(searchTerm, toolSubtitle); card.classList.toggle('hidden', !isMatch); if (isMatch) { @@ -243,17 +265,6 @@ const init = () => { } }); - const shortcutK = document.getElementById('shortcut'); - const isIosOrAndroid = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent); - - if (isIosOrAndroid) { - shortcutK.style.display = 'none'; - } else { - shortcutK.textContent = navigator.userAgent.toUpperCase().includes('MAC') - ? '⌘ + K' - : 'Ctrl + K'; - } - dom.toolGrid.addEventListener('click', (e) => { // @ts-expect-error TS(2339) FIXME: Property 'closest' does not exist on type 'EventTa... Remove this comment to see the full error message const card = e.target.closest('.tool-card'); diff --git a/src/js/types/form-creator.ts b/src/js/types/form-creator.ts new file mode 100644 index 0000000..f0f7c5b --- /dev/null +++ b/src/js/types/form-creator.ts @@ -0,0 +1,40 @@ +export interface FormField { + id: string + type: 'text' | 'checkbox' | 'radio' | 'dropdown' | 'optionlist' | 'button' | 'signature' | 'date' | 'image' + x: number + y: number + width: number + height: number + name: string + defaultValue: string + fontSize: number + alignment: 'left' | 'center' | 'right' + textColor: string + required: boolean + readOnly: boolean + tooltip: string + combCells: number + maxLength: number + options?: string[] + checked?: boolean + exportValue?: string + groupName?: string + label?: string + pageIndex: number + action?: 'none' | 'reset' | 'print' | 'url' | 'js' | 'showHide' + actionUrl?: string + jsScript?: string + targetFieldName?: string + visibilityAction?: 'show' | 'hide' | 'toggle' + dateFormat?: string + multiline?: boolean + borderColor?: string + hideBorder?: boolean +} + +export interface PageData { + index: number + width: number + height: number + pdfPageData?: string +} diff --git a/src/js/types/index.ts b/src/js/types/index.ts new file mode 100644 index 0000000..1e88b7b --- /dev/null +++ b/src/js/types/index.ts @@ -0,0 +1,2 @@ +export * from './ocr.js'; +export * from './form-creator.js'; diff --git a/src/js/types/ocr.ts b/src/js/types/ocr.ts new file mode 100644 index 0000000..84e7d53 --- /dev/null +++ b/src/js/types/ocr.ts @@ -0,0 +1,10 @@ +export interface Word { + text: string; + bbox: { + x0: number; + y0: number; + x1: number; + y1: number; + }; + confidence: number; +} diff --git a/src/js/ui.ts b/src/js/ui.ts index e85d265..f929b49 100644 --- a/src/js/ui.ts +++ b/src/js/ui.ts @@ -1309,12 +1309,21 @@ export const toolTemplates = {
- - + +
+ + +
@@ -1323,10 +1332,42 @@ export const toolTemplates = {
+
+ + +
+
diff --git a/src/js/utils/font-loader.ts b/src/js/utils/font-loader.ts new file mode 100644 index 0000000..7d2bc83 --- /dev/null +++ b/src/js/utils/font-loader.ts @@ -0,0 +1,281 @@ +import { languageToFontFamily, fontFamilyToUrl } from '../config/font-mappings.js'; + +const fontCache: Map = new Map(); + +const DB_NAME = 'bentopdf-fonts'; +const DB_VERSION = 1; +const STORE_NAME = 'fonts'; + +async function openFontDB(): Promise { + return new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME, DB_VERSION); + + request.onerror = () => reject(request.error); + request.onsuccess = () => resolve(request.result); + + request.onupgradeneeded = (event) => { + const db = (event.target as IDBOpenDBRequest).result; + if (!db.objectStoreNames.contains(STORE_NAME)) { + db.createObjectStore(STORE_NAME); + } + }; + }); +} + +async function getCachedFontFromDB(fontFamily: string): Promise { + try { + const db = await openFontDB(); + return new Promise((resolve, reject) => { + const transaction = db.transaction(STORE_NAME, 'readonly'); + const store = transaction.objectStore(STORE_NAME); + const request = store.get(fontFamily); + + request.onsuccess = () => resolve(request.result || null); + request.onerror = () => reject(request.error); + }); + } catch (error) { + console.warn('IndexedDB read failed:', error); + return null; + } +} + +async function saveFontToDB(fontFamily: string, fontBuffer: ArrayBuffer): Promise { + try { + const db = await openFontDB(); + return new Promise((resolve, reject) => { + const transaction = db.transaction(STORE_NAME, 'readwrite'); + const store = transaction.objectStore(STORE_NAME); + const request = store.put(fontBuffer, fontFamily); + + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }); + } catch (error) { + console.warn('IndexedDB write failed:', error); + } +} + +export async function getFontForLanguage(lang: string): Promise { + const fontFamily = languageToFontFamily[lang] || 'Noto Sans'; + + if (fontCache.has(fontFamily)) { + return fontCache.get(fontFamily)!; + } + const cachedFont = await getCachedFontFromDB(fontFamily); + if (cachedFont) { + fontCache.set(fontFamily, cachedFont); + return cachedFont; + } + + try { + const fontUrl = fontFamilyToUrl[fontFamily] || fontFamilyToUrl['Noto Sans']; + + const fontResponse = await fetch(fontUrl); + + if (!fontResponse.ok) { + throw new Error(`Failed to fetch font file: ${fontResponse.statusText}`); + } + + const fontBuffer = await fontResponse.arrayBuffer(); + + fontCache.set(fontFamily, fontBuffer); + await saveFontToDB(fontFamily, fontBuffer); + + return fontBuffer; + } catch (error) { + console.warn(`Failed to fetch font for ${lang} (${fontFamily}), falling back to default.`, error); + + if (fontFamily !== 'Noto Sans') { + return await getFontForLanguage('eng'); + } + + throw error; + } +} + +export function detectScripts(text: string): string[] { + const scripts = new Set(); + + // Japanese: Hiragana (\u3040-\u309F) & Katakana (\u30A0-\u30FF) + if (/[\u3040-\u309F\u30A0-\u30FF]/.test(text)) { + scripts.add('jpn'); + } + + // Korean: Hangul Syllables (\uAC00-\uD7A3) & Jamo (\u1100-\u11FF) + if (/[\uAC00-\uD7A3\u1100-\u11FF]/.test(text)) { + scripts.add('kor'); + } + + // Chinese: CJK Unified Ideographs (\u4E00-\u9FFF) & Ext A (\u3400-\u4DBF) + if (/[\u4E00-\u9FFF\u3400-\u4DBF]/.test(text)) { + scripts.add('chi_sim'); + } + + // Check for Arabic + if (/[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF]/.test(text)) { + scripts.add('ara'); + } + + // Check for Devanagari (Hindi, Marathi, etc.) + if (/[\u0900-\u097F]/.test(text)) scripts.add('hin'); + + // Check for Bengali + if (/[\u0980-\u09FF]/.test(text)) scripts.add('ben'); + + // Check for Tamil + if (/[\u0B80-\u0BFF]/.test(text)) scripts.add('tam'); + + // Check for Telugu + if (/[\u0C00-\u0C7F]/.test(text)) scripts.add('tel'); + + // Check for Kannada + if (/[\u0C80-\u0CFF]/.test(text)) scripts.add('kan'); + + // Check for Malayalam + if (/[\u0D00-\u0D7F]/.test(text)) scripts.add('mal'); + + // Check for Gujarati + if (/[\u0A80-\u0AFF]/.test(text)) scripts.add('guj'); + + // Check for Punjabi (Gurmukhi) + if (/[\u0A00-\u0A7F]/.test(text)) scripts.add('pan'); + + // Check for Oriya + if (/[\u0B00-\u0B7F]/.test(text)) scripts.add('ori'); + + // Check for Sinhala + if (/[\u0D80-\u0DFF]/.test(text)) scripts.add('sin'); + + // Check for Thai + if (/[\u0E00-\u0E7F]/.test(text)) scripts.add('tha'); + + // Check for Lao + if (/[\u0E80-\u0EFF]/.test(text)) scripts.add('lao'); + + // Check for Khmer + if (/[\u1780-\u17FF]/.test(text)) scripts.add('khm'); + + // Check for Myanmar + if (/[\u1000-\u109F]/.test(text)) scripts.add('mya'); + + // Check for Tibetan + if (/[\u0F00-\u0FFF]/.test(text)) scripts.add('bod'); + + // Check for Georgian + if (/[\u10A0-\u10FF]/.test(text)) scripts.add('kat'); + + // Check for Armenian + if (/[\u0530-\u058F]/.test(text)) scripts.add('hye'); + + // Check for Hebrew + if (/[\u0590-\u05FF]/.test(text)) scripts.add('heb'); + + // Check for Ethiopic + if (/[\u1200-\u137F]/.test(text)) scripts.add('amh'); + + // Check for Cherokee + if (/[\u13A0-\u13FF]/.test(text)) scripts.add('chr'); + + // Check for Syriac + if (/[\u0700-\u074F]/.test(text)) scripts.add('syr'); + + if (scripts.size === 0 || /[a-zA-Z]/.test(text)) { + scripts.add('eng'); + } + + return Array.from(scripts); +} + +export function getLanguageForChar(char: string): string { + const code = char.charCodeAt(0); + + // Latin (Basic + Supplement + Extended) + if (code <= 0x024F) return 'eng'; + + // Japanese: Hiragana & Katakana + if ( + (code >= 0x3040 && code <= 0x309F) || // Hiragana + (code >= 0x30A0 && code <= 0x30FF) // Katakana + ) return 'jpn'; + + // Korean: Hangul Syllables & Jamo + if ( + (code >= 0xAC00 && code <= 0xD7A3) || // Hangul Syllables + (code >= 0x1100 && code <= 0x11FF) // Hangul Jamo + ) return 'kor'; + + // Chinese: CJK Unified Ideographs (Han) + if ( + (code >= 0x4E00 && code <= 0x9FFF) || // CJK Unified + (code >= 0x3400 && code <= 0x4DBF) // CJK Ext A + ) return 'chi_sim'; + + // Arabic + if ((code >= 0x0600 && code <= 0x06FF) || (code >= 0x0750 && code <= 0x077F) || (code >= 0x08A0 && code <= 0x08FF)) return 'ara'; + + // Devanagari + if (code >= 0x0900 && code <= 0x097F) return 'hin'; + + // Bengali + if (code >= 0x0980 && code <= 0x09FF) return 'ben'; + + // Tamil + if (code >= 0x0B80 && code <= 0x0BFF) return 'tam'; + + // Telugu + if (code >= 0x0C00 && code <= 0x0C7F) return 'tel'; + + // Kannada + if (code >= 0x0C80 && code <= 0x0CFF) return 'kan'; + + // Malayalam + if (code >= 0x0D00 && code <= 0x0D7F) return 'mal'; + + // Gujarati + if (code >= 0x0A80 && code <= 0x0AFF) return 'guj'; + + // Punjabi (Gurmukhi) + if (code >= 0x0A00 && code <= 0x0A7F) return 'pan'; + + // Oriya + if (code >= 0x0B00 && code <= 0x0B7F) return 'ori'; + + // Sinhala + if (code >= 0x0D80 && code <= 0x0DFF) return 'sin'; + + // Thai + if (code >= 0x0E00 && code <= 0x0E7F) return 'tha'; + + // Lao + if (code >= 0x0E80 && code <= 0x0EFF) return 'lao'; + + // Khmer + if (code >= 0x1780 && code <= 0x17FF) return 'khm'; + + // Myanmar + if (code >= 0x1000 && code <= 0x109F) return 'mya'; + + // Tibetan + if (code >= 0x0F00 && code <= 0x0FFF) return 'bod'; + + // Georgian + if (code >= 0x10A0 && code <= 0x10FF) return 'kat'; + + // Armenian + if (code >= 0x0530 && code <= 0x058F) return 'hye'; + + // Hebrew + if (code >= 0x0590 && code <= 0x05FF) return 'heb'; + + // Ethiopic + if (code >= 0x1200 && code <= 0x137F) return 'amh'; + + // Cherokee + if (code >= 0x13A0 && code <= 0x13FF) return 'chr'; + + // Syriac + if (code >= 0x0700 && code <= 0x074F) return 'syr'; + + // Default to English (Latin) + return 'eng'; +} diff --git a/src/pages/form-creator.html b/src/pages/form-creator.html index 3206a6f..e0deb9b 100644 --- a/src/pages/form-creator.html +++ b/src/pages/form-creator.html @@ -205,6 +205,27 @@ disabled> +
+ Go to: + + +
+
+ + + + x + + +