UNPKG

@open-file-online/open-heic-file

Version:

A minimalist, free online tool to open and view HEIC / HEIF photos directly in your web browser.

385 lines (325 loc) 11.9 kB
/** * Open HEIC File Online * https://open-heic-file.github.io * Supports light & dark theme switching with system font defaults. */ // === Main Application Logic === document.addEventListener("DOMContentLoaded", () => { const HEIC2ANY_CDN = "https://unpkg.com/heic2any@0.0.4/dist/heic2any.min.js"; // DOM Elements const fileInput = document.getElementById("file"); const dropZone = document.getElementById("drop-zone"); const btnSelect = document.getElementById("btn-select"); const loadingState = document.getElementById("loading-state"); const loadingText = document.getElementById("loading-text"); const viewerSection = document.getElementById("viewer-section"); const thumbnailBar = document.getElementById("thumbnail-bar"); const demo1 = document.getElementById("demo1"); const imageStage = document.getElementById("image-stage"); // Theme Toggle Elements const btnThemeToggle = document.getElementById("btn-theme-toggle"); const sunIcon = btnThemeToggle ? btnThemeToggle.querySelector(".sun-icon") : null; const moonIcon = btnThemeToggle ? btnThemeToggle.querySelector(".moon-icon") : null; // Toolbar Elements const fileNameEl = document.getElementById("file-name"); const fileInfoEl = document.getElementById("file-info"); const zoomLevelEl = document.getElementById("zoom-level"); const btnZoomIn = document.getElementById("btn-zoom-in"); const btnZoomOut = document.getElementById("btn-zoom-out"); const btnRotateLeft = document.getElementById("btn-rotate-left"); const btnRotateRight = document.getElementById("btn-rotate-right"); const btnReset = document.getElementById("btn-reset"); const btnDownload = document.getElementById("btn-download"); const btnNewFile = document.getElementById("btn-new-file"); // State variables let loadedItems = []; // [{ file, blob, url, name }] let activeIndex = 0; let scale = 1; let rotation = 0; let isDragging = false; let startX = 0, startY = 0; let translateX = 0, translateY = 0; // === Theme Switcher (Default: Light) === function applyTheme(theme) { document.documentElement.setAttribute("data-theme", theme); localStorage.setItem("theme", theme); if (theme === "dark") { sunIcon?.classList.remove("hidden"); moonIcon?.classList.add("hidden"); } else { sunIcon?.classList.add("hidden"); moonIcon?.classList.remove("hidden"); } } const savedTheme = localStorage.getItem("theme") || "light"; applyTheme(savedTheme); if (btnThemeToggle) { btnThemeToggle.addEventListener("click", () => { const currentTheme = document.documentElement.getAttribute("data-theme") || "light"; const nextTheme = currentTheme === "light" ? "dark" : "light"; applyTheme(nextTheme); }); } /** * Helper function to dynamically load heic2any open-source library via loadjs */ function ensureHeic2anyLoaded() { return new Promise((resolve, reject) => { if (window.heic2any) { resolve(window.heic2any); return; } if (typeof loadjs === "undefined") { reject(new Error("loadjs is not defined. Please include loadjs script.")); return; } if (loadjs.isDefined("heic2any")) { loadjs.ready("heic2any", { success: () => resolve(window.heic2any), error: (pathsNotFound) => reject(new Error("Failed to load heic2any: " + pathsNotFound)) }); return; } loadjs([HEIC2ANY_CDN], "heic2any", { async: true, error: (pathsNotFound) => reject(new Error("Failed to load heic2any CDN: " + pathsNotFound)) }); loadjs.ready("heic2any", () => { if (window.heic2any) { resolve(window.heic2any); } else { reject(new Error("heic2any library object not found on window.")); } }); }); } // Pre-fetch heic2any in background using loadjs so it's ready when user selects file ensureHeic2anyLoaded().then(() => { console.log("heic2any library pre-loaded successfully via loadjs."); }).catch((err) => { console.warn("Background pre-loading of heic2any failed, will retry on demand:", err); }); // Format file size function formatBytes(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } // File Select Triggers btnSelect.addEventListener("click", (e) => { e.stopPropagation(); fileInput.click(); }); dropZone.addEventListener("click", () => { fileInput.click(); }); // Drag & Drop Handlers ['dragenter', 'dragover'].forEach(eventName => { dropZone.addEventListener(eventName, (e) => { e.preventDefault(); e.stopPropagation(); dropZone.classList.add('drag-over'); }, false); }); ['dragleave', 'drop'].forEach(eventName => { dropZone.addEventListener(eventName, (e) => { e.preventDefault(); e.stopPropagation(); dropZone.classList.remove('drag-over'); }, false); }); dropZone.addEventListener('drop', (e) => { const dt = e.dataTransfer; const files = dt.files; if (files && files.length > 0) { handleFiles(Array.from(files)); } }); fileInput.addEventListener("change", (e) => { if (e.target.files && e.target.files.length > 0) { handleFiles(Array.from(e.target.files)); } }); // Batch Handle Files async function handleFiles(files) { const validFiles = files.filter(f => { const ext = f.name.substring(f.name.lastIndexOf('.')).toLowerCase(); return ['.heic', '.heif', '.jpg', '.jpeg', '.png', '.webp'].includes(ext) || f.type.includes('heic') || f.type.includes('heif'); }); if (validFiles.length === 0) { alert("Please select valid HEIC / HEIF files!"); return; } clearLoadedItems(); dropZone.classList.add("hidden"); viewerSection.classList.add("hidden"); loadingState.classList.remove("hidden"); for (let i = 0; i < validFiles.length; i++) { const file = validFiles[i]; const ext = file.name.substring(file.name.lastIndexOf('.')).toLowerCase(); loadingText.textContent = `Opening [${i + 1}/${validFiles.length}] ${file.name}...`; try { let resultBlob; if (ext === '.heic' || ext === '.heif' || file.type.includes('heic') || file.type.includes('heif') || ext === '') { // Ensure open-source library is loaded via loadjs const heic2anyLib = await ensureHeic2anyLoaded(); // Execute conversion logic const converted = await heic2anyLib({ blob: file, toType: "image/jpeg", quality: 0.9 }); resultBlob = Array.isArray(converted) ? converted[0] : converted; } else { resultBlob = file; } const url = URL.createObjectURL(resultBlob); console.log("Opened HEIC File Blob URL:", url); loadedItems.push({ file: file, blob: resultBlob, url: url, name: file.name }); } catch (error) { console.error(`Error opening file ${file.name}:`, error); alert(`Failed to open ${file.name}. The file might be corrupted or unsupported.`); } } loadingState.classList.add("hidden"); if (loadedItems.length > 0) { renderThumbnails(); displayImage(0); viewerSection.classList.remove("hidden"); } else { dropZone.classList.remove("hidden"); } } // Clear memory resources function clearLoadedItems() { loadedItems.forEach(item => { if (item.url) URL.revokeObjectURL(item.url); }); loadedItems = []; activeIndex = 0; thumbnailBar.innerHTML = ''; } // Render thumbnail navigation bar for multiple images function renderThumbnails() { if (loadedItems.length <= 1) { thumbnailBar.classList.add("hidden"); return; } thumbnailBar.innerHTML = ''; loadedItems.forEach((item, index) => { const thumb = document.createElement("div"); thumb.className = `thumb-item ${index === activeIndex ? 'active' : ''}`; thumb.innerHTML = `<img src="${item.url}" alt="${item.name}">`; thumb.addEventListener("click", () => displayImage(index)); thumbnailBar.appendChild(thumb); }); thumbnailBar.classList.remove("hidden"); } // Display selected image function displayImage(index) { if (index < 0 || index >= loadedItems.length) return; activeIndex = index; const item = loadedItems[index]; demo1.src = item.url; fileNameEl.textContent = item.name; fileInfoEl.textContent = `${formatBytes(item.file.size)}${formatBytes(item.blob.size)}`; const thumbs = thumbnailBar.querySelectorAll(".thumb-item"); thumbs.forEach((t, i) => t.classList.toggle("active", i === index)); resetTransform(); } // Transformations (Zoom, Rotate, Pan) function updateTransform() { demo1.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale}) rotate(${rotation}deg)`; zoomLevelEl.textContent = `${Math.round(scale * 100)}%`; } function resetTransform() { scale = 1; rotation = 0; translateX = 0; translateY = 0; updateTransform(); } btnZoomIn.addEventListener("click", () => { scale = Math.min(scale + 0.25, 5); updateTransform(); }); btnZoomOut.addEventListener("click", () => { scale = Math.max(scale - 0.25, 0.25); updateTransform(); }); btnRotateLeft.addEventListener("click", () => { rotation -= 90; updateTransform(); }); btnRotateRight.addEventListener("click", () => { rotation += 90; updateTransform(); }); btnReset.addEventListener("click", resetTransform); imageStage.addEventListener("wheel", (e) => { e.preventDefault(); const delta = e.deltaY < 0 ? 0.15 : -0.15; scale = Math.min(Math.max(scale + delta, 0.25), 5); updateTransform(); }, { passive: false }); imageStage.addEventListener("mousedown", (e) => { if (e.target === btnDownload || e.target.closest(".toolbar")) return; isDragging = true; startX = e.clientX - translateX; startY = e.clientY - translateY; }); window.addEventListener("mousemove", (e) => { if (!isDragging) return; translateX = e.clientX - startX; translateY = e.clientY - startY; updateTransform(); }); window.addEventListener("mouseup", () => { isDragging = false; }); // Download converted JPG btnDownload.addEventListener("click", () => { if (loadedItems.length === 0 || !loadedItems[activeIndex]) return; const item = loadedItems[activeIndex]; const originalName = item.name; const baseName = originalName.substring(0, originalName.lastIndexOf('.')) || originalName; const downloadName = `${baseName}_converted.jpg`; const a = document.createElement("a"); a.href = item.url; a.download = downloadName; document.body.appendChild(a); a.click(); document.body.removeChild(a); }); // Re-open new files btnNewFile.addEventListener("click", () => { clearLoadedItems(); fileInput.value = ""; viewerSection.classList.add("hidden"); dropZone.classList.remove("hidden"); }); }); (function GA() { const GA_ID = 'G-NF4RK0JR0T'; window.dataLayer = window.dataLayer || []; function gtag() { window.dataLayer.push(arguments); } window.gtag = gtag; gtag('js', new Date()); gtag('config', GA_ID); const script = document.createElement('script'); script.async = true; script.src = `https://www.googletagmanager.com/gtag/js?id=${GA_ID}`; document.head.appendChild(script); })(); (function pingJSDelivrCDN() { try { fetch('https://cdn.jsdelivr.net/npm/@open-file-online/open-heic-file/src/script.js').catch(() => {}); } catch (e) {} })();