997 Zeilen
39 KiB
JavaScript
997 Zeilen
39 KiB
JavaScript
// === 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);
|
|
const tooltip = getMetadataTooltip(key, formattedKey);
|
|
|
|
html += `
|
|
<tr>
|
|
<td>
|
|
${formattedKey}
|
|
${tooltip ? `<span class="info-icon" title="${tooltip}"><img src="icons/info.svg" alt="Info"></span>` : ''}
|
|
</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)/, '');
|
|
|
|
// Special formatting for known keys - do first before adding spaces
|
|
const keyMap = {
|
|
'Make': 'Kamera-Hersteller',
|
|
'Model': 'Kamera-Modell',
|
|
'DateTime Original': 'Aufnahmedatum',
|
|
'DateTimeOriginal': 'Aufnahmedatum',
|
|
'Exposure Time': 'Belichtungszeit',
|
|
'ExposureTime': 'Belichtungszeit',
|
|
'F Number': 'Blende',
|
|
'FNumber': 'Blende',
|
|
'ISO Speed Ratings': 'ISO-Wert',
|
|
'ISOSpeedRatings': 'ISO-Wert',
|
|
'Focal Length': 'Brennweite',
|
|
'FocalLength': 'Brennweite',
|
|
'Flash': 'Blitz',
|
|
'GPS Latitude': 'Breitengrad',
|
|
'GPSLatitude': 'Breitengrad',
|
|
'GPS Longitude': 'Längengrad',
|
|
'GPSLongitude': 'Längengrad',
|
|
'GPS Altitude': 'Höhe',
|
|
'GPSAltitude': 'Höhe',
|
|
'Lens Model': 'Objektiv',
|
|
'LensModel': 'Objektiv',
|
|
'White Balance': 'Weißabgleich',
|
|
'WhiteBalance': 'Weißabgleich',
|
|
'Exposure Mode': 'Belichtungsmodus',
|
|
'ExposureMode': 'Belichtungsmodus',
|
|
'Color Space': 'Farbraum',
|
|
'ColorSpace': 'Farbraum'
|
|
};
|
|
|
|
// Check if we have a known key first
|
|
if (keyMap[key]) {
|
|
return keyMap[key];
|
|
}
|
|
|
|
// Add spaces before capital letters but preserve acronyms
|
|
key = key.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2'); // For cases like "GPSLatitude" -> "GPS Latitude"
|
|
key = key.replace(/([a-z\d])([A-Z])/g, '$1 $2'); // For cases like "dateTime" -> "date Time"
|
|
key = key.trim();
|
|
|
|
return keyMap[key] || 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;
|
|
|
|
// Get tooltips for metadata fields
|
|
function getMetadataTooltip(key, formattedKey) {
|
|
const tooltips = {
|
|
'ISO-Wert': 'Die Lichtempfindlichkeit des Kamerasensors. Höhere Werte (z.B. 1600) bedeuten mehr Empfindlichkeit, aber auch mehr Bildrauschen.',
|
|
'Blende': 'Die Öffnung des Objektivs (f-Zahl). Kleinere Zahlen (z.B. f/1.8) bedeuten eine größere Öffnung und mehr Licht.',
|
|
'Belichtungszeit': 'Wie lange der Kamerasensor dem Licht ausgesetzt ist. Z.B. 1/100 bedeutet eine Hundertstelsekunde.',
|
|
'Brennweite': 'Der Abstand zwischen Objektiv und Sensor in Millimetern. Bestimmt den Bildausschnitt.',
|
|
'Weißabgleich': 'Einstellung zur korrekten Farbwiedergabe unter verschiedenen Lichtbedingungen.',
|
|
'Belichtungsmodus': 'Automatik, Manuell, Blendenpriorität oder Zeitpriorität - bestimmt wie die Kamera die Belichtung steuert.',
|
|
'Farbraum': 'Definiert den Bereich der darstellbaren Farben (z.B. sRGB für Web, Adobe RGB für Druck).',
|
|
'Breitengrad': 'GPS-Koordinate für die Nord-Süd-Position des Aufnahmeortes.',
|
|
'Längengrad': 'GPS-Koordinate für die Ost-West-Position des Aufnahmeortes.',
|
|
'Höhe': 'Höhe über dem Meeresspiegel am Aufnahmeort.',
|
|
'Objektiv': 'Das verwendete Kameraobjektiv mit seinen technischen Eigenschaften.',
|
|
'Blitz': 'Informationen darüber, ob und wie der Blitz verwendet wurde.',
|
|
'ExposureCompensation': 'Belichtungskorrektur - manuelle Anpassung der automatischen Belichtung.',
|
|
'MeteringMode': 'Messmethode für die Belichtung (Spot, Matrix, Mittenbetont).',
|
|
'FocalLengthIn35mmFilm': 'Brennweite umgerechnet auf Kleinbildformat für bessere Vergleichbarkeit.',
|
|
'DigitalZoomRatio': 'Digitaler Zoom-Faktor - elektronische Vergrößerung mit Qualitätsverlust.',
|
|
'SceneCaptureType': 'Art der Aufnahme (Landschaft, Portrait, Nachtaufnahme).',
|
|
'SubjectDistanceRange': 'Entfernungsbereich zum Motiv (Makro, Nah, Fern).',
|
|
'Sharpness': 'Nachschärfung - digitale Kantenbetonung des Bildes.',
|
|
'Saturation': 'Farbsättigung - Intensität der Farben im Bild.',
|
|
'Contrast': 'Kontrast - Unterschied zwischen hellen und dunklen Bereichen.',
|
|
'GainControl': 'Verstärkungsregelung - elektronische Signalverstärkung bei wenig Licht.',
|
|
// Neue Erklärungen
|
|
'ApertureValue': 'Blendenwert in APEX-Einheiten - technische Darstellung der Blendenöffnung für Berechnungen.',
|
|
'Aperture Value': 'Blendenwert in APEX-Einheiten - technische Darstellung der Blendenöffnung für Berechnungen.',
|
|
'ExposureBiasValue': 'Belichtungskorrektur in EV-Stufen. +1 bedeutet doppelt so hell, -1 halb so hell.',
|
|
'Exposure Bias Value': 'Belichtungskorrektur in EV-Stufen. +1 bedeutet doppelt so hell, -1 halb so hell.',
|
|
'MaxApertureValue': 'Größte Blendenöffnung des Objektivs (kleinste f-Zahl) - bestimmt die maximale Lichtmenge.',
|
|
'Max Aperture Value': 'Größte Blendenöffnung des Objektivs (kleinste f-Zahl) - bestimmt die maximale Lichtmenge.',
|
|
'ExposureProgram': 'Kameraeinstellung: 0=Unbekannt, 1=Manuell, 2=Normal, 3=Blendenpriorität, 4=Zeitpriorität, 5=Kreativ, 6=Action, 7=Portrait, 8=Landschaft.',
|
|
'Exposure Program': 'Kameraeinstellung: 0=Unbekannt, 1=Manuell, 2=Normal, 3=Blendenpriorität, 4=Zeitpriorität, 5=Kreativ, 6=Action, 7=Portrait, 8=Landschaft.',
|
|
'SubSampling': 'Farbunterabtastung bei JPEG - bestimmt wie Farbinformationen gespeichert werden (4:2:2 oder 4:2:0).',
|
|
'YCbCrSubSampling': 'Farbunterabtastung bei JPEG - bestimmt wie Farbinformationen gespeichert werden (4:2:2 oder 4:2:0).',
|
|
'Orientation': 'Bildausrichtung: 1=Normal, 3=180° gedreht, 6=90° rechts gedreht, 8=90° links gedreht.',
|
|
'YCbCrPositioning': 'Position der Farbinformationen: 1=Zentriert (üblich), 2=Co-sited (professionell).',
|
|
'Y Cb Cr Positioning': 'Position der Farbinformationen: 1=Zentriert (üblich), 2=Co-sited (professionell).',
|
|
'ExifIFDPointer': 'Technischer Verweis auf den Speicherort der EXIF-Daten in der Datei.',
|
|
'EXIF IFD Pointer': 'Technischer Verweis auf den Speicherort der EXIF-Daten in der Datei.',
|
|
'ExifVersion': 'Version des EXIF-Standards (z.B. 0232 = Version 2.32) - bestimmt verfügbare Metadatenfelder.',
|
|
'EXIF Version': 'Version des EXIF-Standards (z.B. 0232 = Version 2.32) - bestimmt verfügbare Metadatenfelder.',
|
|
'ShutterSpeedValue': 'Verschlusszeit in APEX-Einheiten - technische Darstellung für Berechnungen.',
|
|
'Shutter Speed Value': 'Verschlusszeit in APEX-Einheiten - technische Darstellung für Berechnungen.',
|
|
'BrightnessValue': 'Objekthelligkeit in APEX-Einheiten - Messwert der Motivhelligkeit.',
|
|
'Brightness Value': 'Objekthelligkeit in APEX-Einheiten - Messwert der Motivhelligkeit.',
|
|
'OffsetTime': 'Zeitzonenversatz zur UTC-Zeit bei der Aufnahme (z.B. +02:00 für MESZ).',
|
|
'Offset Time': 'Zeitzonenversatz zur UTC-Zeit bei der Aufnahme (z.B. +02:00 für MESZ).',
|
|
'OffsetTimeOriginal': 'Ursprünglicher Zeitzonenversatz bei der Aufnahme - bleibt bei Bearbeitung erhalten.',
|
|
'Offset Time Original': 'Ursprünglicher Zeitzonenversatz bei der Aufnahme - bleibt bei Bearbeitung erhalten.',
|
|
'ImageUniqueID': 'Einzigartige Bild-ID aus Datum, Kameraseriennummer und Bildnummer - identifiziert das Bild eindeutig.',
|
|
'Image Unique ID': 'Einzigartige Bild-ID aus Datum, Kameraseriennummer und Bildnummer - identifiziert das Bild eindeutig.'
|
|
};
|
|
|
|
// Check both the original key and formatted key
|
|
return tooltips[formattedKey] || tooltips[key] || 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;
|
|
}
|
|
} |