import fs from 'node:fs'; import path from 'node:path'; import {fileURLToPath} from 'node:url'; import {chromium} from 'playwright-core'; const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..'); const outputDir=path.join(root,'research'); const candidates=[process.env.CHROME_PATH,'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe','C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe','C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe','/usr/bin/google-chrome','/usr/bin/chromium'].filter(Boolean); const executablePath=candidates.find(file=>fs.existsSync(file)); if(!executablePath)throw Error('Chrome or Edge was not found for the image-format benchmark.'); fs.mkdirSync(outputDir,{recursive:true}); const browser=await chromium.launch({headless:true,executablePath}); try{ const page=await browser.newPage({viewport:{width:900,height:700}}); const result=await page.evaluate(async()=>{ const width=800,height=500; const makeCanvas=()=>Object.assign(document.createElement('canvas'),{width,height}); const seeded=seed=>()=>{seed^=seed<<13;seed^=seed>>>17;seed^=seed<<5;return (seed>>>0)/4294967296}; const drawPhoto=()=>{const c=makeCanvas(),x=c.getContext('2d'),rand=seeded(82173);const sky=x.createLinearGradient(0,0,0,height);sky.addColorStop(0,'#5aa9e6');sky.addColorStop(.52,'#d7f0ff');sky.addColorStop(.53,'#678d58');sky.addColorStop(1,'#1f402b');x.fillStyle=sky;x.fillRect(0,0,width,height);for(let i=0;i<320;i++){const px=rand()*width,py=180+rand()*320,r=3+rand()*35;x.fillStyle=`hsla(${75+rand()*70},${35+rand()*45}%,${20+rand()*42}%,${.12+rand()*.42})`;x.beginPath();x.arc(px,py,r,0,Math.PI*2);x.fill()}for(let i=0;i<9000;i++){const v=Math.floor(rand()*255);x.fillStyle=`rgba(${v},${v},${v},.035)`;x.fillRect(rand()*width,rand()*height,1,1)}return c}; const drawGraphic=()=>{const c=makeCanvas(),x=c.getContext('2d');x.fillStyle='#f8fafc';x.fillRect(0,0,width,height);x.fillStyle='#0f172a';x.font='700 46px Arial';x.fillText('ToolkitPoint format test',46,76);x.font='22px Arial';x.fillStyle='#475569';x.fillText('Flat colours, sharp edges, gradients and text',46,112);const colors=['#2563eb','#06b6d4','#f59e0b','#7c3aed','#ef4444'];colors.forEach((color,i)=>{x.fillStyle=color;x.fillRect(46+i*142,160,112,112)});const g=x.createLinearGradient(46,0,754,0);g.addColorStop(0,'#2563eb');g.addColorStop(.5,'#06b6d4');g.addColorStop(1,'#f59e0b');x.fillStyle=g;x.fillRect(46,312,708,82);x.strokeStyle='#0f172a';x.lineWidth=3;for(let i=0;i<14;i++){x.beginPath();x.moveTo(46+i*54,432);x.lineTo(72+i*54,458);x.stroke()}return c}; const drawDocument=()=>{const c=makeCanvas(),x=c.getContext('2d'),rand=seeded(44031);x.fillStyle='#f5f0e6';x.fillRect(0,0,width,height);x.strokeStyle='#cbc2b2';x.strokeRect(35,25,width-70,height-50);x.fillStyle='#171717';x.font='700 34px Georgia';x.fillText('Document scan sample',64,78);x.font='17px Georgia';for(let row=0;row<15;row++){const y=120+row*22;x.fillStyle=row%5===0?'#333':'#555';x.fillRect(65,y,590+(row%4)*28,2)}x.fillStyle='#2563eb';x.fillRect(65,455,160,5);for(let i=0;i<7000;i++){const v=165+Math.floor(rand()*70);x.fillStyle=`rgba(${v},${v-5},${v-12},.035)`;x.fillRect(rand()*width,rand()*height,1,1)}return c}; const scenes=[['photo-like',drawPhoto()],['flat-graphic',drawGraphic()],['document-scan',drawDocument()]]; const settings=[['PNG','image/png'],['JPEG 90','image/jpeg',.9],['JPEG 75','image/jpeg',.75],['JPEG 55','image/jpeg',.55],['WebP 85','image/webp',.85],['WebP 70','image/webp',.7],['WebP 50','image/webp',.5]]; const blobFor=(canvas,type,quality)=>new Promise(resolve=>canvas.toBlob(resolve,type,quality)); const pixels=canvas=>canvas.getContext('2d').getImageData(0,0,width,height).data; const decode=async blob=>{const bitmap=await createImageBitmap(blob),c=makeCanvas();c.getContext('2d').drawImage(bitmap,0,0);bitmap.close();return c}; const rows=[],files={}; for(const [scene,source] of scenes){ const original=pixels(source);let pngBytes=0; for(const [label,type,quality] of settings){ const blob=await blobFor(source,type,quality);if(!blob)continue; if(label==='PNG')pngBytes=blob.size; const rendered=pixels(await decode(blob));let error=0; for(let i=0;iitem.scene===scene))row.percentOfPng=Number((row.bytes/pngBytes*100).toFixed(1)); } const preview=document.createElement('canvas');preview.width=1200;preview.height=630; const p=preview.getContext('2d'),background=p.createLinearGradient(0,0,1200,630);background.addColorStop(0,'#eef4ff');background.addColorStop(1,'#ecfeff');p.fillStyle=background;p.fillRect(0,0,1200,630); p.fillStyle='#ffffff';p.beginPath();p.roundRect(38,38,1124,554,36);p.fill(); p.fillStyle='#0f172a';p.font='700 30px Arial';p.fillText('PNG vs JPEG vs WebP',80,84);p.fillStyle='#2563eb';p.font='700 18px Arial';p.fillText('TOOLKITPOINT BENCHMARK',895,81); const panelWidth=340,panelHeight=195,panelY=108,panelXs=[80,430,780],formatColors={'PNG':'#7c3aed','JPEG 75':'#f59e0b','WebP 70':'#06b6d4'}; scenes.forEach(([scene,source],index)=>{const x=panelXs[index];p.save();p.beginPath();p.roundRect(x,panelY,panelWidth,panelHeight,22);p.clip();p.drawImage(source,x,panelY,panelWidth,panelHeight);p.restore();p.strokeStyle='#dbe5f4';p.lineWidth=2;p.beginPath();p.roundRect(x,panelY,panelWidth,panelHeight,22);p.stroke();const sceneRows=rows.filter(row=>row.scene===scene&&['PNG','JPEG 75','WebP 70'].includes(row.label));sceneRows.forEach((row,rowIndex)=>{const y=350+rowIndex*67,maxWidth=250,width=Math.max(16,maxWidth*Math.sqrt(row.percentOfPng/100));p.fillStyle='#eff4fb';p.beginPath();p.roundRect(x,y,maxWidth,28,14);p.fill();p.fillStyle=formatColors[row.label];p.beginPath();p.roundRect(x,y,width,28,14);p.fill();p.fillStyle='#0f172a';p.font='700 20px Arial';p.fillText(row.label,x+260,y+22)});}); const previewBlob=await blobFor(preview,'image/png');files['image-format-benchmark-preview.png']=Array.from(new Uint8Array(await previewBlob.arrayBuffer())); return {width,height,rows,files}; }); const generatedAt='2026-09-15'; const publicRows=result.rows.map(row=>({...row,bytes:row.bytes})); const json={title:'ToolkitPoint browser image-format benchmark',generatedAt,method:{browser:path.basename(executablePath),dimensions:`${result.width}x${result.height}`,scenes:['photo-like synthetic scene','flat graphic with text and gradients','document-scan simulation'],comparison:'Browser Canvas encoders; PNG is the per-scene size baseline. PSNR compares decoded RGB pixels with the original canvas. Higher PSNR means less pixel error.',limitations:'Synthetic scenes are reproducible but cannot represent every camera, screenshot, transparency, colour-profile, metadata, or browser-encoder workload.'},rows:publicRows}; fs.writeFileSync(path.join(outputDir,'image-format-benchmark.json'),JSON.stringify(json,null,2)+'\n'); const csv=['scene,format,mime,quality,bytes,kilobytes,percent_of_png,psnr_db',...publicRows.map(row=>[row.scene,row.label,row.mime,row.quality??'',row.bytes,row.kilobytes,row.percentOfPng,row.psnrDb??'lossless'].join(','))].join('\n')+'\n'; fs.writeFileSync(path.join(outputDir,'image-format-benchmark.csv'),csv); const chosen=['image-format-benchmark-preview.png','photo-like-png.png','photo-like-jpeg-75.jpg','photo-like-webp-70.webp','flat-graphic-png.png','flat-graphic-jpeg-75.jpg','flat-graphic-webp-70.webp','document-scan-png.png','document-scan-jpeg-75.jpg','document-scan-webp-70.webp']; for(const name of chosen){const bytes=result.files[name];if(bytes)fs.writeFileSync(path.join(outputDir,name),Buffer.from(bytes));} console.log(JSON.stringify({passed:true,browser:path.basename(executablePath),generatedAt,rows:publicRows.length,files:chosen.filter(name=>fs.existsSync(path.join(outputDir,name)))},null,2)); }finally{await browser.close()}