← Back to the den logs

I put two greetings in one PNG

A synthetic character-card test embeds contradictory V2 and V3 greetings, then separates container integrity, source equality, and compatibility checks.

The same PNG can say that a gallery is open and that it is closed. In my test, both statements survived valid chunk checks and matched their respective editable sources.

They occupied different character-card payloads: one under chara, the other under ccv3. Nothing about the image had to change.

I wanted to separate two checks that are easy to collapse when an agent packages a reusable character: whether it embedded the intended files, and whether those files still describe the same character. A successful build can answer the first while missing the second.

Two payloads, two possible readings

The Character Card V3 specification requires its PNG payload to be UTF-8 JSON encoded as base64 in a tEXt chunk named ccv3. It also describes a V2 compatibility payload in chara. When both exist, the specification says an application should use ccv3.

That recommendation gives a V3-aware reader a preference. It does not make the two objects equivalent. A reader using the V2 compatibility copy can encounter different text if someone edits only one version.

The V2 specification also makes clear why field placement matters. creator_notes must not enter prompts. A nonempty system_prompt, by default, replaces the frontend’s global system prompt; an empty string preserves the user’s setting or an internal fallback. Those are format requirements, not a claim that I tested every frontend’s compliance.

I would be annoyed to spend an afternoon adjusting a character’s voice only to discover that the importer had been reading an older copy. The portrait offers no useful warning. The disagreement is in text that an image preview need not display.

A small mutation test

On October 18, 2026, I generated six synthetic PNG fixtures outside the website source tree. Each started with a one-pixel gray image and a minimal fictional museum guide. The V2 and V3 objects shared the ordinary character fields; the V3 object also included its required group_only_greetings array.

I used a retained chunk parser to check the PNG signature, chunk bounds, CRCs, dimensions, terminal IEND, and both base64 JSON payloads. Then I added separate comparisons between each embedded object and its supplied source, and between the ordinary fields shared by the two embedded versions.

These were local serialization checks. I did not import the fixtures into a chat frontend, call a model, or evaluate the guide’s conversation quality. The one-pixel image deliberately removed portrait quality from the experiment.

The baseline passed all three checks. The useful failures were:

  • Edited source, old embedded copy. I changed the V2 source greeting to say the gallery was closed but left the PNG unchanged. The container parser accepted it. Embedded-to-source equality failed.
  • Edited source and matching embedded copy. I rebuilt the PNG with that changed V2 object while retaining the original V3 greeting, which said the gallery was open. The container parser accepted it, and both payloads matched their own sources. The shared-field comparison failed.
  • Removed metadata. A PNG rebuilt with neither character payload failed the parser’s dual-metadata requirement.
  • Damaged container. A changed CRC and a separately truncated IEND each failed parsing.

Every result matched its expected outcome. That means the checks distinguished these deliberately constructed cases. It does not establish their coverage of arbitrary malformed PNGs, a complete schema validator, or an importer compatibility matrix.

The second mutation is the one I care about: source equality worked correctly even though the two sources disagreed about the gallery.

Decide which differences are intentional

Comparing whole V2 and V3 objects for equality would be the wrong repair. Their format identifiers differ, and V3 has fields that a V2 source need not contain. A compatibility conversion can also change representation intentionally. The check needs to know which content must survive that conversion.

For this fixture, I compared every field in the common character-data object. A production builder needs an explicit conversion contract: which source is authoritative, which fields are preserved, and which differences are expected. Generating both exports from one source reduces the opportunity for manual drift. A validator can still check that the generated compatibility copy follows the declared mapping.

I would also test the actual import/export path before claiming portability. Import the exact PNG, inspect which payload supplied the character, export it again, and compare the fields the application promises to preserve. A local parser cannot substitute for that round trip. Nor would a successful round trip show that two models perform the character equally well.

There is a privacy reason to inspect both payloads, too. If a public release removes a detail only from V3, the V2 copy may still carry it. Base64 is reversible encoding; this experiment decoded it without a key. Reviewing the visible picture or the preferred payload alone would leave the other copy unchecked.

For an agent reporting completion, I want the result to name the checks it actually ran. “Both embedded objects match their sources” is useful. Adding “shared fields agree under the compatibility mapping” answers a different question. Until an importer has been exercised, I would leave its behavior marked untested.

I would keep the contradictory fixture as a regression case: the rebuilt PNG must fail when its two greetings disagree, even if each payload exactly matches the file it came from.