Technology Aug 24, 2026 · 6 min read

The Lottielab watermark is layer 12345679

There is a tool on GitHub called "lottielab-watermark-remover". It is a few lines long. All it does is walk the layers array of a Lottie file and delete the one whose index is 12345679. That single hardcoded number tells you everything about how these watermarks work. Disclosure before we go furthe...

DE
DEV Community
by Harsh Pal
The Lottielab watermark is layer 12345679

There is a tool on GitHub called "lottielab-watermark-remover". It is a few lines long. All it does is walk the layers array of a Lottie file and delete the one whose index is 12345679. That single hardcoded number tells you everything about how these watermarks work.

Disclosure before we go further: I build Lotiqlab,
which is one of the tools that does this. I am writing about the format rather than the product, and everything below is checkable against any Lottie file you have lying around.

What a Lottie file actually is?

Strip a Lottie down and it is one JSON object with about eight keys that
matter:

{
  "v": "5.9.0",
  "fr": 60,
  "ip": 0,
  "op": 180,
  "w": 512,
  "h": 512,
  "assets": [],
  "layers": []
}

fr is the frame rate. ip and op are the in and out points in frames. w
and h are the artboard size. assets holds images and precomps. layers is
an array of layer objects, drawn in order.

Everything you can see in the animation is a layer object in that array, or a
layer object inside a precomp in assets. There is nothing else.

Where the mark sits

Layers are painted in array order and the mark has to land on top, so it goes
at the front of the array. Exporters vary, so check both ends rather than
assuming.

Its layer object has the same fields as everything else:

{
  "ty": 4,
  "nm": "Made with Lottielab",
  "ind": 12345679,
  "ip": 0,
  "op": 180,
  "ks": {
    "o": { "a": 0, "k": 100 },
    "p": { "a": 0, "k": [256, 480, 0] }
  },
  "shapes": []
}

ty is the type: 4 for a shape layer, 2 for an image, 5 for text. nm is the
name, and that is the field that gives it away, because nobody bothers hiding
it. ind is the index. ks holds the transform. ip and op bound the
frames it shows for.

That is the whole thing. No isWatermark boolean, no checksum over the rest of
the document, nothing a player could verify.

Why nobody ships a tamper proof one

The obvious question is why a vendor does not make this harder. They cannot,
and the reason is built into the format rather than into anyone's effort level.

Lottie exists because it ships editable vector data — a few kilobytes that
scale to any size and recolour at runtime. Flatten the mark into the artwork's
pixels and you have destroyed the properties that made anyone want the format.
Encrypt or sign part of the file and lottie-web will not parse it.

You could imagine a runtime that checked a signature before drawing. But the
runtimes are open source and already inside millions of apps, so no single
vendor gets to change what they enforce. The watermark is an honour system, and
honour systems hold for legal and social reasons, not technical ones.

The one thing that actually breaks

Layer objects reference each other. A layer's parent field holds the ind of
the layer it is transform parented to, which is how a designer makes several
elements move as a group.

Delete a layer that something else was parented to and you have left a pointer
into nothing. Some runtimes treat the child as unparented and it snaps to a new
position. Some ignore it. This is the entire risk surface of removing a
watermark.

Here is a remover that handles it:

const DEFAULT_PATTERN = /lottielab|lottiefiles|made with/i;

function stripWatermark(anim, pattern = DEFAULT_PATTERN) {
  const strip = (layers) => {
    if (!Array.isArray(layers)) return;

    // `ind` is scoped to its own layers array, so track removals per comp.
    const removed = new Set();

    for (let i = layers.length - 1; i >= 0; i--) {
      if (pattern.test(layers[i].nm || '')) {
        removed.add(layers[i].ind);
        layers.splice(i, 1);
      }
    }

    // Anything parented to a layer we just deleted now points at nothing.
    for (const layer of layers) {
      if (removed.has(layer.parent)) delete layer.parent;
    }
  };

  strip(anim.layers);
  for (const asset of anim.assets || []) strip(asset.layers);

  return anim;
}

Two details worth stealing. Iterate backwards, because splicing forwards skips
the element after every removal. And keep removed local to each strip call:
ind values are unique within a layers array, not across the file, so a
document-wide set will happily orphan a legitimate layer in a different precomp
that happens to share an index.

The three places a mark can hide

Difficulty is decided entirely by which of these you have.

  1. A shape or text layer in the root layers array. Delete the object. The artwork underneath is a separate object and does not care.
  2. A layer inside a precomp in assets. Harder to find, identical to fix, because a precomp is just another layers array.
  3. A raster image in assets. The mark was composited into a PNG before export, so it shares pixels with whatever it sat on. The JSON has no way to separate them, and no amount of clever traversal changes that.

.lottie is a zip, not JSON

A .lottie file is a ZIP archive holding a manifest.json, one or more
animation JSONs, and any binary assets. That is what makes it noticeably
smaller over the wire.

Conceptually nothing changes. Practically you cannot open it in a text editor
and start deleting:

import JSZip from 'jszip';

async function stripDotLottie(file) {
  const zip = await JSZip.loadAsync(file);

  for (const path of Object.keys(zip.files)) {
    if (!path.startsWith('animations/') || !path.endsWith('.json')) continue;

    const anim = JSON.parse(await zip.file(path).async('string'));
    zip.file(path, JSON.stringify(stripWatermark(anim)));
  }

  return zip.generateAsync({ type: 'blob' });
}

If you are building tooling for this

Detection is mostly string matching on nm across the root layers and every
precomp. That catches the known vendors and misses anything renamed. Matching
on a hardcoded ind like 12345679 is even more brittle, and it will break
the day Lottielab changes the number.

The part actually worth getting right is the parent cleanup. A remover that
deletes the layer and leaves dangling parent values will sometimes produce a
file that plays subtly wrong, and the user will blame their animation rather
than your tool. Re-index, or at minimum null the orphans. Then play the file
through once before you hand it back.

I write about Lottie internals while building Lotiqlab, a browser-based motion design platform. The longer version of this teardown has diagrams.

DE
Source

This article was originally published by DEV Community and written by Harsh Pal.

Read original article on DEV Community
Back to Discover

Reading List