"""Decode actual ICO directory entries and PNG payload pixels. Requires Pillow.
Usage: python decode_ico.py wide.ico --out decoded-wide
"""
from pathlib import Path
import argparse,csv,gzip,hashlib,io,json,struct
from PIL import Image

def decode(path,out):
 data=Path(path).read_bytes();out=Path(out);out.mkdir(parents=True,exist_ok=True)
 if len(data)<6:raise ValueError('Truncated ICO header')
 reserved,kind,count=struct.unpack_from('<HHH',data)
 if reserved!=0 or kind!=1 or count==0 or 6+16*count>len(data):raise ValueError('Invalid ICO directory')
 entries=[];ranges=[]
 with gzip.open(out/'pixels.csv.gz','wt',newline='',encoding='utf-8') as f:
  writer=csv.writer(f);writer.writerow(['frame','width','height','x','y','r','g','b','a'])
  for index in range(count):
   width,height,colors,res,planes,bpp,length,offset=struct.unpack_from('<BBBBHHII',data,6+16*index)
   width=width or 256;height=height or 256
   if offset<6+16*count or offset+length>len(data):raise ValueError('Frame is outside payload area')
   if any(offset<b and offset+length>a for a,b in ranges):raise ValueError('Overlapping frame payloads')
   ranges.append((offset,offset+length));payload=data[offset:offset+length]
   if not payload.startswith(b'\x89PNG\r\n\x1a\n'):raise ValueError('This decoder accepts PNG-encoded frames only')
   ihdr=struct.unpack_from('>II',payload,16)
   if ihdr!=(width,height):raise ValueError('ICO entry and PNG IHDR disagree')
   image=Image.open(io.BytesIO(payload)).convert('RGBA');alpha=image.getchannel('A');hist=alpha.histogram();box=alpha.getbbox()
   (out/f'frame-{width}.png').write_bytes(payload)
   positions={'corner':(0,0),'left_quarter':(width//4,height//2),'right_quarter':(3*width//4,height//2),'center':(width//2,height//2)}
   item={'index':index,'width':width,'height':height,'planes':planes,'bit_count':bpp,'offset':offset,'bytes':length,'png_ihdr':list(ihdr),'bbox_nonzero_alpha_exclusive':list(box) if box else None,'transparent_pixels':hist[0],'opaque_pixels':hist[255],'partial_alpha_pixels':sum(hist[1:255]),'samples':{k:{'xy':list(xy),'rgba':list(image.getpixel(xy))} for k,xy in positions.items()},'payload_sha256':hashlib.sha256(payload).hexdigest()}
   for y in range(height):
    for x in range(width):writer.writerow([index,width,height,x,y,*image.getpixel((x,y))])
   entries.append(item)
 if max(b for a,b in ranges)!=len(data):raise ValueError('Unexpected trailing payload data')
 result={'file':Path(path).name,'bytes':len(data),'sha256':hashlib.sha256(data).hexdigest(),'directory_count':count,'entries':entries}
 (out/'measurements.json').write_text(json.dumps(result,indent=2),encoding='utf-8')
 return result

if __name__=='__main__':
 p=argparse.ArgumentParser();p.add_argument('ico');p.add_argument('--out',default='decoded-ico');a=p.parse_args();print(json.dumps(decode(a.ico,a.out),indent=2))
