Last active
October 4, 2025 13:10
-
-
Save neutrixs/c6b3635678bd1a523d02fd1ac7aef949 to your computer and use it in GitHub Desktop.
Convert your EXR + tonemapped jpg to google's ultra HDR format that instagram accepts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // vibe coded. it works tho, so don't ask me | |
| import puppeteer from 'puppeteer'; | |
| import { promises as fs } from 'fs'; | |
| import http from 'http'; | |
| import path from 'path'; | |
| async function createUltraHDR(hdrPath, sdrPath, outputPath) { | |
| console.log('Starting local file server...'); | |
| // Create a simple HTTP server to serve files | |
| const server = http.createServer(async (req, res) => { | |
| const filePath = path.join(process.cwd(), req.url.slice(1)); | |
| try { | |
| const data = await fs.readFile(filePath); | |
| res.writeHead(200, { | |
| 'Access-Control-Allow-Origin': '*', | |
| 'Access-Control-Allow-Methods': 'GET', | |
| 'Access-Control-Allow-Headers': '*' | |
| }); | |
| res.end(data); | |
| } catch (err) { | |
| res.writeHead(404, { | |
| 'Access-Control-Allow-Origin': '*' | |
| }); | |
| res.end('Not found'); | |
| } | |
| }); | |
| await new Promise(resolve => server.listen(8888, resolve)); | |
| console.log('File server running on http://localhost:8888'); | |
| console.log('Starting browser...'); | |
| const browser = await puppeteer.launch({ | |
| headless: false, // Change to false to see what's happening | |
| args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'], | |
| devtools: true // Open devtools automatically | |
| }); | |
| const page = await browser.newPage(); | |
| // Increase timeout | |
| page.setDefaultTimeout(120000); // 2 minutes | |
| // Log console messages from the page | |
| page.on('console', msg => console.log('PAGE:', msg.text())); | |
| page.on('pageerror', err => console.error('PAGE ERROR:', err)); | |
| // Use local file URLs instead of base64 | |
| const hdrUrl = `http://localhost:8888/${hdrPath}`; | |
| const sdrUrl = `http://localhost:8888/${sdrPath}`; | |
| const hdrFormat = hdrPath.endsWith('.exr') ? 'exr' : hdrPath.endsWith('.hdr') ? 'hdr' : 'other'; | |
| // Create a page with the encoding script | |
| await page.setContent(` | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <script type="importmap"> | |
| { | |
| "imports": { | |
| "three": "https://unpkg.com/[email protected]/build/three.module.js", | |
| "three/examples/": "https://unpkg.com/[email protected]/examples/", | |
| "@monogrid/gainmap-js/encode": "https://unpkg.com/@monogrid/[email protected]/dist/encode.js", | |
| "@monogrid/gainmap-js/libultrahdr": "https://unpkg.com/@monogrid/[email protected]/dist/libultrahdr.js" | |
| } | |
| } | |
| </script> | |
| </head> | |
| <body> | |
| <canvas id="canvas" style="display:none"></canvas> | |
| <script type="module"> | |
| import { encode, findTextureMinMax } from '@monogrid/gainmap-js/encode'; | |
| import { encodeJPEGMetadata } from '@monogrid/gainmap-js/libultrahdr'; | |
| import * as THREE from 'three'; | |
| import { EXRLoader } from 'https://unpkg.com/[email protected]/examples/jsm/loaders/EXRLoader.js'; | |
| import { RGBELoader } from 'https://unpkg.com/[email protected]/examples/jsm/loaders/RGBELoader.js'; | |
| window.encodeUltraHDR = async function(hdrDataUrl, sdrDataUrl, hdrFormat) { | |
| try { | |
| let hdrTexture; | |
| // Load HDR based on format | |
| if (hdrFormat === 'exr') { | |
| const loader = new EXRLoader(); | |
| hdrTexture = await loader.loadAsync(hdrDataUrl); | |
| } else if (hdrFormat === 'hdr') { | |
| const loader = new RGBELoader(); | |
| hdrTexture = await loader.loadAsync(hdrDataUrl); | |
| } else { | |
| const hdrImg = await new Promise((resolve, reject) => { | |
| const img = new Image(); | |
| img.crossOrigin = 'anonymous'; | |
| img.onload = () => resolve(img); | |
| img.onerror = reject; | |
| img.src = hdrDataUrl; | |
| }); | |
| hdrTexture = new THREE.Texture(hdrImg); | |
| hdrTexture.needsUpdate = true; | |
| } | |
| // Find max content boost | |
| const textureMax = findTextureMinMax(hdrTexture); | |
| const maxContentBoost = Math.max(...textureMax); | |
| console.log('Max content boost:', maxContentBoost); | |
| // Encode gain map | |
| const encodingResult = encode({ | |
| image: hdrTexture, | |
| maxContentBoost: maxContentBoost | |
| }); | |
| // Load SDR image to check dimensions and re-compress in consistent format | |
| const sdrImg = await new Promise((resolve, reject) => { | |
| const img = new Image(); | |
| img.crossOrigin = 'anonymous'; | |
| img.onload = () => resolve(img); | |
| img.onerror = reject; | |
| img.src = sdrDataUrl; | |
| }); | |
| console.log('SDR actual dimensions:', sdrImg.width, 'x', sdrImg.height); | |
| // Re-compress SDR through canvas to ensure consistent format with gain map | |
| const sdrCanvas = document.createElement('canvas'); | |
| sdrCanvas.width = sdrImg.width; | |
| sdrCanvas.height = sdrImg.height; | |
| const sdrCtx = sdrCanvas.getContext('2d'); | |
| sdrCtx.drawImage(sdrImg, 0, 0); | |
| const sdrDataUrl2 = sdrCanvas.toDataURL('image/jpeg', 0.95); | |
| const sdrResponse2 = await fetch(sdrDataUrl2); | |
| const sdrArrayBuffer2 = await sdrResponse2.arrayBuffer(); | |
| const sdrJpeg = new Uint8Array(sdrArrayBuffer2); | |
| console.log('Re-compressed SDR JPEG size:', sdrJpeg.length, 'First bytes:', Array.from(sdrJpeg.slice(0, 4))); | |
| // Get gain map ImageData | |
| const gainMapImageData = new ImageData( | |
| encodingResult.gainMap.toArray(), | |
| encodingResult.gainMap.width, | |
| encodingResult.gainMap.height | |
| ); | |
| console.log('Gain map dimensions:', gainMapImageData.width, 'x', gainMapImageData.height); | |
| // Compress gain map to JPEG with vertical flip | |
| const tempCanvas = document.createElement('canvas'); | |
| tempCanvas.width = gainMapImageData.width; | |
| tempCanvas.height = gainMapImageData.height; | |
| const tempCtx = tempCanvas.getContext('2d'); | |
| tempCtx.putImageData(gainMapImageData, 0, 0); | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = gainMapImageData.width; | |
| canvas.height = gainMapImageData.height; | |
| const ctx = canvas.getContext('2d'); | |
| // Flip vertically | |
| ctx.translate(0, canvas.height); | |
| ctx.scale(1, -1); | |
| ctx.drawImage(tempCanvas, 0, 0); | |
| const gainMapDataUrl = canvas.toDataURL('image/jpeg', 0.95); | |
| const gainMapResponse = await fetch(gainMapDataUrl); | |
| const gainMapArrayBuffer = await gainMapResponse.arrayBuffer(); | |
| const gainMapJpeg = new Uint8Array(gainMapArrayBuffer); | |
| console.log('Gain map JPEG size:', gainMapJpeg.length, 'First bytes:', Array.from(gainMapJpeg.slice(0, 4))); | |
| // Get metadata | |
| const metadata = encodingResult.getMetadata(); | |
| // Encode final JPEG with proper format | |
| const ultraHDRJpeg = await encodeJPEGMetadata({ | |
| ...metadata, | |
| sdr: { | |
| data: sdrJpeg, | |
| mimeType: 'image/jpeg', | |
| width: sdrImg.width, | |
| height: sdrImg.height | |
| }, | |
| gainMap: { | |
| data: gainMapJpeg, | |
| mimeType: 'image/jpeg', | |
| width: gainMapImageData.width, | |
| height: gainMapImageData.height | |
| } | |
| }); | |
| // Cleanup | |
| encodingResult.gainMap.dispose(); | |
| encodingResult.sdr.dispose(); | |
| hdrTexture.dispose(); | |
| // Convert to base64 in chunks to avoid stack overflow | |
| const chunkSize = 8192; | |
| let base64 = ''; | |
| for (let i = 0; i < ultraHDRJpeg.length; i += chunkSize) { | |
| const chunk = ultraHDRJpeg.subarray(i, i + chunkSize); | |
| base64 += String.fromCharCode(...chunk); | |
| } | |
| return btoa(base64); | |
| } catch (err) { | |
| console.error('Encoding error:', err); | |
| throw err; | |
| } | |
| }; | |
| console.log('Ready'); | |
| </script> | |
| </body> | |
| </html> | |
| `); | |
| // Wait for script to load | |
| await page.waitForFunction(() => window.encodeUltraHDR !== undefined); | |
| console.log('Browser ready, encoding...'); | |
| // Run encoding with direct URLs | |
| const resultBase64 = await page.evaluate( | |
| async (hdrUrl, sdrUrl, format) => { | |
| return await window.encodeUltraHDR(hdrUrl, sdrUrl, format); | |
| }, | |
| hdrUrl, | |
| sdrUrl, | |
| hdrFormat | |
| ); | |
| console.log('Writing output...'); | |
| const outputBuffer = Buffer.from(resultBase64, 'base64'); | |
| await fs.writeFile(outputPath, outputBuffer); | |
| await browser.close(); | |
| server.close(); | |
| console.log(`Ultra HDR JPEG saved to: ${outputPath}`); | |
| } | |
| // Usage - Change these to your actual file names | |
| const hdrPath = 'test.exr'; // Your EXR or HDR file | |
| const sdrPath = 'test.jpg'; // Your custom tone-mapped JPEG | |
| const outputPath = 'output_ultrahdr.jpg'; | |
| createUltraHDR(hdrPath, sdrPath, outputPath) | |
| .then(() => console.log('Done! Upload output_ultrahdr.jpg to Instagram')) | |
| .catch(err => console.error('Error:', err)); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment