diff --git a/.github/workflows/build-and-publish.yml b/.github/workflows/build-and-publish.yml index cdfc700..681df57 100644 --- a/.github/workflows/build-and-publish.yml +++ b/.github/workflows/build-and-publish.yml @@ -6,7 +6,7 @@ on: - 'main' tags: - 'v*' - workflow_dispatch: + workflow_dispatch: jobs: docker-build-and-push: @@ -19,10 +19,10 @@ jobs: mode: - name: default simple_mode: false - suffix: "" + suffix: '' - name: simple simple_mode: true - suffix: "-simple" + suffix: '-simple' steps: - name: Set up QEMU uses: docker/setup-qemu-action@v3 diff --git a/SIMPLE_MODE.md b/SIMPLE_MODE.md index 5a9873e..efb8bff 100644 --- a/SIMPLE_MODE.md +++ b/SIMPLE_MODE.md @@ -66,6 +66,7 @@ npm run serve:simple ``` This command automatically: + - Sets `SIMPLE_MODE=true` - Builds the project with Simple Mode enabled - Serves the built files on `http://localhost:3000` @@ -118,7 +119,7 @@ Open `http://localhost:3000` in your browser. # Test Normal Mode docker run -p 3000:80 bentopdf/bentopdf:latest -# Test Simple Mode +# Test Simple Mode docker run -p 3001:80 bentopdf/bentopdf-simple:latest ``` @@ -140,16 +141,19 @@ When Simple Mode is working correctly, you should see: ## 📦 Available Docker Images ### Normal Mode (Full Branding) + - `bentopdf/bentopdf:latest` - `bentopdf/bentopdf:v1.0.0` (versioned) ### Simple Mode (Clean Interface) + - `bentopdf/bentopdf-simple:latest` - `bentopdf/bentopdf-simple:v1.0.0` (versioned) ## 🚀 Production Deployment Examples ### Internal Company Tool + ```yaml services: bentopdf: @@ -157,14 +161,12 @@ services: container_name: bentopdf restart: unless-stopped ports: - - "80:80" + - '80:80' environment: - PUID=1000 - PGID=1000 ``` - - ## ⚠️ Important Notes - **Pre-built images**: Use `bentopdf/bentopdf-simple:latest` for Simple Mode diff --git a/docker-compose.yml b/docker-compose.yml index 3906b17..29f39a7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,9 +1,9 @@ services: bentopdf: - # simple mode - bentopdf/bentopdf-simple:latest - # default mode - bentopdf/bentopdf:latest + # simple mode - bentopdf/bentopdf-simple:latest + # default mode - bentopdf/bentopdf:latest image: bentopdf/bentopdf-simple:latest container_name: bentopdf restart: unless-stopped ports: - - '3000:80' \ No newline at end of file + - '3000:80' diff --git a/src/js/handlers/fileHandler.ts b/src/js/handlers/fileHandler.ts index dc0a698..9d6f250 100644 --- a/src/js/handlers/fileHandler.ts +++ b/src/js/handlers/fileHandler.ts @@ -7,7 +7,7 @@ import { renderFileDisplay, switchView, } from '../ui.js'; -import { readFileAsArrayBuffer } from '../utils/helpers.js'; +import { formatIsoDate, readFileAsArrayBuffer } from '../utils/helpers.js'; import { setupCanvasEditor } from '../canvasEditor.js'; import { toolLogic } from '../logic/index.js'; import { renderDuplicateOrganizeThumbnails } from '../logic/duplicate-organize.js'; @@ -124,12 +124,13 @@ async function handleSinglePdfUpload(toolId, file) { const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfBytes as ArrayBuffer, }).promise; - const [metadata, fieldObjects] = await Promise.all([ + const [metadataResult, fieldObjects] = await Promise.all([ pdfjsDoc.getMetadata(), pdfjsDoc.getFieldObjects(), ]); - const { info, metadata: rawXmpString } = metadata; + const { info, metadata } = metadataResult; + const rawXmpString = metadata ? metadata.getRaw() : null; resultsDiv.textContent = ''; // Clear safely @@ -184,14 +185,29 @@ async function handleSinglePdfUpload(toolId, file) { const infoSection = createSection('Info Dictionary'); if (info && Object.keys(info).length > 0) { for (const key in info) { - let value = info[key] || '- Not Set -'; - if ( + let value = info[key]; + let displayValue; + + if (value === null || typeof value === 'undefined') { + displayValue = '- Not Set -'; + } else if (typeof value === 'object' && value.name) { + displayValue = value.name; + } else if (typeof value === 'object') { + try { + displayValue = JSON.stringify(value); + } catch { + displayValue = '[object Object]'; + } + } else if ( (key === 'CreationDate' || key === 'ModDate') && typeof value === 'string' ) { - value = parsePdfDate(value); + displayValue = parsePdfDate(value); + } else { + displayValue = String(value); } - infoSection.ul.appendChild(createListItem(key, String(value))); + + infoSection.ul.appendChild(createListItem(key, displayValue)); } } else { infoSection.ul.innerHTML = `
  • - No Info Dictionary data found -
  • `; @@ -212,19 +228,119 @@ async function handleSinglePdfUpload(toolId, file) { } resultsDiv.appendChild(fieldsSection.wrapper); - const xmpSection = createSection('XMP Metadata (Raw XML)'); - const xmpContainer = document.createElement('div'); - xmpContainer.className = - 'bg-gray-900 p-4 rounded-lg border border-gray-700'; + const createXmpListItem = (key, value, indent = 0) => { + const li = document.createElement('li'); + li.className = 'flex flex-col sm:flex-row'; + + const strong = document.createElement('strong'); + strong.className = 'w-56 flex-shrink-0 text-gray-400'; + strong.textContent = key; + strong.style.paddingLeft = `${indent * 1.2}rem`; + + const div = document.createElement('div'); + div.className = 'flex-grow text-white break-all'; + div.textContent = value; + + li.append(strong, div); + return li; + }; + + const createXmpHeaderItem = (key, indent = 0) => { + const li = document.createElement('li'); + li.className = 'flex pt-2'; + const strong = document.createElement('strong'); + strong.className = 'w-full flex-shrink-0 text-gray-300 font-medium'; + strong.textContent = key; + strong.style.paddingLeft = `${indent * 1.2}rem`; + li.append(strong); + return li; + }; + + const appendXmpNodes = (xmlNode, ulElement, indentLevel) => { + const xmpDateKeys = [ + 'xap:CreateDate', + 'xap:ModifyDate', + 'xap:MetadataDate', + ]; + + const childNodes = Array.from(xmlNode.children); + + for (const child of childNodes) { + if ((child as Element).nodeType !== 1) continue; + + let key = (child as Element).tagName; + const elementChildren = Array.from((child as Element).children).filter( + (c) => c.nodeType === 1 + ); + + if (key === 'rdf:li') { + appendXmpNodes(child, ulElement, indentLevel); + continue; + } + if (key === 'rdf:Alt') { + key = '(alt container)'; + } + + if ( + (child as Element).getAttribute('rdf:parseType') === 'Resource' && + elementChildren.length === 0 + ) { + ulElement.appendChild( + createXmpListItem(key, '(Empty Resource)', indentLevel) + ); + continue; + } + + if (elementChildren.length > 0) { + ulElement.appendChild(createXmpHeaderItem(key, indentLevel)); + appendXmpNodes(child, ulElement, indentLevel + 1); + } else { + let value = (child as Element).textContent.trim(); + if (value) { + if (xmpDateKeys.includes(key)) { + value = formatIsoDate(value); + } + ulElement.appendChild( + createXmpListItem(key, value, indentLevel) + ); + } + } + } + }; + + const xmpSection = createSection('XMP Metadata'); if (rawXmpString) { - const pre = document.createElement('pre'); - pre.className = 'text-xs text-gray-300 whitespace-pre-wrap break-all'; - pre.textContent = String(rawXmpString); - xmpContainer.appendChild(pre); + try { + const parser = new DOMParser(); + const xmlDoc = parser.parseFromString( + rawXmpString, + 'application/xml' + ); + + const descriptions = xmlDoc.getElementsByTagName('rdf:Description'); + if (descriptions.length > 0) { + for (const desc of descriptions) { + appendXmpNodes(desc, xmpSection.ul, 0); + } + } else { + appendXmpNodes(xmlDoc.documentElement, xmpSection.ul, 0); + } + + if (xmpSection.ul.children.length === 0) { + xmpSection.ul.innerHTML = `
  • - No parseable XMP properties found -
  • `; + } + } catch (xmlError) { + console.error('Failed to parse XMP XML:', xmlError); + xmpSection.ul.innerHTML = `
  • - Error parsing XMP XML. Displaying raw. -
  • `; + const pre = document.createElement('pre'); + pre.className = + 'text-xs text-gray-300 whitespace-pre-wrap break-all'; + pre.textContent = rawXmpString; + xmpSection.ul.appendChild(pre); + } } else { - xmpContainer.innerHTML = `

    - No XMP metadata found -

    `; + xmpSection.ul.innerHTML = `
  • - No XMP metadata found -
  • `; } - xmpSection.wrapper.appendChild(xmpContainer); resultsDiv.appendChild(xmpSection.wrapper); resultsDiv.classList.remove('hidden'); diff --git a/src/js/main.ts b/src/js/main.ts index dc529e8..8ac0a92 100644 --- a/src/js/main.ts +++ b/src/js/main.ts @@ -19,10 +19,11 @@ const init = () => { if (nav) { // Hide the entire nav but we'll create a minimal one with just logo nav.style.display = 'none'; - + // Create a simple nav with just logo on the right const simpleNav = document.createElement('nav'); - simpleNav.className = 'bg-gray-800 border-b border-gray-700 sticky top-0 z-30'; + simpleNav.className = + 'bg-gray-800 border-b border-gray-700 sticky top-0 z-30'; simpleNav.innerHTML = `
    @@ -58,7 +59,9 @@ const init = () => { faqSection.style.display = 'none'; } - const testimonialsSection = document.getElementById('testimonials-section'); + const testimonialsSection = document.getElementById( + 'testimonials-section' + ); if (testimonialsSection) { testimonialsSection.style.display = 'none'; } @@ -72,7 +75,7 @@ const init = () => { const footer = document.querySelector('footer'); if (footer) { footer.style.display = 'none'; - + const simpleFooter = document.createElement('footer'); simpleFooter.className = 'mt-16 border-t-2 border-gray-700 py-8'; simpleFooter.innerHTML = ` diff --git a/src/js/utils/helpers.ts b/src/js/utils/helpers.ts index 7dd1622..affb8f6 100644 --- a/src/js/utils/helpers.ts +++ b/src/js/utils/helpers.ts @@ -123,3 +123,26 @@ export function parsePageRanges(rangeString: any, totalPages: any) { // @ts-expect-error TS(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message return Array.from(indices).sort((a, b) => a - b); } + +/** + * Formats an ISO 8601 date string (e.g., "2008-02-21T17:15:56-08:00") + * into a localized, human-readable string. + * @param {string} isoDateString - The ISO 8601 date string. + * @returns {string} A localized date and time string, or the original string if parsing fails. + */ +export function formatIsoDate(isoDateString) { + if (!isoDateString || typeof isoDateString !== 'string') { + return isoDateString; // Return original value if it's not a valid string + } + try { + const date = new Date(isoDateString); + // Check if the date object is valid + if (isNaN(date.getTime())) { + return isoDateString; // Return original string if the date is invalid + } + return date.toLocaleString(); + } catch (e) { + console.error('Could not parse ISO date:', e); + return isoDateString; // Return original string on any error + } +}