Quarter-turn pixel workbench — Verify rotation direction, output extent and saved/reopened pixel identity while preserving the original image.
HTML for this example
Select the code and use your copy shortcut. Changes in this preview are temporary.
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Quarter-turn pixel workbench</title><style>
*{box-sizing:border-box}body{margin:0;background:#f3f4fa;color:#27334e;font:15px/1.5 system-ui,sans-serif}main{max-width:1080px;margin:auto;padding:30px}h1{font-size:clamp(29px,4vw,42px);letter-spacing:-.04em;margin:5px 0 10px}h2{font-size:21px;margin:0 0 14px}p{margin:0 0 12px}.eyebrow{font-size:11px;font-weight:750;text-transform:uppercase;letter-spacing:.14em;color:#62718f}.small{font-size:13px;color:#62718f}.toolbar{display:flex;gap:8px;flex-wrap:wrap;margin:24px 0 18px}button{font:650 14px system-ui;min-height:44px;padding:10px 17px;background:#4352a2;color:white;border:1px solid #4352a2;border-radius:6px;cursor:pointer}button.secondary{background:white;color:#4352a2}button[aria-pressed=true]{background:#28336c;color:white}:focus-visible{outline:3px solid #d08b33;outline-offset:3px}.compare{display:grid;grid-template-columns:1fr 1fr;gap:22px}.panel{background:white;border:1px solid #d6dcec;border-radius:11px;padding:23px}.grid{display:grid;gap:5px;max-width:320px;margin:20px auto}.pixel{position:relative;min-height:83px;display:flex;flex-direction:column;align-items:center;justify-content:center;border:2px solid #132d4980;border-radius:5px;font:750 30px system-ui;text-shadow:0 0 4px #fff}.pixel small{font:600 11px system-ui;display:block;background:#ffffffd9;color:#243553;text-shadow:none;padding:1px 5px;border-radius:3px}.pixel[data-pixel=A],.pixel[data-pixel=C],.pixel[data-pixel=E]{color:white;text-shadow:0 0 4px #222}.pixel[data-selected=true]{outline:4px solid #e28e26;outline-offset:2px}.matrix{font:700 23px/1.7 ui-monospace,Consolas,monospace;white-space:pre-wrap;text-align:center;letter-spacing:.1em;color:#354583;margin:8px 0}.dimension{font-size:14px;font-weight:650}.mapping{padding:18px 22px;background:#e6eafb;border-left:4px solid #697ac3;margin-top:20px;border-radius:5px}.mapping p{margin:5px 0 0}.lower{margin-top:26px}.fields{display:grid;grid-template-columns:1fr 1fr;gap:18px}label{font-size:13px;font-weight:650;display:block}input,textarea{font:14px/1.6 ui-monospace,Consolas,monospace;color:#27334e;width:100%;padding:11px;border:1px solid #9eabc5;border-radius:5px;display:block;margin:6px 0 13px;background:#fafbff}textarea{min-height:155px;resize:vertical}.result{font-size:15px;background:#edf1fb;border-left:4px solid #697ac3;padding:14px;margin-top:13px;min-height:52px}.error{color:#a02c2c}.row{display:flex;gap:10px;align-items:center;flex-wrap:wrap;justify-content:space-between}details{border-top:1px solid #d6dcec;margin-top:25px;padding-top:17px}summary{cursor:pointer;min-height:40px;font-weight:700}.packet{min-height:360px}footer{font-size:13px;color:#62718f;max-width:95ch;margin:25px 0}[hidden]{display:none!important}@media(max-width:700px){main{padding:23px 16px}.compare,.fields{grid-template-columns:1fr}.panel{padding:19px}.grid{max-width:290px}.pixel{min-height:75px}.toolbar button{flex:1;padding:10px}.matrix{font-size:21px}}@media(max-width:380px){main{padding:20px 12px}.pixel{min-height:70px}}
</style></head><body><main><p class="eyebrow">Python / Pillow companion · original six-pixel fixture</p><h1>Track the pixels through a quarter-turn.</h1><p>Compare the coordinate permutation, then check a manually supplied native pixel record.</p><div class="toolbar"><button id="ccw" type="button" aria-pressed="true">+90° · counterclockwise</button><button id="cw" type="button" class="secondary" aria-pressed="false">−90° · clockwise</button><button id="crop" type="button" class="secondary" aria-pressed="false">+90° · same extent</button></div><section class="compare"><div class="panel"><h2>Original input</h2><p class="dimension">Width 2 × height 3 · opaque RGB</p><div id="source-grid" class="grid" style="grid-template-columns:repeat(2,1fr)"></div><p class="matrix">A B<br>C D<br>E F</p><p class="small">Upper-left origin. Select an ID to trace its coordinates; each tile represents one authored pixel.</p></div><div class="panel"><h2 id="case-title">Expected +90° expanded</h2><p id="dimensions" class="dimension">Width 3 × height 2 · RGB</p><div id="output-grid" class="grid" style="grid-template-columns:repeat(3,1fr)"></div><p id="matrix" class="matrix" role="status">B D F / A C E</p><p id="extent-note" class="small">Default center · no translation · NEAREST · expand=True</p></div></section><section class="mapping"><strong id="mapping-rule">x′ = y; y′ = width − 1 − x</strong><p id="pixel-detail">A: input (0, 0) → expected (0, 1) · RGB (255, 0, 0)</p></section><section class="panel lower"><h2>Compare a native decoded record</h2><p class="small">Paste JSON recorded after saving and reopening your own PNG. This checks the supplied text against the fixture; it does not open a file or run Pillow.</p><label for="actual">Reopened output JSON · format, mode, size [width, height], and rows of RGB triples<textarea id="actual" spellcheck="false" placeholder='{"format":"PNG","mode":"RGB","size":[3,2],"pixels":[[[0,255,0],[255,255,0],[0,255,255]],[[255,0,0],[0,0,255],[255,0,255]]]}'></textarea></label><button type="button" id="compare-record">Compare supplied record</button><p id="record-result" class="result" role="status">No native record supplied.</p><p class="small" style="margin-top:12px">Source path/hash, output paths/hashes, installed Python/Pillow version, and actual before/after source-byte evidence remain in your native record. Pixel agreement alone does not establish that the source file was preserved.</p></section><details><summary>Native source specification · save to a new PNG, then reopen</summary><p class="small">Replace the paths in your own environment. The source must match every supplied RGB tuple. The destination check refuses existing output files. No native Python or PNG file is created by this page.</p><div class="row"><label for="native-source">Current case · Python source</label><button type="button" id="select-source" class="secondary">Select text</button></div><textarea id="native-source" class="packet" readonly spellcheck="false"></textarea><p id="select-status" class="small" role="status"></p></details><footer>The grids are authored coordinate models, not loaded images or actual Pillow output. Exact expectations apply only to this static 2×3 RGB fixture at ±90°, default center, no translation, NEAREST, PNG output. Same-extent crop/fill placement remains pending native inspection. No arbitrary-angle, EXIF, animation, lossy-file conversion, download, or persistence is provided. The manuscript cites Pillow 12.3.0; its stable documentation URL now displays 13.0.0. Neither establishes the installed version.</footer></main><script>
'use strict';const $=id=>document.getElementById(id);const pixels=[{id:'A',x:0,y:0,rgb:[255,0,0]},{id:'B',x:1,y:0,rgb:[0,255,0]},{id:'C',x:0,y:1,rgb:[0,0,255]},{id:'D',x:1,y:1,rgb:[255,255,0]},{id:'E',x:0,y:2,rgb:[255,0,255]},{id:'F',x:1,y:2,rgb:[0,255,255]}];let mode='ccw',selected='A';
function rotate(point,direction){return direction==='ccw'?{x:point.y,y:1-point.x}:{x:2-point.y,y:point.x};}
function matrix(direction){const rows=[[],[]];for(const p of pixels){const q=rotate(p,direction);rows[q.y][q.x]=p.id;}return rows;}
function tile(p,position,prefix){const b=document.createElement('button');b.type='button';b.className='pixel';b.id=prefix+'-'+p.id;b.style.background='rgb('+p.rgb.join(',')+')';b.setAttribute('data-pixel',p.id);b.setAttribute('data-selected',String(p.id===selected));b.setAttribute('aria-label',p.id+' at '+position.x+', '+position.y+', RGB '+p.rgb.join(', '));let t=document.createElement('span');t.textContent=p.id;b.appendChild(t);let c=document.createElement('small');c.textContent='('+position.x+', '+position.y+')';b.appendChild(c);b.addEventListener('click',()=>{selected=p.id;render();});return b;}
function compareRecord(text,currentMode){if(!text.trim())return{valid:false,message:'Paste a native decoded record before comparing.'};let v;try{v=JSON.parse(text);}catch{return{valid:false,message:'Invalid JSON. Supply format, mode, size, and pixel rows.'};}if(v===null||Array.isArray(v)||typeof v!=='object')return{valid:false,message:'The record must be a JSON object.'};const size=currentMode==='crop'?[2,3]:[3,2];if(v.format!=='PNG'||v.mode!=='RGB'||!Array.isArray(v.size)||v.size.length!==2||v.size.some((n,i)=>n!==size[i]))return{valid:false,message:'Record mismatch: expected PNG / RGB / size ['+size.join(', ')+'].'};if(currentMode==='crop')return{valid:true,message:'Extent matches 2 × 3. Exact cropped pixels/fill remain unverified; no six-pixel retention claim.'};const ids=matrix(currentMode);if(!Array.isArray(v.pixels)||v.pixels.length!==2)return{valid:false,message:'Expected two complete rows of three RGB pixels.'};for(let y=0;y<2;y++){if(!Array.isArray(v.pixels[y])||v.pixels[y].length!==3)return{valid:false,message:'Expected three pixels in row '+y+'.'};for(let x=0;x<3;x++){const expected=pixels.find(p=>p.id===ids[y][x]).rgb,actual=v.pixels[y][x];if(!Array.isArray(actual)||actual.length!==3||actual.some((n,i)=>n!==expected[i]))return{valid:false,message:'Pixel mismatch at ('+x+', '+y+'): expected '+ids[y][x]+' RGB ('+expected.join(', ')+').'};}}return{valid:true,message:'Supplied record matches PNG / RGB / 3 × 2 and all six expected pixel tuples. Native execution and source preservation are not verified by this text comparison.'};}
function source(){const angle=mode==='cw'?-90:90,expand=mode!=='crop';return['from pathlib import Path','import hashlib, json','from PIL import Image, __version__ as pillow_version','','source_path = Path("source.png")','output_path = Path("'+(mode==='cw'?'clockwise':mode==='crop'?'same_extent':'counterclockwise')+'.png")','if source_path.resolve() == output_path.resolve() or output_path.exists():',' raise ValueError("Choose a distinct new output destination")','before = hashlib.sha256(source_path.read_bytes()).hexdigest()','expected_source = [(255,0,0),(0,255,0),(0,0,255),',' (255,255,0),(255,0,255),(0,255,255)]','with Image.open(source_path) as original:',' original.load()',' if original.mode != "RGB" or original.size != (2, 3):',' raise ValueError("Wrong source mode or dimensions")',' actual_source = [original.getpixel((x,y)) for y in range(3) for x in range(2)]',' if actual_source != expected_source:',' raise ValueError("Source pixels do not match fixture")',' rotated = original.rotate('+angle+', resample=Image.Resampling.NEAREST, expand='+String(expand===true?'True':'False')+')',' rotated.save(output_path, format="PNG")','after = hashlib.sha256(source_path.read_bytes()).hexdigest()','assert before == after, "Source bytes changed"','with Image.open(output_path) as result:',' result.load()',' record = {"format":result.format, "mode":result.mode,',' "size":list(result.size),',' "pixels":[[result.getpixel((x,y)) for x in range(result.width)]',' for y in range(result.height)]}','print(json.dumps(record))','print("Pillow", pillow_version, "source_sha256", before)','print("output_sha256", hashlib.sha256(output_path.read_bytes()).hexdigest())','# Compare reopened dimensions and all expected tuples independently.','# Exact same-extent crop/fill pixels have no authored key in this packet.'].join('\n');}
function render(){$('source-grid').replaceChildren();$('output-grid').replaceChildren();pixels.forEach(p=>$('source-grid').appendChild(tile(p,p,'input')));for(const m of['ccw','cw','crop'])$(m).setAttribute('aria-pressed',String(mode===m));$('case-title').textContent=mode==='cw'?'Expected −90° expanded':mode==='crop'?'Expected +90° same extent':'Expected +90° expanded';$('dimensions').textContent=mode==='crop'?'Width 2 × height 3 · extent only':'Width 3 × height 2 · RGB';$('output-grid').hidden=mode==='crop';if(mode!=='crop'){const ids=matrix(mode);for(const row of ids)for(const id of row){const p=pixels.find(p=>p.id===id);$('output-grid').appendChild(tile(p,rotate(p,mode),'output'));}$('matrix').textContent=ids.map(row=>row.join(' ')).join(' / ');}else $('matrix').textContent='Crop/fill pixels pending';$('extent-note').textContent='Default center · no translation · NEAREST · expand='+(mode==='crop'?'False':'True');$('mapping-rule').textContent=mode==='cw'?'x′ = height − 1 − y; y′ = x':mode==='crop'?'Same extent is not a six-pixel permutation guarantee.':'x′ = y; y′ = width − 1 − x';const p=pixels.find(p=>p.id===selected),q=rotate(p,mode);$('pixel-detail').textContent=p.id+': input ('+p.x+', '+p.y+') → '+(mode==='crop'?'cropped placement pending':'expected ('+q.x+', '+q.y+')')+' · RGB ('+p.rgb.join(', ')+')';$('native-source').value=source();$('select-status').textContent='';}
for(const m of['ccw','cw','crop'])$(m).addEventListener('click',()=>{mode=m;render();$('record-result').textContent='Current case changed. Compare a record for these settings; prior acceptance is cleared.';$('record-result').className='result';});$('actual').addEventListener('input',()=>{$('record-result').textContent='Not compared. Current edited record has no acceptance.';$('record-result').className='result';});$('compare-record').addEventListener('click',()=>{const r=compareRecord($('actual').value,mode);$('record-result').textContent=r.message;$('record-result').className='result'+(r.valid?'':' error');});$('select-source').addEventListener('click',()=>{$('native-source').focus();$('native-source').select();$('select-status').textContent='Source text selected. Copy manually into your own native environment.';});render();
</script></body></html>