diff --git a/Examples/AdaptiveCardVisualizer/AdaptiveCardVisualizer.xcodeproj/project.pbxproj b/Examples/AdaptiveCardVisualizer/AdaptiveCardVisualizer.xcodeproj/project.pbxproj index d490b75..1154db0 100644 --- a/Examples/AdaptiveCardVisualizer/AdaptiveCardVisualizer.xcodeproj/project.pbxproj +++ b/Examples/AdaptiveCardVisualizer/AdaptiveCardVisualizer.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 82C107E8252F7CA60046764A /* Photos.json in Resources */ = {isa = PBXBuildFile; fileRef = 82C107E7252F7CA60046764A /* Photos.json */; }; + 82C107E9252F7CA60046764A /* Photos.json in Resources */ = {isa = PBXBuildFile; fileRef = 82C107E7252F7CA60046764A /* Photos.json */; }; 9913B76B2514D5EC002C695C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9913B7562514D5EC002C695C /* Assets.xcassets */; }; 9913B76C2514D5EC002C695C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9913B7562514D5EC002C695C /* Assets.xcassets */; }; 9913B7822514D695002C695C /* ActivityUpdate.json in Resources */ = {isa = PBXBuildFile; fileRef = 9913B7812514D695002C695C /* ActivityUpdate.json */; }; @@ -62,6 +64,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 82C107E7252F7CA60046764A /* Photos.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Photos.json; sourceTree = ""; }; 9913B7562514D5EC002C695C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 9913B75B2514D5EC002C695C /* AdaptiveCardVisualizer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AdaptiveCardVisualizer.app; sourceTree = BUILT_PRODUCTS_DIR; }; 9913B75E2514D5EC002C695C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -186,6 +189,7 @@ 9913B7D92514F18F002C695C /* WeatherCompact.json */, 9913B7DE2514F458002C695C /* WeatherLarge.json */, 9913B7ED2514F6A8002C695C /* FlightDetails.json */, + 82C107E7252F7CA60046764A /* Photos.json */, ); path = Cards; sourceTree = ""; @@ -311,6 +315,7 @@ 9913B78A2514D6B5002C695C /* prism.js in Resources */, 9913B7D52514E065002C695C /* StockUpdate.json in Resources */, 9913B7822514D695002C695C /* ActivityUpdate.json in Resources */, + 82C107E8252F7CA60046764A /* Photos.json in Resources */, 9913B7DF2514F458002C695C /* WeatherLarge.json in Resources */, 9913B78E2514D6B5002C695C /* okaidia.css in Resources */, 9913B7CB2514DB63002C695C /* FlightUpdate.json in Resources */, @@ -331,6 +336,7 @@ 9913B78B2514D6B5002C695C /* prism.js in Resources */, 9913B7D62514E065002C695C /* StockUpdate.json in Resources */, 9913B7832514D695002C695C /* ActivityUpdate.json in Resources */, + 82C107E9252F7CA60046764A /* Photos.json in Resources */, 9913B7E02514F458002C695C /* WeatherLarge.json in Resources */, 9913B78F2514D6B5002C695C /* okaidia.css in Resources */, 9913B7CC2514DB63002C695C /* FlightUpdate.json in Resources */, diff --git a/Examples/AdaptiveCardVisualizer/Cards/Photos.json b/Examples/AdaptiveCardVisualizer/Cards/Photos.json new file mode 100644 index 0000000..6a4010d --- /dev/null +++ b/Examples/AdaptiveCardVisualizer/Cards/Photos.json @@ -0,0 +1,69 @@ +{ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.0", + "body": [ + { + "type": "TextBlock", + "text": "Here are some cool photos", + "size": "large" + }, + { + "type": "TextBlock", + "text": "from picsum.photos", + "size": "medium", + "weight": "lighter", + "spacing": "none" + }, + { + "type": "ImageSet", + "images": [ + { + "type": "Image", + "url": "https://picsum.photos/200/200?image=100", + "altText": "White beach panorama" + }, + { + "type": "Image", + "url": "https://picsum.photos/300/200?image=200", + "altText": "Cow on a grassy field" + }, + { + "type": "Image", + "url": "https://picsum.photos/300/200?image=301", + "altText": "Orange leaves on the sidewalk of a park" + }, + { + "type": "Image", + "url": "https://picsum.photos/200/200?image=400", + "altText": "Green leaves" + }, + { + "type": "Image", + "url": "https://picsum.photos/300/200?image=500", + "altText": "Top of a sky scrapper" + }, + { + "type": "Image", + "url": "https://picsum.photos/200/200?image=600", + "altText": "Foggy forest" + }, + { + "type": "Image", + "url": "https://picsum.photos/300/200?image=700", + "altText": "Picure of the blue ocean" + }, + { + "type": "Image", + "url": "https://picsum.photos/300/200?image=800", + "altText": "Crowded train station" + }, + { + "type": "Image", + "url": "https://picsum.photos/300/200?image=900", + "altText": "Sunset under a dock" + } + ] + } + ] +} diff --git a/Examples/AdaptiveCardVisualizer/Shared/SampleCard.swift b/Examples/AdaptiveCardVisualizer/Shared/SampleCard.swift index 7093a2d..3754f91 100644 --- a/Examples/AdaptiveCardVisualizer/Shared/SampleCard.swift +++ b/Examples/AdaptiveCardVisualizer/Shared/SampleCard.swift @@ -61,6 +61,12 @@ extension SampleCard { resourceName: "FlightDetails.json" ) + static let photos = SampleCard( + id: "Photos", + title: "Photos", + resourceName: "Photos.json" + ) + static let all: [SampleCard] = [ .gitHubRepository, .activityUpdate, @@ -71,5 +77,6 @@ extension SampleCard { .weatherCompact, .weatherLarge, .flightDetails, + .photos, ] } diff --git a/Sources/AdaptiveCardUI/Logic/FeatureAdaptation/CardElement+FeatureAdaptable.swift b/Sources/AdaptiveCardUI/Logic/FeatureAdaptation/CardElement+FeatureAdaptable.swift index e4b59ec..9be49cf 100644 --- a/Sources/AdaptiveCardUI/Logic/FeatureAdaptation/CardElement+FeatureAdaptable.swift +++ b/Sources/AdaptiveCardUI/Logic/FeatureAdaptation/CardElement+FeatureAdaptable.swift @@ -9,7 +9,7 @@ extension CardElement: FeatureAdaptable { } switch self { - case .textBlock, .image, .richTextBlock, .factSet, .custom: + case .textBlock, .image, .richTextBlock, .factSet, .imageSet, .custom: return self case var .actionSet(actionSet): var elementShouldFallback = false diff --git a/Sources/AdaptiveCardUI/Logic/Store/AdaptiveCard+DuplicateIdentifiers.swift b/Sources/AdaptiveCardUI/Logic/Store/AdaptiveCard+DuplicateIdentifiers.swift index 8841fc7..d94ad92 100644 --- a/Sources/AdaptiveCardUI/Logic/Store/AdaptiveCard+DuplicateIdentifiers.swift +++ b/Sources/AdaptiveCardUI/Logic/Store/AdaptiveCard+DuplicateIdentifiers.swift @@ -27,6 +27,8 @@ private extension CardElement { return [container.id] + container.items.flatMap(\.identifiers) case let .columnSet(columnSet): return [columnSet.id] + columnSet.columns.flatMap(\.identifiers) + case let .imageSet(imageSet): + return [imageSet.id] + imageSet.images.map(\.id) case .unknown: return [] } diff --git a/Sources/AdaptiveCardUI/Logic/Store/AdaptiveCard+ImageURLs.swift b/Sources/AdaptiveCardUI/Logic/Store/AdaptiveCard+ImageURLs.swift index 026bd29..c732d16 100644 --- a/Sources/AdaptiveCardUI/Logic/Store/AdaptiveCard+ImageURLs.swift +++ b/Sources/AdaptiveCardUI/Logic/Store/AdaptiveCard+ImageURLs.swift @@ -28,6 +28,8 @@ private extension CardElement { [container.backgroundImage?.url].compactMap { $0 } case let .columnSet(columnSet): return columnSet.columns.flatMap(\.imageURLs) + case let .imageSet(imageSet): + return imageSet.images.map(\.url) } } } diff --git a/Sources/AdaptiveCardUI/Logic/ToggleVisibility/CardElement+Toggleable.swift b/Sources/AdaptiveCardUI/Logic/ToggleVisibility/CardElement+Toggleable.swift index 9672c22..b1af53a 100644 --- a/Sources/AdaptiveCardUI/Logic/ToggleVisibility/CardElement+Toggleable.swift +++ b/Sources/AdaptiveCardUI/Logic/ToggleVisibility/CardElement+Toggleable.swift @@ -21,6 +21,13 @@ extension CardElement: Toggleable { } else { return false } + case var .imageSet(imageSet): + if imageSet.images.toggleVisibility(of: target) { + self = .imageSet(imageSet) + return true + } else { + return false + } case .textBlock, .image, .richTextBlock, .actionSet, .factSet, .custom, .unknown: return false } @@ -52,6 +59,9 @@ private extension CardElement { case var .factSet(element): element.toggleVisibility(isVisible) self = .factSet(element) + case var .imageSet(element): + element.toggleVisibility(isVisible) + self = .imageSet(element) case var .custom(element): element.toggleVisibility(isVisible) self = .custom(element) diff --git a/Sources/AdaptiveCardUI/Logic/ToggleVisibility/Image+Toggleable.swift b/Sources/AdaptiveCardUI/Logic/ToggleVisibility/Image+Toggleable.swift new file mode 100644 index 0000000..cf24527 --- /dev/null +++ b/Sources/AdaptiveCardUI/Logic/ToggleVisibility/Image+Toggleable.swift @@ -0,0 +1,15 @@ +import Foundation + +extension Image: Toggleable { + mutating func toggleVisibility(of target: TargetElement) -> Bool { + guard id == target.elementId else { return false } + + if let isVisible = target.isVisible { + self.isVisible = isVisible + } else { + isVisible.toggle() + } + + return true + } +} diff --git a/Sources/AdaptiveCardUI/Model/Containers/ImageSet.swift b/Sources/AdaptiveCardUI/Model/Containers/ImageSet.swift new file mode 100644 index 0000000..f4ebf3e --- /dev/null +++ b/Sources/AdaptiveCardUI/Model/Containers/ImageSet.swift @@ -0,0 +1,57 @@ +import DefaultCodable +import Foundation + +public enum MediumImageSize: DefaultValueProvider { + public static var `default` = ImageSize.medium +} + +/// The `ImageSet` element displays a collection of images. +public struct ImageSet: CardElementProtocol, Codable, Equatable { + /// A unique identifier associated with the item. + @ItemIdentifier public var id: String + + /// If `false`, this item will be removed from the visual tree. + @Default public var isVisible: Bool + + /// When `true`, draw a separating line at the top of the element. + @Default public var separator: Bool + + /// Controls the amount of spacing between this element and the preceding element. + @Default public var spacing: Spacing + + /// Describes what to do when an unknown element is encountered or the requires of this or any children can’t be met. + @Default public var fallback: Fallback + + /// A series of key/value pairs indicating features that the item requires with corresponding minimum version. + /// When a feature is missing or of insufficient version, fallback is triggered. + @Default public var requires: [String: SemanticVersion] + + /// The array of `Image` elements to show. + public var images: [Image] + + /// Controls the approximate size of each image. + /// + /// `auto` and `stretch` are not supported for ImageSet. The size will + /// default to `medium` if those values are set. + @Default public var imageSize: ImageSize + + public init( + id: String = "", + isVisible: Bool = true, + separator: Bool = false, + spacing: Spacing = .default, + fallback: Fallback = .none, + requires: [String: SemanticVersion] = [:], + images: [Image], + imageSize: ImageSize = .medium + ) { + self.id = id + self.isVisible = isVisible + self.separator = separator + self.spacing = spacing + self.fallback = fallback + self.requires = requires + self.images = images + self.imageSize = imageSize + } +} diff --git a/Sources/AdaptiveCardUI/Model/Elements/CardElement.swift b/Sources/AdaptiveCardUI/Model/Elements/CardElement.swift index 0ed5d82..7f837cc 100644 --- a/Sources/AdaptiveCardUI/Model/Elements/CardElement.swift +++ b/Sources/AdaptiveCardUI/Model/Elements/CardElement.swift @@ -24,6 +24,9 @@ public indirect enum CardElement { /// A series of facts (i.e. name / value pairs) in a tabular form. case factSet(FactSet) + /// A collection of images. + case imageSet(ImageSet) + /// A custom card element. case custom(CustomCardElement) @@ -56,6 +59,8 @@ extension CardElement: Codable { self = .columnSet(try ColumnSet(from: decoder)) case String(describing: FactSet.self): self = .factSet(try FactSet(from: decoder)) + case String(describing: ImageSet.self): + self = .imageSet(try ImageSet(from: decoder)) default: if let decodeCustomCardElement = Self.customCardElementDecoders[type] { self = .custom(try decodeCustomCardElement(decoder)) @@ -90,6 +95,9 @@ extension CardElement: Codable { case let .factSet(element): try container.encode(String(describing: FactSet.self), forKey: .type) try element.encode(to: encoder) + case let .imageSet(element): + try container.encode(String(describing: ImageSet.self), forKey: .type) + try element.encode(to: encoder) case let .custom(element): let typeName = type(of: element).typeName guard let encodeCustomCardElement = Self.customCardElementEncoders[typeName] else { @@ -126,6 +134,8 @@ public extension CardElement { return element[keyPath: keyPath] case let .factSet(element): return element[keyPath: keyPath] + case let .imageSet(element): + return element[keyPath: keyPath] case let .custom(element): return element[keyPath: keyPath] case let .unknown(element): @@ -174,6 +184,8 @@ extension CardElement: Equatable { return l == r case let (.factSet(l), .factSet(r)): return l == r + case let (.imageSet(l), .imageSet(r)): + return l == r case let (.custom(l), .custom(r)): let typeName = type(of: l).typeName return Self.customCardElementIsEqual[typeName]?(l, r) ?? false diff --git a/Sources/AdaptiveCardUI/UI/CardElement/CardElementList.swift b/Sources/AdaptiveCardUI/UI/CardElement/CardElementList.swift index dd462b2..fd5db22 100644 --- a/Sources/AdaptiveCardUI/UI/CardElement/CardElementList.swift +++ b/Sources/AdaptiveCardUI/UI/CardElement/CardElementList.swift @@ -62,7 +62,7 @@ private extension CardElement { var bleed: Bool { switch self { - case .textBlock, .image, .richTextBlock, .actionSet, .factSet, .custom: + case .textBlock, .image, .richTextBlock, .actionSet, .factSet, .imageSet, .custom: return false case let .container(element): return element.bleed diff --git a/Sources/AdaptiveCardUI/UI/CardElement/CardElementView.swift b/Sources/AdaptiveCardUI/UI/CardElement/CardElementView.swift index c741a8d..53ee0f3 100644 --- a/Sources/AdaptiveCardUI/UI/CardElement/CardElementView.swift +++ b/Sources/AdaptiveCardUI/UI/CardElement/CardElementView.swift @@ -26,6 +26,8 @@ ColumnSetView(columnSet) case let .factSet(factSet): FactSetView(factSet) + case let .imageSet(imageSet): + ImageSetView(imageSet) case let .custom(customCardElement): CustomCardElementView(customCardElement) default: diff --git a/Sources/AdaptiveCardUI/UI/Helpers/FlowLayout.swift b/Sources/AdaptiveCardUI/UI/Helpers/FlowLayout.swift new file mode 100644 index 0000000..6bf3576 --- /dev/null +++ b/Sources/AdaptiveCardUI/UI/Helpers/FlowLayout.swift @@ -0,0 +1,41 @@ +#if canImport(SwiftUI) + + import SwiftUI + + // Adapted from https://gist.github.com/chriseidhof/3c6ea3fb2102052d1898d8ea27fbee07 + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) + struct FlowLayout { + private let proposedSize: CGSize + private let horizontalSpacing: CGFloat + private let verticalSpacing: CGFloat + + private var position = CGPoint.zero + private var lineHeight: CGFloat = 0 + + var size: CGSize { + CGSize(width: proposedSize.width, height: position.y + lineHeight) + } + + init(proposedSize: CGSize, horizontalSpacing: CGFloat, verticalSpacing: CGFloat) { + self.proposedSize = proposedSize + self.horizontalSpacing = horizontalSpacing + self.verticalSpacing = verticalSpacing + } + + mutating func addElementWithSize(_ size: CGSize) -> CGRect { + if position.x + size.width > proposedSize.width { + position.x = 0 + position.y += lineHeight + verticalSpacing + lineHeight = 0 + } + + let result = CGRect(origin: position, size: size) + + lineHeight = max(lineHeight, size.height) + position.x += size.width + horizontalSpacing + + return result + } + } + +#endif diff --git a/Sources/AdaptiveCardUI/UI/Image/ImageSetView.swift b/Sources/AdaptiveCardUI/UI/Image/ImageSetView.swift new file mode 100644 index 0000000..354b47f --- /dev/null +++ b/Sources/AdaptiveCardUI/UI/Image/ImageSetView.swift @@ -0,0 +1,74 @@ +#if canImport(SwiftUI) + + import SwiftUI + + @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) + struct ImageSetView: View { + @Environment(\.spacingConfiguration) private var spacingConfiguration + + @State private var imageSizes: [String: CGSize] = [:] + @State private var height: CGFloat? + + private let images: [Image] + + init(_ imageSet: ImageSet) { + images = imageSet.images.map { + var result = $0 + result.size = imageSet.supportedImageSize + return result + } + } + + var body: some View { + GeometryReader { proxy in + makeBody(for: proxy.size) + } + .frame(height: height) + .onCollectedSizesChange { collectedSizes in + imageSizes = collectedSizes + } + .onPreferenceChange(ContentSizeKey.self) { contentSize in + height = contentSize?.height + } + } + + private func makeBody(for proposedSize: CGSize) -> some View { + var offsets: [String: CGSize] = [:] + var layout = FlowLayout( + proposedSize: proposedSize, + horizontalSpacing: spacingConfiguration.default, + verticalSpacing: spacingConfiguration.default + ) + + for image in images { + guard let size = imageSizes[image.id] else { + continue + } + let frame = layout.addElementWithSize(size) + offsets[image.id] = CGSize(width: frame.origin.x, height: frame.origin.y) + } + + return ZStack(alignment: .topLeading) { + ForEach(images, id: \.id) { image in + ImageView(image) + .fixedSize() + .collectSize(tag: image.id) + .offset(offsets[image.id] ?? .zero) + } + } + .preference(key: ContentSizeKey.self, value: layout.size) + } + } + + private extension ImageSet { + var supportedImageSize: ImageSize { + switch imageSize { + case .auto, .stretch: + return .medium + default: + return imageSize + } + } + } + +#endif diff --git a/Tests/AdaptiveCardUITests/Logic/AdaptiveCardImageURLsTests.swift b/Tests/AdaptiveCardUITests/Logic/AdaptiveCardImageURLsTests.swift index 247ff8d..c503e4f 100644 --- a/Tests/AdaptiveCardUITests/Logic/AdaptiveCardImageURLsTests.swift +++ b/Tests/AdaptiveCardUITests/Logic/AdaptiveCardImageURLsTests.swift @@ -32,6 +32,11 @@ final class AdaptiveCardImageURLsTests: XCTestCase { ] ) ), + .imageSet( + ImageSet(images: [ + Image(url: URL(string: "https://example.com/image4.png")!), + ]) + ), ], actions: [ .openURL( @@ -69,6 +74,7 @@ final class AdaptiveCardImageURLsTests: XCTestCase { URL(string: "https://example.com/actionIcon1.png")!, URL(string: "https://example.com/image2.png")!, URL(string: "https://example.com/columnBackground.png")!, + URL(string: "https://example.com/image4.png")!, URL(string: "https://example.com/actionIcon2.png")!, URL(string: "https://example.com/image3.png")!, URL(string: "https://example.com/containerBackground.png")!, diff --git a/Tests/AdaptiveCardUITests/Logic/AdaptiveCardToggleVisibilityTests.swift b/Tests/AdaptiveCardUITests/Logic/AdaptiveCardToggleVisibilityTests.swift index 84fea68..57122c0 100644 --- a/Tests/AdaptiveCardUITests/Logic/AdaptiveCardToggleVisibilityTests.swift +++ b/Tests/AdaptiveCardUITests/Logic/AdaptiveCardToggleVisibilityTests.swift @@ -201,4 +201,41 @@ final class AdaptiveCardToggleVisibilityTests: XCTestCase { // then XCTAssertEqual(expected, adaptiveCard) } + + func testImageSetItemToggleVisibilityUpdatesVisibility() { + // given + let targetElement = TargetElement(elementId: "imageToToggle") + var adaptiveCard = AdaptiveCard( + body: [ + .imageSet( + ImageSet( + id: "someImageSet", + images: [ + Image(id: "someId", url: URL(string: "https://example.com/image1.png")!), + Image(id: targetElement.elementId, isVisible: false, url: URL(string: "https://example.com/image2.png")!), + ] + ) + ), + ] + ) + let expected = AdaptiveCard( + body: [ + .imageSet( + ImageSet( + id: "someImageSet", + images: [ + Image(id: "someId", url: URL(string: "https://example.com/image1.png")!), + Image(id: targetElement.elementId, isVisible: true, url: URL(string: "https://example.com/image2.png")!), + ] + ) + ), + ] + ) + + // when + adaptiveCard.toggleVisibility(of: [targetElement]) + + // then + XCTAssertEqual(expected, adaptiveCard) + } } diff --git a/Tests/AdaptiveCardUITests/UI/ImageSetRenderingTests.swift b/Tests/AdaptiveCardUITests/UI/ImageSetRenderingTests.swift new file mode 100644 index 0000000..9f8ed93 --- /dev/null +++ b/Tests/AdaptiveCardUITests/UI/ImageSetRenderingTests.swift @@ -0,0 +1,23 @@ +#if os(iOS) && canImport(SwiftUI) + + import SnapshotTesting + import SwiftUI + import XCTest + + import AdaptiveCardUI + + @available(iOS 14.0, *) + final class ImageSetRenderingTests: XCTestCase { + func testImageSet() { + let view = AdaptiveCardView(url: fixtureURL("imageSet.json")) + .animation(nil) + .adaptiveCardConfiguration(.test) + + let vc = UIHostingController(rootView: view) + vc.view.frame = CGRect(x: 0, y: 0, width: 300, height: 260) + + assertSnapshot(matching: vc, as: .wait(for: 0.25, on: .image)) + } + } + +#endif diff --git a/Tests/AdaptiveCardUITests/UI/__Fixtures__/imageSet.json b/Tests/AdaptiveCardUITests/UI/__Fixtures__/imageSet.json new file mode 100644 index 0000000..0b343b5 --- /dev/null +++ b/Tests/AdaptiveCardUITests/UI/__Fixtures__/imageSet.json @@ -0,0 +1,40 @@ +{ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.0", + "body": [ + { + "type": "ImageSet", + "imageSize": "small", + "images": [ + { + "type": "Image", + "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAABGdBTUEAALGPC/xhBQAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAKKADAAQAAAABAAAAKAAAAABZLkSWAAABdElEQVRYCc2TCw6CMBBEEbwXeDNv5kE4C4mUMKTAtt0fFBMziu3um5fYNI6vcRyH8HYc2bw9h03T9F3nDV5zW69Bq7l+ntd7WnQDjOw18WerABfAyB543Cy6AFLGqGegl6QZkLCH/S4WzYA5U7nf0KKUJsCMPew1WzQBcgxxzqANlWpAhj3sM1lUA0rMSM6iFVIFKLCHPWqLKkCNEc2d0E4MqLBnsigG1JoIlJq7IkCDPbVFEaDGAMiQ0hlsQAd7YBT9o9mA0uagoVIyiwXoaA+8bIssQEljEJSSO7MIeIE9sLMsFgG5TbFVkpzZWcAL7aFH0WIWkNMQm7RZ2pEEvMEeOmUtJgFLzTDdI3O7SMAb7aFf0iIJmGuEid6Z2nkCrGAPXUmLJ8BUE0y5MqndO8CK9tD7ZHEHSDXAzbvyyLABPsAeHOwsboBHcpyukTHLAvgge/CxWVwAY2KcqJ1gegV785dfbSBqf9d1nxak1IHazwLbH4DVAnPf+eoYAAAAAElFTkSuQmCC" + }, + { + "type": "Image", + "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAABGdBTUEAALGPC/xhBQAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAKKADAAQAAAABAAAAKAAAAABZLkSWAAABdElEQVRYCc2TCw6CMBBEEbwXeDNv5kE4C4mUMKTAtt0fFBMziu3um5fYNI6vcRyH8HYc2bw9h03T9F3nDV5zW69Bq7l+ntd7WnQDjOw18WerABfAyB543Cy6AFLGqGegl6QZkLCH/S4WzYA5U7nf0KKUJsCMPew1WzQBcgxxzqANlWpAhj3sM1lUA0rMSM6iFVIFKLCHPWqLKkCNEc2d0E4MqLBnsigG1JoIlJq7IkCDPbVFEaDGAMiQ0hlsQAd7YBT9o9mA0uagoVIyiwXoaA+8bIssQEljEJSSO7MIeIE9sLMsFgG5TbFVkpzZWcAL7aFH0WIWkNMQm7RZ2pEEvMEeOmUtJgFLzTDdI3O7SMAb7aFf0iIJmGuEid6Z2nkCrGAPXUmLJ8BUE0y5MqndO8CK9tD7ZHEHSDXAzbvyyLABPsAeHOwsboBHcpyukTHLAvgge/CxWVwAY2KcqJ1gegV785dfbSBqf9d1nxak1IHazwLbH4DVAnPf+eoYAAAAAElFTkSuQmCC", + "imageSize": "large" + }, + { + "type": "Image", + "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAABGdBTUEAALGPC/xhBQAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAKKADAAQAAAABAAAAKAAAAABZLkSWAAABdElEQVRYCc2TCw6CMBBEEbwXeDNv5kE4C4mUMKTAtt0fFBMziu3um5fYNI6vcRyH8HYc2bw9h03T9F3nDV5zW69Bq7l+ntd7WnQDjOw18WerABfAyB543Cy6AFLGqGegl6QZkLCH/S4WzYA5U7nf0KKUJsCMPew1WzQBcgxxzqANlWpAhj3sM1lUA0rMSM6iFVIFKLCHPWqLKkCNEc2d0E4MqLBnsigG1JoIlJq7IkCDPbVFEaDGAMiQ0hlsQAd7YBT9o9mA0uagoVIyiwXoaA+8bIssQEljEJSSO7MIeIE9sLMsFgG5TbFVkpzZWcAL7aFH0WIWkNMQm7RZ2pEEvMEeOmUtJgFLzTDdI3O7SMAb7aFf0iIJmGuEid6Z2nkCrGAPXUmLJ8BUE0y5MqndO8CK9tD7ZHEHSDXAzbvyyLABPsAeHOwsboBHcpyukTHLAvgge/CxWVwAY2KcqJ1gegV785dfbSBqf9d1nxak1IHazwLbH4DVAnPf+eoYAAAAAElFTkSuQmCC", + "width": "80px", + "backgroundColor": "#982374" + }, + { + "type": "Image", + "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAABGdBTUEAALGPC/xhBQAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAKKADAAQAAAABAAAAKAAAAABZLkSWAAABdElEQVRYCc2TCw6CMBBEEbwXeDNv5kE4C4mUMKTAtt0fFBMziu3um5fYNI6vcRyH8HYc2bw9h03T9F3nDV5zW69Bq7l+ntd7WnQDjOw18WerABfAyB543Cy6AFLGqGegl6QZkLCH/S4WzYA5U7nf0KKUJsCMPew1WzQBcgxxzqANlWpAhj3sM1lUA0rMSM6iFVIFKLCHPWqLKkCNEc2d0E4MqLBnsigG1JoIlJq7IkCDPbVFEaDGAMiQ0hlsQAd7YBT9o9mA0uagoVIyiwXoaA+8bIssQEljEJSSO7MIeIE9sLMsFgG5TbFVkpzZWcAL7aFH0WIWkNMQm7RZ2pEEvMEeOmUtJgFLzTDdI3O7SMAb7aFf0iIJmGuEid6Z2nkCrGAPXUmLJ8BUE0y5MqndO8CK9tD7ZHEHSDXAzbvyyLABPsAeHOwsboBHcpyukTHLAvgge/CxWVwAY2KcqJ1gegV785dfbSBqf9d1nxak1IHazwLbH4DVAnPf+eoYAAAAAElFTkSuQmCC" + }, + { + "type": "Image", + "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAABGdBTUEAALGPC/xhBQAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAKKADAAQAAAABAAAAKAAAAABZLkSWAAABdElEQVRYCc2TCw6CMBBEEbwXeDNv5kE4C4mUMKTAtt0fFBMziu3um5fYNI6vcRyH8HYc2bw9h03T9F3nDV5zW69Bq7l+ntd7WnQDjOw18WerABfAyB543Cy6AFLGqGegl6QZkLCH/S4WzYA5U7nf0KKUJsCMPew1WzQBcgxxzqANlWpAhj3sM1lUA0rMSM6iFVIFKLCHPWqLKkCNEc2d0E4MqLBnsigG1JoIlJq7IkCDPbVFEaDGAMiQ0hlsQAd7YBT9o9mA0uagoVIyiwXoaA+8bIssQEljEJSSO7MIeIE9sLMsFgG5TbFVkpzZWcAL7aFH0WIWkNMQm7RZ2pEEvMEeOmUtJgFLzTDdI3O7SMAb7aFf0iIJmGuEid6Z2nkCrGAPXUmLJ8BUE0y5MqndO8CK9tD7ZHEHSDXAzbvyyLABPsAeHOwsboBHcpyukTHLAvgge/CxWVwAY2KcqJ1gegV785dfbSBqf9d1nxak1IHazwLbH4DVAnPf+eoYAAAAAElFTkSuQmCC" + }, + { + "type": "Image", + "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAABGdBTUEAALGPC/xhBQAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAKKADAAQAAAABAAAAKAAAAABZLkSWAAABdElEQVRYCc2TCw6CMBBEEbwXeDNv5kE4C4mUMKTAtt0fFBMziu3um5fYNI6vcRyH8HYc2bw9h03T9F3nDV5zW69Bq7l+ntd7WnQDjOw18WerABfAyB543Cy6AFLGqGegl6QZkLCH/S4WzYA5U7nf0KKUJsCMPew1WzQBcgxxzqANlWpAhj3sM1lUA0rMSM6iFVIFKLCHPWqLKkCNEc2d0E4MqLBnsigG1JoIlJq7IkCDPbVFEaDGAMiQ0hlsQAd7YBT9o9mA0uagoVIyiwXoaA+8bIssQEljEJSSO7MIeIE9sLMsFgG5TbFVkpzZWcAL7aFH0WIWkNMQm7RZ2pEEvMEeOmUtJgFLzTDdI3O7SMAb7aFf0iIJmGuEid6Z2nkCrGAPXUmLJ8BUE0y5MqndO8CK9tD7ZHEHSDXAzbvyyLABPsAeHOwsboBHcpyukTHLAvgge/CxWVwAY2KcqJ1gegV785dfbSBqf9d1nxak1IHazwLbH4DVAnPf+eoYAAAAAElFTkSuQmCC" + } + ] + } + ] +} diff --git a/Tests/AdaptiveCardUITests/UI/__Snapshots__/ImageSetRenderingTests/testImageSet.1.png b/Tests/AdaptiveCardUITests/UI/__Snapshots__/ImageSetRenderingTests/testImageSet.1.png new file mode 100644 index 0000000..3bf3bf3 Binary files /dev/null and b/Tests/AdaptiveCardUITests/UI/__Snapshots__/ImageSetRenderingTests/testImageSet.1.png differ