From 1245b3a3e541d80005020813b9da4d28a1eb1106 Mon Sep 17 00:00:00 2001 From: tomcrane Date: Sat, 12 Oct 2024 19:16:28 +0100 Subject: [PATCH 1/4] Make iiif-net parse all the Manifests in the Cookbook --- .../Serialisation/CookbookDeserialization.cs | 49 ++++++++++++++++ .../V3/Content/{Audio.cs => Sound.cs} | 4 +- .../IIIF/Presentation/V3/Selectors/ISource.cs | 6 ++ .../Presentation/V3/Selectors/SvgSelector.cs | 7 +++ .../IIIF/Presentation/V3/SpecificResource.cs | 10 ++-- .../ExternalResourceConverter.cs | 2 +- .../Deserialisation/PaintableConverter.cs | 3 +- .../ResourceBaseV3Converter.cs | 2 +- .../Deserialisation/ResourceConverter.cs | 2 +- .../Deserialisation/SelectorConverter.cs | 4 +- .../IIIF/Serialisation/SourceConverter.cs | 57 +++++++++++++++++++ .../IIIF/Serialisation/TargetConverter.cs | 10 ++-- 12 files changed, 141 insertions(+), 15 deletions(-) create mode 100644 src/IIIF/IIIF.Tests/Serialisation/CookbookDeserialization.cs rename src/IIIF/IIIF/Presentation/V3/Content/{Audio.cs => Sound.cs} (58%) create mode 100644 src/IIIF/IIIF/Presentation/V3/Selectors/ISource.cs create mode 100644 src/IIIF/IIIF/Presentation/V3/Selectors/SvgSelector.cs create mode 100644 src/IIIF/IIIF/Serialisation/SourceConverter.cs diff --git a/src/IIIF/IIIF.Tests/Serialisation/CookbookDeserialization.cs b/src/IIIF/IIIF.Tests/Serialisation/CookbookDeserialization.cs new file mode 100644 index 0000000..6e6f0f7 --- /dev/null +++ b/src/IIIF/IIIF.Tests/Serialisation/CookbookDeserialization.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using IIIF.Presentation.V3; +using IIIF.Serialisation; + +namespace IIIF.Tests.Serialisation; + +public class CookbookDeserialization +{ + private Collection theseusCollection; + private HttpClient httpClient; + private List skip; + + public CookbookDeserialization() + { + httpClient = new HttpClient(); + var s = httpClient.GetStringAsync("https://theseus-viewer.netlify.app/cookbook-collection.json").Result; + theseusCollection = s.FromJson(); + + // these have bugs in the cookbook, see https://github.com/IIIF/cookbook-recipes/pull/546 + skip = new List + { + "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" + }; + } + + [Fact] + public void Can_Deserialize_Cookbook_Collection() + { + foreach (var item in theseusCollection.Items!) + { + if (item is Manifest manifestRef) + { + if (!skip.Contains(manifestRef.Id)) + { + var s = httpClient.GetStringAsync(manifestRef.Id).Result; + var manifest = s.FromJson(); + // perfunctory assertion + manifest.Id.Should().Be(manifestRef.Id); + } + } + // Do collections too... + } + + } + +} \ 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/ISource.cs b/src/IIIF/IIIF/Presentation/V3/Selectors/ISource.cs new file mode 100644 index 0000000..4f5fe76 --- /dev/null +++ b/src/IIIF/IIIF/Presentation/V3/Selectors/ISource.cs @@ -0,0 +1,6 @@ +namespace IIIF.Presentation.V3.Selectors; + +public class ISource +{ + +} \ 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; From d709b639c9700e2605a88a56de2f40a703c5fc35 Mon Sep 17 00:00:00 2001 From: Donald Gray Date: Mon, 28 Oct 2024 11:22:03 +0000 Subject: [PATCH 2/4] Remove unused ISource class --- src/IIIF/IIIF/Presentation/V3/Selectors/ISource.cs | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 src/IIIF/IIIF/Presentation/V3/Selectors/ISource.cs diff --git a/src/IIIF/IIIF/Presentation/V3/Selectors/ISource.cs b/src/IIIF/IIIF/Presentation/V3/Selectors/ISource.cs deleted file mode 100644 index 4f5fe76..0000000 --- a/src/IIIF/IIIF/Presentation/V3/Selectors/ISource.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace IIIF.Presentation.V3.Selectors; - -public class ISource -{ - -} \ No newline at end of file From 659445942fa5e8ccf610e8d374bb20df10dbea9a Mon Sep 17 00:00:00 2001 From: Donald Gray Date: Mon, 28 Oct 2024 11:22:28 +0000 Subject: [PATCH 3/4] Use theory for cookbook tests This allows easier visibility of which tests pass and fail --- .../Serialisation/CookbookDeserialization.cs | 48 +++------------- .../Data/CookbookManifestData.cs | 55 +++++++++++++++++++ 2 files changed, 63 insertions(+), 40 deletions(-) create mode 100644 src/IIIF/IIIF.Tests/Serialisation/Data/CookbookManifestData.cs diff --git a/src/IIIF/IIIF.Tests/Serialisation/CookbookDeserialization.cs b/src/IIIF/IIIF.Tests/Serialisation/CookbookDeserialization.cs index 6e6f0f7..e11fe3b 100644 --- a/src/IIIF/IIIF.Tests/Serialisation/CookbookDeserialization.cs +++ b/src/IIIF/IIIF.Tests/Serialisation/CookbookDeserialization.cs @@ -1,49 +1,17 @@ -using System; -using System.Collections.Generic; -using System.Net.Http; using IIIF.Presentation.V3; -using IIIF.Serialisation; +using IIIF.Tests.Serialisation.Data; namespace IIIF.Tests.Serialisation; +[Trait("Category", "Cookbook")] public class CookbookDeserialization { - private Collection theseusCollection; - private HttpClient httpClient; - private List skip; - - public CookbookDeserialization() + [Theory] + [ClassData(typeof(CookbookManifestData))] + public void Can_Deserialize_Cookbook_Manifest(string manifestId, Manifest manifest) { - httpClient = new HttpClient(); - var s = httpClient.GetStringAsync("https://theseus-viewer.netlify.app/cookbook-collection.json").Result; - theseusCollection = s.FromJson(); - - // these have bugs in the cookbook, see https://github.com/IIIF/cookbook-recipes/pull/546 - skip = new List - { - "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" - }; + // perfunctory assertion + manifest.Should().NotBeNull(); + manifest.Id.Should().Be(manifestId); } - - [Fact] - public void Can_Deserialize_Cookbook_Collection() - { - foreach (var item in theseusCollection.Items!) - { - if (item is Manifest manifestRef) - { - if (!skip.Contains(manifestRef.Id)) - { - var s = httpClient.GetStringAsync(manifestRef.Id).Result; - var manifest = s.FromJson(); - // perfunctory assertion - manifest.Id.Should().Be(manifestRef.Id); - } - } - // Do collections too... - } - - } - } \ 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..93daee2 --- /dev/null +++ b/src/IIIF/IIIF.Tests/Serialisation/Data/CookbookManifestData.cs @@ -0,0 +1,55 @@ +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; + + var iiif = resource.Content.ReadAsStream().FromJsonStream(); + return iiif; + } + } + + public IEnumerator GetEnumerator() => data.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} \ No newline at end of file From 1f41564094795c84cd364ec5ce1f5021855eba85 Mon Sep 17 00:00:00 2001 From: Donald Gray Date: Mon, 28 Oct 2024 11:32:15 +0000 Subject: [PATCH 4/4] Handle manifests that fail to deserialize --- .../Serialisation/CookbookDeserialization.cs | 2 +- .../Data/CookbookManifestData.cs | 117 ++++++++++-------- 2 files changed, 64 insertions(+), 55 deletions(-) diff --git a/src/IIIF/IIIF.Tests/Serialisation/CookbookDeserialization.cs b/src/IIIF/IIIF.Tests/Serialisation/CookbookDeserialization.cs index e11fe3b..48e112e 100644 --- a/src/IIIF/IIIF.Tests/Serialisation/CookbookDeserialization.cs +++ b/src/IIIF/IIIF.Tests/Serialisation/CookbookDeserialization.cs @@ -11,7 +11,7 @@ public class CookbookDeserialization public void Can_Deserialize_Cookbook_Manifest(string manifestId, Manifest manifest) { // perfunctory assertion - manifest.Should().NotBeNull(); + 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 index 93daee2..b2f4735 100644 --- a/src/IIIF/IIIF.Tests/Serialisation/Data/CookbookManifestData.cs +++ b/src/IIIF/IIIF.Tests/Serialisation/Data/CookbookManifestData.cs @@ -1,55 +1,64 @@ -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; - - var iiif = resource.Content.ReadAsStream().FromJsonStream(); - return iiif; - } - } - - public IEnumerator GetEnumerator() => data.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +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