feat(file-handler): improve metadata display and XMP parsing

- Add new formatIsoDate helper for human-readable dates
- Enhance PDF info dictionary display with better null handling
- Implement structured XMP metadata parsing with formatted output
- Improve error handling for metadata processing
This commit is contained in:
abdullahalam123
2025-10-21 13:38:54 +05:30
parent dbc1b8eb7f
commit 739dac55ee
6 changed files with 175 additions and 31 deletions

View File

@@ -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

View File

@@ -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`
@@ -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

View File

@@ -1,7 +1,7 @@
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

View File

@@ -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 = `<li><span class="text-gray-500 italic">- No Info Dictionary data found -</span></li>`;
@@ -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 = `<li><span class="text-gray-500 italic">- No parseable XMP properties found -</span></li>`;
}
} catch (xmlError) {
console.error('Failed to parse XMP XML:', xmlError);
xmpSection.ul.innerHTML = `<li><span class="text-red-500 italic">- Error parsing XMP XML. Displaying raw. -</span></li>`;
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 = `<p class="text-gray-500 italic">- No XMP metadata found -</p>`;
xmpSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No XMP metadata found -</span></li>`;
}
xmpSection.wrapper.appendChild(xmpContainer);
resultsDiv.appendChild(xmpSection.wrapper);
resultsDiv.classList.remove('hidden');

View File

@@ -22,7 +22,8 @@ const init = () => {
// 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 = `
<div class="container mx-auto px-4">
<div class="flex justify-start items-center h-16">
@@ -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';
}

View File

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