# 本文公开核验脚本：仅验证随附原创文件及声明的有限条件。
import json, csv, hashlib, re, struct, subprocess, sys, zlib
from fractions import Fraction
from pathlib import Path
P = Path(__file__).resolve().parent
def read(name): return json.loads((P/name).read_text(encoding='utf-8-sig'))
def digest(name): return hashlib.sha256((P/name).read_bytes()).hexdigest()
def probe(name, *args):
    command = ['ffprobe','-v','error',*args,'-of','json',str(P/name)]
    run = subprocess.run(command,capture_output=True,text=True,encoding='utf-8',timeout=25)
    assert run.returncode == 0, run.stderr
    return json.loads(run.stdout)
def decode(name):
    command=['ffmpeg','-v','error','-xerror','-i',str(P/name),'-map','0:v?','-map','0:a?','-f','null','-']
    run=subprocess.run(command,capture_output=True,text=True,encoding='utf-8',timeout=25)
    assert run.returncode == 0, run.stderr
    assert not run.stderr.strip(), run.stderr
    return {'file':name,'exit_code':run.returncode,'stderr':run.stderr}
def packets(name, select='v:0'):
    return probe(name,'-select_streams',select,'-show_packets')['packets']
def streams(name): return probe(name,'-show_streams','-show_format')
def stamp(v,base): return Fraction(int(v))*Fraction(base)
def framehash(name):
    run=subprocess.run(['ffmpeg','-v','error','-i',str(P/name),'-map','0:V:0','-f','framemd5','-'],capture_output=True,text=True,encoding='utf-8',timeout=25)
    assert run.returncode==0,run.stderr
    return [x.split(',')[-1].strip() for x in run.stdout.splitlines() if x and not x.startswith('#')]
def emit(value): print(json.dumps(value,ensure_ascii=False,indent=2))

from PIL import Image, __version__ as pillow_version
def pil_frames(name):
    out=[]
    with Image.open(P/name) as im:
        for i in range(im.n_frames):
            im.seek(i);im.load();rgba=im.convert('RGBA')
            out.append({'index':i,'size':list(rgba.size),'duration_ms':im.info.get('duration'),'pixel_15_15':list(rgba.getpixel((15,15))),'pixel_45_5':list(rgba.getpixel((45,5))),'rgba_sha256':hashlib.sha256(rgba.tobytes()).hexdigest()})
    return out
def png_chunks(name):
    b=(P/name).read_bytes();assert b[:8]==b'\x89PNG\r\n\x1a\n';i=8;out=[]
    while i<len(b):
        n=struct.unpack_from('>I',b,i)[0];t=b[i+4:i+8];d=b[i+8:i+8+n];crc=struct.unpack_from('>I',b,i+8+n)[0]
        assert zlib.crc32(t+d)&0xffffffff==crc
        if t==b'fcTL':
            v=struct.unpack('>IIIIIHHBB',d);out.append(dict(zip(['sequence','width','height','x','y','delay_num','delay_den','dispose','blend'],v)))
        i+=12+n
    assert i==len(b)
    return out
def webp_chunks(name):
    b=(P/name).read_bytes();assert b[:4]==b'RIFF' and b[8:12]==b'WEBP';assert int.from_bytes(b[4:8],'little')+8==len(b);i=12;out=[]
    u=lambda d:int.from_bytes(d,'little')
    while i<len(b):
        t=b[i:i+4];n=u(b[i+4:i+8]);d=b[i+8:i+8+n];assert len(d)==n
        if t==b'ANMF':out.append({'x':u(d[:3])*2,'y':u(d[3:6])*2,'width':u(d[6:9])+1,'height':u(d[9:12])+1,'duration_ms':u(d[12:15]),'flags':d[15]})
        i+=8+n+(n&1)
    assert i==len(b)
    return out
def gif_blocks(name):
    b=(P/name).read_bytes();assert b[:6] in [b'GIF87a',b'GIF89a'];w,h=struct.unpack_from('<HH',b,6);packed=b[10];i=13;gct=b''
    if packed&128:n=3*(2**((packed&7)+1));gct=b[i:i+n];i+=n
    gce={};out=[]
    def skip(j):
        while True:
            n=b[j];j+=1
            if n==0:return j
            j+=n;assert j<=len(b)
    while i<len(b):
        marker=b[i];i+=1
        if marker==0x3b:break
        if marker==0x21:
            label=b[i];i+=1
            if label==0xf9:
                assert b[i]==4;flags,delay,trans=struct.unpack_from('<BHB',b,i+1);gce={'delay_cs':delay,'disposal':(flags>>2)&7,'transparent':bool(flags&1),'transparent_index':trans};i+=6
            else:i=skip(i)
        elif marker==0x2c:
            x,y,fw,fh,flags=struct.unpack_from('<HHHHB',b,i);i+=9;lct=b''
            if flags&128:n=3*(2**((flags&7)+1));lct=b[i:i+n];i+=n
            palette=lct or gct;out.append({'x':x,'y':y,'width':fw,'height':fh,'local_palette':bool(lct),'palette_first_two':[list(palette[:3]),list(palette[3:6])],**gce});i+=1;i=skip(i);gce={}
        else:raise AssertionError(('未知GIF块',marker))
    return {'canvas':[w,h],'frames':out}

def apng_raw(name):
    data=(P/name).read_bytes();i=8;frames=[];current=None;seen_fctl=False;default_excluded=False
    while i<len(data):
        size=struct.unpack_from('>I',data,i)[0];tag=data[i+4:i+8];payload=data[i+8:i+8+size]
        if tag==b'fcTL':
            values=struct.unpack('>IIIIIHHBB',payload);current={'width':values[1],'height':values[2],'compressed':bytearray()};frames.append(current);seen_fctl=True
        elif tag==b'IDAT':
            if not seen_fctl:default_excluded=True
            else:current['compressed'].extend(payload)
        elif tag==b'fdAT':current['compressed'].extend(payload[4:])
        i+=size+12
    pixels=[]
    for frame in frames:
        raw=zlib.decompress(frame['compressed']);stride=frame['width']*4+1;assert len(raw)==stride*frame['height']
        assert all(raw[y*stride]==0 for y in range(frame['height'])), '本例独立解压器仅处理原创filter0行'
        pixels.append(list(raw[1:5]));assert all(raw[y*stride+1:y*stride+5]==raw[1:5] for y in range(frame['height']))
    return {'default_image_excluded':default_excluded,'pixels':pixels,'scope':'独立zlib解压原创filter0 RGBA8帧，不调用APNG loader'}

result={f:{'fctl':png_chunks(f),'decoded':pil_frames(f),'raw_frames':apng_raw(f)} for f in ['source.apng','over.apng']};a=result['source.apng']['decoded'][1]['pixel_15_15'];b=result['over.apng']['decoded'][1]['pixel_15_15'];assert a!=b and a[3]==128 and b[3]>a[3]
for value in result.values():
 assert value['raw_frames']['default_image_excluded'] is False;assert value['raw_frames']['pixels']==[[255,0,0,255],[0,0,255,128]];assert [f['dispose'] for f in value['fctl']]==[0,0]
assert result['source.apng']['fctl'][1]['blend']==0 and result['over.apng']['fctl'][1]['blend']==1
reference=[127,0,128,255];ref=Image.open(P/'reference-over.png').convert('RGBA');assert list(ref.getpixel((15,15)))==reference;assert Fraction(128,255)+Fraction(255,255)*(1-Fraction(128,255))==1
result['reference_comparison']={'pillow':pillow_version,'source_rgba':a,'apng_loader_over_rgba':b,'eight_bit_direct_channel_reference_rgba':reference,'png3_over_expected_alpha':255,'loader_matches_reference':b==reference,'note':'alpha255由OVER公式独立得出；RGB127/128是8-bit直通通道及Pillow alpha_composite参考，不是显示色彩准确性认证'}
emit(result)

