From 79a503f8242294480d8a5b5fea90bccbdfc3ab8b Mon Sep 17 00:00:00 2001 From: Ilker Manap Date: Fri, 9 Oct 2026 21:22:02 +0200 Subject: [PATCH] A picture from ComfyUI loses the workflow written into it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ComfyUI saves the whole workflow — prompt, checkpoint, nodes — as PNG text chunks in every picture. They are taken out before the picture is sent. Co-Authored-By: Claude Opus 5.5 (1M context) --- internal/comfy/comfy.go | 35 +++++++++++++++++++++++++++++++++++ internal/comfy/comfy_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/internal/comfy/comfy.go b/internal/comfy/comfy.go index c2fc066..fd45bc4 100644 --- a/internal/comfy/comfy.go +++ b/internal/comfy/comfy.go @@ -14,6 +14,7 @@ import ( "context" "crypto/rand" "embed" + "encoding/binary" "encoding/hex" "encoding/json" "errors" @@ -444,9 +445,43 @@ func (c *Client) get(ctx context.Context, u, apiKey string) (Image, error) { if ct == "" || ct == "application/octet-stream" { ct = http.DetectContentType(b) } + if ct == "image/png" { + b = withoutPNGText(b) + } return Image{Data: b, ContentType: ct}, nil } +// withoutPNGText is a PNG without its text chunks. ComfyUI writes the whole +// workflow into every picture it saves — the prompt, and the checkpoint and +// nodes behind it — and what a picture says about how it was made is the +// operator's to publish, not something to hand every client. A file that is +// not a well-formed PNG is returned as it came. +func withoutPNGText(b []byte) []byte { + const sig = "\x89PNG\r\n\x1a\n" + if len(b) < len(sig) || string(b[:len(sig)]) != sig { + return b + } + out := make([]byte, 0, len(b)) + out = append(out, b[:len(sig)]...) + for i := len(sig); i < len(b); { + if i+12 > len(b) { + return b + } + n := int(binary.BigEndian.Uint32(b[i:])) + end := i + 12 + n + if n < 0 || end > len(b) { + return b + } + switch string(b[i+4 : i+8]) { + case "tEXt", "iTXt", "zTXt": + default: + out = append(out, b[i:end]...) + } + i = end + } + return out +} + // refusal is what /prompt said when it would not queue a workflow. func refusal(e, nodes json.RawMessage) string { var top struct { diff --git a/internal/comfy/comfy_test.go b/internal/comfy/comfy_test.go index bbf8fe0..9fb3111 100644 --- a/internal/comfy/comfy_test.go +++ b/internal/comfy/comfy_test.go @@ -2,6 +2,7 @@ package comfy import ( "context" + "encoding/binary" "encoding/json" "fmt" "net/http" @@ -196,3 +197,31 @@ func TestAClientThatLeavesTakesItsWorkflowOffTheCard(t *testing.T) { t.Errorf("deleted %v, interrupted %v", f.deleted, f.interrupted) } } + +func TestAPictureLosesTheWorkflowComfyUIWroteIntoIt(t *testing.T) { + chunk := func(kind, data string) []byte { + b := make([]byte, 0, 12+len(data)) + b = binary.BigEndian.AppendUint32(b, uint32(len(data))) + b = append(b, kind...) + b = append(b, data...) + return append(b, 0, 0, 0, 0) // a CRC nobody here checks + } + png := []byte("\x89PNG\r\n\x1a\n") + png = append(png, chunk("IHDR", "0123456789abc")...) + png = append(png, chunk("tEXt", "prompt\x00{\"1\":...}")...) + png = append(png, chunk("IDAT", "pixels")...) + png = append(png, chunk("iTXt", "workflow\x00...")...) + png = append(png, chunk("IEND", "")...) + got := string(withoutPNGText(png)) + if strings.Contains(got, "prompt") || strings.Contains(got, "workflow") { + t.Errorf("text left in: %q", got) + } + for _, kind := range []string{"IHDR", "IDAT", "pixels", "IEND"} { + if !strings.Contains(got, kind) { + t.Errorf("%s gone: %q", kind, got) + } + } + if broken := png[:30]; string(withoutPNGText(broken)) != string(broken) { + t.Error("a truncated PNG was changed") + } +}