@@ -132,23 +132,19 @@ AnimationComposition::Ptr LottieReader::parseFile (const File& file,
132132 return {};
133133 }
134134
135- // .lottie files are ZIP archives
136- if (file. getFileExtension (). equalsIgnoreCase ( " .lottie " ))
137- return parseFromZip ( file, {}, options, outError );
135+ LottieLoadOptions opts = options;
136+ if (opts. resourceDirectory == File ( ))
137+ opts. resourceDirectory = file. getParentDirectory ( );
138138
139- const String text = file.loadFileAsString ();
140- if (text. isEmpty () )
139+ auto stream = file.createInputStream ();
140+ if (stream == nullptr )
141141 {
142142 if (outError != nullptr )
143- *outError = " Empty file: " + file.getFullPathName ();
143+ *outError = " Failed to open file: " + file.getFullPathName ();
144144 return {};
145145 }
146146
147- LottieLoadOptions opts = options;
148- if (opts.resourceDirectory == File ())
149- opts.resourceDirectory = file.getParentDirectory ();
150-
151- return parseData (text, opts, outError);
147+ return parseStream (*stream, opts, outError);
152148}
153149
154150// ==============================================================================
@@ -169,6 +165,78 @@ AnimationComposition::Ptr LottieReader::parseData (const String& jsonText,
169165 return reader.parseRoot (root);
170166}
171167
168+ // ==============================================================================
169+ AnimationComposition::Ptr LottieReader::parseStream (InputStream& stream,
170+ const LottieLoadOptions& options,
171+ String* outError)
172+ {
173+ MemoryBlock data;
174+ const auto bytesRead = stream.readIntoMemoryBlock (data);
175+ if (bytesRead == 0 || data.isEmpty ())
176+ {
177+ if (outError != nullptr )
178+ *outError = " Empty or unreadable stream" ;
179+ return {};
180+ }
181+
182+ // .lottie ZIP archives start with "PK" magic bytes
183+ if (data.getSize () >= 2 && memcmp (data.getData (), " PK" , 2 ) == 0 )
184+ {
185+ MemoryInputStream memStream (data, false );
186+ ZipFile zip (memStream);
187+
188+ const auto * manifestEntry = zip.getEntry (" manifest.json" , true );
189+ if (manifestEntry == nullptr )
190+ {
191+ if (outError != nullptr )
192+ *outError = " manifest.json not found in .lottie stream" ;
193+ return {};
194+ }
195+
196+ std::unique_ptr<InputStream> manifestStream (zip.createStreamForEntry (*manifestEntry));
197+ if (manifestStream == nullptr )
198+ return {};
199+
200+ var manifest;
201+ if (JSON::parse (manifestStream->readEntireStreamAsString (), manifest).failed ())
202+ {
203+ if (outError != nullptr )
204+ *outError = " Failed to parse manifest.json" ;
205+ return {};
206+ }
207+
208+ const auto * anims = safeArray (manifest[" animations" ]);
209+ if (anims == nullptr || anims->isEmpty ())
210+ {
211+ if (outError != nullptr )
212+ *outError = " No animations found in manifest" ;
213+ return {};
214+ }
215+
216+ const auto & anim = (*anims)[0 ];
217+ const String animId = varString (anim[" id" ]);
218+ String jsonPath = varString (anim[" path" ]);
219+ if (jsonPath.isEmpty ())
220+ jsonPath = " animations/" + animId + " .json" ;
221+
222+ const auto * jsonEntry = zip.getEntry (jsonPath, true );
223+ if (jsonEntry == nullptr )
224+ {
225+ if (outError != nullptr )
226+ *outError = " Animation JSON not found inside archive: " + jsonPath;
227+ return {};
228+ }
229+
230+ std::unique_ptr<InputStream> jsonStream (zip.createStreamForEntry (*jsonEntry));
231+ if (jsonStream == nullptr )
232+ return {};
233+
234+ return parseData (jsonStream->readEntireStreamAsString (), options, outError);
235+ }
236+
237+ return parseData (data.toString (), options, outError);
238+ }
239+
172240// ==============================================================================
173241std::vector<String> LottieReader::listAnimationIds (const File& lottieZipFile)
174242{
@@ -240,8 +308,8 @@ AnimationComposition::Ptr LottieReader::parseFromZip (const File& lottieZipFile,
240308 if (animationId.isEmpty () || id == animationId)
241309 {
242310 jsonPath = varString (anim[" path" ]);
243- if (! jsonPath.endsWithIgnoreCase ( " .json " ))
244- jsonPath += " .json" ;
311+ if (jsonPath.isEmpty ( ))
312+ jsonPath = " animations/ " + id + " .json" ;
245313 break ;
246314 }
247315 }
@@ -644,7 +712,7 @@ AnimationLayer::Ptr LottieReader::parseLayer (const var& layerObj)
644712 il->assetRefId = varString (layerObj[" refId" ]);
645713 layer = il;
646714 }
647- else if (ty == 5 ) // Text — parsed as NullLayer (text rendering not yet supported)
715+ else if (ty == 5 ) // Text — TODO: parsed as NullLayer (text rendering not yet supported)
648716 {
649717 layer = new NullLayer ();
650718 }
@@ -656,7 +724,7 @@ AnimationLayer::Ptr LottieReader::parseLayer (const var& layerObj)
656724 if (layer == nullptr )
657725 return {};
658726
659- // Self-parenting check (gap 22)
727+ // Self-parenting check
660728 if (layer->parentId >= 0 && layer->id == layer->parentId )
661729 {
662730 if (errorOut_ != nullptr )
0 commit comments