import SwiftUI import WebKit /// A picture Markdown names, at its own size unless that's wider than the /// page. One that won't load says what it was of. struct MarkdownImage: View { let urls: [URL] let alt: String private enum Loaded { case bitmap(UIImage) case svg(Data, CGSize) } @State private var loaded: Loaded? @State private var failed = false var body: some View { Group { switch loaded { case .bitmap(let image): Image(uiImage: image).resizable().sized(image.size) case .svg(let data, let size): SVGView(data: data).sized(size) case nil: if failed { if !alt.isEmpty { Text(alt).foregroundStyle(.secondary) } } else { RoundedRectangle(cornerRadius: Radius.small).fill(Color.raised).frame(width: 120, height: 24) } } } .accessibilityLabel(alt) .task(id: urls) { await load() } } private func load() async { for url in urls { guard let data = try? await Client.shared.picture(url) else { continue } if let image = UIImage(data: data), image.size.width > 0 { loaded = .bitmap(image) return } if let size = SVGSize.of(data) { loaded = .svg(data, size) return } } failed = true } } private extension View { func sized(_ size: CGSize) -> some View { aspectRatio(size.width / size.height, contentMode: .fit).frame(maxWidth: size.width) } } /// What an SVG says its size is, from its width and height, or its viewBox. enum SVGSize { static func of(_ data: Data) -> CGSize? { guard let text = String(data: data.prefix(4096), encoding: .utf8), let open = text.firstMatch(of: #/]*>/#)?.output else { return nil } let tag = String(open) func number(_ name: String) -> CGFloat? { guard let found = try? Regex("\\s\(name)=[\"']([0-9.]+)(px)?[\"']").firstMatch(in: tag), let value = found[1].substring else { return nil } return Double(value).map { CGFloat($0) } } if let w = number("width"), let h = number("height"), w > 0, h > 0 { return CGSize(width: w, height: h) } if let box = tag.firstMatch(of: #/viewBox=["'][-0-9.]+[\s,]+[-0-9.]+[\s,]+([0-9.]+)[\s,]+([0-9.]+)["']/#), let w = Double(box.1), let h = Double(box.2), w > 0, h > 0 { return CGSize(width: w, height: h) } return CGSize(width: 240, height: 240) } } /// An SVG drawn by WebKit as an image, in which it can run no script and fetch nothing. private struct SVGView: UIViewRepresentable { let data: Data func makeUIView(context: Context) -> WKWebView { let view = WKWebView(frame: .zero) view.isOpaque = false view.backgroundColor = .clear view.scrollView.isScrollEnabled = false view.isUserInteractionEnabled = false view.loadHTMLString( "", baseURL: nil) return view } func updateUIView(_ view: WKWebView, context: Context) {} } /// Pictures side by side, wrapping as words do, as a README's row of badges. struct FlowRow: Layout { var spacing: CGFloat = 6 private func rows(_ width: CGFloat, _ subviews: Subviews) -> (CGSize, [(Int, CGPoint, CGSize)]) { var placed: [(Int, CGPoint, CGSize)] = [] var x: CGFloat = 0, y: CGFloat = 0, tallest: CGFloat = 0, widest: CGFloat = 0 for (i, view) in subviews.enumerated() { let size = view.sizeThatFits(ProposedViewSize(width: width, height: nil)) if x > 0, x + size.width > width { x = 0 y += tallest + spacing tallest = 0 } placed.append((i, CGPoint(x: x, y: y), size)) x += size.width + spacing tallest = max(tallest, size.height) widest = max(widest, x - spacing) } return (CGSize(width: widest, height: y + tallest), placed) } func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { rows(proposal.width ?? .infinity, subviews).0 } func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { for (i, at, size) in rows(bounds.width, subviews).1 { subviews[i].place(at: CGPoint(x: bounds.minX + at.x, y: bounds.minY + at.y), proposal: ProposedViewSize(size)) } } }