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
+ }
+}