feat(exif): write the Xiaomi watermark blob (0x889e) so HyperOS Gallery can frame our captures
HyperOS Gallery reads EXIF 0x889e (a JSON blob) to build its watermark frame; without it a photo fails with "cannot recognize the parameters". CameraX/HAL never supplies that vendor tag, so a capture from this app lacked it while a stock-camera photo carried it. Read back the same props the stock camera derives the blob from (ro.product.device / ro.product.marketname / Build.MANUFACTURER) via the native module, synthesize the blob in exifWrite, and write it only when the source has none and the device is Xiaomi/Redmi/POCO - a source blob is never overwritten.
This commit is contained in:
@@ -25,6 +25,17 @@ export interface ExifStamp {
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
dpi?: number | null;
|
||||
// Device identity for the Gallery watermark blob (ref. watermarkBlob). Only
|
||||
// used when the source photo carries no blob of its own.
|
||||
device?: DeviceIdentity | null;
|
||||
}
|
||||
|
||||
/** Raw device identity for the HyperOS Gallery watermark tag (0x889e). */
|
||||
export interface DeviceIdentity {
|
||||
device: string; // ro.product.device codename, e.g. "aurora"
|
||||
marketName: string; // ro.product.marketname, e.g. "Xiaomi 14 Ultra"
|
||||
manufacturer: string; // Build.MANUFACTURER, e.g. "Xiaomi"
|
||||
model: string; // Build.MODEL — fallback when marketName is empty
|
||||
}
|
||||
|
||||
interface Entry {
|
||||
@@ -158,6 +169,31 @@ function tzOffset(d: Date): string {
|
||||
return `${mins < 0 ? '-' : '+'}${p(Math.floor(a / 60))}:${p(a % 60)}`;
|
||||
}
|
||||
|
||||
// The watermark blob HyperOS Gallery reads back (EXIF tag 0x889e) before it
|
||||
// will open its watermark editor: without it, a photo edited here fails with
|
||||
// "cannot recognize the parameters". Only the stock camera app writes this tag
|
||||
// — CameraX/HAL never does — so a camera capture has to synthesize it from the
|
||||
// same props the stock app derives it from. Shape matches the stock bytes:
|
||||
// {"buildDevice":"aurora",...,"waterLogo":"XIAOMI","waterName":"14 Ultra"}.
|
||||
function watermarkBlob(d: DeviceIdentity): string {
|
||||
const brand = d.manufacturer.trim();
|
||||
const market = d.marketName.trim();
|
||||
// "Xiaomi 14 Ultra" -> "14 Ultra": Gallery prints waterName, not the brand.
|
||||
const waterName = market.toLowerCase().startsWith(`${brand.toLowerCase()} `)
|
||||
? market.slice(brand.length + 1)
|
||||
: market || d.model;
|
||||
return JSON.stringify({
|
||||
buildDevice: d.device,
|
||||
customize: '',
|
||||
cvLensName: '',
|
||||
filterName: '',
|
||||
livephotoInfo: '',
|
||||
version: 2,
|
||||
waterLogo: brand.toUpperCase(),
|
||||
waterName,
|
||||
});
|
||||
}
|
||||
|
||||
// --- parse -----------------------------------------------------------------
|
||||
|
||||
interface ParsedTiff {
|
||||
@@ -356,6 +392,12 @@ export function writeJpegExif(jpeg: Uint8Array, stamp: ExifStamp, sourceJpeg?: U
|
||||
ifd0.set(0x0128, shortEntry(0x0128, 2)); // resolution unit = inch
|
||||
}
|
||||
|
||||
// HyperOS Gallery refuses to open its watermark editor without 0x889e, and a
|
||||
// CameraX capture never carries it. Never overwrite a blob the source has.
|
||||
if (stamp.device && !exif.has(0x889e) && /xiaomi|redmi|poco/i.test(stamp.device.manufacturer)) {
|
||||
exif.set(0x889e, asciiEntry(0x889e, watermarkBlob(stamp.device)));
|
||||
}
|
||||
|
||||
if (stamp.gps) {
|
||||
const { latitude: lat, longitude: lon } = stamp.gps;
|
||||
gps.set(0x0001, asciiEntry(0x0001, lat >= 0 ? 'N' : 'S'));
|
||||
|
||||
Reference in New Issue
Block a user