web: keep a saved photo's look in EXIF and its own row

EXPORT no longer burns the caption strip: the pixels stay the photo's own
and the look travels as metadata — ImageDescription (0x010e) for the tag,
UserComment (0x9286, ASCII header) for the recipe JSON.

SAVE PHOTO now stores the look with the frame (photos.recipe) and the
uploader's consent for the community film strip (photos.consent, PATCH
/api/photos/:id for the owner). The landing reel skips non-consented frames,
and a new MY PHOTOS tab lists the account's saves, reopens one with the
settings it was stored with, and carries the two consent switches.
This commit is contained in:
2026-09-18 12:49:05 +07:00
parent cf4b01d4b3
commit 0627f8dd91
9 changed files with 292 additions and 39 deletions
+22
View File
@@ -32,6 +32,11 @@ export interface ExifStamp {
// Device identity for the Gallery watermark blob (ref. watermarkBlob). Only
// used when the source photo carries no blob of its own.
device?: DeviceIdentity | null;
// What the studio would once have burned into the pixels: the frame's tagline
// as ImageDescription (0x010e) and the whole look as JSON in UserComment
// (0x9286). Kept in metadata only — an exported file must stay clean.
imageDescription?: string | null;
userComment?: string | null;
}
/** Raw device identity for the HyperOS Gallery watermark tag (0x889e). */
@@ -114,6 +119,16 @@ function undefinedEntry(tag: number, text: string): Entry {
return { tag, type: UNDEFINED, count: data.length, data };
}
// UserComment (0x9286) must open with an 8-byte character-code header; the rest
// is UTF-8, which every reader that accepts "ASCII" here decodes correctly.
function userCommentEntry(text: string): Entry {
const body = new TextEncoder().encode(text);
const data = new Uint8Array(8 + body.length);
data.set([0x41, 0x53, 0x43, 0x49, 0x49, 0, 0, 0]); // "ASCII\0\0\0"
data.set(body, 8);
return { tag: 0x9286, type: UNDEFINED, count: data.length, data };
}
function longEntry(tag: number, v: number): Entry {
const data = new Uint8Array(4);
u32w(data, 0, v);
@@ -384,6 +399,13 @@ export function writeJpegExif(jpeg: Uint8Array, stamp: ExifStamp, sourceJpeg?: U
ifd0.set(0x0131, asciiEntry(0x0131, stamp.software ?? SOFTWARE));
ifd0.set(0x0112, shortEntry(0x0112, 1)); // upright — the pixels are already straight
// The frame's own labels, metadata-only: ImageDescription names the look and
// UserComment carries the settings so the file reopens as it was saved.
const desc = stamp.imageDescription?.trim();
if (desc) ifd0.set(0x010e, asciiEntry(0x010e, desc));
const comment = stamp.userComment?.trim();
if (comment) exif.set(0x9286, userCommentEntry(comment));
// Baseline tags a conforming EXIF block is expected to carry. Framing apps
// reject a file that omits them, and HALs (the emulator's especially) do not
// always supply them, so fill the gaps and keep whatever the source had.