lottie_fixup 1.1.0
lottie_fixup: ^1.1.0 copied to clipboard
Fixes Lottie/Bodymovin exports that crash or freeze the lottie Flutter package: malformed layers, assets, masks, shapes, and unexecuted expressions.
1.1.0 #
- Fixed a real rendering bug: a layer using After Effects' Auto-Orient
("Orient Along Path",
"ao": 1) spins round and round inlottieinstead of banking along its motion path.lottie(still as of 3.5.1) converts the direction of travel to degrees, then passes that toMatrix4.rotateZ, which takes radians — so a layer that should bank 60° along a curved path is drawn turning 60 radians, nearly 10 full spins, with an extra jolt wherever its heading crosses ±180°. It also ignores the layer's own rotation while auto-orient is on. The newbakeAutoOrientpass, run last byfix(and so byfixupLottieDecoderand the CLI), works out the orientation After Effects shows — the motion path's tangent at the layer's position along it, honoring each segment's easing, or the actual velocity for a position with separated dimensions — adds the layer's own rotation, writes the sum as rotation keyframes (one per frame, thinned wherever a straight line already fits within 0.01°, with a near-instant snap at a sharp corner in the path), and turnsaooff. While the layer pauses, or its position jumps (like the seam of a baked'cycle'loop), it keeps facing its last heading. A layer that never moves just hasaoturned off, keeping its own rotation. - 3D layers (
"ddd": 1, which After Effects orients in 3D) and layers whose position or rotation still carries an expression nothing could bake are left as-is and reported instead:AutoOrientBakeResult.skippedLayers,Diagnosis.skippedAutoOrientLayers, and a! skipped auto-orientline from the CLI. - New
FixResult.autoOrientandDiagnosis.autoOrientLayersToBake/skippedAutoOrientLayers, all with defaults, so existing constructor calls keep compiling.
1.0.1 #
- Shortened pubspec description.
1.0.0 #
- Fixed a real crash risk:
bakePropertyExpressionsonly ever caught its ownExpressionEvalErroraround parsing and per-frame sampling, so anything else — a malformed numeric literal in an expression (the tokenizer accepted e.g.1.2.3as one token, thennum.parsethrew aFormatException), or a cross-layer/.valueAtTime()reference into a layer whose property was shaped unexpectedly (a plainTypeErrorfrom an invalid cast) — propagated all the way up throughfix/fixupLottieDecoderuncaught, crashing the whole file's processing (and the widget) over a single bad expression instead of leaving just that one property unsupported. Both bake paths (_tryBakeNumeric,_tryBakeShapePath) now catch broadly at that boundary, so one malformed expression or malformed referenced property never takes down every other, unrelated, otherwise-fine property in the same document. - Fixed a real correctness bug: a division that lands on exactly zero at
a sampled frame (e.g.
1 / (time - 1)) silently producedInfinity/NaN— Dart's arithmetic doesn't throw on this — which then crashedjsonEncodelater, at serialization time, with a much less useful error pointing nowhere near the actual expression. Non-finite results are now rejected where a value is finalized into a keyframe, so the property is reported unsupported instead — except where the non-finite value was only ever an intermediate step already handled byclamp()(e.g.clamp(1 / x, -100, 100)), which still bakes correctly. - Fixed a real correctness bug: a
wiggle()-only knot cache was shared across everywiggle()call in an expression, keyed only by knot index — so a layered "octave noise" rig likewiggle(1, 50) + wiggle(5, 10)(a common After Effects technique) had its second call silently read back the first call's cached knot whenever their indices happened to coincide, producing incorrect, correlated motion instead of two independent noise signals. Eachwiggle()call site now gets its own independent knot cache. - Fixed a real correctness bug:
bakePropertyExpressionswalked and baked layers/properties top-down in document order, so a cross-layer reference (or, as of this release, a same-layerthisLayer/transformreference — see below) to a property that itself had its own not-yet-baked expression copied or sampled that property's stale pre-expression value whenever the source happened to appear later in the walk — common, since layer array order has no relationship to pickwhip direction. Once copied, it was never revisited even after the source was correctly baked moments later. Property baking is now dependency-ordered: baking a property first recursively bakes any not-yet-baked property it references (anywhere in its expression, not just a bare reference), regardless of where either one sits in the document. A genuine reference cycle (already undefined behavior in After Effects itself) bakes against the cycle-closing property's current state rather than recursing forever. bakePropertyExpressions/cross-layer references:skew/skewAxisare now recognized transform properties (alongsideposition/rotation/scale/opacity/anchorPoint), sothisComp.layer('Name').transform.skewno longer reports "unknown transform property".- New same-layer reference support:
thisLayer.transform.<prop>and baretransform.<prop>(sugar for the same thing) now resolve to the expression's own layer, reusing the same cross-layer machinery —.valueAtTime(), dense sampling when combined with other math, and exact keyframe copying for a bare reference all work identically to athisComp.layer(...)reference. This is exact, not an approximation (like the pre-existing cross-layer support it reuses), so it isn't gated byBakeOptions. - New: structural crash detection and repair, extending
sanitizeCrashingLayersbeyond audio layers/empty precomps to six more crash-causing JSON shapes — each grounded in a specific, confirmed non-null-assertion or unassigned-late-field crash in thelottiepackage's own parser/model source, the same standard the audio-layer fix was already held to:- A precomp layer (
ty: 0) whoserefIddoesn't resolve to any asset (missing, non-string, or no match) is removed —composition.getPrecomps(refId)!throws building the render tree otherwise. Unlike a danglingrefIdthis package's own pruning might create (already guarded against separately), this can be present in the input file itself. - A text layer (
ty: 5) missingt/t.dis removed —lottie's text layer unconditionally null-checks it while being built. - An asset entry with no usable
id(missing,null, a bool, an array, or an object — a number is fine,lottiecoerces it) is removed — such an asset can never legitimately be referenced, andlottie's own asset parser crashes reading itsid. - A
masksPropertiesentry missingmode,pt, orois removed — each is read into a variable with no fallback, solottiecrashes building the mask itself, not just when it's later applied. - A gradient-fill/gradient-stroke/solid-stroke shape-content item
(
gf/gs/st) missing a required companion field (gradient colors/start/end point, or stroke color/width) is removed, recursing through shape groups.lottie's own source confirms this really happens in the wild: a code comment there specifically calls out Telegram's Lottie export omitting the sibling opacity field in these same objects. - An out-of-range
lc/lj(line cap/join, valid range1..3) on ags/stitem is cleared — just that one field, leaving the rest of the stroke intact — instead of indexinglottie's internal enum list out of bounds. - New, diagnostic-only (matching the existing
layersMissingTransformtreatment — reported, not altered, since there's no principled safe default for an arbitrary property): any animatable-value-shaped object ({"a":..,"k":..}, or a gradient's{"p":..,"k":..}) found with a missing or emptykis reported via the newSanitizeResult.propertiesWithEmptyKeyframes—lottiecrashes indexing the last element of an empty keyframe list, and this is the broadest-blast-radius issue found (it can occur on essentially any animatable property anywhere in the file). SanitizeResult's new fields are all optional/defaulted, keeping the door open for a caller to construct one directly (e.g. in a test) without naming every field.
- A precomp layer (
diagnose/Diagnosis: gains a newsanitizefield (aSanitizeResult) surfacing all of the above, plus the pre-existinglayersMissingTransformdiagnostic, whichDiagnosisnever exposed before. Computed on its own fresh decode, independent of the loop/property-expression bake passes, so a structurally-broken layer being removed there never changes whatloopExpressionsToBake/propertyExpressionsToBake/unsupportedExpressionsreport for the same document.- CLI:
diagnoseandfixboth report all of the above;fixnow prints its diagnostic-only warnings (missing transform, empty keyframes) even on a file where nothing else needed fixing, rather than only printingnothing to fix.and silently dropping them.
0.3.0 #
- Fixed a real correctness bug:
bakePropertyExpressionsused to only ever look at an expression's last statement, silently discarding any statement before it — soposterizeTime(6); $bm_rt = wiggle(3, 30);baked "successfully" but withposterizeTimedropped entirely, producing smooth per-frame interpolation instead of a 6-updates-per-second stepped hold. Fixed by replacing the single-expression extractor with a real statement interpreter that runs the whole expression body in order. - The expression evaluator now understands a much larger, still-deliberately
small subset of AE expression syntax as part of that rewrite:
if/else(with<,>,<=,>=,==,!=,&&,||,!, and? :), and localvarbindings — e.g. branching a property's value bytime, or a two-step computation likevar s = ...; $bm_rt = [s, s, 100];..valueAtTime(t)on a cross-layer transform reference, for delayed/trailing "follow" rigs.- The
Math.*namespace (Math.sin,Math.cos,Math.abs,Math.sqrt,Math.pow,Math.max/Math.min,Math.PI, etc.), not just barePI. linear()/ease()/easeIn()/easeOut()interpolation helpers andclamp().add()/sub()/mul()/div()vector math and thevalueidentifier (the property's own pre-expression value), plus array literals ([a, b, c]) as a standalone expression, not just index access.posterizeTime(fps)actually posterizes (quantizes)timefor the rest of the expression's evaluation instead of being a no-op;seedRandom()is now recognized (a no-op, since this package's seeding is already deterministic and independent of it) instead of making the whole expression unsupported.
bakePropertyExpressionsnow also bakes a supported expression on a property that's already keyframed (previously only never-keyframed properties were eligible; an expression on real keyframes was always reported as unsupported). The original keyframe curve — linearly sampled, per frame — becomes thevalue/wiggle()base, matching how After Effects itself evaluates an expression layered on top of real keyframes.bakePropertyExpressions: awiggle(freq, amp)-only expression on a shape path (ty: "sh"'sks) is now baked too, wiggling each vertex independently (own seed, own knots) and leavingi/otangent handles andcuntouched. Any other expression on a path remains unsupported — arithmetic directly on a path value isn't something After Effects supports either.- New
BakeOptions, for opting out of the specific parts of expression baking above that are an approximation or a judgment call rather than an exact match to After Effects (random()/wiggle(), baking on an already-keyframed property, shape-pathwiggle(),ease()/easeIn()/easeOut()) — every option defaults totrue(bake everything, today's behavior), so this is purely additive. Threaded throughfix,diagnose, the newfixupLottieDecoderWithOptions(fixupLottieDecoderkeeps its no-argument default-options signature), and new CLI flags (--no-random-wiggle,--no-keyframed-properties,--no-shape-path-wiggle,--no-approximate-easing).
0.2.0 #
bakeLoopExpressions:loopIn('pingpong')is now baked too, mirroring the segment backward from the first keyframe the same wayloopOut('pingpong')already mirrors it forward — previously reported as unsupported.bakeLoopExpressions: the duration-basedloopOutDuration(type, durationSeconds)/loopInDuration(type, durationSeconds)variants are now baked in'cycle'/'pingpong'mode, by repeating a fixed-length span of time (rather than the whole keyframed segment) ending/starting at the boundary keyframe, holding flat wherever that span reaches past the actually-keyframed range. A duration shorter than the keyframed segment itself isn't supported (would need interpolating a cut point mid-segment) and is reported instead.- Fixed a latent crash in the
'pingpong'mirroring helper when a keyframe had noi/oeasing recorded at all (falls back to linear instead of a null-cast error). - New
bakePropertyExpressions(exported fromcore.dart), run byfixafterbakeLoopExpressions: bakes expressions on a property that was never manually keyframed ("a": 0) that aren't a loop call —time-based motion (e.g.time * 180for continuous rotation), cross-layer links (thisComp.layer('Name').transform.position, copied exactly when that's the whole expression, otherwise sampled alongside other arithmetic), andrandom()/wiggle()(a seeded, deterministic approximation — After Effects' own noise/PRNG isn't reproducible bit-for-bit, but this is reproducible across builds and beats a frozen property). Diagnosis(breaking): addspropertyExpressionsToBake, andunsupportedExpressionsnow reflects what's left after both bake passes run (previously onlybakeLoopExpressions'), so it no longer lists expressionsbakePropertyExpressionscan actually handle.FixResult(breaking): addspropertyBake(aPropertyBakeResult).- CLI: reports unbaked/baked property expressions alongside loop expressions.
0.1.1 #
bakeLoopExpressions(fix): expressions on a scalar property that was never manually keyframed ("a": 0, e.g. rotation or opacity with arandom()/loopOut()expression but no keyframes) are no longer silently invisible to bothdiagnoseandfix. The detection only checkedk is List, which holds for a keyframe array ("a": 1) but also — wrongly — for a non-animated multi-dimensional property's raw[x, y, z]value; a non-animated scalar property's rawk(a plain number) failed the check and skipped detection entirely, rather than being reported viaskippedExpressionslike every other unsupported case. Detection now checks thatkis actually a list of keyframe objects, which also prevents attempting to bake aloopOut/loopIncall found on a non-animated multi-dimensional property (previously would have crashed doing keyframe arithmetic on raw numbers).bakeLoopExpressions:loopIn('cycle')is now detected and baked (tiling the segment backward from its first keyframe down todoc['ip']) — previously onlyloopOutwas recognized at all, so a property usingloopInalone was silently left frozen with no warning.bakeLoopExpressions: loop mode is now parsed precisely instead of guessed fromexpr.contains('pingpong'), so a mode other than'cycle'/'pingpong'is never silently mis-baked as'cycle'again.bakeLoopExpressions:'offset'and'continue'are now baked too, for bothloopOutandloopIn, on any property whose keyframe values are plain numbers (position, scale, rotation, opacity...).'offset'repeats the segment shape but shifts each repetition's values bylast - first, so the property keeps trending instead of snapping back to the start.'continue'doesn't repeat keyframes at all — it extrapolates a single straight segment past the boundary keyframe at that segment's own rate of change. Left untouched and reported viaskippedExpressionswhen the property's values aren't plain numbers (e.g. a shape path), same asloopIn('pingpong')(not yet supported) and an expression that calls bothloopInandloopOuton the same property.Diagnosis(breaking):loopOutOccurrences(a raw substring count over the file) is replaced byloopExpressionsToBakeandunsupportedExpressions, computed with the same detection logic asbakeLoopExpressions— sodiagnosenow agrees withfixabout what will and won't be baked, and also surfaces expressions other thanloopOut.- CLI
diagnose: reports unsupported expressions in addition to the bakeable-expression count.
0.1.0 #
- Initial release.
sanitizeCrashingLayers: removes audio layers (ty: 6) that crash thelottieFlutter package's layer parser, drops empty precomps, and prunes assets left unreferenced by that.bakeLoopExpressions: bakesloopOut('pingpong')/loopOut('cycle')expressions into real keyframes, sincelottiedoes not execute expressions.diagnose: read-only report of the same issues, without modifying the file.fix: runs both fixes together.- CLI:
dart run lottie_fixup diagnose|fix <file...>. fixupLottieDecoder: aLottieDecoderfor thelottiepackage that runsfixon the bytes before parsing, fixing raw exports at load time with no build step. Requires the Flutter SDK.