Allow external images to be loaded, and add .lottie file support (#374)

* Allow external images to be loaded by LottieVisualSource, and add .lottie file support.

External images (i.e. images that are not embedded in the .JSON file) are a problem for LottieVisualSource because it doesn't know how to get them. For securit reasons, we don't want LottieVisualSource to have the ability to read files that are referenced from a Lottie .json file, and in many cases LottieVisualSource wouldn't be able to resolve the file because it doesn't have permissions.

This change adds an ImageAssetDelegate that the user can set on the AnimatedVisualSource in order to handle reading of external images.

This change also adds support for the .lottie format (see https://dotlottie.io). There is a parser for .lottie files, and support in LottieViewer for displaying Lotties from a .lottie file. Because .lottie files package images together with the .json Lottie file, it enables LottieViewer to display Lotties that have external images.

* Fix some bugs that were obvious when I started to CR.

* CR feedback.

* CR feedback.

* CR feedback.

* Fix typo in header.
This commit is contained in:
Simeon 2020-10-23 15:28:58 -07:00 коммит произвёл GitHub
Родитель 09224fc42b
Коммит 300df24bbe
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
22 изменённых файлов: 1101 добавлений и 281 удалений

Просмотреть файл

@ -145,6 +145,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompMetadata.dll", "dlls\Co
EndProject
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "NullablesAttributes", "source\NullablesAttributes\NullablesAttributes.shproj", "{E32587A8-94E8-4B68-91AD-F3612A48A62B}"
EndProject
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "DotLottie", "source\DotLottie\DotLottie.shproj", "{7012420D-624C-4BD4-A1D2-1C6C1655ED3A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotLottie.dll", "dlls\DotLottie\DotLottie.dll.csproj", "{AB2ACC11-DE31-4E47-8A5B-895D6934684F}"
EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
source\LottieToWinComp\LottieToWinComp.projitems*{0340244a-683c-405e-838b-f93872779532}*SharedItemsImports = 13
@ -157,6 +161,7 @@ Global
source\YamlData\YamlData.projitems*{39c6b7f3-5e75-4019-82ab-00fd8a0a06e2}*SharedItemsImports = 13
source\LottieReader\LottieReader.projitems*{4e7d8957-3f5f-46e1-99a8-2012b806c9b0}*SharedItemsImports = 13
source\CompMetadata\CompMetadata.projitems*{5120efd7-a556-46bf-8d56-f65f1ef9a305}*SharedItemsImports = 4
source\DotLottie\DotLottie.projitems*{5120efd7-a556-46bf-8d56-f65f1ef9a305}*SharedItemsImports = 4
source\GenericData\GenericData.projitems*{5120efd7-a556-46bf-8d56-f65f1ef9a305}*SharedItemsImports = 4
source\LottieData\LottieData.projitems*{5120efd7-a556-46bf-8d56-f65f1ef9a305}*SharedItemsImports = 4
source\LottieMetadata\LottieMetadata.projitems*{5120efd7-a556-46bf-8d56-f65f1ef9a305}*SharedItemsImports = 4
@ -174,6 +179,7 @@ Global
source\NullablesAttributes\NullablesAttributes.projitems*{68317393-f5a5-4b2c-918a-688db2c10f54}*SharedItemsImports = 5
source\WinCompData\WinCompData.projitems*{68317393-f5a5-4b2c-918a-688db2c10f54}*SharedItemsImports = 5
source\NullablesAttributes\NullablesAttributes.projitems*{6ab50ed0-6273-4919-9ade-50195664ef15}*SharedItemsImports = 4
source\DotLottie\DotLottie.projitems*{7012420d-624c-4bd4-a1d2-1c6c1655ed3a}*SharedItemsImports = 13
source\UIData\UIData.projitems*{74601e6c-2dfe-4842-b170-047941abff2c}*SharedItemsImports = 13
source\GenericData\GenericData.projitems*{77bcd724-8555-463b-985f-f8e8110164c4}*SharedItemsImports = 13
source\Lottie\Lottie.projitems*{8ef7bd77-28e9-4998-8dbb-8036f988fe65}*SharedItemsImports = 13
@ -182,6 +188,7 @@ Global
source\UIDataCodeGen\UIDataCodeGen.projitems*{9b6c0b7f-0d0f-4086-9746-0d34d7667db5}*SharedItemsImports = 5
source\CompMetadata\CompMetadata.projitems*{a262757c-9f1a-4f6e-9188-849f4b709d67}*SharedItemsImports = 5
source\GenericData\GenericData.projitems*{a687177e-31ff-4f05-89c6-03657c96a166}*SharedItemsImports = 5
source\DotLottie\DotLottie.projitems*{ab2acc11-de31-4e47-8a5b-895d6934684f}*SharedItemsImports = 5
source\CompMetadata\CompMetadata.projitems*{b0197c19-bdf5-473e-a022-e21f6122eee5}*SharedItemsImports = 13
source\LottieData\LottieData.projitems*{b3db16ee-a821-4474-a188-e64926529bbd}*SharedItemsImports = 13
source\LottieReader\LottieReader.projitems*{bb081e5a-cf3c-490f-8f8e-450a79f6ca33}*SharedItemsImports = 5
@ -205,6 +212,7 @@ Global
source\UIDataCodeGen\UIDataCodeGen.projitems*{d02be6c8-14db-4b4f-8600-f3c9b69c104d}*SharedItemsImports = 13
source\NullablesAttributes\NullablesAttributes.projitems*{e32587a8-94e8-4b68-91ad-f3612a48a62b}*SharedItemsImports = 13
source\CompMetadata\CompMetadata.projitems*{e392bad0-f936-4b64-a445-552597795cc7}*SharedItemsImports = 5
source\DotLottie\DotLottie.projitems*{e392bad0-f936-4b64-a445-552597795cc7}*SharedItemsImports = 5
source\GenericData\GenericData.projitems*{e392bad0-f936-4b64-a445-552597795cc7}*SharedItemsImports = 5
source\LottieData\LottieData.projitems*{e392bad0-f936-4b64-a445-552597795cc7}*SharedItemsImports = 5
source\LottieMetadata\LottieMetadata.projitems*{e392bad0-f936-4b64-a445-552597795cc7}*SharedItemsImports = 5
@ -451,6 +459,26 @@ Global
{A262757C-9F1A-4F6E-9188-849F4B709D67}.Release|ARM64.ActiveCfg = Release|Any CPU
{A262757C-9F1A-4F6E-9188-849F4B709D67}.Release|x64.ActiveCfg = Release|Any CPU
{A262757C-9F1A-4F6E-9188-849F4B709D67}.Release|x86.ActiveCfg = Release|Any CPU
{AB2ACC11-DE31-4E47-8A5B-895D6934684F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AB2ACC11-DE31-4E47-8A5B-895D6934684F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AB2ACC11-DE31-4E47-8A5B-895D6934684F}.Debug|ARM.ActiveCfg = Debug|Any CPU
{AB2ACC11-DE31-4E47-8A5B-895D6934684F}.Debug|ARM.Build.0 = Debug|Any CPU
{AB2ACC11-DE31-4E47-8A5B-895D6934684F}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{AB2ACC11-DE31-4E47-8A5B-895D6934684F}.Debug|ARM64.Build.0 = Debug|Any CPU
{AB2ACC11-DE31-4E47-8A5B-895D6934684F}.Debug|x64.ActiveCfg = Debug|Any CPU
{AB2ACC11-DE31-4E47-8A5B-895D6934684F}.Debug|x64.Build.0 = Debug|Any CPU
{AB2ACC11-DE31-4E47-8A5B-895D6934684F}.Debug|x86.ActiveCfg = Debug|Any CPU
{AB2ACC11-DE31-4E47-8A5B-895D6934684F}.Debug|x86.Build.0 = Debug|Any CPU
{AB2ACC11-DE31-4E47-8A5B-895D6934684F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AB2ACC11-DE31-4E47-8A5B-895D6934684F}.Release|Any CPU.Build.0 = Release|Any CPU
{AB2ACC11-DE31-4E47-8A5B-895D6934684F}.Release|ARM.ActiveCfg = Release|Any CPU
{AB2ACC11-DE31-4E47-8A5B-895D6934684F}.Release|ARM.Build.0 = Release|Any CPU
{AB2ACC11-DE31-4E47-8A5B-895D6934684F}.Release|ARM64.ActiveCfg = Release|Any CPU
{AB2ACC11-DE31-4E47-8A5B-895D6934684F}.Release|ARM64.Build.0 = Release|Any CPU
{AB2ACC11-DE31-4E47-8A5B-895D6934684F}.Release|x64.ActiveCfg = Release|Any CPU
{AB2ACC11-DE31-4E47-8A5B-895D6934684F}.Release|x64.Build.0 = Release|Any CPU
{AB2ACC11-DE31-4E47-8A5B-895D6934684F}.Release|x86.ActiveCfg = Release|Any CPU
{AB2ACC11-DE31-4E47-8A5B-895D6934684F}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -485,6 +513,8 @@ Global
{B0197C19-BDF5-473E-A022-E21F6122EEE5} = {AB232F35-AAF7-4AE2-B1D2-45DD9BC2F7D7}
{A262757C-9F1A-4F6E-9188-849F4B709D67} = {C75BD686-21A6-4EB3-8D4B-D5A01C019C52}
{E32587A8-94E8-4B68-91AD-F3612A48A62B} = {AB232F35-AAF7-4AE2-B1D2-45DD9BC2F7D7}
{7012420D-624C-4BD4-A1D2-1C6C1655ED3A} = {AB232F35-AAF7-4AE2-B1D2-45DD9BC2F7D7}
{AB2ACC11-DE31-4E47-8A5B-895D6934684F} = {C75BD686-21A6-4EB3-8D4B-D5A01C019C52}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {51B9BB4C-5196-41CF-950C-12B04AD8A61C}

Просмотреть файл

@ -47,6 +47,7 @@
</PackageReference>
</ItemGroup>
<Import Project="..\source\DotLottie\DotLottie.projitems" Label="Shared" />
<Import Project="..\source\CompMetadata\CompMetadata.projitems" Label="Shared" />
<Import Project="..\source\GenericData\GenericData.projitems" Label="Shared" />
<Import Project="..\source\Lottie\Lottie.projitems" Label="Shared" />

Просмотреть файл

@ -266,6 +266,7 @@
<Import Project="..\source\WinStorageStreamsData\WinStorageStreamsData.projitems" Label="Shared" />
<Import Project="..\source\WinUIXamlMediaData\WinUIXamlMediaData.projitems" Label="Shared" />
<Import Project="..\source\YamlData\YamlData.projitems" Label="Shared" />
<Import Project="..\source\DotLottie\DotLottie.projitems" Label="Shared" />
<Target Name="Pack">
<!-- Dummy target to mute warnings about attempts to create a NuPkg -->
</Target>

Просмотреть файл

@ -123,6 +123,7 @@ namespace LottieViewer
SuggestedStartLocation = PickerLocationId.ComputerFolder,
};
filePicker.FileTypeFilter.Add(".json");
filePicker.FileTypeFilter.Add(".lottie");
StorageFile? file = null;
try
@ -181,7 +182,7 @@ namespace LottieViewer
{
var items = await e.DataView.GetStorageItemsAsync();
var filteredItems = items.Where(IsJsonFile);
var filteredItems = items.Where(IsJsonOrLottieFile);
if (!filteredItems.Any() || filteredItems.Skip(1).Any())
{
@ -261,7 +262,10 @@ namespace LottieViewer
[Conditional("DebugDragDrop")]
static void DebugDragDrop(string text) => Debug.WriteLine(text);
static bool IsJsonFile(IStorageItem item) => item.IsOfType(StorageItemTypes.File) && item.Name.EndsWith(".json", StringComparison.InvariantCultureIgnoreCase);
static bool IsJsonOrLottieFile(IStorageItem item) =>
item.IsOfType(StorageItemTypes.File) &&
(item.Name.EndsWith(".json", StringComparison.InvariantCultureIgnoreCase) ||
item.Name.EndsWith(".lottie", StringComparison.InvariantCultureIgnoreCase));
bool _ignoreScrubberValueChanges;

Просмотреть файл

@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<Configurations>Debug;Release</Configurations>
<LangVersion>latest</LangVersion>
<DefineConstants>PUBLIC_DotLottie</DefineConstants>
</PropertyGroup>
<Import Project="..\..\source\DotLottie\DotLottie.projitems" Label="Shared" />
</Project>

Просмотреть файл

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
<HasSharedItems>true</HasSharedItems>
<SharedGUID>7012420d-624c-4bd4-a1d2-1c6c1655ed3a</SharedGUID>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)DotLottieFile.cs" />
<Compile Include="$(MSBuildThisFileDirectory)DotLottieFileAnimation.cs" />
<Compile Include="$(MSBuildThisFileDirectory)InvalidLottieFileException.cs" />
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Label="Globals">
<ProjectGuid>7012420d-624c-4bd4-a1d2-1c6c1655ed3a</ProjectGuid>
<MinimumVisualStudioVersion>14.0</MinimumVisualStudioVersion>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.Default.props" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.props" />
<Import Project="DotLottie.projitems" Label="Shared" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" />
</Project>

Просмотреть файл

@ -0,0 +1,399 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
namespace Microsoft.Toolkit.Uwp.UI.Lottie.DotLottie
{
/// <summary>
/// Provides access to the contents of a .lottie file.
/// See https://dotlottie.io.
/// </summary>
sealed class DotLottieFile : IDisposable
{
readonly Manifest _manifest;
DotLottieFile(ZipArchive zipArchive, Manifest manifest)
{
ZipArchive = zipArchive;
_manifest = manifest;
Animations = manifest.Animations.Select(a =>
new DotLottieFileAnimation(
this,
a.Id,
a.Loop)).ToArray();
}
/// <summary>
/// The name of the tool that generated the .lottie file.
/// </summary>
public string? Generator => _manifest.Generator;
/// <summary>
/// The .lottie file format version.
/// </summary>
public string Version => _manifest.Version;
/// <summary>
/// The author of the .lottie file.
/// </summary>
public string? Author => _manifest.Author;
/// <summary>
/// Returns the animations that are contained in the .lottie file.
/// </summary>
public IReadOnlyList<DotLottieFileAnimation> Animations { get; }
/// <summary>
/// Opens a file given its path in the .lottie file.
/// </summary>
/// <param name="path">The path to the file.</param>
/// <returns>A stream containing the file, or null if the file is not found.</returns>
public Stream? OpenFile(string path)
{
var zipEntry = GetEntryForPath(path);
return zipEntry is null ? null : zipEntry.Open();
}
/// <summary>
/// Opens a file given its path in the .lottie file.
/// </summary>
/// <param name="path">The path to the file.</param>
/// <returns>A stream containing the file, or null if the file is not found.</returns>
public MemoryStream? OpenFileAsMemoryStream(string path)
{
var zipEntry = GetEntryForPath(path);
if (zipEntry is null)
{
return null;
}
var buffer = new byte[zipEntry.Length];
using (var zipEntryStream = zipEntry.Open())
{
if (zipEntryStream.Read(buffer, 0, buffer.Length) != buffer.Length)
{
return null;
}
}
return new MemoryStream(buffer, writable: false);
}
/// <summary>
/// Returns a <see cref="DotLottieFile"/> that reads from the given <see cref="ZipArchive"/>.
/// </summary>
/// <param name="zipArchive">The <see cref="ZipArchive"/>.</param>
/// <returns>A <see cref="DotLottieFile"/> or null on error.</returns>
public static async Task<DotLottieFile?> FromZipArchiveAsync(
ZipArchive zipArchive)
{
// The manifest contains information about the animation.
var manifestEntry = zipArchive.GetEntry("manifest.json");
if (manifestEntry is null)
{
// Not a valid .lottie file.
return null;
}
var manifestBytes = new byte[manifestEntry.Length];
using var manifestStream = manifestEntry.Open();
var bytesRead = await manifestStream.ReadAsync(
manifestBytes,
0,
manifestBytes.Length);
if (bytesRead != manifestBytes.Length)
{
// Failed to read the manifest
return null;
}
var manifest = Manifest.ParseManifest(manifestBytes);
return manifest is null ? null : new DotLottieFile(zipArchive, manifest);
}
public void Dispose()
{
ZipArchive.Dispose();
}
internal ZipArchive ZipArchive { get; }
ZipArchiveEntry GetEntryForPath(string path)
{
if (path.StartsWith("/"))
{
path = path.Substring(1);
}
return ZipArchive.GetEntry(path);
}
static void ConsumeToken(ref Utf8JsonReader reader)
{
if (!reader.Read())
{
throw new InvalidLottieFileException();
}
}
sealed class Animation
{
Animation(string id, double speed, string themeColor, bool loop)
{
Id = id;
Speed = speed;
ThemeColor = themeColor;
Loop = loop;
}
public string Id { get; }
public double Speed { get; }
public string ThemeColor { get; }
public bool Loop { get; }
internal static Animation ParseAnimationObject(ref Utf8JsonReader reader)
{
string? id = null;
var speed = 1.0;
var loop = false;
var themeColor = "#000000";
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonTokenType.PropertyName:
var propertyName = reader.GetString();
ConsumeToken(ref reader);
switch (propertyName)
{
case "id":
id = reader.GetString();
break;
case "speed":
if (!reader.TryGetDouble(out speed))
{
throw new InvalidLottieFileException();
}
break;
case "themeColor":
themeColor = reader.GetString();
break;
case "loop":
loop = reader.GetBoolean();
break;
default:
// Unrecognized property. Ignore.
reader.Skip();
break;
}
break;
case JsonTokenType.EndObject:
return id is null
? throw new InvalidLottieFileException()
: new Animation(id, speed, themeColor, loop);
default:
throw new InvalidLottieFileException();
}
}
throw new InvalidLottieFileException();
}
}
sealed class Manifest
{
Manifest(
IReadOnlyList<Animation> animations,
string generator,
int? revision,
string version,
string? author)
{
Animations = animations;
Generator = generator;
Revision = revision;
Version = version;
Author = author;
}
public IReadOnlyList<Animation> Animations { get; }
public string? Author { get; }
public string? Generator { get; }
public int? Revision { get; }
public string Version { get; }
internal static Manifest? ParseManifest(byte[] manifestBytes)
{
var reader = new Utf8JsonReader(
manifestBytes,
new JsonReaderOptions
{
// Be resilient about trailing commas - ignore them.
AllowTrailingCommas = true,
// Be resilient about comments - ignore them.
CommentHandling = JsonCommentHandling.Skip,
// Fail if the JSON exceeds this depth.
MaxDepth = 5,
});
try
{
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonTokenType.StartObject:
return ParseManifestObject(ref reader);
default:
return null;
}
}
}
catch (InvalidLottieFileException)
{
// Ignore the exception and return null to indicate the error.
}
return null;
}
static Manifest? ParseManifestObject(ref Utf8JsonReader reader)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new InvalidLottieFileException();
}
IReadOnlyList<Animation>? animations = null;
string? author = null;
string? generator = null;
int? revision = null;
string? version = null;
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonTokenType.EndObject:
return animations is null || version is null || generator is null
? null
: new Manifest(
animations,
generator,
revision,
version,
author);
case JsonTokenType.PropertyName:
var propertyName = reader.GetString();
ConsumeToken(ref reader);
switch (propertyName)
{
case "animations":
animations = ParseAnimationsArray(ref reader);
break;
case "author":
author = reader.GetString();
break;
case "generator":
generator = reader.GetString();
break;
case "custom":
// For now we just ignore the custom object.
reader.Skip();
break;
case "version":
version = reader.GetString();
break;
case "revision":
revision = reader.GetInt32();
break;
default:
// Unrecognized property. Ignore.
reader.Skip();
break;
}
break;
default:
throw new InvalidLottieFileException();
}
}
throw new InvalidLottieFileException();
}
static IReadOnlyList<Animation> ParseAnimationsArray(
ref Utf8JsonReader reader)
{
if (reader.TokenType != JsonTokenType.StartArray)
{
throw new InvalidLottieFileException();
}
var animations = new List<Animation>();
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonTokenType.StartObject:
var animationObject =
Animation.ParseAnimationObject(ref reader);
animations.Add(animationObject);
break;
case JsonTokenType.EndArray:
return animations.ToArray();
default:
throw new InvalidLottieFileException();
}
}
throw new InvalidLottieFileException();
}
}
}
}

Просмотреть файл

@ -0,0 +1,33 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable enable
using System.IO;
namespace Microsoft.Toolkit.Uwp.UI.Lottie.DotLottie
{
sealed class DotLottieFileAnimation
{
readonly DotLottieFile _owner;
internal DotLottieFileAnimation(
DotLottieFile owner,
string id,
bool loop)
{
_owner = owner;
Id = id;
Loop = loop;
}
public string Id { get; }
public bool Loop { get; }
public string Path => $"animations/{Id}.json";
public Stream Open() => _owner.ZipArchive.GetEntry(Path).Open();
}
}

Просмотреть файл

@ -0,0 +1,14 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable enable
using System;
namespace Microsoft.Toolkit.Uwp.UI.Lottie.DotLottie
{
sealed class InvalidLottieFileException : Exception
{
}
}

Просмотреть файл

@ -5,6 +5,7 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
@ -19,10 +20,12 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie
/// translation of a composition and some metadata. This allows multiple instances of the translation
/// to be instantiated without requiring repeated translations.
/// </summary>
sealed class ContentFactory : IAnimatedVisualSource
sealed class AnimatedVisualFactory
: IAnimatedVisualSource
{
internal static readonly ContentFactory FailedContent = new ContentFactory(null);
readonly Dictionary<Uri, ICompositionSurface?> _imageCache = new Dictionary<Uri, ICompositionSurface?>();
readonly LottieVisualDiagnostics? _diagnostics;
Loader? _loader;
WinCompData.Visual? _wincompDataRootVisual;
WinCompData.CompositionPropertySet? _wincompDataThemingPropertySet;
CompositionPropertySet? _themingPropertySet;
@ -30,8 +33,9 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie
double _height;
TimeSpan _duration;
internal ContentFactory(LottieVisualDiagnostics? diagnostics)
internal AnimatedVisualFactory(Loader loader, LottieVisualDiagnostics? diagnostics)
{
_loader = loader;
_diagnostics = diagnostics;
}
@ -72,7 +76,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie
{
var sw = Stopwatch.StartNew();
var instantiator = new Instantiator(compositor);
var instantiator = new Instantiator(compositor, surfaceResolver: LoadImageFromUri);
// _wincompDataRootVisual != null is implied by CanInstantiate.
var result = new DisposableAnimatedVisual((Visual)instantiator.GetInstance(_wincompDataRootVisual!))
@ -95,8 +99,30 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie
diags.InstantiationTime = sw.Elapsed;
}
// After the first instantiation, all the images are cached so the
// loader is no longer needed.
_loader?.Dispose();
_loader = null;
return result;
}
}
ICompositionSurface? LoadImageFromUri(Uri uri)
{
if (!_imageCache.TryGetValue(uri, out var result))
{
// The loader will not be null, because either this is the
// first instantiation of the animated visual in which case the
// image loader hasn't been set to null, or it's a second instantiation
// so the images are already cached.
result = _loader!.LoadImage(uri);
// Cache the result so we can share the surfaces.
_imageCache.Add(uri, result);
}
return result;
}
}
}

Просмотреть файл

@ -0,0 +1,103 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable enable
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.IO.Compression;
using System.Threading.Tasks;
using Microsoft.Toolkit.Uwp.UI.Lottie.DotLottie;
using Windows.Storage;
using Windows.UI.Composition;
using Windows.UI.Xaml.Media;
namespace Microsoft.Toolkit.Uwp.UI.Lottie
{
/// <summary>
/// Loads files that conform to the .lottie spec. See: https://dotlottie.io/.
/// </summary>
sealed class DotLottieLoader : Loader
{
DotLottieFile? _dotLottieFile;
DotLottieLoader()
{
}
[return: NotNullIfNotNull("file")]
internal static async Task<AnimatedVisualFactory?> LoadAsync(
StorageFile file,
LottieVisualOptions options)
{
var stream = (await file.OpenReadAsync()).AsStreamForRead();
return await LoadAsync(file.Name, stream, options);
}
static async Task<AnimatedVisualFactory?> LoadAsync(
string fileName,
Stream stream,
LottieVisualOptions options)
{
ZipArchive zipArchive;
try
{
zipArchive = new ZipArchive(stream, ZipArchiveMode.Read);
}
catch (InvalidDataException)
{
// Not a valid zip file.
return null;
}
var loader = new DotLottieLoader();
return await Loader.LoadAsync(
() => loader.GetJsonStreamAsync(zipArchive, fileName),
loader,
options);
}
async Task<(string?, Stream?)> GetJsonStreamAsync(ZipArchive zipArchive, string fileName)
{
_dotLottieFile = await DotLottieFile.FromZipArchiveAsync(zipArchive);
if (_dotLottieFile is null)
{
return (null, null);
}
var firstAnimation = _dotLottieFile.Animations[0];
return (fileName, firstAnimation.Open());
}
internal override ICompositionSurface? LoadImage(Uri imageUri)
{
if (!imageUri.IsAbsoluteUri || imageUri.Authority != "localhost")
{
return null;
}
// Load the image from the .lottie file. This is loaded into a MemoryStream
// because the streams that come from ZipArchive cannot be randomly accessed
// as required by LoadedImageSurface. This also has the benefit that it is
// safe to Dispose the DotLottieFile as soon as the last image has started
// decoding, becuase we already have all the bytes in the MemoryStream.
var imageStream = _dotLottieFile!.OpenFileAsMemoryStream(imageUri.AbsolutePath);
if (imageStream is null)
{
return null;
}
return LoadedImageSurface.StartLoadFromStream(imageStream.AsRandomAccessStream());
}
public override void Dispose()
{
_dotLottieFile?.Dispose();
}
}
}

Просмотреть файл

@ -0,0 +1,21 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable enable
using System;
using Windows.UI.Composition;
namespace Microsoft.Toolkit.Uwp.UI.Lottie
{
/// <summary>
/// A delegate that returns an <see cref="ICompositionSurface"/> for the given image uri.
/// </summary>
/// <returns>A surface for the image referenced by <paramref name="imageUri"/>
/// or null.</returns>
/// <remarks>Users can provide an <see cref="ImageAssetHandler"/> in order to
/// provide a bitmap for an image referenced in a Lottie file.
/// <seealso cref="LottieVisualSource.SetImageAssetHandler(ImageAssetHandler?)"/></remarks>
public delegate ICompositionSurface? ImageAssetHandler(Uri imageUri);
}

Просмотреть файл

@ -0,0 +1,61 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable enable
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading.Tasks;
using Windows.Storage.Streams;
using Windows.UI.Composition;
namespace Microsoft.Toolkit.Uwp.UI.Lottie
{
/// <summary>
/// A loader that loads from an <see cref="IInputStream"/>.
/// </summary>
sealed class InputStreamLoader : Loader
{
readonly ImageAssetHandler? _imageLoader;
readonly IInputStream _inputStream;
InputStreamLoader(ImageAssetHandler? imageLoader, IInputStream inputStream)
{
_imageLoader = imageLoader;
_inputStream = inputStream;
}
[return: NotNullIfNotNull("inputStream")]
internal static async Task<AnimatedVisualFactory?> LoadAsync(
ImageAssetHandler? imageLoader,
IInputStream inputStream,
LottieVisualOptions options)
{
if (inputStream is null)
{
return null;
}
var loader = new InputStreamLoader(imageLoader, inputStream);
return await Loader.LoadAsync(
loader.GetJsonStreamAsync,
loader,
options);
}
Task<(string?, Stream?)> GetJsonStreamAsync()
{
return Task.FromResult(((string?)string.Empty, (Stream?)_inputStream.AsStreamForRead()));
}
internal override ICompositionSurface? LoadImage(Uri imageUri) =>
_imageLoader is null ? null : _imageLoader(imageUri);
public override void Dispose()
{
// Nothing to dispose.
}
}
}

Просмотреть файл

@ -32,15 +32,16 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie
{
readonly Wc.Compositor _c;
readonly Dictionary<object, object> _cache = new Dictionary<object, object>(new ReferenceEqualsComparer());
readonly Func<Uri, Wc.ICompositionSurface?>? _surfaceResolver;
#if ReuseExpressionAnimation
// The one and only ExpressionAnimation - reset and reparameterized for each time we need one.
readonly Wc.ExpressionAnimation _expressionAnimation;
#endif
public Instantiator(Wc.Compositor compositor)
public Instantiator(Wc.Compositor compositor, Func<Uri, Wc.ICompositionSurface?>? surfaceResolver)
{
_c = compositor;
_surfaceResolver = surfaceResolver;
#if ReuseExpressionAnimation
_expressionAnimation = _c.CreateExpressionAnimation();
#endif
@ -1431,9 +1432,9 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie
_ => throw new InvalidOperationException(),
};
Wm.LoadedImageSurface? GetLoadedImageSurface(Wmd.LoadedImageSurface obj)
Wc.ICompositionSurface? GetLoadedImageSurface(Wmd.LoadedImageSurface obj)
{
if (GetExisting<Wm.LoadedImageSurface>(obj, out var result))
if (GetExisting<Wc.ICompositionSurface>(obj, out var result))
{
return result;
}
@ -1445,8 +1446,10 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie
result = Wm.LoadedImageSurface.StartLoadFromStream(bytes.AsBuffer().AsStream().AsRandomAccessStream());
break;
case Wmd.LoadedImageSurface.LoadedImageSurfaceType.FromUri:
// Loading image surface from Uri is not supported yet.
result = null;
var uri = ((Wmd.LoadedImageSurfaceFromUri)obj).Uri;
// Ask the resolver to convert the URI to a surface. It can return null on failure.
result = _surfaceResolver?.Invoke(uri);
break;
default:
throw new InvalidOperationException();

Просмотреть файл

@ -4,11 +4,6 @@
#nullable enable
#if DEBUG
// Uncomment this to slow down async awaits for testing.
//#define SlowAwaits
#endif
using System;
using System.Collections.Generic;
using System.Diagnostics;
@ -20,30 +15,36 @@ using Microsoft.Toolkit.Uwp.UI.Lottie.LottieData;
using Microsoft.Toolkit.Uwp.UI.Lottie.LottieData.Serialization;
using Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp;
using Windows.Foundation.Metadata;
using Windows.Storage;
using Windows.Storage.Streams;
using Windows.UI.Composition;
namespace Microsoft.Toolkit.Uwp.UI.Lottie
{
/// <summary>
/// Handles loading a composition from a Lottie file. The result of the load
/// is a <see cref="ContentFactory"/> that can be used to instantiate a
/// is a <see cref="AnimatedVisualFactory"/> that can be used to instantiate a
/// Composition tree that will render the Lottie.
/// </summary>
abstract class Loader
abstract class Loader : IDisposable
{
// Identifies the bound property names in SourceMetadata.
static readonly Guid s_propertyBindingNamesKey = new Guid("A115C46A-254C-43E6-A3C7-9DE516C3C3C8");
// Private constructor prevents subclassing outside of this class.
Loader()
{
}
internal abstract ICompositionSurface? LoadImage(Uri imageUri);
private protected abstract Task<(string?, Stream?)> GetJsonStreamAsync();
// Asynchronously loads WinCompData from a Lottie file.
internal async Task<ContentFactory> LoadAsync(LottieVisualOptions options)
/// <summary>
/// Asynchonously loads an <see cref="AnimatedVisualFactory"/> that can be
/// used to instantiate IAnimatedVisual instances.
/// </summary>
/// <param name="jsonLoader">A delegate that asynchronously loads the JSON for
/// a Lottie file.</param>
/// <param name="imageLoader">A delegate that loads images that support a Lottie file.</param>
/// <param name="options">Options.</param>
/// <returns>An <see cref="AnimatedVisualFactory"/> that can be used
/// to instantiate IAnimatedVisual instances.</returns>
private protected static async Task<AnimatedVisualFactory?> LoadAsync(
Func<Task<(string? name, Stream? stream)>> jsonLoader,
Loader imageLoader,
LottieVisualOptions options)
{
LottieVisualDiagnostics? diagnostics = null;
var timeMeasurer = TimeMeasurer.Create();
@ -53,135 +54,144 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie
diagnostics = new LottieVisualDiagnostics { Options = options };
}
var result = new ContentFactory(diagnostics);
var result = new AnimatedVisualFactory(imageLoader, diagnostics);
// Get the file name and JSON contents.
(var fileName, var jsonStream) = await GetJsonStreamAsync();
if (diagnostics != null)
try
{
diagnostics.FileName = fileName ?? string.Empty;
diagnostics.ReadTime = timeMeasurer.GetElapsedAndRestart();
}
if (jsonStream is null)
{
// Failed to load ...
return result;
}
// Parsing large Lottie files can take significant time. Do it on
// another thread.
LottieComposition? lottieComposition = null;
await CheckedAwaitAsync(Task.Run(() =>
{
lottieComposition =
LottieCompositionReader.ReadLottieCompositionFromJsonStream(
jsonStream,
LottieCompositionReader.Options.IgnoreMatchNames,
out var readerIssues);
// Get the file name and JSON contents.
(var fileName, var jsonStream) = await jsonLoader();
if (diagnostics != null)
{
diagnostics.JsonParsingIssues = ToIssues(readerIssues);
}
}));
if (diagnostics != null)
{
diagnostics.ParseTime = timeMeasurer.GetElapsedAndRestart();
}
if (lottieComposition is null)
{
// Failed to load...
return result;
}
if (diagnostics != null)
{
// Save the LottieComposition in the diagnostics so that the xml and codegen
// code can be derived from it.
diagnostics.LottieComposition = lottieComposition;
// Validate the composition and report if issues are found.
diagnostics.LottieValidationIssues = ToIssues(LottieCompositionValidator.Validate(lottieComposition));
diagnostics.ValidationTime = timeMeasurer.GetElapsedAndRestart();
}
result.SetDimensions(
width: lottieComposition.Width,
height: lottieComposition.Height,
duration: lottieComposition.Duration);
// Translating large Lotties can take significant time. Do it on another thread.
WinCompData.Visual? wincompDataRootVisual = null;
uint requiredUapVersion = 0;
var optimizationEnabled = options.HasFlag(LottieVisualOptions.Optimize);
TranslationResult translationResult;
await CheckedAwaitAsync(Task.Run(() =>
{
// Generate property bindings only if the diagnostics object was requested.
// This is because the binding information is output in the diagnostics object
// so there's no point translating bindings if the diagnostics object
// isn't available.
var makeColorsBindable = diagnostics != null && options.HasFlag(LottieVisualOptions.BindableColors);
translationResult = LottieToWinCompTranslator.TryTranslateLottieComposition(
lottieComposition: lottieComposition,
configuration: new TranslatorConfiguration
{
TranslatePropertyBindings = makeColorsBindable,
GenerateColorBindings = makeColorsBindable,
TargetUapVersion = GetCurrentUapVersion(),
});
wincompDataRootVisual = translationResult.RootVisual;
requiredUapVersion = translationResult.MinimumRequiredUapVersion;
if (diagnostics != null)
{
diagnostics.TranslationIssues = ToIssues(translationResult.TranslationIssues);
diagnostics.TranslationTime = timeMeasurer.GetElapsedAndRestart();
// If there were any property bindings, save them in the Diagnostics object.
if (translationResult.SourceMetadata.TryGetValue(s_propertyBindingNamesKey, out var propertyBindingNames))
{
diagnostics.ThemePropertyBindings = (IReadOnlyList<PropertyBinding>)propertyBindingNames;
}
diagnostics.FileName = fileName ?? string.Empty;
diagnostics.ReadTime = timeMeasurer.GetElapsedAndRestart();
}
// Optimize the resulting translation. This will usually significantly reduce the size of
// the Composition code, however it might slow down loading too much on complex Lotties.
if (wincompDataRootVisual != null && optimizationEnabled)
if (jsonStream is null)
{
// Optimize.
wincompDataRootVisual = UIData.Tools.Optimizer.Optimize(wincompDataRootVisual, ignoreCommentProperties: true);
// Failed to load ...
return result;
}
// Parsing large Lottie files can take significant time. Do it on
// another thread.
LottieComposition? lottieComposition = null;
await Task.Run(() =>
{
lottieComposition =
LottieCompositionReader.ReadLottieCompositionFromJsonStream(
jsonStream,
LottieCompositionReader.Options.IgnoreMatchNames,
out var readerIssues);
if (diagnostics != null)
{
diagnostics.OptimizationTime = timeMeasurer.GetElapsedAndRestart();
diagnostics.JsonParsingIssues = ToIssues(readerIssues);
}
}
}));
});
if (wincompDataRootVisual is null)
{
// Failed.
return result;
}
else
{
if (diagnostics != null)
{
// Save the root visual so diagnostics can generate XML and codegen.
diagnostics.RootVisual = wincompDataRootVisual;
diagnostics.RequiredUapVersion = requiredUapVersion;
diagnostics.ParseTime = timeMeasurer.GetElapsedAndRestart();
}
result.SetRootVisual(wincompDataRootVisual);
return result;
if (lottieComposition is null)
{
// Failed to load...
return result;
}
if (diagnostics != null)
{
// Save the LottieComposition in the diagnostics so that the xml and codegen
// code can be derived from it.
diagnostics.LottieComposition = lottieComposition;
// Validate the composition and report if issues are found.
diagnostics.LottieValidationIssues = ToIssues(LottieCompositionValidator.Validate(lottieComposition));
diagnostics.ValidationTime = timeMeasurer.GetElapsedAndRestart();
}
result.SetDimensions(
width: lottieComposition.Width,
height: lottieComposition.Height,
duration: lottieComposition.Duration);
// Translating large Lotties can take significant time. Do it on another thread.
WinCompData.Visual? wincompDataRootVisual = null;
uint requiredUapVersion = 0;
var optimizationEnabled = options.HasFlag(LottieVisualOptions.Optimize);
TranslationResult translationResult;
await Task.Run(() =>
{
// Generate property bindings only if the diagnostics object was requested.
// This is because the binding information is output in the diagnostics object
// so there's no point translating bindings if the diagnostics object
// isn't available.
var makeColorsBindable = diagnostics != null && options.HasFlag(LottieVisualOptions.BindableColors);
translationResult = LottieToWinCompTranslator.TryTranslateLottieComposition(
lottieComposition: lottieComposition,
configuration: new TranslatorConfiguration
{
TranslatePropertyBindings = makeColorsBindable,
GenerateColorBindings = makeColorsBindable,
TargetUapVersion = GetCurrentUapVersion(),
});
wincompDataRootVisual = translationResult.RootVisual;
requiredUapVersion = translationResult.MinimumRequiredUapVersion;
if (diagnostics != null)
{
diagnostics.TranslationIssues = ToIssues(translationResult.TranslationIssues);
diagnostics.TranslationTime = timeMeasurer.GetElapsedAndRestart();
// If there were any property bindings, save them in the Diagnostics object.
if (translationResult.SourceMetadata.TryGetValue(s_propertyBindingNamesKey, out var propertyBindingNames))
{
diagnostics.ThemePropertyBindings = (IReadOnlyList<PropertyBinding>)propertyBindingNames;
}
}
// Optimize the resulting translation. This will usually significantly reduce the size of
// the Composition code, however it might slow down loading too much on complex Lotties.
if (wincompDataRootVisual != null && optimizationEnabled)
{
// Optimize.
wincompDataRootVisual = UIData.Tools.Optimizer.Optimize(wincompDataRootVisual, ignoreCommentProperties: true);
if (diagnostics != null)
{
diagnostics.OptimizationTime = timeMeasurer.GetElapsedAndRestart();
}
}
});
if (wincompDataRootVisual is null)
{
// Failed.
return result;
}
else
{
if (diagnostics != null)
{
// Save the root visual so diagnostics can generate XML and codegen.
diagnostics.RootVisual = wincompDataRootVisual;
diagnostics.RequiredUapVersion = requiredUapVersion;
}
result.SetRootVisual(wincompDataRootVisual);
return result;
}
}
catch
{
// Swallow exceptions. There's nowhere to report them.
}
return result;
}
static IReadOnlyList<Issue> ToIssues(IEnumerable<(string Code, string Description)> issues)
@ -190,12 +200,6 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie
static IReadOnlyList<Issue> ToIssues(IEnumerable<TranslationIssue> issues)
=> issues.Select(issue => new Issue(code: issue.Code, description: issue.Description)).ToArray();
static async Task<(string?, Stream?)> GetStorageFileStreamAsync(StorageFile storageFile)
{
var randomAccessStream = await storageFile.OpenReadAsync();
return (storageFile.Name, randomAccessStream.AsStreamForRead());
}
/// <summary>
/// Gets the highest UAP version supported by the current process.
/// </summary>
@ -217,88 +221,6 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie
return versionToTest - 1;
}
[Conditional("DEBUG")]
static void AssertNotNull<T>(T obj)
where T : class
{
if (obj is null)
{
Debug.Assert(obj != null, "Unexpected null");
}
}
// A loader that loads from an IInputStream.
internal sealed class FromInputStream : Loader
{
readonly IInputStream _inputStream;
internal FromInputStream(IInputStream inputStream)
{
AssertNotNull(inputStream);
_inputStream = inputStream;
}
// Turn off the warning about lacking an await. This method has to return a Task
// and the easiest way to do that when you do not need the asynchrony is to declare
// the method as async and return the value. This will cause C# to wrap the value in
// a Task.
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
private protected override async Task<(string?, Stream?)> GetJsonStreamAsync()
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
{
return (string.Empty, _inputStream.AsStreamForRead());
}
}
// A loader that loads from a StorageFile.
internal sealed class FromStorageFile : Loader
{
readonly StorageFile _storageFile;
internal FromStorageFile(StorageFile storageFile)
{
AssertNotNull(storageFile);
_storageFile = storageFile;
}
private protected override Task<(string?, Stream?)> GetJsonStreamAsync() =>
GetStorageFileStreamAsync(_storageFile);
}
// A loader that loads from a Uri.
internal sealed class FromUri : Loader
{
readonly Uri _uri;
internal FromUri(Uri uri)
{
AssertNotNull(uri);
_uri = uri;
}
private protected override async Task<(string?, Stream?)> GetJsonStreamAsync()
{
var absoluteUri = Uris.GetAbsoluteUri(_uri);
if (absoluteUri != null)
{
if (absoluteUri.Scheme.StartsWith("ms-"))
{
return await GetStorageFileStreamAsync(await StorageFile.GetFileFromApplicationUriAsync(absoluteUri));
}
else
{
var winrtClient = new Windows.Web.Http.HttpClient();
var response = await winrtClient.GetAsync(absoluteUri);
var result = await response.Content.ReadAsInputStreamAsync();
return (absoluteUri.LocalPath, result.AsStreamForRead());
}
}
return (null, null);
}
}
// Specializes the Stopwatch to do just the one thing we need of it - get the time
// elapsed since the last call then restart the Stopwatch to start measuring again.
readonly struct TimeMeasurer
@ -317,22 +239,6 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie
}
}
// For testing purposes, slows down a task.
#if SlowAwaits
const int _checkedDelayMs = 5;
async
#endif
static Task CheckedAwaitAsync(Task task)
{
#if SlowAwaits
await Task.Delay(_checkedDelayMs);
await task;
await Task.Delay(_checkedDelayMs);
#else
#pragma warning disable VSTHRD003 // Avoid awaiting foreign Tasks
return task;
#pragma warning restore VSTHRD003 // Avoid awaiting foreign Tasks
#endif
}
public abstract void Dispose();
}
}

Просмотреть файл

@ -6,15 +6,20 @@
<SharedGUID>8ef7bd77-28e9-4998-8dbb-8036f988fe65</SharedGUID>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)ContentFactory.cs" />
<Compile Include="$(MSBuildThisFileDirectory)AnimatedVisualFactory.cs" />
<Compile Include="$(MSBuildThisFileDirectory)DisposableAnimatedVisual.cs" />
<Compile Include="$(MSBuildThisFileDirectory)DotLottieLoader.cs" />
<Compile Include="$(MSBuildThisFileDirectory)GenericDataToJson.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ImageAssetHandler.cs" />
<Compile Include="$(MSBuildThisFileDirectory)InputStreamLoader.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Instantiator.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Loader.cs" />
<Compile Include="$(MSBuildThisFileDirectory)LottieVisualDiagnostics.cs" />
<Compile Include="$(MSBuildThisFileDirectory)LottieVisualOptions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)LottieVisualSource.cs" />
<Compile Include="$(MSBuildThisFileDirectory)StorageFileLoader.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UriLoader.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Uris.cs" />
</ItemGroup>
</Project>

Просмотреть файл

@ -26,7 +26,8 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie
EventRegistrationTokenTable<TypedEventHandler<IDynamicAnimatedVisualSource?, object?>>? _compositionInvalidatedEventTokenTable;
int _loadVersion;
Uri? _uriSource;
ContentFactory? _contentFactory;
AnimatedVisualFactory? _animatedVisualFactory;
ImageAssetHandler? _imageAssetHandler;
/// <summary>
/// Gets the options for the <see cref="LottieVisualSource"/>.
@ -103,7 +104,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie
public IAsyncAction SetSourceAsync(IInputStream stream)
{
_uriSource = null;
return LoadAsync(stream is null ? null : new Loader.FromInputStream(stream)).AsAsyncAction();
return LoadAsync(InputStreamLoader.LoadAsync(_imageAssetHandler, stream, Options)).AsAsyncAction();
}
/// <summary>
@ -115,7 +116,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie
public IAsyncAction SetSourceAsync(StorageFile file)
{
_uriSource = null;
return LoadAsync(file is null ? null : new Loader.FromStorageFile(file)).AsAsyncAction();
return LoadAsync(StorageFileLoader.LoadAsync(_imageAssetHandler, file, Options)).AsAsyncAction();
}
/// <summary>
@ -133,7 +134,8 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie
// This will not trigger loading because it will be seen as no change
// from the current (just set) _uriSource value.
UriSource = sourceUri;
return LoadAsync(sourceUri is null ? null : new Loader.FromUri(sourceUri)).AsAsyncAction();
return LoadAsync(UriLoader.LoadAsync(_imageAssetHandler, sourceUri, Options)).AsAsyncAction();
}
/// <summary>
@ -157,6 +159,20 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie
}
}
/// <summary>
/// Sets a delegate that returns an <see cref="ICompositionSurface"/> for the given image uri.
/// If this is null, no images will be loaded from references to external images.
/// </summary>
/// <remarks>Most Lottie files do not reference external images, but those that do
/// will refer to the files via a uri. It is up to the user of <see cref="LottieVisualSource"/>
/// to manage the loading of the image, and return an <see cref="ICompositionSurface"/> for
/// that image. Alternatively the delegate may return null, and the image will not be
/// displayed.</remarks>
public void SetImageAssetHandler(ImageAssetHandler? imageAssetHandler)
{
_imageAssetHandler = imageAssetHandler;
}
/// <summary>
/// Implements <see cref="IAnimatedVisualSource"/>.
/// </summary>
@ -169,7 +185,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie
Compositor compositor,
out object? diagnostics)
{
if (_contentFactory is null)
if (_animatedVisualFactory is null)
{
// No content has been loaded yet.
// Return an IAnimatedVisual that produces nothing.
@ -178,10 +194,10 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie
}
else
{
// Some content was loaded. Ask the contentFactory to produce an
// Some content was loaded. Ask the factory to produce an
// IAnimatedVisual. If it returns null, the player will treat it
// as an error.
return _contentFactory.TryCreateAnimatedVisual(compositor, out diagnostics);
return _animatedVisualFactory.TryCreateAnimatedVisual(compositor, out diagnostics);
}
}
@ -212,7 +228,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie
{
try
{
await LoadAsync(new Loader.FromUri(UriSource));
await LoadAsync(UriLoader.LoadAsync(_imageAssetHandler, UriSource, Options));
}
catch
{
@ -222,14 +238,14 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie
}
// Starts loading. Completes the returned task when the load completes or is replaced by another load.
async Task LoadAsync(Loader? loader)
async Task LoadAsync(Task<AnimatedVisualFactory?> loader)
{
var loadVersion = ++_loadVersion;
var oldContentFactory = _contentFactory;
_contentFactory = null;
var oldFactory = _animatedVisualFactory;
_animatedVisualFactory = null;
if (oldContentFactory != null)
if (oldFactory != null)
{
// Notify all listeners that their existing content is no longer valid.
// They should stop showing the content. We will notify them again when the
@ -237,23 +253,13 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie
NotifyListenersThatCompositionChanged();
}
if (loader is null)
{
// No loader means clear out what you previously loaded.
return;
}
// Disable the warning about the task possibly having being started in
// another context. There is no other context here.
#pragma warning disable VSTHRD003
ContentFactory contentFactory;
try
{
contentFactory = await loader.LoadAsync(Options);
}
catch
{
// Set the content factory to one that will return a null IAnimatedVisual to
// indicate that something went wrong. If the load succeeds this will get overwritten.
contentFactory = ContentFactory.FailedContent;
}
// Wait for the loader to finish.
var factory = await loader;
#pragma warning restore VSTHRD003
if (loadVersion != _loadVersion)
{
@ -261,19 +267,19 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie
return;
}
if (contentFactory is null)
if (factory is null)
{
// Load didn't produce anything.
return;
}
// We are the the most recent load. Save the result.
_contentFactory = contentFactory;
_animatedVisualFactory = factory;
// Notify all listeners that they should try to create their instance of the content again.
NotifyListenersThatCompositionChanged();
if (!contentFactory.CanInstantiate)
if (!factory.CanInstantiate)
{
// The load did not produce any content. Throw an exception so the caller knows.
throw new ArgumentException("Failed to load animated visual.");

Просмотреть файл

@ -0,0 +1,83 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable enable
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading.Tasks;
using Windows.Storage;
using Windows.UI.Composition;
namespace Microsoft.Toolkit.Uwp.UI.Lottie
{
/// <summary>
/// Loads files from a <see cref="StorageFile"/>. Supports raw
/// JSON files and .lottie files.
/// </summary>
sealed class StorageFileLoader : Loader
{
readonly ImageAssetHandler? _imageLoader;
readonly StorageFile _storageFile;
StorageFileLoader(ImageAssetHandler? imageLoader, StorageFile storageFile)
{
_imageLoader = imageLoader;
_storageFile = storageFile;
}
[return: NotNullIfNotNull("file")]
internal static async Task<AnimatedVisualFactory?> LoadAsync(
ImageAssetHandler? imageLoader,
StorageFile file,
LottieVisualOptions options)
{
if (file is null)
{
return null;
}
if (file.Name.EndsWith(".lottie", StringComparison.OrdinalIgnoreCase))
{
// It's a .lottie file. Defer to the DotLottieLoader.
return await DotLottieLoader.LoadAsync(file, options);
}
var loader = new StorageFileLoader(imageLoader, file);
return await Loader.LoadAsync(
loader.GetJsonStreamAsync,
loader,
options);
}
// Starts loading from an ms-appx asset file. This loads embedded assets.
internal static async Task<AnimatedVisualFactory?> LoadAsync(
ImageAssetHandler? imageLoader,
Uri applicationUri,
LottieVisualOptions options)
{
Debug.Assert(applicationUri.AbsoluteUri.StartsWith("ms-"), "Precondition");
var file = await StorageFile.GetFileFromApplicationUriAsync(applicationUri);
return await LoadAsync(imageLoader, file, options);
}
async Task<(string?, Stream?)> GetJsonStreamAsync()
{
var randomAccessStream = await _storageFile.OpenReadAsync();
// Assume it's a JSON stream.
return (_storageFile.Name, randomAccessStream.AsStreamForRead());
}
internal override ICompositionSurface? LoadImage(Uri imageUri) =>
_imageLoader is null ? null : _imageLoader(imageUri);
public override void Dispose()
{
// Nothing to dispose
}
}
}

Просмотреть файл

@ -0,0 +1,79 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable enable
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading.Tasks;
using Windows.UI.Composition;
namespace Microsoft.Toolkit.Uwp.UI.Lottie
{
/// <summary>
/// Loads files from a Uri.
/// </summary>
sealed class UriLoader : Loader
{
readonly ImageAssetHandler? _imageLoader;
UriLoader(ImageAssetHandler? imageLoader)
{
_imageLoader = imageLoader;
}
[return: NotNullIfNotNull("uri")]
internal static async Task<AnimatedVisualFactory?> LoadAsync(
ImageAssetHandler? imageLoader,
Uri uri,
LottieVisualOptions options)
{
if (uri is null)
{
return null;
}
var absoluteUri = Uris.GetAbsoluteUri(uri);
if (absoluteUri.Scheme.StartsWith("ms-"))
{
// The URI is an application URI. Defer to the StorageFileLoader.
return await StorageFileLoader.LoadAsync(imageLoader, absoluteUri, options);
}
else
{
var loader = new UriLoader(imageLoader);
return await Loader.LoadAsync(
() => GetJsonStreamAsync(uri),
loader,
options);
}
}
static async Task<(string?, Stream?)> GetJsonStreamAsync(Uri uri)
{
var absoluteUri = Uris.GetAbsoluteUri(uri);
if (absoluteUri != null)
{
var winrtClient = new Windows.Web.Http.HttpClient();
var response = await winrtClient.GetAsync(absoluteUri);
var result = await response.Content.ReadAsInputStreamAsync();
return (absoluteUri.LocalPath, result.AsStreamForRead());
}
return (null, null);
}
internal override ICompositionSurface? LoadImage(Uri imageUri) =>
_imageLoader is null ? null : _imageLoader(imageUri);
public override void Dispose()
{
// Nothing to dispose.
}
}
}

Просмотреть файл

@ -43,14 +43,22 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.LottieToWinComp
var embeddedImageAsset = (EmbeddedImageAsset)imageAsset;
surface = LoadedImageSurface.StartLoadFromStream(embeddedImageAsset.Bytes);
surface.SetName(imageAsset.Id);
surface.SetDescription(context, $"Image: \"{embeddedImageAsset.Id}\" {embeddedImageAsset.Format} {imageSize}.");
if (context.Translation.AddDescriptions)
{
surface.SetDescription(context, $"Image: \"{embeddedImageAsset.Id}\" {embeddedImageAsset.Format} {imageSize}.");
}
break;
case ImageAsset.ImageAssetType.External:
var externalImageAsset = (ExternalImageAsset)imageAsset;
surface = LoadedImageSurface.StartLoadFromUri(new Uri($"file://localhost/{externalImageAsset.Path}{externalImageAsset.FileName}"));
surface.SetName(externalImageAsset.FileName);
var path = externalImageAsset.Path + externalImageAsset.FileName;
surface.SetDescription(context, $"\"{path}\" {imageSize}.");
if (context.Translation.AddDescriptions)
{
surface.SetDescription(context, $"\"{path}\" {imageSize}.");
}
context.Issues.ImageFileRequired(path);
break;
default:

Просмотреть файл

@ -33,8 +33,8 @@ namespace Microsoft.Toolkit.Uwp.UI.Lottie.WinCompData.MetaData
// 7 = 1809 / 10.0.17763 / RS5 / October 2018 Update
// 8 = 1903 / 10.0.18362 / 19H1 / May 2019 Update
// 9 = 1909 / 10.0.18363 / 19H2 / November 2019 Update
// 10 = 2004 / 10.0.19041 / 20H1 / ?????
// 11 = ???? / 10.0.????? / 20H2 / ?????
// 10 = 2004 / 10.0.19041 / 20H1 / May 2020 Update
// 11 = 20H2 / 10.0.19042 / 20H2 / October 2020 Update
}
}
}