images-free.openapps.xyz

Source

This app’s code and revision history are public. Download a copy or explore the files below.

Public
The app’s code and revision history are public.
Owned app · rev b674d6e71b51
Run it yourself
Take the whole tree and run it on your own machine. The download needs no account. Cloning needs a sign-in.
Download .zip
bounded clone 604467e480f8468389092a3b
A copy you download is not bounded. The guarantees this app runs under - declared egress, proven invariants, writes that fail closed - live in the runtime serving this page, not in the files. What you get is auditable source you can read and run, with no runtime guarantees attached.
← All files
src/lib/meta.ts
// Photo details (EXIF, XMP, IPTC, comments) inside JPEG, PNG and WebP files.
// Removing them here never touches the pixels; it only drops the blocks of the file that carry them.

type Segment = { marker: number; start: number; end: number };

/** The header segments of a JPEG, up to the start of the image data. */
function jpegSegments(b: Uint8Array): { segments: Segment[]; dataStart: number } {
  const segments: Segment[] = [];
  let i = 2;
  while (i + 4 <= b.length && b[i] === 0xff) {
    const marker = b[i + 1];
    if (marker === 0xd8 || (marker >= 0xd0 && marker <= 0xd7) || marker === 0x01) { i += 2; continue; }
    if (marker === 0xda) break; // start of scan: image data follows
    const end = i + 2 + ((b[i + 2] << 8) | b[i + 3]);
    segments.push({ marker, start: i, end });
    i = end;
  }
  return { segments, dataStart: i };
}

const isExif = (b: Uint8Array, s: Segment) =>
  s.marker === 0xe1 && String.fromCharCode(...b.subarray(s.start + 4, s.start + 10)) === "Exif\0\0";

/** Finds the orientation tag in an EXIF block and returns where its value sits. */
function orientationAt(b: Uint8Array, s: Segment): number | null {
  const tiff = s.start + 10;
  const little = b[tiff] === 0x49;
  const u16 = (o: number) => (little ? b[o] | (b[o + 1] << 8) : (b[o] << 8) | b[o + 1]);
  const u32 = (o: number) => (little ? (b[o] | (b[o + 1] << 8) | (b[o + 2] << 16)) + b[o + 3] * 2 ** 24 : b[o] * 2 ** 24 + ((b[o + 1] << 16) | (b[o + 2] << 8) | b[o + 3]));
  const ifd = tiff + u32(tiff + 4);
  if (ifd + 2 > s.end) return null;
  const count = u16(ifd);
  for (let k = 0; k < count; k++) {
    const e = ifd + 2 + k * 12;
    if (e + 12 > s.end) return null;
    if (u16(e) === 0x0112) return e + 8;
  }
  return null;
}

function readOrientation(b: Uint8Array, s: Segment): number {
  const at = orientationAt(b, s);
  if (at === null) return 1;
  return b[s.start + 10] === 0x49 ? b[at] : b[at + 1];
}

function join(parts: Uint8Array[]): Uint8Array {
  const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
  let at = 0;
  for (const p of parts) { out.set(p, at); at += p.length; }
  return out;
}

/** A minimal EXIF block holding only an orientation, so a turned photo still shows the right way up. */
function orientationOnly(o: number): Uint8Array {
  // APP1, length 34, "Exif\0\0", big-endian TIFF header, one IFD entry: 0x0112 SHORT 1 = o, no next IFD.
  return new Uint8Array([0xff, 0xe1, 0, 34, 0x45, 0x78, 0x69, 0x66, 0, 0, 0x4d, 0x4d, 0, 0x2a, 0, 0, 0, 8,
    0, 1, 0x01, 0x12, 0, 3, 0, 0, 0, 1, 0, o, 0, 0, 0, 0, 0, 0, 0, 0]);
}

/** A JPEG without its photo details. Keeps the color profile, and the orientation if the photo is turned. */
export function stripJpeg(b: Uint8Array): Uint8Array {
  const { segments, dataStart } = jpegSegments(b);
  const exif = segments.find((s) => isExif(b, s));
  const orientation = exif ? readOrientation(b, exif) : 1;
  const keep = segments.filter((s) => {
    if (s.marker === 0xfe) return false; // comment
    if (s.marker < 0xe1 || s.marker > 0xef) return true; // tables, frame header, JFIF
    const id = String.fromCharCode(...b.subarray(s.start + 4, s.start + 18));
    return (s.marker === 0xe2 && id.startsWith("ICC_PROFILE")) || (s.marker === 0xee && id.startsWith("Adobe"));
  });
  const parts = [b.subarray(0, 2)];
  const app0 = keep[0]?.marker === 0xe0 ? keep.shift()! : null;
  if (app0) parts.push(b.subarray(app0.start, app0.end));
  if (orientation > 1 && orientation <= 8) parts.push(orientationOnly(orientation));
  for (const s of keep) parts.push(b.subarray(s.start, s.end));
  parts.push(b.subarray(dataStart));
  return join(parts);
}

/** Copies the photo details of `source` into a freshly made JPEG, marked upright (its pixels already are). */
export function copyExif(made: Uint8Array, source: Uint8Array): Uint8Array {
  const exif = jpegSegments(source).segments.find((s) => isExif(source, s));
  if (!exif) return made;
  const block = source.slice(exif.start, exif.end);
  const at = orientationAt(source, exif);
  if (at !== null) {
    const o = at - exif.start;
    if (block[10] === 0x49) { block[o] = 1; block[o + 1] = 0; } else { block[o] = 0; block[o + 1] = 1; }
  }
  const { segments } = jpegSegments(made);
  const after = segments[0]?.marker === 0xe0 ? segments[0].end : 2;
  return join([made.subarray(0, after), block, made.subarray(after)]);
}

/** A PNG without text, EXIF and time chunks. */
export function stripPng(b: Uint8Array): Uint8Array {
  const drop = new Set(["tEXt", "zTXt", "iTXt", "eXIf", "tIME"]);
  const parts = [b.subarray(0, 8)];
  for (let i = 8; i + 12 <= b.length;) {
    const len = ((b[i] << 24) >>> 0) + ((b[i + 1] << 16) | (b[i + 2] << 8) | b[i + 3]);
    const end = i + 12 + len;
    if (!drop.has(String.fromCharCode(...b.subarray(i + 4, i + 8)))) parts.push(b.subarray(i, end));
    i = end;
  }
  return join(parts);
}

/** A WebP without EXIF and XMP chunks. */
export function stripWebp(b: Uint8Array): Uint8Array {
  const parts: Uint8Array[] = [];
  for (let i = 12; i + 8 <= b.length;) {
    const fourcc = String.fromCharCode(...b.subarray(i, i + 4));
    const len = b[i + 4] | (b[i + 5] << 8) | (b[i + 6] << 16) | (b[i + 7] << 24);
    const end = Math.min(b.length, i + 8 + len + (len & 1));
    if (fourcc === "VP8X") {
      const chunk = b.slice(i, end);
      chunk[8] &= ~0x0c; // clear the EXIF and XMP flags
      parts.push(chunk);
    } else if (fourcc !== "EXIF" && fourcc !== "XMP ") parts.push(b.subarray(i, end));
    i = end;
  }
  const body = join(parts);
  const head = b.slice(0, 12);
  new DataView(head.buffer).setUint32(4, body.length + 4, true);
  return join([head, body]);
}

Change history

Quality pass: background removal, blur background, paste images
b674d6e71b · 5 hours ago
Bounded source sync
c6622bcbd1 · 1 day ago
Bounded source sync
92ecc89502 · 1 day ago
Bounded source sync
ae2f8b8be0 · 1 day ago