diff --git a/client/README.md b/client/README.md index eb4704c..d35fd5e 100644 --- a/client/README.md +++ b/client/README.md @@ -58,7 +58,7 @@ the prompt sets what the interior tries to match. After the render the client measures the band against its own backdrop (the per-channel median of the band: the VAE shifts a saturated `--bg` by tens of levels, `#12fa5f` came back `#1fef6d`; cream stayed within 2), repaints the whole band with that colour (if the median is more than 48 levels from `--bg` on any channel the band is mostly content, and -`--bg` is the reference instead, so a band of subject cannot pass as its own backdrop), and prints it with the per-side count of pixels that were content: +`--bg` is the reference instead, so a band of subject cannot pass as its own backdrop; the guarantee is a flat band within 48 levels of `--bg`, so a band that is uniformly 30 levels off, which the VAE can do to a saturated colour, passes), and prints it with the per-side count of pixels that were content: `margin 6.0%: backdrop #fef9ec, cleared 802 of 245760 band px (0.33%; left 646 top 0 right 156 bottom 0)`. If more than `--max-clip` percent of the band (default 0.5) was content the file is still written, already cleared, and the client exits **65**: the subject ran into the frame, so it is cut flat at the seam. Re-seed or ask for more room. @@ -80,7 +80,7 @@ the subject also contains (green in a green-eyed character) is untouched except a rectangle, so the pin cannot ask for a vignette or a round sticker. A subject cannot be asked to stay off the seam: only the exit code tells you it did not. -Offline test (fake ComfyUI on 127.0.0.1, 146 checks): `python3 tests/test-raven-image.py`. +Offline test (fake ComfyUI on 127.0.0.1, 158 checks): `python3 tests/test-raven-image.py`. ## What it does diff --git a/client/raven-image b/client/raven-image index 1e4318f..425bb43 100644 --- a/client/raven-image +++ b/client/raven-image @@ -148,7 +148,7 @@ def png_decode(data): while pos < len(data): n, typ = struct.unpack(">I4s", data[pos:pos + 8]) body = data[pos + 8:pos + 8 + n] - if len(body) != n or struct.unpack(">I", data[pos + 8 + n:pos + 12 + n])[0] != zlib.crc32(typ + body): + if struct.unpack(">I", data[pos + 8 + n:pos + 12 + n])[0] != zlib.crc32(typ + body): raise ValueError("bad chunk length or CRC") pos += 12 + n if typ == b"IHDR": @@ -160,8 +160,6 @@ def png_decode(data): if hdr is None or not idat: raise ValueError("no IHDR or no image data") w, h, depth, ctype, _, _, interlace = hdr - if not 0 < w * h <= 2 ** 26: - raise ValueError(f"image size {w}x{h} is out of range") if depth != 8 or ctype not in (2, 6) or interlace: raise ValueError(f"only 8-bit non-interlaced RGB/RGBA is handled (got depth {depth}, colour type {ctype}, interlace {interlace})") bpp = 3 if ctype == 2 else 4 diff --git a/tests/test-raven-image.py b/tests/test-raven-image.py index efdf8f4..bc8b57a 100644 --- a/tests/test-raven-image.py +++ b/tests/test-raven-image.py @@ -292,16 +292,32 @@ for nm, sc, probe in (("vertical", pocket_v, (80, 90)), ("horizontal", pocket_h, _, _, bpp_, rows_, _ = mod2.png_decode(pic) w2, h2, bpp2, rows2, keep2 = mod2.png_decode(mod2.clear_edges(pic, 6, True, 0x1ec85a)[0]) check(rows2[probe[1]][probe[0] * 4 + 3] == 0, f"flood fill: backdrop pocket behind a 1-px {nm} corridor is keyed out") -# decoder hardening: a flipped byte breaks the CRC, a huge IHDR is refused before allocation +# BG_TOL: a flat band within 48 levels of --bg is the pinned colour after the VAE's shift (observed up to 30), repainted as the band's own colour; 49 is content +for off, want_clean in ((48, True), (49, False)): + flat = (BG[0] + off, BG[1], BG[2]) + p_, back4, clip4, band4 = mod2.clear_edges(mkpng(160, 160, lambda x, y, f=flat: f), 6, False, 0x1ec85a) + check((sum(clip4.values()) == 0) == want_clean and back4 == (flat if want_clean else BG), f"BG_TOL: flat band {off} levels off --bg {'is' if want_clean else 'is not'} a backdrop ({back4}, {sum(clip4.values())} cleared)") + if want_clean: + r4 = mod2.png_decode(p_)[3] + check(bytes(r4[3][3:6]) == bytes(flat), "BG_TOL: a clean band is repainted with its own colour, so the seam stays invisible") +# decoder hardening: a flipped byte breaks the CRC, and every size check refuses on its own (each blob is otherwise well-formed) +def ck(t, b): return struct.pack(">I", len(b)) + t + b + struct.pack(">I", zlib.crc32(t + b)) +def blob(w, h, depth=8, ctype=2, idat=None, il=0): + return (b"\x89PNG\r\n\x1a\n" + ck(b"IHDR", struct.pack(">IIBBBBB", w, h, depth, ctype, 0, 0, il)) + + ck(b"IDAT", idat if idat is not None else zlib.compress(b"\0" * (h * (w * 3 + 1)) if w * h < 10 ** 7 else b"")) + ck(b"IEND", b"")) good = mkpng(8, 8, lambda x, y: BG) bad = bytearray(good); bad[40] ^= 1 -for blob, what in ((bytes(bad), "flipped byte"), (good[:16] + struct.pack(">II", 60000, 60000) + good[24:], "huge IHDR")): - try: mod2.png_decode(blob); check(False, f"png_decode refuses {what}") +for data, what in ((bytes(bad), "flipped byte (CRC)"), (blob(2 ** 21, 2 ** 21), "a header that promises far more than the data"), (blob(8, 8, idat=zlib.compress(b"\0" * (8 * 25 - 1))), "image data shorter than the header says"), + (blob(8, 8, idat=zlib.compress(b"\0" * (8 * 25 + 500))), "image data longer than the header says"), (blob(8, 8, depth=16), "16-bit"), + (blob(8, 8, ctype=3), "palette"), (blob(8, 8, il=1), "interlaced"), (good[:-20], "truncated"), + (good[:8] + struct.pack(">I", 10 ** 6) + good[12:], "chunk length past the end")): + try: mod2.png_decode(data); check(False, f"png_decode refuses {what}") except (ValueError, zlib.error, struct.error): check(True, "") # size and interior validation, before any request refused(["--size", "foo", "cap"], "--size is WxH", "--size foo") refused(["--size", "1000x1024", "cap"], "multiples of 16", "--size not on the grid") refused(["--margin", "40", "--size", "64x64", "cap"], "leaves no room", "margin eating the interior") +refused(["--margin", "5", "--size", "32x512", "cap"], "leaves no room", "margin eating one axis only") # -n: a dirty image is not forgotten by a later one; an unreadable render is reported, not fatal to the batch seen["view"] = mkpng(160, 160, lambda x, y: RED if x < 12 else BG) p, g = run("--margin", "6", "--bg", "1ec85a", "-n", "2", "--size", "160x160", "--no-enhance", "cap")