{"id":2230,"date":"2025-12-04T14:18:13","date_gmt":"2025-12-04T14:18:13","guid":{"rendered":"https:\/\/altsl.com\/?page_id=2230"},"modified":"2025-12-04T14:23:18","modified_gmt":"2025-12-04T14:23:18","slug":"png-compressor","status":"publish","type":"page","link":"https:\/\/altsl.com\/nl\/tools\/png-compressor\/","title":{"rendered":"PNG Compressor"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"2230\" class=\"elementor elementor-2230\" data-elementor-post-type=\"page\">\n\t\t\t\t<div data-particle_enable=\"false\" data-particle-mobile-disabled=\"false\" class=\"elementor-element elementor-element-06a856b e-flex e-con-boxed e-con e-parent\" data-id=\"06a856b\" data-element_type=\"container\" data-e-type=\"container\" data-settings=\"{&quot;background_background&quot;:&quot;classic&quot;}\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t<div data-particle_enable=\"false\" data-particle-mobile-disabled=\"false\" class=\"elementor-element elementor-element-a2af690 e-con-full e-flex e-con e-child\" data-id=\"a2af690\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t<div class=\"elementor-element elementor-element-97dade6 elementor-widget elementor-widget-heading\" data-id=\"97dade6\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"heading.default\">\n\t\t\t\t\t<h1 class=\"elementor-heading-title elementor-size-default\">PNG COMPRESSOR<\/h1>\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-dc1319d elementor-widget elementor-widget-heading\" data-id=\"dc1319d\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"heading.default\">\n\t\t\t\t\t<h2 class=\"elementor-heading-title elementor-size-default\">Compress your PNG files so they stay an acceptable size for the Second Life Marketplace<\/h2>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div data-particle_enable=\"false\" data-particle-mobile-disabled=\"false\" class=\"elementor-element elementor-element-ece22fc e-con-full e-flex e-con e-parent\" data-id=\"ece22fc\" data-element_type=\"container\" data-e-type=\"container\" data-settings=\"{&quot;background_background&quot;:&quot;classic&quot;}\">\n\t\t\t\t<div class=\"elementor-element elementor-element-136732a elementor-widget elementor-widget-html\" data-id=\"136732a\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t\t<!-- PNG Compressor (pngquant-style) \u2014 Improved, robust version -->\r\n<div class=\"asl-wrapper\">\r\n  <style>\r\n    \/* your styling kept intact (trimmed to essentials for brevity) *\/\r\n    .asl-wrapper{ font-family:var(--e-global-typography-text-font-family,\"Onest\",sans-serif); color:var(--e-global-color-text,#ABABAB); font-size:14px;}\r\n    .asl-card{ background:#0A0A0A;border-radius:4px;padding:18px;box-shadow:0 0 10px rgba(0,0,0,0.5);border:1px solid rgba(255,255,255,0.08); }\r\n    .asl-container{ max-width:1100px;margin:0 auto;display:grid;grid-template-columns:1fr 420px;gap:20px;align-items:start;}\r\n    .asl-header{ display:flex;justify-content:space-between;gap:12px;margin-bottom:15px;border-bottom:1px solid rgba(255,255,255,0.05);padding-bottom:15px; }\r\n    .asl-title h1{ margin:0;font-size:20px;color:var(--e-global-color-accent,#FFFFFF);font-weight:700;}\r\n    .asl-drop{ border:2px dashed rgba(255,255,255,0.15); border-radius:4px; padding:25px 18px; display:flex;align-items:center;justify-content:center; cursor:copy; transition:all .2s; text-align:center; }\r\n    .asl-drop.drag{ border-color:var(--e-global-color-accent,#FFF); background:rgba(255,255,255,0.03); transform:scale(1.01); }\r\n    .asl-controls{ display:flex; gap:10px; margin-top:12px; }\r\n    .asl-button{ min-width:160px;padding:12px;border-radius:4px;border:none;color:#FFF;font-weight:600;cursor:pointer;text-transform:uppercase; letter-spacing:1px;}\r\n    #compressBtn{ background:#5D0090; } #downloadBtn{ background:#004A8F; }\r\n    .asl-meta{ font-size:12px;color:var(--e-global-color-text,#777); }\r\n    .asl-list{ display:flex;flex-direction:column;gap:6px;max-height:400px;overflow:auto;margin-top:10px;}\r\n    .asl-file{ display:flex;justify-content:space-between;align-items:center;padding:8px;border-radius:4px;background:rgba(255,255,255,0.03); }\r\n    .previewCanvas{ width:100%; background:#050505;border-radius:4px;border:1px solid rgba(255,255,255,0.06); margin-top:12px; display:block; }\r\n    .asl-form-row{ display:flex; gap:8px; align-items:center; margin-top:6px; }\r\n    .asl-form-row label{ color:var(--e-global-color-accent,#FFF); font-weight:600; min-width:90px; }\r\n    .asl-form-row input, .asl-form-row select { padding:6px 8px; border-radius:4px; border:1px solid rgba(255,255,255,0.1); background:#000; color:#FFF; width:110px; }\r\n    @media(max-width:980px){ .asl-container{ grid-template-columns:1fr; } .asl-controls{ flex-direction:column; } .asl-button{ width:100%; } }\r\n  <\/style>\r\n\r\n  <div class=\"asl-card asl-container\" id=\"pngApp\">\r\n    <div>\r\n      <div class=\"asl-header\">\r\n        <div class=\"asl-title\">\r\n          <div><h1>PNG Compressor \u2014 pngquant-style<\/h1><p class=\"asl-lead\">Client-side, lossy palette quantization. No uploads. Toggle target to force <1 MB.<\/p><\/div>\r\n        <\/div>\r\n      <\/div>\r\n\r\n      <div id=\"dropZone\" class=\"asl-drop\">\r\n        <div>\r\n          <strong style=\"color:var(--e-global-color-accent,#FFF)\">Drop PNG here<\/strong><br>\r\n          <span class=\"asl-meta\">or <input id=\"fileInput\" type=\"file\" accept=\"image\/png,image\/*\" style=\"display:inline-block;margin-left:6px\" \/><\/span>\r\n        <\/div>\r\n      <\/div>\r\n\r\n      <div class=\"asl-controls\">\r\n        <button id=\"compressBtn\" class=\"asl-button\" disabled><i class=\"fas fa-compress\"><\/i> COMPRESS<\/button>\r\n        <button id=\"downloadBtn\" class=\"asl-button\" disabled><i class=\"fas fa-download\"><\/i> DOWNLOAD<\/button>\r\n      <\/div>\r\n\r\n      <canvas id=\"previewCanvas\" class=\"previewCanvas\" width=\"800\" height=\"450\"><\/canvas>\r\n\r\n      <div style=\"margin-top:12px;display:flex;gap:10px;flex-wrap:wrap;align-items:center;\">\r\n        <div class=\"asl-form-row\"><label for=\"resizeMode\">Resize<\/label><select id=\"resizeMode\"><option value=\"none\">No resize<\/option><option value=\"percent\">Percent (%)<\/option><option value=\"pixel\">Width (px)<\/option><\/select><\/div>\r\n        <div class=\"asl-form-row\"><label for=\"resizeValue\">Value<\/label><input id=\"resizeValue\" type=\"number\" min=\"1\" value=\"100\" \/><\/div>\r\n        <div class=\"asl-form-row\"><label for=\"dither\">Dither<\/label><select id=\"dither\"><option value=\"floyd\">Floyd-Steinberg<\/option><option value=\"none\">None<\/option><\/select><\/div>\r\n        <div class=\"asl-form-row\"><label for=\"targetToggle\">Target<\/label><select id=\"targetToggle\"><option value=\"none\">No target<\/option><option value=\"1mb\">1 MB<\/option><\/select><\/div>\r\n        <div class=\"asl-form-row\"><label for=\"startColors\">Start colors<\/label><input id=\"startColors\" type=\"number\" min=\"2\" max=\"256\" value=\"256\" \/><\/div>\r\n      <\/div>\r\n\r\n      <div id=\"statusLine\" class=\"asl-meta\" style=\"margin-top:8px\">No file loaded.<\/div>\r\n    <\/div>\r\n\r\n    <aside>\r\n      <div class=\"asl-card\" style=\"padding:14px;\">\r\n        <div style=\"display:flex;justify-content:space-between;align-items:center;\">\r\n          <div><strong>Output<\/strong><div class=\"asl-meta\">Compressed file & stats<\/div><\/div>\r\n          <div id=\"sizeMeta\" class=\"asl-meta\">\u2014<\/div>\r\n        <\/div>\r\n        <div id=\"filesList\" class=\"asl-list\"><\/div>\r\n\r\n        <div style=\"border-top:1px dashed rgba(255,255,255,0.06);padding-top:12px;margin-top:12px\">\r\n          <div class=\"asl-meta\">Tip: For the best quality & smallest sizes, point the widget to a libimagequant\/pngequant WASM wrapper (instructions below). Otherwise the fallback quantizer will be used.<\/div>\r\n        <\/div>\r\n      <\/div>\r\n    <\/aside>\r\n  <\/div>\r\n<\/div>\r\n\r\n<!-- libs -->\r\n<link rel=\"stylesheet\" href=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/font-awesome\/5.15.3\/css\/all.min.css\">\r\n<script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/upng-js\/2.1.0\/UPNG.min.js\"><\/script>\r\n\r\n<script>\r\n\/* --------------------------\r\n  CONFIG\r\n  Set USE_WASM = true to try to load a libimagequant-wasm wrapper.\r\n  You must host the wrapper (JS + .wasm) or use a CDN that serves .wasm with correct headers.\r\n  Example wrapper CDN script URL (placeholder): \r\n    https:\/\/cdn.jsdelivr.net\/npm\/@fe-daily\/libimagequant-wasm\/dist\/libimagequant-wasm.min.js\r\n  If the script fails to load, the widget falls back to the UPNG-based encoder.\r\n--------------------------- *\/\r\nconst USE_WASM = true;\r\nconst WASM_SCRIPT_URL = 'https:\/\/cdn.jsdelivr.net\/npm\/@fe-daily\/libimagequant-wasm\/dist\/libimagequant-wasm.min.js'; \/\/ try this; if you host your own, replace with your URL\r\n\r\n\/* --------------------------\r\n  App logic\r\n--------------------------- *\/\r\ndocument.addEventListener('DOMContentLoaded', () => {\r\n  \/\/ elements\r\n  const dropZone = document.getElementById('dropZone');\r\n  const fileInput = document.getElementById('fileInput');\r\n  const compressBtn = document.getElementById('compressBtn');\r\n  const downloadBtn = document.getElementById('downloadBtn');\r\n  const previewCanvas = document.getElementById('previewCanvas');\r\n  const ctx = previewCanvas.getContext('2d');\r\n  const statusLine = document.getElementById('statusLine');\r\n  const filesList = document.getElementById('filesList');\r\n  const sizeMeta = document.getElementById('sizeMeta');\r\n\r\n  const resizeMode = document.getElementById('resizeMode');\r\n  const resizeValue = document.getElementById('resizeValue');\r\n  const targetToggle = document.getElementById('targetToggle');\r\n  const startColorsInput = document.getElementById('startColors');\r\n  const ditherSelect = document.getElementById('dither');\r\n\r\n  let currentImage = null;\r\n  let lastBlob = null;\r\n  let lastName = 'compressed.png';\r\n  let wasmReady = false;\r\n  let wasmModule = null;\r\n\r\n  \/\/ Helpers\r\n  function humanSize(bytes){\r\n    if(bytes===0) return '0 B';\r\n    const units=['B','KB','MB','GB'];\r\n    const i=Math.floor(Math.log(bytes)\/Math.log(1024));\r\n    return (bytes\/Math.pow(1024,i)).toFixed(2)+' '+units[i];\r\n  }\r\n  function clearOutput(){ filesList.innerHTML=''; sizeMeta.textContent='\u2014'; downloadBtn.disabled=true; lastBlob=null; }\r\n\r\n  \/\/ Attempt to load WASM wrapper\r\n  async function tryLoadWasm(){\r\n    if(!USE_WASM) return;\r\n    statusLine.textContent = 'Attempting to load WASM quantizer...';\r\n    try {\r\n      await new Promise((resolve, reject) => {\r\n        const s=document.createElement('script');\r\n        s.src=WASM_SCRIPT_URL;\r\n        s.onload=resolve; s.onerror=()=>reject(new Error('WASM script failed to load (CORS or not found)'));\r\n        document.head.appendChild(s);\r\n      });\r\n      \/\/ wrapper should register a global (implementation-dependent).\r\n      \/\/ Common wrappers expose window.libimagequant or window.LIQ - try both.\r\n      wasmModule = window.libimagequant || window.LIQ || window.imagequant || null;\r\n      if(!wasmModule){\r\n        \/\/ Some wrappers require async init function - try known init names\r\n        if(window.initLibImageQuant) {\r\n          wasmModule = await window.initLibImageQuant();\r\n        } else {\r\n          wasmModule = null;\r\n        }\r\n      }\r\n      if(wasmModule) {\r\n        wasmReady=true; statusLine.textContent='WASM quantizer loaded (high-quality path enabled).';\r\n      } else {\r\n        wasmReady=false; statusLine.textContent='WASM script loaded but wrapper API not found \u2014 falling back to JS quantizer.';\r\n      }\r\n    } catch(err){\r\n      console.warn('WASM load error', err);\r\n      wasmReady=false;\r\n      statusLine.textContent='WASM quantizer not available \u2014 using built-in fallback (UPNG).';\r\n    }\r\n  }\r\n\r\n  \/\/ normalize image to 8-bit RGBA via canvas (avoids 16-bit PNG issues)\r\n  async function normalizeToRGBA(file){\r\n    return new Promise((resolve, reject) => {\r\n      const url = URL.createObjectURL(file);\r\n      const img = new Image();\r\n      img.onload = () => {\r\n        \/\/ clamp huge images\r\n        const MAX_W = 8192;\r\n        let w = img.naturalWidth, h = img.naturalHeight;\r\n        if(w > MAX_W){\r\n          const r = MAX_W \/ w;\r\n          w = MAX_W; h = Math.round(h * r);\r\n        }\r\n        const c = document.createElement('canvas');\r\n        c.width = w; c.height = h;\r\n        const cctx = c.getContext('2d');\r\n        cctx.imageSmoothingEnabled = true;\r\n        cctx.drawImage(img, 0, 0, w, h);\r\n        const imgd = cctx.getImageData(0,0,w,h);\r\n        resolve({imgEl: img, imageData: imgd});\r\n      };\r\n      img.onerror = (e) => reject(new Error('Failed to load image'));\r\n      img.src = url;\r\n    });\r\n  }\r\n\r\n  \/\/ Resize utility: returns ImageData for requested width\r\n  function getResizedImageData(img, targetW){\r\n    const tmp = document.createElement('canvas');\r\n    tmp.width = targetW;\r\n    const ratio = targetW \/ img.naturalWidth;\r\n    tmp.height = Math.round(img.naturalHeight * ratio);\r\n    const tctx = tmp.getContext('2d');\r\n    tctx.imageSmoothingEnabled = true;\r\n    tctx.drawImage(img, 0, 0, tmp.width, tmp.height);\r\n    return tctx.getImageData(0,0,tmp.width,tmp.height);\r\n  }\r\n\r\n  \/\/ UPNG encode helper (fallback path)\r\n  function upngEncodeFromImageData(imgData, colors){\r\n    \/\/ UPNG.encode expects an ArrayBuffer RGBA; we provide one.\r\n    try {\r\n      const rgbaBuf = imgData.data.buffer.slice(0); \/\/ copy\r\n      const ab = UPNG.encode([rgbaBuf], imgData.width, imgData.height, colors|0);\r\n      return new Blob([ab], {type:'image\/png'});\r\n    } catch(e){\r\n      console.error('UPNG encode error', e);\r\n      throw e;\r\n    }\r\n  }\r\n\r\n  \/\/ Main compress flow (tries WASM first if available, otherwise UPNG fallback)\r\n  async function compressFlow(){\r\n    if(!currentImage) return;\r\n    compressBtn.disabled = true;\r\n    clearOutput();\r\n    statusLine.textContent = 'Preparing image...';\r\n\r\n    \/\/ Prepare resized image data according to settings\r\n    let targetImgData, targetW, targetH;\r\n    if(resizeMode.value === 'none'){\r\n      \/\/ keep natural but clamp width\r\n      targetW = currentImage.naturalWidth;\r\n      targetH = currentImage.naturalHeight;\r\n      if(targetW > 8192){\r\n        const r = 8192\/targetW; targetW = 8192; targetH = Math.round(targetH * r);\r\n      }\r\n      const tmpC = document.createElement('canvas');\r\n      tmpC.width = targetW; tmpC.height = targetH;\r\n      const tctx = tmpC.getContext('2d'); tctx.drawImage(currentImage,0,0,targetW,targetH);\r\n      targetImgData = tctx.getImageData(0,0,targetW,targetH);\r\n    } else if(resizeMode.value === 'percent'){\r\n      const pct = Math.max(1, Math.min(1000, Number(resizeValue.value)||100));\r\n      targetW = Math.max(1, Math.round(currentImage.naturalWidth * pct\/100));\r\n      targetImgData = getResizedImageData(currentImage, targetW);\r\n      targetW = targetImgData.width; targetH = targetImgData.height;\r\n    } else { \/\/ pixel width\r\n      const px = Math.max(1, Math.min(8192, Number(resizeValue.value)||currentImage.naturalWidth));\r\n      targetImgData = getResizedImageData(currentImage, px);\r\n      targetW = targetImgData.width; targetH = targetImgData.height;\r\n    }\r\n\r\n    \/\/ preview update\r\n    previewCanvas.width = targetW; previewCanvas.height = targetH;\r\n    ctx.putImageData(targetImgData, 0, 0);\r\n\r\n    const tgt = targetToggle.value === '1mb' ? 1024*1024 : null;\r\n    const startColors = Math.max(2, Math.min(256, Number(startColorsInput.value)||256));\r\n    const sequence = [startColors, 192, 128, 96, 64, 48, 32, 24, 16, 8, 4].filter((v,i,arr)=>arr.indexOf(v)===i && v>=2);\r\n\r\n    statusLine.textContent = 'Encoding...';\r\n\r\n    let bestBlob = null;\r\n    let bestColors = null;\r\n    const attempts = [];\r\n\r\n    \/\/ If WASM is ready and exposes a quantize API, try it first\r\n    if(wasmReady && wasmModule && typeof wasmModule.quantize === 'function'){\r\n      \/\/ Example API expected: wasmModule.quantize(imageData, width, height, { colors })\r\n      \/\/ Different wrappers vary \u2014 below we try a few common names safely.\r\n      for(const colors of sequence){\r\n        try {\r\n          statusLine.textContent = `WASM: trying ${colors} colors...`;\r\n          let out;\r\n          \/\/ try known wrapper signatures:\r\n          if(typeof wasmModule.quantize === 'function'){\r\n            out = await wasmModule.quantize(targetImgData.data, targetW, targetH, {colors});\r\n          } else if(typeof wasmModule.compress === 'function'){\r\n            out = await wasmModule.compress(targetImgData.data, targetW, targetH, colors);\r\n          } else {\r\n            throw new Error('WASM wrapper API not recognized');\r\n          }\r\n          \/\/ out expected to be Uint8Array or ArrayBuffer PNG bytes\r\n          const blob = (out instanceof Blob) ? out : new Blob([out.buffer||out], {type:'image\/png'});\r\n          attempts.push({colors, size: blob.size, via: 'wasm'});\r\n          if(!bestBlob || blob.size < bestBlob.size){ bestBlob = blob; bestColors = colors; }\r\n          if(tgt && blob.size <= tgt){ break; }\r\n        } catch(err){\r\n          console.warn('WASM attempt error', err);\r\n          break; \/\/ if WASM errors repeatedly, break to fallback\r\n        }\r\n      }\r\n    }\r\n\r\n    \/\/ If no good WASM result, use UPNG fallback\r\n    if(!bestBlob){\r\n      for(const colors of sequence){\r\n        try {\r\n          statusLine.textContent = `Fallback: trying ${colors} colors...`;\r\n          const blob = upngEncodeFromImageData(targetImgData, colors);\r\n          attempts.push({colors, size: blob.size, via: 'upng'});\r\n          if(!bestBlob || blob.size < bestBlob.size){ bestBlob = blob; bestColors = colors; }\r\n          if(tgt && blob.size <= tgt){ break; }\r\n          \/\/ tiny delay so UI updates for big images\r\n          await new Promise(r => setTimeout(r, 30));\r\n        } catch(e){\r\n          console.warn('UPNG error', e);\r\n          statusLine.textContent = 'Encoding failed during fallback.';\r\n          break;\r\n        }\r\n      }\r\n    }\r\n\r\n    if(bestBlob){\r\n      lastBlob = bestBlob;\r\n      lastName = (lastName || 'compressed.png');\r\n      sizeMeta.textContent = `${humanSize(bestBlob.size)} \u2014 colors ${bestColors}`;\r\n      const row = document.createElement('div'); row.className='asl-file';\r\n      const left = document.createElement('div'); left.textContent = `Result (${bestColors} colors) \u2014 ${humanSize(bestBlob.size)}`;\r\n      const actions = document.createElement('div');\r\n      const dl = document.createElement('a'); dl.textContent='Download'; dl.href = URL.createObjectURL(bestBlob); dl.download = lastName; dl.style.color='inherit';\r\n      actions.appendChild(dl); row.appendChild(left); row.appendChild(actions); filesList.appendChild(row);\r\n      \/\/ breakdown\r\n      const br = document.createElement('div'); br.className='asl-meta'; br.style.marginTop='8px';\r\n      br.textContent = 'Attempts: ' + attempts.map(a=>`${a.via}\/${a.colors}\u2192${humanSize(a.size)}`).join(', ');\r\n      filesList.appendChild(br);\r\n      statusLine.textContent = 'Done \u2014 click Download to save.';\r\n      downloadBtn.disabled = false;\r\n    } else {\r\n      statusLine.textContent = 'Compression failed. Try reducing the image size (resize) or enable WASM quantizer (host the WASM wrapper).';\r\n      const errNote = document.createElement('div'); errNote.className='asl-meta'; errNote.style.marginTop='8px';\r\n      errNote.textContent = 'If failure persists, please upload the image to test or try smaller dimensions.';\r\n      filesList.appendChild(errNote);\r\n      downloadBtn.disabled = true;\r\n    }\r\n\r\n    compressBtn.disabled = false;\r\n  }\r\n\r\n  \/\/ UI wiring\r\n  ['dragenter','dragover'].forEach(e => dropZone.addEventListener(e, ev => { ev.preventDefault(); dropZone.classList.add('drag'); }));\r\n  ['dragleave','drop'].forEach(e => dropZone.addEventListener(e, ev => { ev.preventDefault(); dropZone.classList.remove('drag'); }));\r\n  dropZone.addEventListener('drop', async ev => {\r\n    const dt = ev.dataTransfer; if(!dt || !dt.files || dt.files.length===0) return;\r\n    handleFile(dt.files[0]);\r\n  });\r\n  fileInput.addEventListener('change', ev => { if(fileInput.files && fileInput.files[0]) handleFile(fileInput.files[0]); });\r\n\r\n  async function handleFile(file){\r\n    clearOutput();\r\n    if(!file.type.startsWith('image\/')) { statusLine.textContent = 'Please provide an image file.'; return; }\r\n    lastName = file.name.replace(\/\\.[^\/.]+$\/,'') + '.png';\r\n    statusLine.textContent = 'Loading & normalizing image...';\r\n    try {\r\n      const {imgEl, imageData} = await normalizeToRGBA(file);\r\n      currentImage = imgEl;\r\n      \/\/ show preview scaled\r\n      const maxW = 800;\r\n      let w = imgEl.naturalWidth, h = imgEl.naturalHeight;\r\n      if(w > maxW){ const r = maxW\/w; w = Math.round(w*r); h = Math.round(h*r); }\r\n      previewCanvas.width = w; previewCanvas.height = h;\r\n      const tmp = document.createElement('canvas'); tmp.width = w; tmp.height = h;\r\n      tmp.getContext('2d').putImageData(getResizedImageData(imgEl, w), 0, 0); \/\/ draw scaled preview\r\n      ctx.drawImage(tmp, 0, 0);\r\n      statusLine.textContent = `Loaded: ${file.name} \u2014 ${imgEl.naturalWidth}\u00d7${imgEl.naturalHeight}`;\r\n      compressBtn.disabled = false;\r\n    } catch(e){\r\n      console.error('load\/normalize failed', e);\r\n      statusLine.textContent = 'Failed to load image. Try a different file.';\r\n      compressBtn.disabled = true;\r\n    }\r\n  }\r\n\r\n  compressBtn.addEventListener('click', compressFlow);\r\n  downloadBtn.addEventListener('click', () => {\r\n    if(!lastBlob) return;\r\n    const a = document.createElement('a'); a.href = URL.createObjectURL(lastBlob); a.download = lastName; document.body.appendChild(a); a.click(); a.remove();\r\n  });\r\n\r\n  \/\/ load wasm wrapper (non-blocking)\r\n  tryLoadWasm();\r\n});\r\n<\/script>\r\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>PNG COMPRESSOR Compress your PNG files so they stay an acceptable size for the Second Life Marketplace PNG Compressor \u2014 pngquant-style Client-side, lossy palette quantization. No uploads. Toggle target to force<\/p>","protected":false},"author":1,"featured_media":482,"parent":1613,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-2230","page","type-page","status-publish","has-post-thumbnail","hentry"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.6 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Second Life PNG Compressor - Alt Weekend Sales<\/title>\n<meta name=\"description\" content=\"Compress your PNG image files so they stay an acceptable size for the Second Life Marketplace and other places.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/altsl.com\/nl\/tools\/png-compressor\/\" \/>\n<meta property=\"og:locale\" content=\"nl_NL\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Second Life PNG Compressor - Alt Weekend Sales\" \/>\n<meta property=\"og:description\" content=\"Compress your PNG image files so they stay an acceptable size for the Second Life Marketplace and other places.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/altsl.com\/nl\/tools\/png-compressor\/\" \/>\n<meta property=\"og:site_name\" content=\"Alt Weekend Sales\" \/>\n<meta property=\"article:modified_time\" content=\"2025-12-04T14:23:18+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/altsl.com\/wp-content\/uploads\/2025\/10\/Alt_Banner.png\" \/>\n\t<meta property=\"og:image:width\" content=\"851\" \/>\n\t<meta property=\"og:image:height\" content=\"284\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Geschatte leestijd\" \/>\n\t<meta name=\"twitter:data1\" content=\"6 minuten\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/altsl.com\\\/tools\\\/png-compressor\\\/\",\"url\":\"https:\\\/\\\/altsl.com\\\/tools\\\/png-compressor\\\/\",\"name\":\"Second Life PNG Compressor - Alt Weekend Sales\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/altsl.com\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/altsl.com\\\/tools\\\/png-compressor\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/altsl.com\\\/tools\\\/png-compressor\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/altsl.com\\\/wp-content\\\/uploads\\\/2025\\\/10\\\/Alt_Banner.png\",\"datePublished\":\"2025-12-04T14:18:13+00:00\",\"dateModified\":\"2025-12-04T14:23:18+00:00\",\"description\":\"Compress your PNG image files so they stay an acceptable size for the Second Life Marketplace and other places.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/altsl.com\\\/tools\\\/png-compressor\\\/#breadcrumb\"},\"inLanguage\":\"nl-NL\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/altsl.com\\\/tools\\\/png-compressor\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"nl-NL\",\"@id\":\"https:\\\/\\\/altsl.com\\\/tools\\\/png-compressor\\\/#primaryimage\",\"url\":\"https:\\\/\\\/altsl.com\\\/wp-content\\\/uploads\\\/2025\\\/10\\\/Alt_Banner.png\",\"contentUrl\":\"https:\\\/\\\/altsl.com\\\/wp-content\\\/uploads\\\/2025\\\/10\\\/Alt_Banner.png\",\"width\":851,\"height\":284},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/altsl.com\\\/tools\\\/png-compressor\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/altsl.com\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Tools\",\"item\":\"https:\\\/\\\/altsl.com\\\/tools\\\/\"},{\"@type\":\"ListItem\",\"position\":3,\"name\":\"PNG Compressor\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/altsl.com\\\/#website\",\"url\":\"https:\\\/\\\/altsl.com\\\/\",\"name\":\"Alt Weekend Sale\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\\\/\\\/altsl.com\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/altsl.com\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"nl-NL\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/altsl.com\\\/#organization\",\"name\":\"Alt Weekend Sale\",\"url\":\"https:\\\/\\\/altsl.com\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"nl-NL\",\"@id\":\"https:\\\/\\\/altsl.com\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/altsl.com\\\/wp-content\\\/uploads\\\/2025\\\/10\\\/OBzIOE4.png\",\"contentUrl\":\"https:\\\/\\\/altsl.com\\\/wp-content\\\/uploads\\\/2025\\\/10\\\/OBzIOE4.png\",\"width\":1024,\"height\":747,\"caption\":\"Alt Weekend Sale\"},\"image\":{\"@id\":\"https:\\\/\\\/altsl.com\\\/#\\\/schema\\\/logo\\\/image\\\/\"}}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Second Life PNG Compressor - Alt Weekend Sales","description":"Compress your PNG image files so they stay an acceptable size for the Second Life Marketplace and other places.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/altsl.com\/nl\/tools\/png-compressor\/","og_locale":"nl_NL","og_type":"article","og_title":"Second Life PNG Compressor - Alt Weekend Sales","og_description":"Compress your PNG image files so they stay an acceptable size for the Second Life Marketplace and other places.","og_url":"https:\/\/altsl.com\/nl\/tools\/png-compressor\/","og_site_name":"Alt Weekend Sales","article_modified_time":"2025-12-04T14:23:18+00:00","og_image":[{"width":851,"height":284,"url":"https:\/\/altsl.com\/wp-content\/uploads\/2025\/10\/Alt_Banner.png","type":"image\/png"}],"twitter_card":"summary_large_image","twitter_misc":{"Geschatte leestijd":"6 minuten"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/altsl.com\/tools\/png-compressor\/","url":"https:\/\/altsl.com\/tools\/png-compressor\/","name":"Second Life PNG Compressor - Alt Weekend Sales","isPartOf":{"@id":"https:\/\/altsl.com\/#website"},"primaryImageOfPage":{"@id":"https:\/\/altsl.com\/tools\/png-compressor\/#primaryimage"},"image":{"@id":"https:\/\/altsl.com\/tools\/png-compressor\/#primaryimage"},"thumbnailUrl":"https:\/\/altsl.com\/wp-content\/uploads\/2025\/10\/Alt_Banner.png","datePublished":"2025-12-04T14:18:13+00:00","dateModified":"2025-12-04T14:23:18+00:00","description":"Compress your PNG image files so they stay an acceptable size for the Second Life Marketplace and other places.","breadcrumb":{"@id":"https:\/\/altsl.com\/tools\/png-compressor\/#breadcrumb"},"inLanguage":"nl-NL","potentialAction":[{"@type":"ReadAction","target":["https:\/\/altsl.com\/tools\/png-compressor\/"]}]},{"@type":"ImageObject","inLanguage":"nl-NL","@id":"https:\/\/altsl.com\/tools\/png-compressor\/#primaryimage","url":"https:\/\/altsl.com\/wp-content\/uploads\/2025\/10\/Alt_Banner.png","contentUrl":"https:\/\/altsl.com\/wp-content\/uploads\/2025\/10\/Alt_Banner.png","width":851,"height":284},{"@type":"BreadcrumbList","@id":"https:\/\/altsl.com\/tools\/png-compressor\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/altsl.com\/"},{"@type":"ListItem","position":2,"name":"Tools","item":"https:\/\/altsl.com\/tools\/"},{"@type":"ListItem","position":3,"name":"PNG Compressor"}]},{"@type":"WebSite","@id":"https:\/\/altsl.com\/#website","url":"https:\/\/altsl.com\/","name":"Alt Weekend Sale","description":"","publisher":{"@id":"https:\/\/altsl.com\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/altsl.com\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"nl-NL"},{"@type":"Organization","@id":"https:\/\/altsl.com\/#organization","name":"Alt Weekend Sale","url":"https:\/\/altsl.com\/","logo":{"@type":"ImageObject","inLanguage":"nl-NL","@id":"https:\/\/altsl.com\/#\/schema\/logo\/image\/","url":"https:\/\/altsl.com\/wp-content\/uploads\/2025\/10\/OBzIOE4.png","contentUrl":"https:\/\/altsl.com\/wp-content\/uploads\/2025\/10\/OBzIOE4.png","width":1024,"height":747,"caption":"Alt Weekend Sale"},"image":{"@id":"https:\/\/altsl.com\/#\/schema\/logo\/image\/"}}]}},"_links":{"self":[{"href":"https:\/\/altsl.com\/nl\/wp-json\/wp\/v2\/pages\/2230","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/altsl.com\/nl\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/altsl.com\/nl\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/altsl.com\/nl\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/altsl.com\/nl\/wp-json\/wp\/v2\/comments?post=2230"}],"version-history":[{"count":10,"href":"https:\/\/altsl.com\/nl\/wp-json\/wp\/v2\/pages\/2230\/revisions"}],"predecessor-version":[{"id":2240,"href":"https:\/\/altsl.com\/nl\/wp-json\/wp\/v2\/pages\/2230\/revisions\/2240"}],"up":[{"embeddable":true,"href":"https:\/\/altsl.com\/nl\/wp-json\/wp\/v2\/pages\/1613"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/altsl.com\/nl\/wp-json\/wp\/v2\/media\/482"}],"wp:attachment":[{"href":"https:\/\/altsl.com\/nl\/wp-json\/wp\/v2\/media?parent=2230"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}