(function () { // API Gateway HTTP API (heic-presign) var API_BASE_URL = 'https://ho96tvj1qa.execute-api.us-east-1.amazonaws.com'; var STEP_MS = 800; var POLL_MS = 2000; var POLL_MAX = 60; var UPLOAD_TIMEOUT_MS = 10 * 60 * 1000; // 10 min for simple PUT var MULTIPART_THRESHOLD = 5 * 1024 * 1024; // 5MB - use multipart for larger files (avoids timeout on slow connections) var fileInput = document.getElementById('heic-input'); var uploadTrigger = document.getElementById('upload-trigger'); var fileNameEl = document.getElementById('file-name'); var startBtn = document.getElementById('start-btn'); var pipelineEl = document.getElementById('pipeline'); var loaderEl = document.getElementById('converter-loader'); var stepsEl = document.getElementById('pipeline-steps'); var progressFill = document.getElementById('progress-fill'); var uploadedImageTile = document.getElementById('uploaded-image-tile'); var heicPreviewImg = document.getElementById('heic-preview-img'); var heicPreviewFallback = document.getElementById('heic-preview-fallback'); var convertedImageTile = document.getElementById('converted-image-tile'); var jpegPreviewImg = document.getElementById('jpeg-preview-img'); var downloadBtn = document.getElementById('download-btn'); var errorEl = document.getElementById('converter-error'); var heicObjectUrl = null; var lastDownloadUrl = null; var lastDownloadFilename = null; var TRANSPARENT_PIXEL = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; function setStepState(stepIndex, state) { var step = stepsEl.querySelector('.converter-step[data-step="' + stepIndex + '"]'); if (!step) return; step.classList.remove('converter-step--pending', 'converter-step--active', 'converter-step--done'); step.classList.add('converter-step--' + state); } function setProgress(completed) { var pct = (completed / 5) * 100; progressFill.style.width = pct + '%'; progressFill.parentElement.setAttribute('aria-valuenow', completed); } function resetPipeline() { for (var i = 0; i < 5; i++) { setStepState(i, 'pending'); } setProgress(0); pipelineEl.setAttribute('aria-busy', 'false'); if (loaderEl) { loaderEl.classList.remove('converter-loader--visible'); loaderEl.setAttribute('aria-hidden', 'true'); } hideJpegPreview(); } function showHeicPreview(file) { if (heicObjectUrl) URL.revokeObjectURL(heicObjectUrl); heicObjectUrl = null; heicPreviewImg.src = TRANSPARENT_PIXEL; heicPreviewImg.classList.add('converter-preview-img--placeholder'); heicPreviewImg.classList.remove('converter-preview-img--broken'); heicPreviewFallback.setAttribute('aria-hidden', 'true'); heicPreviewFallback.style.display = 'none'; if (!file) { uploadedImageTile.setAttribute('aria-hidden', 'true'); return; } uploadedImageTile.removeAttribute('aria-hidden'); heicObjectUrl = URL.createObjectURL(file); heicPreviewImg.src = heicObjectUrl; heicPreviewImg.onerror = function () { heicPreviewImg.classList.add('converter-preview-img--broken'); heicPreviewImg.classList.remove('converter-preview-img--placeholder'); heicPreviewFallback.setAttribute('aria-hidden', 'false'); heicPreviewFallback.style.display = 'block'; }; heicPreviewImg.onload = function () { heicPreviewImg.classList.remove('converter-preview-img--broken', 'converter-preview-img--placeholder'); heicPreviewFallback.setAttribute('aria-hidden', 'true'); heicPreviewFallback.style.display = 'none'; }; } function hideHeicPreview() { if (heicObjectUrl) { URL.revokeObjectURL(heicObjectUrl); heicObjectUrl = null; } heicPreviewImg.src = TRANSPARENT_PIXEL; heicPreviewImg.classList.add('converter-preview-img--placeholder'); heicPreviewImg.classList.remove('converter-preview-img--broken'); uploadedImageTile.setAttribute('aria-hidden', 'true'); } function showJpegPreview(url, filename) { lastDownloadUrl = url; lastDownloadFilename = filename; jpegPreviewImg.classList.remove('converter-preview-img--broken', 'converter-preview-img--placeholder'); jpegPreviewImg.src = url; convertedImageTile.removeAttribute('aria-hidden'); } function hideJpegPreview() { lastDownloadUrl = null; lastDownloadFilename = null; jpegPreviewImg.src = TRANSPARENT_PIXEL; jpegPreviewImg.classList.add('converter-preview-img--placeholder'); jpegPreviewImg.classList.remove('converter-preview-img--broken'); convertedImageTile.setAttribute('aria-hidden', 'true'); } function showLoader() { if (loaderEl) { loaderEl.classList.add('converter-loader--visible'); loaderEl.setAttribute('aria-hidden', 'false'); } } function hideLoader() { if (loaderEl) { loaderEl.classList.remove('converter-loader--visible'); loaderEl.setAttribute('aria-hidden', 'true'); } } function apiCall(action, body) { return fetch(API_BASE_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }).then(function (r) { if (!r.ok) { return r.json().then(function (j) { throw new Error(j.error || 'Request failed'); }, function () { throw new Error('Request failed: ' + r.status); }); } return r.json(); }); } function isHeicOrHeif(file) { var name = (file.name || '').toLowerCase(); var type = (file.type || '').toLowerCase(); return ( name.endsWith('.heic') || name.endsWith('.heif') || type === 'image/heic' || type === 'image/heif' ); } function showError(msg) { if (errorEl) { errorEl.textContent = msg; errorEl.removeAttribute('hidden'); try { sessionStorage.setItem('heic-converter-last-error', msg); } catch (e) {} } } function hideError() { if (errorEl) { errorEl.setAttribute('hidden', ''); errorEl.textContent = ''; } } (function initErrorDisplay() { try { var last = sessionStorage.getItem('heic-converter-last-error'); if (last) { showError('Last error: ' + last); } } catch (e) {} })(); function getUploadErrorMessage(err) { if (err.name === 'AbortError') { return 'Upload timed out. On slow connections, try a smaller file or use Wi‑Fi.'; } if (err.message && (err.message.indexOf('Failed to fetch') !== -1 || err.message.indexOf('NetworkError') !== -1)) { return 'Network error. Check your connection and try again.'; } return err.message || 'Upload failed. Please try again.'; } function uploadSimple(res, file) { var controller = new AbortController(); var timeoutId = setTimeout(function () { controller.abort(); }, UPLOAD_TIMEOUT_MS); return fetch(res.url, { method: 'PUT', body: file, signal: controller.signal, }).finally(function () { clearTimeout(timeoutId); }); } function uploadMultipart(file) { var CHUNK_SIZE = 5 * 1024 * 1024; // 5MB min per S3 part var totalParts = Math.ceil(file.size / CHUNK_SIZE); return apiCall('upload_init', { action: 'upload_init', filename: file.name }) .then(function (init) { var uploadKey = init.key; var uploadId = init.uploadId; var parts = []; function uploadPart(partNum) { var controller = new AbortController(); var timeoutId = setTimeout(function () { controller.abort(); }, UPLOAD_TIMEOUT_MS); var start = (partNum - 1) * CHUNK_SIZE; var end = Math.min(start + CHUNK_SIZE, file.size); var blob = file.slice(start, end); return apiCall('upload_part', { action: 'upload_part', uploadKey: uploadKey, uploadId: uploadId, partNumber: partNum, }) .then(function (partRes) { return fetch(partRes.url, { method: 'PUT', body: blob, signal: controller.signal, }) .then(function (r) { clearTimeout(timeoutId); if (!r.ok) throw new Error('Part ' + partNum + ' upload failed: ' + r.status); var etag = r.headers.get('ETag'); if (!etag) throw new Error('Part ' + partNum + ': S3 did not return ETag. Check CORS ExposeHeaders on the bucket.'); parts.push({ PartNumber: partNum, ETag: etag }); }) .catch(function (err) { clearTimeout(timeoutId); throw err; }); }); } var chain = Promise.resolve(); for (var i = 1; i <= totalParts; i++) { (function (partNum) { chain = chain.then(function () { return uploadPart(partNum); }); })(i); } return chain.then(function () { return apiCall('upload_complete', { action: 'upload_complete', uploadKey: uploadKey, uploadId: uploadId, parts: parts, }); }); }); } function runConversion() { var file = fileInput.files && fileInput.files[0]; if (!file) { alert('Please choose a HEIC file first.'); return; } if (!isHeicOrHeif(file)) { alert('Only HEIC and HEIF images are supported. Please choose a HEIC or HEIF file.'); return; } if (!API_BASE_URL) { alert('API URL not configured. Set API_BASE_URL in heic-converter.js after deploying the presign API.'); return; } startBtn.disabled = true; pipelineEl.setAttribute('aria-busy', 'true'); resetPipeline(); showLoader(); hideError(); var uploadKey = null; function advance(stepIndex, state) { if (stepIndex > 0) setStepState(stepIndex - 1, 'done'); setStepState(stepIndex, state); setProgress(stepIndex + 1); } advance(0, 'active'); var useMultipart = file.size > MULTIPART_THRESHOLD; var uploadPromise = useMultipart ? uploadMultipart(file) : apiCall('upload', { action: 'upload', filename: file.name }) .then(function (res) { return uploadSimple(res, file).then(function (r) { if (!r.ok) { console.error('S3 PUT failed:', r.status, r.statusText, 'URL:', r.url); r.text().then(function (body) { console.error('S3 response body:', body); }).catch(function () {}); throw new Error('Upload failed: ' + r.status); } return { key: res.key }; }); }); uploadPromise .then(function (res) { uploadKey = res.key; advance(1, 'active'); setTimeout(function () { advance(2, 'active'); }, STEP_MS); setTimeout(function () { advance(3, 'active'); }, STEP_MS * 2); return pollDownload(); }) .then(function () { advance(4, 'active'); setStepState(4, 'done'); pipelineEl.setAttribute('aria-busy', 'false'); startBtn.disabled = false; hideLoader(); }) .catch(function (err) { console.error('HEIC converter error:', err); var msg = getUploadErrorMessage(err); showError(msg); pipelineEl.setAttribute('aria-busy', 'false'); startBtn.disabled = false; hideLoader(); }); function pollDownload() { var count = 0; var filename = uploadKey.replace(/^uploads\//, '').replace(/\.(heic|heif)$/i, '.jpg'); return new Promise(function (resolve, reject) { function poll() { apiCall('download', { action: 'download', uploadKey: uploadKey }) .then(function (res) { showJpegPreview(res.url, filename); resolve(); }) .catch(function () { count++; if (count >= POLL_MAX) { reject(new Error('Conversion timed out. Please try again.')); return; } setTimeout(poll, POLL_MS); }); } setTimeout(poll, POLL_MS); }); } } uploadTrigger.addEventListener('click', function () { fileInput.click(); }); fileInput.addEventListener('change', function () { hideError(); var file = fileInput.files && fileInput.files[0]; if (file) { fileNameEl.textContent = file.name; fileNameEl.classList.remove('muted'); showHeicPreview(file); } else { fileNameEl.textContent = 'No file selected'; fileNameEl.classList.add('muted'); hideHeicPreview(); hideJpegPreview(); } resetPipeline(); }); downloadBtn.addEventListener('click', function () { if (!lastDownloadUrl || !lastDownloadFilename) return; downloadBtn.disabled = true; fetch(lastDownloadUrl) .then(function (r) { if (!r.ok) throw new Error('Fetch failed'); return r.blob(); }) .then(function (blob) { var blobWithType = new Blob([blob], { type: 'image/jpeg' }); var url = URL.createObjectURL(blobWithType); var a = document.createElement('a'); a.href = url; a.download = lastDownloadFilename; a.style.display = 'none'; document.body.appendChild(a); a.click(); setTimeout(function () { document.body.removeChild(a); URL.revokeObjectURL(url); }, 100); }) .catch(function (err) { console.error('Download failed:', err); alert('Download failed. Try right-clicking the preview image and choosing "Save image as".'); }) .finally(function () { downloadBtn.disabled = false; }); }); startBtn.addEventListener('click', runConversion); })();