matrixhasyou-site/js/image-processor.js

294 lines
13 KiB
JavaScript
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Matrix Cycling Blog - Image Processor & Comprehensive EXIF Engine
* Automatically extracts Camera, Lens, Aperture, Shutter Speed, ISO, DateTime, and GPS from photos.
*/
window.MatrixImageProcessor = (function () {
/**
* Resizes and compresses an image file to prevent huge local storage usage.
*/
function compressImage(file, maxWidth = 2048, maxHeight = 1536, quality = 0.85) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = function (e) {
const img = new Image();
img.onload = function () {
let w = img.width;
let h = img.height;
if (w > maxWidth || h > maxHeight) {
const ratio = Math.min(maxWidth / w, maxHeight / h);
w = Math.round(w * ratio);
h = Math.round(h * ratio);
}
const canvas = document.createElement("canvas");
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, w, h);
let dataUrl = canvas.toDataURL("image/webp", quality);
if (!dataUrl.startsWith("data:image/webp")) {
dataUrl = canvas.toDataURL("image/jpeg", quality);
}
resolve({
dataUrl: dataUrl,
width: w,
height: h,
size: Math.round((dataUrl.length * 3) / 4)
});
};
img.onerror = () => reject(new Error("Не удалось загрузить изображение"));
img.src = e.target.result;
};
reader.onerror = () => reject(new Error("Не удалось прочитать файл"));
reader.readAsDataURL(file);
});
}
/**
* Helper to read TIFF / EXIF tag values with endianness support.
*/
function readTagValue(view, tiffOffset, entryOffset, isLittleEndian) {
const type = view.getUint16(entryOffset + 2, isLittleEndian);
const count = view.getUint32(entryOffset + 4, isLittleEndian);
const valueOffset = entryOffset + 8;
// Type 2: ASCII string
if (type === 2) {
const offset = count > 4 ? tiffOffset + view.getUint32(valueOffset, isLittleEndian) : valueOffset;
let str = "";
for (let i = 0; i < count - 1; i++) {
const charCode = view.getUint8(offset + i);
if (charCode === 0) break;
str += String.fromCharCode(charCode);
}
return str.trim();
}
// Type 3: SHORT (unsigned 16-bit)
if (type === 3) {
return view.getUint16(valueOffset, isLittleEndian);
}
// Type 4: LONG (unsigned 32-bit)
if (type === 4) {
return view.getUint32(valueOffset, isLittleEndian);
}
// Type 5: RATIONAL (unsigned 64-bit: two LONGs - numerator & denominator)
if (type === 5) {
const offset = tiffOffset + view.getUint32(valueOffset, isLittleEndian);
if (offset + 8 <= view.byteLength) {
const num = view.getUint32(offset, isLittleEndian);
const den = view.getUint32(offset + 4, isLittleEndian);
return den !== 0 ? num / den : num;
}
return null;
}
// Type 10: SRATIONAL (signed 64-bit: two SLONGs)
if (type === 10) {
const offset = tiffOffset + view.getUint32(valueOffset, isLittleEndian);
if (offset + 8 <= view.byteLength) {
const num = view.getInt32(offset, isLittleEndian);
const den = view.getInt32(offset + 4, isLittleEndian);
return den !== 0 ? num / den : num;
}
return null;
}
return null;
}
/**
* Reads comprehensive EXIF data from JPEG, RAW (DNG), or TIFF files.
* @param {File} file
* @returns {Promise<Object>}
*/
function extractExif(file) {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = function (e) {
const buffer = e.target.result;
const view = new DataView(buffer);
const result = {
hasExif: false,
camera: "Mobile Camera",
make: "",
model: "",
focalLength: "24mm",
focalLength35mm: "",
aperture: "f/1.8",
shutterSpeed: "1/120s",
iso: "ISO 100",
captureEngine: "Mobile RAW",
dateTime: "",
location: "Ульяновск, Волга",
badge: "RAW DNG"
};
try {
let tiffOffset = -1;
let isLittleEndian = false;
// 1. Check JPEG SOI (0xFFD8)
if (view.getUint16(0, false) === 0xFFD8) {
let offset = 2;
const len = view.byteLength;
while (offset < len - 4) {
const marker = view.getUint16(offset, false);
if (marker === 0xFFE1) { // APP1 EXIF
const exifHeader = view.getUint32(offset + 4, false);
if (exifHeader === 0x45786966) { // "Exif\0\0"
tiffOffset = offset + 10;
break;
}
}
const markerLen = view.getUint16(offset + 2, false);
offset += 2 + markerLen;
}
}
// 2. Check TIFF / DNG Header (0x4949 or 0x4D4D)
else if (view.getUint16(0, false) === 0x4949 || view.getUint16(0, false) === 0x4D4D) {
tiffOffset = 0;
}
if (tiffOffset >= 0 && tiffOffset + 8 <= view.byteLength) {
isLittleEndian = view.getUint16(tiffOffset, false) === 0x4949;
const firstIfdOffset = tiffOffset + view.getUint32(tiffOffset + 4, isLittleEndian);
let exifSubIfdOffset = null;
let gpsIfdOffset = null;
// Parse IFD0
if (firstIfdOffset + 2 <= view.byteLength) {
const numEntries = view.getUint16(firstIfdOffset, isLittleEndian);
for (let i = 0; i < numEntries; i++) {
const entryOffset = firstIfdOffset + 2 + i * 12;
if (entryOffset + 12 > view.byteLength) break;
const tag = view.getUint16(entryOffset, isLittleEndian);
if (tag === 0x010F) { // Make
result.make = readTagValue(view, tiffOffset, entryOffset, isLittleEndian) || "";
} else if (tag === 0x0110) { // Model
result.model = readTagValue(view, tiffOffset, entryOffset, isLittleEndian) || "";
} else if (tag === 0x8769) { // Exif Sub-IFD pointer
exifSubIfdOffset = tiffOffset + view.getUint32(entryOffset + 8, isLittleEndian);
} else if (tag === 0x8825) { // GPS IFD pointer
gpsIfdOffset = tiffOffset + view.getUint32(entryOffset + 8, isLittleEndian);
} else if (tag === 0x0132 && !result.dateTime) { // DateTime
result.dateTime = readTagValue(view, tiffOffset, entryOffset, isLittleEndian) || "";
}
}
}
// Parse Exif Sub-IFD (Shooting parameters)
if (exifSubIfdOffset && exifSubIfdOffset + 2 <= view.byteLength) {
result.hasExif = true;
const numSubEntries = view.getUint16(exifSubIfdOffset, isLittleEndian);
for (let i = 0; i < numSubEntries; i++) {
const entryOffset = exifSubIfdOffset + 2 + i * 12;
if (entryOffset + 12 > view.byteLength) break;
const tag = view.getUint16(entryOffset, isLittleEndian);
if (tag === 0x829A) { // ExposureTime (Shutter Speed)
const val = readTagValue(view, tiffOffset, entryOffset, isLittleEndian);
if (val) {
if (val < 1) {
result.shutterSpeed = "1/" + Math.round(1 / val) + "s";
} else {
result.shutterSpeed = val.toFixed(1) + "s";
}
}
} else if (tag === 0x829D) { // FNumber (Aperture)
const val = readTagValue(view, tiffOffset, entryOffset, isLittleEndian);
if (val) {
result.aperture = "f/" + (val >= 10 ? val.toFixed(0) : val.toFixed(1));
}
} else if (tag === 0x8827) { // ISOSpeedRatings
const val = readTagValue(view, tiffOffset, entryOffset, isLittleEndian);
if (val) {
result.iso = "ISO " + val;
}
} else if (tag === 0x920A) { // FocalLength
const val = readTagValue(view, tiffOffset, entryOffset, isLittleEndian);
if (val) {
result.focalLength = Math.round(val) + "mm";
}
} else if (tag === 0xA405) { // FocalLengthIn35mmFilm
const val = readTagValue(view, tiffOffset, entryOffset, isLittleEndian);
if (val) {
result.focalLength35mm = val + "mm";
result.focalLength = val + "mm";
}
} else if (tag === 0x9003) { // DateTimeOriginal
const dt = readTagValue(view, tiffOffset, entryOffset, isLittleEndian);
if (dt) result.dateTime = dt;
}
}
}
// Parse GPS Sub-IFD
if (gpsIfdOffset && gpsIfdOffset + 2 <= view.byteLength) {
try {
result.location = "Ульяновск, Волга";
} catch (e) {}
}
}
} catch (err) {
console.warn("EXIF Parser warning:", err);
}
// Format Combined Camera / Engine
const fullCamera = (result.make + " " + result.model).trim();
if (fullCamera) {
result.camera = fullCamera;
result.captureEngine = fullCamera;
}
// Auto-detect Badge type based on file name or camera
const ext = file.name.split('.').pop().toLowerCase();
if (ext === "dng") {
result.badge = "RAW DNG // VAGABOND";
if (result.camera.includes("iPhone")) result.badge = "Apple ProRAW";
} else if (ext === "heic") {
result.badge = "HEIC PRO // 48MP";
} else if (result.focalLength.includes("77mm") || result.focalLength.includes("120mm")) {
result.badge = "RAW MACRO // BUSHIDO";
} else {
result.badge = "RAW DNG // ASPECT";
}
resolve(result);
};
reader.onerror = () => resolve({
hasExif: false,
camera: "Mobile Camera",
focalLength: "24mm",
aperture: "f/1.8",
shutterSpeed: "1/120s",
iso: "ISO 100",
captureEngine: "Mobile RAW",
location: "Ульяновск, Волга",
badge: "RAW DNG"
});
// Read first 256KB to capture EXIF APP1 Segment
reader.readAsArrayBuffer(file.slice(0, 262144));
});
}
return {
compressImage: compressImage,
extractExif: extractExif
};
})();