blobs: las fotos ya no suben con la ubicacion dentro
stripImageMetadata dependia de sharp, y cuando no esta devolvia la imagen
intacta. En Android sharp no se empaqueta —lleva binarios nativos y el bundle
del movil va sin ninguno— asi que las fotos subian con su EXIF entero: marca,
modelo, numero de serie, fecha y coordenadas GPS. Y sin sharp instalado,
tambien en escritorio.
Ahora hay un limpiador que no depende de nada: recorre el contenedor y tira los
bloques de metadatos sin decodificar ni recomprimir, asi que los pixeles quedan
byte a byte como estaban.
JPEG fuera APP1 (Exif y XMP), APP13 (IPTC), APP14, COM y demas.
Se quedan JFIF y el perfil de color, que afectan a como se ve.
PNG fuera tEXt, zTXt, iTXt, eXIf, tIME, dSIG.
WebP fuera los bloques EXIF y XMP del contenedor RIFF.
De la orientacion se guarda solo eso: sin sharp no se pueden girar los pixeles,
y tirar la etiqueta dejaria tumbadas las fotos verticales. Se reconstruye un
bloque EXIF de 32 bytes con esa unica etiqueta, que no dice nada de quien hizo
la foto ni donde. Si sharp esta, se sigue prefiriendo su camino, que ademas gira
la imagen de verdad.
Comprobado con una foto que llevaba GPS, marca, modelo, numero de serie y fecha:
- En el movil y en escritorio, el blob guardado queda con 32 bytes de EXIF
(solo la orientacion, que sobrevive) y cero rastros de lo demas.
- Los pixeles son identicos antes y despues, en JPEG, PNG y WebP.
- El perfil de color ICC se conserva.
- Sin metadatos, progresivo, GIF, fichero corrupto, fichero vacio y algo que
no es una imagen: se devuelven tal cual, sin excepciones.
This commit is contained in:
parent
9cadc45c1d
commit
58824032f8
1 changed files with 154 additions and 3 deletions
|
|
@ -11,15 +11,166 @@ try {
|
|||
} catch (e) {
|
||||
}
|
||||
|
||||
const stripImageMetadata = async (buffer) => {
|
||||
if (typeof sharp !== "function") return buffer;
|
||||
/**
|
||||
* Quitar metadatos de una imagen SIN sharp.
|
||||
*
|
||||
* sharp no se empaqueta en Android (lleva binarios nativos y el bundle del movil
|
||||
* va sin ninguno), asi que alli esta funcion devolvia la imagen intacta y las
|
||||
* fotos subian con su EXIF entero, ubicacion GPS incluida.
|
||||
*
|
||||
* Esto recorre el contenedor y tira los bloques de metadatos, sin decodificar ni
|
||||
* recomprimir la imagen: los pixeles se quedan byte a byte como estaban.
|
||||
*
|
||||
* De la orientacion se guarda solo eso, la orientacion. Sin sharp no se pueden
|
||||
* girar los pixeles, y tirar la etiqueta dejaria las fotos verticales tumbadas;
|
||||
* asi que se reconstruye un bloque EXIF minimo con esa unica etiqueta, que no
|
||||
* dice nada de quien hizo la foto ni donde.
|
||||
*/
|
||||
|
||||
// JPEG: cada segmento es FF <marca> <longitud:2> <datos>. Se tiran los que llevan
|
||||
// metadatos y se dejan JFIF (APP0) y el perfil de color (APP2), que afectan a como
|
||||
// se ve la imagen.
|
||||
const JPEG_DROP = new Set([
|
||||
0xE1, // APP1 Exif y XMP
|
||||
0xE5, // APP5
|
||||
0xE6, // APP6
|
||||
0xEC, // APP12 Ducky/Picture Info
|
||||
0xED, // APP13 IPTC / Photoshop
|
||||
0xEE, // APP14 Adobe
|
||||
0xFE // COM comentario
|
||||
]);
|
||||
|
||||
const jpegOrientation = (seg) => {
|
||||
// seg empieza en "Exif\0\0"; detras va una cabecera TIFF con la IFD0
|
||||
if (seg.length < 14 || seg.toString("latin1", 0, 6) !== "Exif\0\0") return 0;
|
||||
const tiff = seg.subarray(6);
|
||||
const le = tiff.toString("latin1", 0, 2) === "II";
|
||||
const u16 = (o) => (le ? tiff.readUInt16LE(o) : tiff.readUInt16BE(o));
|
||||
const u32 = (o) => (le ? tiff.readUInt32LE(o) : tiff.readUInt32BE(o));
|
||||
if (u16(2) !== 0x002A) return 0;
|
||||
const ifd0 = u32(4);
|
||||
if (ifd0 + 2 > tiff.length) return 0;
|
||||
const n = u16(ifd0);
|
||||
for (let i = 0; i < n; i++) {
|
||||
const e = ifd0 + 2 + i * 12;
|
||||
if (e + 12 > tiff.length) break;
|
||||
if (u16(e) === 0x0112) { // Orientation
|
||||
const v = u16(e + 8);
|
||||
return v >= 1 && v <= 8 ? v : 0;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
/** Bloque APP1 de 32 bytes con la orientacion y nada mas. */
|
||||
const exifOnlyOrientation = (orientation) => {
|
||||
const b = Buffer.alloc(32);
|
||||
b.write("Exif\0\0", 0, "latin1");
|
||||
b.write("II", 6, "latin1");
|
||||
b.writeUInt16LE(0x002A, 8);
|
||||
b.writeUInt32LE(8, 10); // la IFD0 empieza justo detras de la cabecera TIFF
|
||||
b.writeUInt16LE(1, 14); // una sola entrada
|
||||
b.writeUInt16LE(0x0112, 16); // Orientation
|
||||
b.writeUInt16LE(3, 18); // SHORT
|
||||
b.writeUInt32LE(1, 20); // un valor
|
||||
b.writeUInt16LE(orientation, 24);
|
||||
b.writeUInt32LE(0, 28); // no hay mas IFDs
|
||||
return b;
|
||||
};
|
||||
|
||||
const stripJpeg = (buf) => {
|
||||
if (buf.length < 4 || buf[0] !== 0xFF || buf[1] !== 0xD8) return null;
|
||||
const out = [buf.subarray(0, 2)];
|
||||
let orientation = 0;
|
||||
let i = 2;
|
||||
while (i + 4 <= buf.length) {
|
||||
if (buf[i] !== 0xFF) return null; // no es un JPEG sano: no tocarlo
|
||||
const marker = buf[i + 1];
|
||||
if (marker === 0xD8 || (marker >= 0xD0 && marker <= 0xD9)) { i += 2; continue; }
|
||||
if (marker === 0xDA) { out.push(buf.subarray(i)); break; } // datos comprimidos: hasta el final
|
||||
const len = buf.readUInt16BE(i + 2);
|
||||
if (len < 2 || i + 2 + len > buf.length) return null;
|
||||
const seg = buf.subarray(i + 4, i + 2 + len);
|
||||
if (marker === 0xE1 && !orientation) orientation = jpegOrientation(seg);
|
||||
if (!JPEG_DROP.has(marker)) out.push(buf.subarray(i, i + 2 + len));
|
||||
i += 2 + len;
|
||||
}
|
||||
if (orientation > 1) {
|
||||
const exif = exifOnlyOrientation(orientation);
|
||||
const head = Buffer.alloc(4);
|
||||
head.writeUInt16BE(0xFFE1, 0);
|
||||
head.writeUInt16BE(exif.length + 2, 2);
|
||||
out.splice(1, 0, head, exif);
|
||||
}
|
||||
return Buffer.concat(out);
|
||||
};
|
||||
|
||||
// PNG: cadena de bloques <longitud:4> <tipo:4> <datos> <crc:4>.
|
||||
const PNG_DROP = new Set(["tEXt", "zTXt", "iTXt", "eXIf", "tIME", "dSIG"]);
|
||||
|
||||
const stripPng = (buf) => {
|
||||
const SIG = Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);
|
||||
if (buf.length < 8 || !buf.subarray(0, 8).equals(SIG)) return null;
|
||||
const out = [buf.subarray(0, 8)];
|
||||
let i = 8;
|
||||
while (i + 8 <= buf.length) {
|
||||
const len = buf.readUInt32BE(i);
|
||||
const type = buf.toString("latin1", i + 4, i + 8);
|
||||
const end = i + 12 + len;
|
||||
if (end > buf.length) return null;
|
||||
if (!PNG_DROP.has(type)) out.push(buf.subarray(i, end));
|
||||
i = end;
|
||||
if (type === "IEND") break;
|
||||
}
|
||||
return Buffer.concat(out);
|
||||
};
|
||||
|
||||
// WebP: contenedor RIFF con bloques <tipo:4> <longitud:4> <datos> (+1 de relleno
|
||||
// si la longitud es impar).
|
||||
const stripWebp = (buf) => {
|
||||
if (buf.length < 12) return null;
|
||||
if (buf.toString("latin1", 0, 4) !== "RIFF" || buf.toString("latin1", 8, 12) !== "WEBP") return null;
|
||||
const out = [];
|
||||
let i = 12;
|
||||
while (i + 8 <= buf.length) {
|
||||
const type = buf.toString("latin1", i, i + 4);
|
||||
const len = buf.readUInt32LE(i + 4);
|
||||
const end = i + 8 + len + (len % 2);
|
||||
if (end > buf.length) return null;
|
||||
if (type !== "EXIF" && type !== "XMP ") out.push(buf.subarray(i, end));
|
||||
i = end;
|
||||
}
|
||||
const body = Buffer.concat(out);
|
||||
const head = Buffer.alloc(12);
|
||||
head.write("RIFF", 0, "latin1");
|
||||
head.writeUInt32LE(body.length + 4, 4);
|
||||
head.write("WEBP", 8, "latin1");
|
||||
return Buffer.concat([head, body]);
|
||||
};
|
||||
|
||||
/** Limpia segun el formato; si algo no cuadra, devuelve la imagen tal cual. */
|
||||
const stripMetadataPure = (buffer) => {
|
||||
try {
|
||||
return await sharp(buffer).rotate().toBuffer();
|
||||
const out = stripJpeg(buffer) || stripPng(buffer) || stripWebp(buffer);
|
||||
return out && out.length ? out : buffer;
|
||||
} catch {
|
||||
return buffer;
|
||||
}
|
||||
};
|
||||
|
||||
const stripImageMetadata = async (buffer) => {
|
||||
// Con sharp se prefiere su camino: ademas de quitar metadatos, aplica la
|
||||
// orientacion girando los pixeles de verdad.
|
||||
if (typeof sharp === "function") {
|
||||
try {
|
||||
return await sharp(buffer).rotate().toBuffer();
|
||||
} catch {
|
||||
return stripMetadataPure(buffer);
|
||||
}
|
||||
}
|
||||
return stripMetadataPure(buffer);
|
||||
};
|
||||
|
||||
const PDF_METADATA_KEYS = [
|
||||
'/Title', '/Author', '/Subject', '/Keywords',
|
||||
'/Creator', '/Producer', '/CreationDate', '/ModDate'
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue