Initial commit
Dieser Commit ist enthalten in:
914
app.js
Normale Datei
914
app.js
Normale Datei
@ -0,0 +1,914 @@
|
||||
// === METADATEN-CRAWLER JAVASCRIPT ===
|
||||
|
||||
// DOM Elements
|
||||
const fileDropZone = document.getElementById('fileDropZone');
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const loading = document.getElementById('loading');
|
||||
const metadataContainer = document.getElementById('metadataContainer');
|
||||
const fileInfo = document.getElementById('fileInfo');
|
||||
const imagePreview = document.getElementById('imagePreview');
|
||||
const metadataContent = document.getElementById('metadataContent');
|
||||
|
||||
// Prevent default drag behaviors
|
||||
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
||||
fileDropZone.addEventListener(eventName, preventDefaults, false);
|
||||
document.body.addEventListener(eventName, preventDefaults, false);
|
||||
});
|
||||
|
||||
function preventDefaults(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
// Highlight drop zone when item is dragged over it
|
||||
['dragenter', 'dragover'].forEach(eventName => {
|
||||
fileDropZone.addEventListener(eventName, highlight, false);
|
||||
});
|
||||
|
||||
['dragleave', 'drop'].forEach(eventName => {
|
||||
fileDropZone.addEventListener(eventName, unhighlight, false);
|
||||
});
|
||||
|
||||
function highlight(e) {
|
||||
fileDropZone.classList.add('drag-over');
|
||||
}
|
||||
|
||||
function unhighlight(e) {
|
||||
fileDropZone.classList.remove('drag-over');
|
||||
}
|
||||
|
||||
// Handle dropped files
|
||||
fileDropZone.addEventListener('drop', handleFileDrop, false);
|
||||
|
||||
function handleFileDrop(e) {
|
||||
const dt = e.dataTransfer;
|
||||
const files = dt.files;
|
||||
if (files.length > 0) {
|
||||
handleFile(files[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle file selection via click
|
||||
fileDropZone.addEventListener('click', () => fileInput.click());
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
if (e.target.files.length > 0) {
|
||||
handleFile(e.target.files[0]);
|
||||
}
|
||||
});
|
||||
|
||||
// Main file handler
|
||||
async function handleFile(file) {
|
||||
// Show loading
|
||||
loading.classList.add('active');
|
||||
metadataContainer.classList.remove('active');
|
||||
|
||||
// Display basic file info
|
||||
const basicInfo = {
|
||||
name: file.name,
|
||||
size: formatFileSize(file.size),
|
||||
type: file.type,
|
||||
lastModified: new Date(file.lastModified).toLocaleString('de-DE')
|
||||
};
|
||||
|
||||
// Check if it's an image or video
|
||||
if (file.type.startsWith('image/')) {
|
||||
await processImage(file, basicInfo);
|
||||
} else if (file.type.startsWith('video/')) {
|
||||
await processVideo(file, basicInfo);
|
||||
} else {
|
||||
alert('Bitte wählen Sie eine Bild- oder Videodatei aus.');
|
||||
loading.classList.remove('active');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Process image files
|
||||
async function processImage(file, basicInfo) {
|
||||
try {
|
||||
// Read file as ArrayBuffer for ExifReader
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
|
||||
// Extract EXIF data
|
||||
let exifData = {};
|
||||
try {
|
||||
const tags = ExifReader.load(arrayBuffer);
|
||||
exifData = tags;
|
||||
} catch (error) {
|
||||
console.log('Keine EXIF-Daten gefunden oder Fehler beim Lesen:', error);
|
||||
}
|
||||
|
||||
// Create object URL for preview
|
||||
const objectUrl = URL.createObjectURL(file);
|
||||
|
||||
// Get image dimensions
|
||||
const img = new Image();
|
||||
img.onload = function() {
|
||||
basicInfo.width = this.width + ' px';
|
||||
basicInfo.height = this.height + ' px';
|
||||
basicInfo.aspect = (this.width / this.height).toFixed(2);
|
||||
|
||||
displayImageMetadata(basicInfo, exifData, objectUrl);
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
};
|
||||
img.src = objectUrl;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Verarbeiten der Bilddatei:', error);
|
||||
alert('Fehler beim Lesen der Metadaten.');
|
||||
loading.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
// Process video files
|
||||
async function processVideo(file, basicInfo) {
|
||||
try {
|
||||
// Create video element to extract basic metadata
|
||||
const video = document.createElement('video');
|
||||
const objectUrl = URL.createObjectURL(file);
|
||||
|
||||
video.preload = 'metadata';
|
||||
video.onloadedmetadata = function() {
|
||||
basicInfo.duration = formatDuration(video.duration);
|
||||
basicInfo.width = video.videoWidth + ' px';
|
||||
basicInfo.height = video.videoHeight + ' px';
|
||||
basicInfo.aspect = (video.videoWidth / video.videoHeight).toFixed(2);
|
||||
|
||||
displayVideoMetadata(basicInfo, objectUrl);
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
};
|
||||
|
||||
video.src = objectUrl;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Verarbeiten der Videodatei:', error);
|
||||
alert('Fehler beim Lesen der Video-Metadaten.');
|
||||
loading.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
// Display image metadata
|
||||
function displayImageMetadata(basicInfo, exifData, previewUrl) {
|
||||
// Store data for report generation
|
||||
currentFileData = basicInfo;
|
||||
currentPreviewUrl = previewUrl;
|
||||
currentMetadata = {};
|
||||
// Update file info
|
||||
fileInfo.innerHTML = `
|
||||
<h3>${basicInfo.name}</h3>
|
||||
<div class="basic-info">
|
||||
<span class="label">Größe:</span><span>${basicInfo.size}</span>
|
||||
<span class="label">Typ:</span><span>${basicInfo.type}</span>
|
||||
<span class="label">Zuletzt geändert:</span><span>${basicInfo.lastModified}</span>
|
||||
<span class="label">Auflösung:</span><span>${basicInfo.width} × ${basicInfo.height}</span>
|
||||
<span class="label">Seitenverhältnis:</span><span>${basicInfo.aspect}:1</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Show preview
|
||||
imagePreview.innerHTML = `<img src="${previewUrl}" alt="Vorschau">`;
|
||||
|
||||
// Organize metadata into sections
|
||||
const sections = {
|
||||
exif: {
|
||||
title: 'EXIF-Daten',
|
||||
data: {},
|
||||
icon: '📷'
|
||||
},
|
||||
camera: {
|
||||
title: 'Kamera-Informationen',
|
||||
data: {},
|
||||
icon: '📸'
|
||||
},
|
||||
location: {
|
||||
title: 'Standort-Informationen',
|
||||
data: {},
|
||||
icon: '📍'
|
||||
},
|
||||
technical: {
|
||||
title: 'Technische Details',
|
||||
data: {},
|
||||
icon: '⚙️'
|
||||
},
|
||||
other: {
|
||||
title: 'Weitere Metadaten',
|
||||
data: {},
|
||||
icon: '📋'
|
||||
}
|
||||
};
|
||||
|
||||
// Process EXIF data and categorize
|
||||
if (exifData) {
|
||||
for (const [key, value] of Object.entries(exifData)) {
|
||||
if (value && value.description !== undefined) {
|
||||
const desc = value.description;
|
||||
|
||||
// Categorize metadata
|
||||
if (key.includes('Make') || key.includes('Model') || key.includes('Lens')) {
|
||||
sections.camera.data[key] = desc;
|
||||
} else if (key.includes('GPS')) {
|
||||
sections.location.data[key] = desc;
|
||||
} else if (key.includes('DateTime') || key.includes('Date')) {
|
||||
sections.exif.data[key] = desc;
|
||||
} else if (key.includes('ISO') || key.includes('Exposure') || key.includes('Aperture') ||
|
||||
key.includes('FocalLength') || key.includes('Flash')) {
|
||||
sections.technical.data[key] = desc;
|
||||
} else {
|
||||
sections.other.data[key] = desc;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build metadata display
|
||||
let metadataHTML = '';
|
||||
|
||||
for (const [sectionKey, section] of Object.entries(sections)) {
|
||||
if (Object.keys(section.data).length > 0) {
|
||||
metadataHTML += createMetadataSection(section.title, section.data, section.icon);
|
||||
// Store for report
|
||||
currentMetadata[section.title] = section.data;
|
||||
}
|
||||
}
|
||||
|
||||
if (metadataHTML === '') {
|
||||
metadataHTML = '<div class="no-metadata">Keine zusätzlichen Metadaten gefunden.</div>';
|
||||
}
|
||||
|
||||
metadataContent.innerHTML = metadataHTML;
|
||||
|
||||
// Add click handlers for collapsible sections
|
||||
addSectionToggleHandlers();
|
||||
|
||||
// Hide loading and show metadata
|
||||
loading.classList.remove('active');
|
||||
metadataContainer.classList.add('active');
|
||||
}
|
||||
|
||||
// Display video metadata
|
||||
function displayVideoMetadata(basicInfo, previewUrl) {
|
||||
// Store data for report generation
|
||||
currentFileData = basicInfo;
|
||||
currentPreviewUrl = previewUrl;
|
||||
currentMetadata = {};
|
||||
// Update file info
|
||||
fileInfo.innerHTML = `
|
||||
<h3>${basicInfo.name}</h3>
|
||||
<div class="basic-info">
|
||||
<span class="label">Größe:</span><span>${basicInfo.size}</span>
|
||||
<span class="label">Typ:</span><span>${basicInfo.type}</span>
|
||||
<span class="label">Zuletzt geändert:</span><span>${basicInfo.lastModified}</span>
|
||||
<span class="label">Dauer:</span><span>${basicInfo.duration}</span>
|
||||
<span class="label">Auflösung:</span><span>${basicInfo.width} × ${basicInfo.height}</span>
|
||||
<span class="label">Seitenverhältnis:</span><span>${basicInfo.aspect}:1</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Show video preview (poster frame)
|
||||
imagePreview.innerHTML = `
|
||||
<video controls style="max-width: 300px; max-height: 300px; border-radius: 8px;">
|
||||
<source src="${previewUrl}" type="${basicInfo.type}">
|
||||
Ihr Browser unterstützt das Video-Tag nicht.
|
||||
</video>
|
||||
`;
|
||||
|
||||
// For videos, we have limited metadata access from browser
|
||||
const videoMetadata = {
|
||||
'Format': basicInfo.type,
|
||||
'Dauer': basicInfo.duration,
|
||||
'Auflösung': `${basicInfo.width} × ${basicInfo.height}`,
|
||||
'Seitenverhältnis': `${basicInfo.aspect}:1`,
|
||||
'Dateigröße': basicInfo.size
|
||||
};
|
||||
|
||||
let metadataHTML = createMetadataSection('Video-Informationen', videoMetadata, '🎬', 'video');
|
||||
metadataHTML += '<div class="no-metadata">Erweiterte Video-Metadaten können im Browser nur begrenzt ausgelesen werden.</div>';
|
||||
|
||||
// Store for report
|
||||
currentMetadata['Video-Informationen'] = videoMetadata;
|
||||
|
||||
metadataContent.innerHTML = metadataHTML;
|
||||
|
||||
// Add click handlers for collapsible sections
|
||||
addSectionToggleHandlers();
|
||||
|
||||
// Hide loading and show metadata
|
||||
loading.classList.remove('active');
|
||||
metadataContainer.classList.add('active');
|
||||
}
|
||||
|
||||
// Create metadata section HTML
|
||||
function createMetadataSection(title, data, icon = '📋', cssClass = '') {
|
||||
let html = `
|
||||
<div class="metadata-section ${cssClass}">
|
||||
<div class="metadata-section-header ${cssClass}">
|
||||
<span>${icon} ${title}</span>
|
||||
<span class="toggle-icon">▼</span>
|
||||
</div>
|
||||
<div class="metadata-content">
|
||||
<table class="metadata-table">
|
||||
`;
|
||||
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
const formattedKey = formatMetadataKey(key);
|
||||
const formattedValue = formatMetadataValue(key, value);
|
||||
html += `
|
||||
<tr>
|
||||
<td>${formattedKey}</td>
|
||||
<td>${formattedValue}</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
html += `
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
// Device model mapping for cameras
|
||||
const deviceModelMapping = {
|
||||
// Apple iPhone models
|
||||
'iPhone 15 Pro Max': 'Apple iPhone 15 Pro Max',
|
||||
'iPhone 15 Pro': 'Apple iPhone 15 Pro',
|
||||
'iPhone 15 Plus': 'Apple iPhone 15 Plus',
|
||||
'iPhone 15': 'Apple iPhone 15',
|
||||
'iPhone 14 Pro Max': 'Apple iPhone 14 Pro Max',
|
||||
'iPhone 14 Pro': 'Apple iPhone 14 Pro',
|
||||
'iPhone 14 Plus': 'Apple iPhone 14 Plus',
|
||||
'iPhone 14': 'Apple iPhone 14',
|
||||
'iPhone 13 Pro Max': 'Apple iPhone 13 Pro Max',
|
||||
'iPhone 13 Pro': 'Apple iPhone 13 Pro',
|
||||
'iPhone 13': 'Apple iPhone 13',
|
||||
'iPhone 13 mini': 'Apple iPhone 13 mini',
|
||||
'iPhone 12 Pro Max': 'Apple iPhone 12 Pro Max',
|
||||
'iPhone 12 Pro': 'Apple iPhone 12 Pro',
|
||||
'iPhone 12': 'Apple iPhone 12',
|
||||
'iPhone 12 mini': 'Apple iPhone 12 mini',
|
||||
'iPhone SE (3rd generation)': 'Apple iPhone SE (2022)',
|
||||
'iPhone SE (2nd generation)': 'Apple iPhone SE (2020)',
|
||||
'iPhone 11 Pro Max': 'Apple iPhone 11 Pro Max',
|
||||
'iPhone 11 Pro': 'Apple iPhone 11 Pro',
|
||||
'iPhone 11': 'Apple iPhone 11',
|
||||
'iPhone XS Max': 'Apple iPhone XS Max',
|
||||
'iPhone XS': 'Apple iPhone XS',
|
||||
'iPhone XR': 'Apple iPhone XR',
|
||||
'iPhone X': 'Apple iPhone X',
|
||||
'iPhone 8 Plus': 'Apple iPhone 8 Plus',
|
||||
'iPhone 8': 'Apple iPhone 8',
|
||||
'iPhone 7 Plus': 'Apple iPhone 7 Plus',
|
||||
'iPhone 7': 'Apple iPhone 7',
|
||||
'iPhone 6s Plus': 'Apple iPhone 6s Plus',
|
||||
'iPhone 6s': 'Apple iPhone 6s',
|
||||
'iPhone 6 Plus': 'Apple iPhone 6 Plus',
|
||||
'iPhone 6': 'Apple iPhone 6',
|
||||
|
||||
// Apple iPad models
|
||||
'iPad Pro (12.9-inch) (6th generation)': 'Apple iPad Pro 12.9" (2022)',
|
||||
'iPad Pro (11-inch) (4th generation)': 'Apple iPad Pro 11" (2022)',
|
||||
'iPad Air (5th generation)': 'Apple iPad Air (2022)',
|
||||
'iPad (10th generation)': 'Apple iPad (2022)',
|
||||
'iPad mini (6th generation)': 'Apple iPad mini (2021)',
|
||||
|
||||
// Samsung Galaxy models
|
||||
'SM-S928B': 'Samsung Galaxy S24 Ultra',
|
||||
'SM-S926B': 'Samsung Galaxy S24+',
|
||||
'SM-S921B': 'Samsung Galaxy S24',
|
||||
'SM-S918B': 'Samsung Galaxy S23 Ultra',
|
||||
'SM-S916B': 'Samsung Galaxy S23+',
|
||||
'SM-S911B': 'Samsung Galaxy S23',
|
||||
'SM-S908B': 'Samsung Galaxy S22 Ultra',
|
||||
'SM-S906B': 'Samsung Galaxy S22+',
|
||||
'SM-S901B': 'Samsung Galaxy S22',
|
||||
'SM-G998B': 'Samsung Galaxy S21 Ultra',
|
||||
'SM-G996B': 'Samsung Galaxy S21+',
|
||||
'SM-G991B': 'Samsung Galaxy S21',
|
||||
'SM-G988B': 'Samsung Galaxy S20 Ultra',
|
||||
'SM-G986B': 'Samsung Galaxy S20+',
|
||||
'SM-G981B': 'Samsung Galaxy S20',
|
||||
'SM-A546B': 'Samsung Galaxy A54',
|
||||
'SM-A346B': 'Samsung Galaxy A34',
|
||||
'SM-A536B': 'Samsung Galaxy A53',
|
||||
'SM-A336B': 'Samsung Galaxy A33',
|
||||
'SM-A526B': 'Samsung Galaxy A52',
|
||||
'SM-A326B': 'Samsung Galaxy A32',
|
||||
|
||||
// Google Pixel models
|
||||
'Pixel 8 Pro': 'Google Pixel 8 Pro',
|
||||
'Pixel 8': 'Google Pixel 8',
|
||||
'Pixel 7a': 'Google Pixel 7a',
|
||||
'Pixel 7 Pro': 'Google Pixel 7 Pro',
|
||||
'Pixel 7': 'Google Pixel 7',
|
||||
'Pixel 6a': 'Google Pixel 6a',
|
||||
'Pixel 6 Pro': 'Google Pixel 6 Pro',
|
||||
'Pixel 6': 'Google Pixel 6',
|
||||
'Pixel 5a': 'Google Pixel 5a',
|
||||
'Pixel 5': 'Google Pixel 5',
|
||||
'Pixel 4a': 'Google Pixel 4a',
|
||||
'Pixel 4 XL': 'Google Pixel 4 XL',
|
||||
'Pixel 4': 'Google Pixel 4',
|
||||
|
||||
// Xiaomi models
|
||||
'Mi 13 Ultra': 'Xiaomi Mi 13 Ultra',
|
||||
'Mi 13 Pro': 'Xiaomi Mi 13 Pro',
|
||||
'Mi 13': 'Xiaomi Mi 13',
|
||||
'Mi 12 Ultra': 'Xiaomi Mi 12 Ultra',
|
||||
'Mi 12 Pro': 'Xiaomi Mi 12 Pro',
|
||||
'Mi 12': 'Xiaomi Mi 12',
|
||||
'Mi 11 Ultra': 'Xiaomi Mi 11 Ultra',
|
||||
'Mi 11 Pro': 'Xiaomi Mi 11 Pro',
|
||||
'Mi 11': 'Xiaomi Mi 11',
|
||||
'Redmi Note 13 Pro+': 'Xiaomi Redmi Note 13 Pro+',
|
||||
'Redmi Note 13 Pro': 'Xiaomi Redmi Note 13 Pro',
|
||||
'Redmi Note 12 Pro+': 'Xiaomi Redmi Note 12 Pro+',
|
||||
'Redmi Note 12 Pro': 'Xiaomi Redmi Note 12 Pro',
|
||||
'POCO X6 Pro': 'Xiaomi POCO X6 Pro',
|
||||
'POCO F5': 'Xiaomi POCO F5',
|
||||
|
||||
// OnePlus models
|
||||
'LE2125': 'OnePlus 9 Pro',
|
||||
'LE2123': 'OnePlus 9',
|
||||
'IN2023': 'OnePlus 8 Pro',
|
||||
'IN2013': 'OnePlus 8',
|
||||
'HD1913': 'OnePlus 7T Pro',
|
||||
'HD1903': 'OnePlus 7T',
|
||||
'GM1913': 'OnePlus 7 Pro',
|
||||
'GM1903': 'OnePlus 7',
|
||||
'CPH2423': 'OnePlus 11',
|
||||
'CPH2449': 'OnePlus 11R',
|
||||
'CPH2411': 'OnePlus 10 Pro',
|
||||
'CPH2413': 'OnePlus 10T',
|
||||
|
||||
// Huawei models (newer models might not have Google services)
|
||||
'VOG-L29': 'Huawei P30 Pro',
|
||||
'ELE-L29': 'Huawei P30',
|
||||
'CLT-L29': 'Huawei P20 Pro',
|
||||
'EML-L29': 'Huawei P20',
|
||||
'LYA-L29': 'Huawei Mate 20 Pro',
|
||||
'HMA-L29': 'Huawei Mate 20',
|
||||
'NOH-NX9': 'Huawei Mate 60 Pro',
|
||||
'BRA-NX9': 'Huawei Mate 60',
|
||||
|
||||
// Sony models
|
||||
'XQ-DQ54': 'Sony Xperia 1 V',
|
||||
'XQ-CQ54': 'Sony Xperia 5 V',
|
||||
'XQ-BQ52': 'Sony Xperia 1 IV',
|
||||
'XQ-AS52': 'Sony Xperia 5 IV',
|
||||
'XQ-BC52': 'Sony Xperia 1 III',
|
||||
'XQ-AS62': 'Sony Xperia 5 III',
|
||||
|
||||
// OPPO models
|
||||
'CPH2437': 'OPPO Find X6 Pro',
|
||||
'CPH2305': 'OPPO Find X5 Pro',
|
||||
'CPH2173': 'OPPO Find X3 Pro',
|
||||
'CPH2487': 'OPPO Reno 11 Pro',
|
||||
'CPH2481': 'OPPO Reno 10 Pro+',
|
||||
|
||||
// Vivo models
|
||||
'V2302': 'Vivo X100 Pro',
|
||||
'V2250': 'Vivo X90 Pro',
|
||||
'V2145': 'Vivo X80 Pro',
|
||||
'V2183A': 'Vivo X70 Pro+',
|
||||
|
||||
// Realme models
|
||||
'RMX3771': 'Realme GT 5',
|
||||
'RMX3708': 'Realme GT 3',
|
||||
'RMX3300': 'Realme GT 2 Pro',
|
||||
'RMX3360': 'Realme GT Neo 3',
|
||||
|
||||
// Nothing models
|
||||
'A063': 'Nothing Phone (2)',
|
||||
'A013': 'Nothing Phone (1)',
|
||||
|
||||
// ASUS models
|
||||
'ASUS_AI2302': 'ASUS Zenfone 10',
|
||||
'ASUS_AI2202': 'ASUS Zenfone 9',
|
||||
'ASUS_I006D': 'ASUS ROG Phone 7',
|
||||
'ASUS_I005D': 'ASUS ROG Phone 6'
|
||||
};
|
||||
|
||||
// Function to get device name from model
|
||||
function getDeviceFromModel(model) {
|
||||
if (!model) return null;
|
||||
|
||||
// Direct match
|
||||
if (deviceModelMapping[model]) {
|
||||
return deviceModelMapping[model];
|
||||
}
|
||||
|
||||
// Try to match partial model names
|
||||
for (const [key, value] of Object.entries(deviceModelMapping)) {
|
||||
if (model.includes(key) || key.includes(model)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it's a generic Apple device
|
||||
if (model.includes('iPhone') || model.includes('iPad')) {
|
||||
return model; // Return as is, it's already descriptive
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Format metadata keys for display
|
||||
function formatMetadataKey(key) {
|
||||
// Remove common prefixes and format
|
||||
key = key.replace(/^(EXIF|GPS|IPTC|XMP)/, '');
|
||||
|
||||
// Add spaces before capital letters
|
||||
key = key.replace(/([A-Z])/g, ' $1').trim();
|
||||
|
||||
// Special formatting for known keys
|
||||
const keyMap = {
|
||||
'Make': 'Kamera-Hersteller',
|
||||
'Model': 'Kamera-Modell',
|
||||
'DateTime Original': 'Aufnahmedatum',
|
||||
'Exposure Time': 'Belichtungszeit',
|
||||
'F Number': 'Blende',
|
||||
'ISO Speed Ratings': 'ISO-Wert',
|
||||
'Focal Length': 'Brennweite',
|
||||
'Flash': 'Blitz',
|
||||
'GPS Latitude': 'Breitengrad',
|
||||
'GPS Longitude': 'Längengrad',
|
||||
'GPS Altitude': 'Höhe',
|
||||
'Lens Model': 'Objektiv',
|
||||
'White Balance': 'Weißabgleich',
|
||||
'Exposure Mode': 'Belichtungsmodus',
|
||||
'Color Space': 'Farbraum'
|
||||
};
|
||||
|
||||
return keyMap[key.trim()] || key;
|
||||
}
|
||||
|
||||
// Format metadata values for display
|
||||
function formatMetadataValue(key, value) {
|
||||
// Handle camera model with device mapping
|
||||
if (key.includes('Model') && !key.includes('Lens')) {
|
||||
const device = getDeviceFromModel(value);
|
||||
if (device && device !== value) {
|
||||
return `${value} (${device})`;
|
||||
}
|
||||
}
|
||||
|
||||
// Format GPS coordinates
|
||||
if (key.includes('GPS') && (key.includes('Latitude') || key.includes('Longitude'))) {
|
||||
if (typeof value === 'number') {
|
||||
return value.toFixed(6) + '°';
|
||||
}
|
||||
}
|
||||
|
||||
// Format altitude
|
||||
if (key.includes('Altitude') && typeof value === 'number') {
|
||||
return value.toFixed(2) + ' m';
|
||||
}
|
||||
|
||||
// Format focal length
|
||||
if (key.includes('FocalLength') && typeof value === 'number') {
|
||||
return value + ' mm';
|
||||
}
|
||||
|
||||
// Format dates
|
||||
if (key.includes('Date') && value.includes(':')) {
|
||||
try {
|
||||
const date = new Date(value.replace(/^(\d{4}):(\d{2}):(\d{2})/, '$1-$2-$3'));
|
||||
if (!isNaN(date)) {
|
||||
return date.toLocaleString('de-DE');
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
// Add toggle handlers for collapsible sections
|
||||
function addSectionToggleHandlers() {
|
||||
const headers = document.querySelectorAll('.metadata-section-header');
|
||||
headers.forEach(header => {
|
||||
header.addEventListener('click', function() {
|
||||
const section = this.parentElement;
|
||||
section.classList.toggle('collapsed');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Clear metadata and reset
|
||||
function clearMetadata() {
|
||||
metadataContainer.classList.remove('active');
|
||||
fileInput.value = '';
|
||||
imagePreview.innerHTML = '';
|
||||
metadataContent.innerHTML = '';
|
||||
|
||||
// Clear stored data
|
||||
currentFileData = null;
|
||||
currentMetadata = null;
|
||||
currentPreviewUrl = null;
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function formatDuration(seconds) {
|
||||
const h = Math.floor(seconds / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
const s = Math.floor(seconds % 60);
|
||||
|
||||
if (h > 0) {
|
||||
return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
||||
}
|
||||
return `${m}:${s.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
// Store current file data for report generation
|
||||
let currentFileData = null;
|
||||
let currentMetadata = null;
|
||||
let currentPreviewUrl = null;
|
||||
|
||||
// Generate PDF report
|
||||
async function generateReport() {
|
||||
if (!currentFileData) {
|
||||
alert('Keine Datei zum Erstellen eines Berichts vorhanden.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Create loading indicator
|
||||
const originalText = document.getElementById('reportBtn').textContent;
|
||||
document.getElementById('reportBtn').textContent = 'Bericht wird erstellt...';
|
||||
document.getElementById('reportBtn').disabled = true;
|
||||
|
||||
// Import jsPDF and AutoTable dynamically
|
||||
const jsPDFScript = document.createElement('script');
|
||||
jsPDFScript.src = 'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js';
|
||||
document.head.appendChild(jsPDFScript);
|
||||
|
||||
jsPDFScript.onload = async () => {
|
||||
// Load AutoTable plugin
|
||||
const autoTableScript = document.createElement('script');
|
||||
autoTableScript.src = 'https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.8.1/jspdf.plugin.autotable.min.js';
|
||||
document.head.appendChild(autoTableScript);
|
||||
|
||||
autoTableScript.onload = async () => {
|
||||
const { jsPDF } = window.jspdf;
|
||||
const doc = new jsPDF();
|
||||
|
||||
// Set fonts
|
||||
doc.setFont('helvetica');
|
||||
|
||||
// Add header
|
||||
doc.setFontSize(20);
|
||||
doc.setTextColor(35, 45, 83); // Primary color
|
||||
doc.text('Metadaten-Bericht', 105, 20, { align: 'center' });
|
||||
|
||||
// Add generation date
|
||||
doc.setFontSize(10);
|
||||
doc.setTextColor(100);
|
||||
const now = new Date();
|
||||
doc.text(`Erstellt am: ${now.toLocaleString('de-DE')}`, 105, 30, { align: 'center' });
|
||||
|
||||
// Add file info section
|
||||
let yPosition = 50;
|
||||
doc.setFontSize(14);
|
||||
doc.setTextColor(35, 45, 83);
|
||||
doc.text('Datei-Informationen', 20, yPosition);
|
||||
|
||||
yPosition += 5;
|
||||
|
||||
// Basic file info table data
|
||||
const fileInfoData = [
|
||||
['Dateiname', currentFileData.name],
|
||||
['Größe', currentFileData.size],
|
||||
['Typ', currentFileData.type],
|
||||
['Zuletzt geändert', currentFileData.lastModified]
|
||||
];
|
||||
|
||||
if (currentFileData.width) {
|
||||
fileInfoData.push(['Auflösung', `${currentFileData.width} × ${currentFileData.height}`]);
|
||||
fileInfoData.push(['Seitenverhältnis', `${currentFileData.aspect}:1`]);
|
||||
}
|
||||
|
||||
if (currentFileData.duration) {
|
||||
fileInfoData.push(['Dauer', currentFileData.duration]);
|
||||
}
|
||||
|
||||
// Create table for file info
|
||||
doc.autoTable({
|
||||
startY: yPosition,
|
||||
head: [],
|
||||
body: fileInfoData,
|
||||
theme: 'grid',
|
||||
styles: {
|
||||
fontSize: 10,
|
||||
cellPadding: 5
|
||||
},
|
||||
columnStyles: {
|
||||
0: {
|
||||
fontStyle: 'bold',
|
||||
fillColor: [245, 245, 245],
|
||||
textColor: [35, 45, 83],
|
||||
cellWidth: 50
|
||||
}
|
||||
},
|
||||
margin: { left: 20, right: 20 }
|
||||
});
|
||||
|
||||
yPosition = doc.lastAutoTable.finalY + 10;
|
||||
|
||||
// Add thumbnail if it's an image
|
||||
if (currentPreviewUrl && currentFileData.type.startsWith('image/')) {
|
||||
try {
|
||||
// Check if we have enough space on current page
|
||||
const remainingSpace = doc.internal.pageSize.getHeight() - yPosition - 40;
|
||||
if (remainingSpace < 100) { // If less than 100 units remaining, start new page
|
||||
doc.addPage();
|
||||
yPosition = 20;
|
||||
}
|
||||
|
||||
yPosition += 10;
|
||||
doc.setFontSize(14);
|
||||
doc.setTextColor(35, 45, 83);
|
||||
|
||||
// Center the "Vorschau" text
|
||||
const pageWidth = doc.internal.pageSize.getWidth();
|
||||
doc.text('Vorschau', pageWidth / 2, yPosition, { align: 'center' });
|
||||
yPosition += 10;
|
||||
|
||||
// Get image element
|
||||
const img = document.querySelector('.image-preview img');
|
||||
if (img) {
|
||||
// Convert image to base64 with high quality
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Use higher resolution for better quality
|
||||
const maxWidth = 800;
|
||||
const maxHeight = 800;
|
||||
|
||||
let width = img.naturalWidth;
|
||||
let height = img.naturalHeight;
|
||||
|
||||
// Calculate aspect ratio
|
||||
const ratio = Math.min(maxWidth / width, maxHeight / height);
|
||||
if (ratio < 1) {
|
||||
width = width * ratio;
|
||||
height = height * ratio;
|
||||
}
|
||||
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
// Enable image smoothing for better quality
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.imageSmoothingQuality = 'high';
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
|
||||
// Use higher quality JPEG compression
|
||||
const dataUrl = canvas.toDataURL('image/jpeg', 0.95);
|
||||
|
||||
// Calculate display size to fit on page
|
||||
const pageWidth = doc.internal.pageSize.getWidth();
|
||||
const pageHeight = doc.internal.pageSize.getHeight();
|
||||
const maxDisplayWidth = pageWidth - 40; // 20px margin on each side
|
||||
const maxDisplayHeight = pageHeight - yPosition - 40; // Leave space for footer
|
||||
|
||||
// Start with desired width
|
||||
let displayWidth = 80; // Smaller default size
|
||||
let displayHeight = (height / width) * displayWidth;
|
||||
|
||||
// Check if it fits vertically, if not scale down
|
||||
if (displayHeight > maxDisplayHeight) {
|
||||
displayHeight = maxDisplayHeight;
|
||||
displayWidth = (width / height) * displayHeight;
|
||||
}
|
||||
|
||||
// Check if it fits horizontally, if not scale down
|
||||
if (displayWidth > maxDisplayWidth) {
|
||||
displayWidth = maxDisplayWidth;
|
||||
displayHeight = (height / width) * displayWidth;
|
||||
}
|
||||
|
||||
// Center the image horizontally
|
||||
const xPosition = (pageWidth - displayWidth) / 2;
|
||||
|
||||
doc.addImage(dataUrl, 'JPEG', xPosition, yPosition, displayWidth, displayHeight);
|
||||
yPosition += displayHeight + 10;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Hinzufügen des Thumbnails:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Add metadata sections
|
||||
if (currentMetadata && Object.keys(currentMetadata).length > 0) {
|
||||
// Check if we need a new page
|
||||
if (yPosition > 200) {
|
||||
doc.addPage();
|
||||
yPosition = 20;
|
||||
}
|
||||
|
||||
Object.entries(currentMetadata).forEach(([section, data]) => {
|
||||
if (Object.keys(data).length > 0) {
|
||||
// Check if we need a new page
|
||||
if (yPosition > 240) {
|
||||
doc.addPage();
|
||||
yPosition = 20;
|
||||
}
|
||||
|
||||
// Section header
|
||||
doc.setFontSize(12);
|
||||
doc.setTextColor(35, 45, 83);
|
||||
doc.text(section, 20, yPosition);
|
||||
yPosition += 5;
|
||||
|
||||
// Convert data to table format
|
||||
const tableData = Object.entries(data).map(([key, value]) => {
|
||||
// Format the key
|
||||
const formattedKey = formatMetadataKey(key);
|
||||
// Ensure value is string and not too long
|
||||
const displayValue = String(value).length > 100 ? String(value).substring(0, 100) + '...' : String(value);
|
||||
return [formattedKey, displayValue];
|
||||
});
|
||||
|
||||
// Create table for this section
|
||||
doc.autoTable({
|
||||
startY: yPosition,
|
||||
head: [],
|
||||
body: tableData,
|
||||
theme: 'grid',
|
||||
styles: {
|
||||
fontSize: 9,
|
||||
cellPadding: 3
|
||||
},
|
||||
columnStyles: {
|
||||
0: {
|
||||
fontStyle: 'bold',
|
||||
fillColor: [245, 245, 245],
|
||||
textColor: [35, 45, 83],
|
||||
cellWidth: 60
|
||||
},
|
||||
1: {
|
||||
cellWidth: 'auto'
|
||||
}
|
||||
},
|
||||
margin: { left: 20, right: 20 },
|
||||
didDrawPage: function(data) {
|
||||
// Header on new pages
|
||||
if (data.pageNumber > 1) {
|
||||
doc.setFontSize(12);
|
||||
doc.setTextColor(35, 45, 83);
|
||||
doc.text(section + ' (Fortsetzung)', 20, 15);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
yPosition = doc.lastAutoTable.finalY + 10;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add footer
|
||||
const pageCount = doc.internal.getNumberOfPages();
|
||||
for (let i = 1; i <= pageCount; i++) {
|
||||
doc.setPage(i);
|
||||
doc.setFontSize(8);
|
||||
doc.setTextColor(150);
|
||||
doc.text(`Seite ${i} von ${pageCount}`, 105, 285, { align: 'center' });
|
||||
doc.text('Metadaten-Crawler © 2025 IntelSight', 105, 290, { align: 'center' });
|
||||
}
|
||||
|
||||
// Save the PDF
|
||||
doc.save(`Metadaten-Bericht_${currentFileData.name.replace(/\.[^/.]+$/, '')}_${now.getTime()}.pdf`);
|
||||
|
||||
// Restore button
|
||||
document.getElementById('reportBtn').textContent = originalText;
|
||||
document.getElementById('reportBtn').disabled = false;
|
||||
};
|
||||
|
||||
autoTableScript.onerror = () => {
|
||||
alert('Fehler beim Laden der PDF-Tabellen-Bibliothek.');
|
||||
document.getElementById('reportBtn').textContent = originalText;
|
||||
document.getElementById('reportBtn').disabled = false;
|
||||
};
|
||||
};
|
||||
|
||||
jsPDFScript.onerror = () => {
|
||||
alert('Fehler beim Laden der PDF-Bibliothek.');
|
||||
document.getElementById('reportBtn').textContent = originalText;
|
||||
document.getElementById('reportBtn').disabled = false;
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen des Berichts:', error);
|
||||
alert('Fehler beim Erstellen des PDF-Berichts.');
|
||||
document.getElementById('reportBtn').textContent = 'Bericht erstellen';
|
||||
document.getElementById('reportBtn').disabled = false;
|
||||
}
|
||||
}
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren