diff --git a/src/IIIF/IIIF.Tests/Serialisation/CookbookDeserialization.cs b/src/IIIF/IIIF.Tests/Serialisation/CookbookDeserialization.cs new file mode 100644 index 0000000..48e112e --- /dev/null +++ b/src/IIIF/IIIF.Tests/Serialisation/CookbookDeserialization.cs @@ -0,0 +1,17 @@ +using IIIF.Presentation.V3; +using IIIF.Tests.Serialisation.Data; + +namespace IIIF.Tests.Serialisation; + +[Trait("Category", "Cookbook")] +public class CookbookDeserialization +{ + [Theory] + [ClassData(typeof(CookbookManifestData))] + public void Can_Deserialize_Cookbook_Manifest(string manifestId, Manifest manifest) + { + // perfunctory assertion + manifest.Should().NotBeNull($"{manifestId} is a valid cookbook manifest"); + manifest.Id.Should().Be(manifestId); + } +} \ No newline at end of file diff --git a/src/IIIF/IIIF.Tests/Serialisation/Data/CookbookManifestData.cs b/src/IIIF/IIIF.Tests/Serialisation/Data/CookbookManifestData.cs new file mode 100644 index 0000000..b2f4735 --- /dev/null +++ b/src/IIIF/IIIF.Tests/Serialisation/Data/CookbookManifestData.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Net.Http; +using IIIF.Presentation.V3; +using IIIF.Serialisation; + +namespace IIIF.Tests.Serialisation.Data; + +/// +/// Used as [ClassData] - contains Manifests from IIIF Cookbook to validate deserialisation +/// +public class CookbookManifestData : IEnumerable +{ + // This will store { manifest-id, deserialized-manifest } + private readonly List data = new(); + + // these have bugs in the cookbook, see https://github.com/IIIF/cookbook-recipes/pull/546 + private List skip = new() + { + "https://iiif.io/api/cookbook/recipe/0219-using-caption-file/manifest.json", + "https://iiif.io/api/cookbook/recipe/0040-image-rotation-service/manifest-service.json" + }; + + public CookbookManifestData() + { + using var httpClient = new HttpClient(); + var theseusCollection = + GetIIIFResource("https://theseus-viewer.netlify.app/cookbook-collection.json", true); + + foreach (var item in theseusCollection.Items!) + { + if (item is Manifest manifestRef) + { + if (skip.Contains(manifestRef.Id)) continue; + + var iiif = GetIIIFResource(manifestRef.Id); + data.Add(new object[] { manifestRef.Id, iiif }); + } + } + + T GetIIIFResource(string url, bool mustSucceed = false) where T : JsonLdBase + { + var resource = httpClient.GetAsync(url).Result; + if (mustSucceed) resource.EnsureSuccessStatusCode(); + if (!resource.IsSuccessStatusCode) return null; + + try + { + var iiif = resource.Content.ReadAsStream().FromJsonStream(); + return iiif; + } + catch (Exception) + { + if (mustSucceed) throw; + return null; + } + } + } + + public IEnumerator GetEnumerator() => data.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} \ No newline at end of file diff --git a/src/IIIF/IIIF/Presentation/V3/Content/Audio.cs b/src/IIIF/IIIF/Presentation/V3/Content/Sound.cs similarity index 58% rename from src/IIIF/IIIF/Presentation/V3/Content/Audio.cs rename to src/IIIF/IIIF/Presentation/V3/Content/Sound.cs index a9f4c53..e2c17c7 100644 --- a/src/IIIF/IIIF/Presentation/V3/Content/Audio.cs +++ b/src/IIIF/IIIF/Presentation/V3/Content/Sound.cs @@ -2,11 +2,11 @@ namespace IIIF.Presentation.V3.Content; -public class Audio : ExternalResource, ITemporal, IPaintable +public class Sound : ExternalResource, ITemporal, IPaintable { public double? Duration { get; set; } - public Audio() : base("Sound") + public Sound() : base(nameof(Sound)) { } } \ No newline at end of file diff --git a/src/IIIF/IIIF/Presentation/V3/Selectors/SvgSelector.cs b/src/IIIF/IIIF/Presentation/V3/Selectors/SvgSelector.cs new file mode 100644 index 0000000..22572e2 --- /dev/null +++ b/src/IIIF/IIIF/Presentation/V3/Selectors/SvgSelector.cs @@ -0,0 +1,7 @@ +namespace IIIF.Presentation.V3.Selectors; + +public class SvgSelector : ISelector +{ + public string? Type => nameof(SvgSelector); + public string? Value { get; set; } +} \ No newline at end of file diff --git a/src/IIIF/IIIF/Presentation/V3/SpecificResource.cs b/src/IIIF/IIIF/Presentation/V3/SpecificResource.cs index 3166733..99ae862 100644 --- a/src/IIIF/IIIF/Presentation/V3/SpecificResource.cs +++ b/src/IIIF/IIIF/Presentation/V3/SpecificResource.cs @@ -1,13 +1,15 @@ -using IIIF.Presentation.V3.Selectors; -using Newtonsoft.Json; +using IIIF.Presentation.V3.Annotation; +using IIIF.Presentation.V3.Selectors; +using IIIF.Serialisation; namespace IIIF.Presentation.V3; -public class SpecificResource : ResourceBase, IStructuralLocation +public class SpecificResource : ResourceBase, IStructuralLocation, IPaintable { public override string Type => nameof(SpecificResource); - [JsonProperty(Order = 101)] public string Source { get; set; } + [JsonConverter(typeof(SourceConverter))] + [JsonProperty(Order = 101)] public IPaintable Source { get; set; } [JsonProperty(Order = 102)] public ISelector Selector { get; set; } } \ No newline at end of file diff --git a/src/IIIF/IIIF/Serialisation/Deserialisation/ExternalResourceConverter.cs b/src/IIIF/IIIF/Serialisation/Deserialisation/ExternalResourceConverter.cs index 443504a..dd945c7 100644 --- a/src/IIIF/IIIF/Serialisation/Deserialisation/ExternalResourceConverter.cs +++ b/src/IIIF/IIIF/Serialisation/Deserialisation/ExternalResourceConverter.cs @@ -19,7 +19,7 @@ public class ExternalResourceConverter : ReadOnlyConverter var type = jsonObject["type"].Value(); var externalResource = type switch { - nameof(Audio) => new Audio(), + nameof(Sound) => new Sound(), nameof(Video) => new Video(), nameof(Image) => new Image(), _ => new ExternalResource(type) diff --git a/src/IIIF/IIIF/Serialisation/Deserialisation/PaintableConverter.cs b/src/IIIF/IIIF/Serialisation/Deserialisation/PaintableConverter.cs index fb30cfc..f62709b 100644 --- a/src/IIIF/IIIF/Serialisation/Deserialisation/PaintableConverter.cs +++ b/src/IIIF/IIIF/Serialisation/Deserialisation/PaintableConverter.cs @@ -19,11 +19,12 @@ public class PaintableConverter : ReadOnlyConverter IPaintable? paintable = jsonObject["type"].Value() switch { - nameof(Audio) => new Audio(), + nameof(Sound) => new Sound(), nameof(Video) => new Video(), nameof(Image) => new Image(), nameof(Canvas) => new Canvas(), "Choice" => new PaintingChoice(), + nameof(SpecificResource) => new SpecificResource(), _ => null }; diff --git a/src/IIIF/IIIF/Serialisation/Deserialisation/ResourceBaseV3Converter.cs b/src/IIIF/IIIF/Serialisation/Deserialisation/ResourceBaseV3Converter.cs index 1c49518..2a2c66f 100644 --- a/src/IIIF/IIIF/Serialisation/Deserialisation/ResourceBaseV3Converter.cs +++ b/src/IIIF/IIIF/Serialisation/Deserialisation/ResourceBaseV3Converter.cs @@ -44,7 +44,7 @@ public class ResourceBaseV3Converter : ReadOnlyConverter nameof(Annotation) => new Annotation(), nameof(AnnotationCollection) => new AnnotationCollection(), nameof(AnnotationPage) => new AnnotationPage(), - nameof(Audio) => new Audio(), + nameof(Sound) => new Sound(), nameof(Canvas) => new Canvas(), nameof(Collection) => new Collection(), nameof(Image) => new Image(), diff --git a/src/IIIF/IIIF/Serialisation/Deserialisation/ResourceConverter.cs b/src/IIIF/IIIF/Serialisation/Deserialisation/ResourceConverter.cs index 99add41..f4aa666 100644 --- a/src/IIIF/IIIF/Serialisation/Deserialisation/ResourceConverter.cs +++ b/src/IIIF/IIIF/Serialisation/Deserialisation/ResourceConverter.cs @@ -50,7 +50,7 @@ public class ResourceConverter : ReadOnlyConverter nameof(AuthAccessTokenService2) => new AuthAccessTokenService2(), nameof(AuthLogoutService2) => new AuthLogoutService2(), nameof(AuthProbeService2) => new AuthProbeService2(), - nameof(Audio) => new Audio(), + nameof(Sound) => new Sound(), nameof(Video) => new Video(), nameof(Image) => new Image(), _ => null diff --git a/src/IIIF/IIIF/Serialisation/Deserialisation/SelectorConverter.cs b/src/IIIF/IIIF/Serialisation/Deserialisation/SelectorConverter.cs index e141bd0..f935fd3 100644 --- a/src/IIIF/IIIF/Serialisation/Deserialisation/SelectorConverter.cs +++ b/src/IIIF/IIIF/Serialisation/Deserialisation/SelectorConverter.cs @@ -19,8 +19,10 @@ public class SelectorConverter : ReadOnlyConverter { nameof(AudioContentSelector) => new AudioContentSelector(), nameof(ImageApiSelector) => new ImageApiSelector(), + "iiif:ImageApiSelector" => new ImageApiSelector(), nameof(PointSelector) => new PointSelector(), - nameof(VideoContentSelector) => new VideoContentSelector() + nameof(VideoContentSelector) => new VideoContentSelector(), + nameof(SvgSelector) => new SvgSelector() }; serializer.Populate(jsonObject.CreateReader(), selector); diff --git a/src/IIIF/IIIF/Serialisation/SourceConverter.cs b/src/IIIF/IIIF/Serialisation/SourceConverter.cs new file mode 100644 index 0000000..288146e --- /dev/null +++ b/src/IIIF/IIIF/Serialisation/SourceConverter.cs @@ -0,0 +1,57 @@ +using System; +using IIIF.Presentation.V3; +using IIIF.Presentation.V3.Annotation; +using IIIF.Presentation.V3.Content; +using IIIF.Utils; +using Newtonsoft.Json.Linq; + +namespace IIIF.Serialisation; + +public class SourceConverter : JsonConverter +{ + public override IPaintable? ReadJson(JsonReader reader, Type objectType, IPaintable? existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.String) + { + // We do not know that this is a Canvas... + // We would need knowledge of the rest of the IIIF Resource + return new Canvas { Id = reader.Value.ToString() }; + } + else if (reader.TokenType == JsonToken.StartObject) + { + var obj = JObject.Load(reader); + var type = obj["type"].Value(); + IPaintable paintable = type switch + { + nameof(Sound) => new Sound(), + nameof(Video) => new Video(), + nameof(Image) => new Image(), + nameof(Canvas) => new Canvas(), + nameof(SpecificResource) => new SpecificResource() + }; + serializer.Populate(obj.CreateReader(), paintable); + return paintable; + } + + return null; + } + + public override void WriteJson(JsonWriter writer, IPaintable? value, JsonSerializer serializer) + { + if (value is Canvas canvas && (canvas.SerialiseTargetAsId || IsSimpleCanvas(canvas))) + { + writer.WriteValue(canvas.Id); + return; + } + + // Default, pass through behaviour: + JObject.FromObject(value, serializer).WriteTo(writer); + } + + private static bool IsSimpleCanvas(Canvas canvas) + { + return canvas.Width == null && canvas.Duration == null && canvas.Items.IsNullOrEmpty(); + } + +} \ No newline at end of file diff --git a/src/IIIF/IIIF/Serialisation/TargetConverter.cs b/src/IIIF/IIIF/Serialisation/TargetConverter.cs index c9c60e9..f5f91e6 100644 --- a/src/IIIF/IIIF/Serialisation/TargetConverter.cs +++ b/src/IIIF/IIIF/Serialisation/TargetConverter.cs @@ -26,12 +26,14 @@ public class TargetConverter : JsonConverter var obj = JObject.Load(reader); var type = obj["type"].Value(); - return type switch + IStructuralLocation structuralLocation = type switch { - nameof(Canvas) => obj.ToObject(), - nameof(Range) => obj.ToObject(), - nameof(SpecificResource) => obj.ToObject() + nameof(Canvas) => new Canvas(), + nameof(Range) => new Range(), + nameof(SpecificResource) => new SpecificResource() }; + serializer.Populate(obj.CreateReader(), structuralLocation); + return structuralLocation; } return null;