diff --git a/Examples/Basic.xcodeproj/project.pbxproj b/Examples/Basic.xcodeproj/project.pbxproj index 3c5a33a..813eb65 100644 --- a/Examples/Basic.xcodeproj/project.pbxproj +++ b/Examples/Basic.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -312,6 +312,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = C8TWBM2E6Q; INFOPLIST_FILE = Sources/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -331,6 +332,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = C8TWBM2E6Q; INFOPLIST_FILE = Sources/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Examples/Sources/ViewController.swift b/Examples/Sources/ViewController.swift index 83cc130..3b15342 100644 --- a/Examples/Sources/ViewController.swift +++ b/Examples/Sources/ViewController.swift @@ -65,7 +65,7 @@ class ViewController: UIViewController { override func loadView() { let imageView = UIImageView(frame: UIScreen.main.bounds) - imageView.image = SVG(named: "gradient-stroke.svg", in: .samples)?.rasterize() + imageView.image = SVG(named: "shapes.svg", in: .samples)?.rasterize() imageView.contentMode = .scaleAspectFit imageView.backgroundColor = .white self.view = imageView diff --git a/Samples.bundle/base64-image.svg b/Samples.bundle/base64-image.svg new file mode 100644 index 0000000..f3da110 --- /dev/null +++ b/Samples.bundle/base64-image.svg @@ -0,0 +1,8 @@ + + + Plugin Icon + + + + + \ No newline at end of file diff --git a/Samples.bundle/base64.svg b/Samples.bundle/base64.svg index 69e2958..d3e325f 100644 --- a/Samples.bundle/base64.svg +++ b/Samples.bundle/base64.svg @@ -2,6 +2,7 @@ - + width="100" height="46"> + + \ No newline at end of file diff --git a/SwiftDraw/DOM.Image.swift b/SwiftDraw/DOM.Image.swift index f12d92f..95acba4 100644 --- a/SwiftDraw/DOM.Image.swift +++ b/SwiftDraw/DOM.Image.swift @@ -31,16 +31,14 @@ extension DOM { final class Image: GraphicsElement { var href: URL - var width: Coordinate - var height: Coordinate - + var width: Coordinate? + var height: Coordinate? + var x: Coordinate? var y: Coordinate? - init(href: URL, width: Coordinate, height: Coordinate) { + init(href: URL) { self.href = href - self.width = width - self.height = height super.init() } } diff --git a/SwiftDraw/LayerTree.Builder.Layer.swift b/SwiftDraw/LayerTree.Builder.Layer.swift index 5f3cbb2..7f83f9d 100644 --- a/SwiftDraw/LayerTree.Builder.Layer.swift +++ b/SwiftDraw/LayerTree.Builder.Layer.swift @@ -71,9 +71,15 @@ extension LayerTree.Builder { static func makeImageContents(from image: DOM.Image) throws -> LayerTree.Layer.Contents { guard let decoded = image.href.decodedData, - let im = LayerTree.Image(mimeType: decoded.mimeType, data: decoded.data) else { + var im = LayerTree.Image(mimeType: decoded.mimeType, data: decoded.data) else { throw LayerTree.Error.invalid("Cannot decode image") } + + im.origin.x = LayerTree.Float(image.x ?? 0) + im.origin.y = LayerTree.Float(image.y ?? 0) + im.width = image.width.map { LayerTree.Float($0) } + im.height = image.height.map { LayerTree.Float($0) } + return .image(im) } } diff --git a/SwiftDraw/LayerTree.CommandGenerator.swift b/SwiftDraw/LayerTree.CommandGenerator.swift index 5bda210..8496922 100644 --- a/SwiftDraw/LayerTree.CommandGenerator.swift +++ b/SwiftDraw/LayerTree.CommandGenerator.swift @@ -243,7 +243,31 @@ extension LayerTree { func renderCommands(for image: Image) -> [RendererCommand] { guard let renderImage = provider.createImage(from: image) else { return [] } - return [.draw(image: renderImage)] + let size = provider.createSize(from: renderImage) + guard size.width > 0 && size.height > 0 else { return [] } + + let frame = makeImageFrame(for: image, bitmapSize: size) + let rect = provider.createRect(from: frame) + return [.draw(image: renderImage, in: rect)] + } + + func makeImageFrame(for image: Image, bitmapSize: LayerTree.Size) -> LayerTree.Rect { + var frame = LayerTree.Rect( + x: image.origin.x, + y: image.origin.y, + width: image.width ?? bitmapSize.width, + height: image.height ?? bitmapSize.height + ) + + let aspectRatio = bitmapSize.width / bitmapSize.height + + if let height = image.height, image.width == nil { + frame.size.width = height * aspectRatio + } + if let width = image.width, image.height == nil { + frame.size.height = width / aspectRatio + } + return frame } func renderCommands(for text: String, at point: Point, attributes: TextAttributes, colorConverter: ColorConverter = DefaultColorConverter()) -> [RendererCommand] { diff --git a/SwiftDraw/LayerTree.Image.swift b/SwiftDraw/LayerTree.Image.swift index a98e1ce..904c297 100644 --- a/SwiftDraw/LayerTree.Image.swift +++ b/SwiftDraw/LayerTree.Image.swift @@ -32,23 +32,35 @@ import Foundation extension LayerTree { - enum Image: Equatable { - case jpeg(data: Data) - case png(data: Data) - - init?(mimeType: String, data: Data) { - guard data.count > 0 else { return nil } - - switch mimeType { - case "image/png": - self = .png(data: data) - case "image/jpeg": - self = .jpeg(data: data) - case "image/jpg": - self = .jpeg(data: data) - default: - return nil - } + struct Image: Equatable { + + var bitmap: Bitmap + var origin: Point = .zero + var width: LayerTree.Float? + var height: LayerTree.Float? + + enum Bitmap: Equatable { + case jpeg(Data) + case png(Data) + } + + init(bitmap: Bitmap) { + self.bitmap = bitmap + } + + init?(mimeType: String, data: Data) { + guard data.count > 0 else { return nil } + + switch mimeType { + case "image/png": + self.bitmap = .png(data) + case "image/jpeg": + self.bitmap = .jpeg(data) + case "image/jpg": + self.bitmap = .jpeg(data) + default: + return nil + } + } } - } } diff --git a/SwiftDraw/Parser.XML.Image.swift b/SwiftDraw/Parser.XML.Image.swift index be78bbd..37f8dfd 100644 --- a/SwiftDraw/Parser.XML.Image.swift +++ b/SwiftDraw/Parser.XML.Image.swift @@ -33,13 +33,13 @@ extension XMLParser { func parseImage(_ att: AttributeParser) throws -> DOM.Image { let href: DOM.URL = try att.parseUrl("xlink:href") - let width: DOM.Coordinate = try att.parseCoordinate("width") - let height: DOM.Coordinate = try att.parseCoordinate("height") - - let use = DOM.Image(href: href, width: width, height: height) - use.x = try att.parseCoordinate("x") - use.y = try att.parseCoordinate("y") - - return use + + let image = DOM.Image(href: href) + image.x = try att.parseCoordinate("x") + image.y = try att.parseCoordinate("y") + image.width = try att.parseCoordinate("width") + image.height = try att.parseCoordinate("height") + + return image } } diff --git a/SwiftDraw/Renderer.CGText.swift b/SwiftDraw/Renderer.CGText.swift index 209f83b..bf1f06b 100644 --- a/SwiftDraw/Renderer.CGText.swift +++ b/SwiftDraw/Renderer.CGText.swift @@ -209,7 +209,11 @@ struct CGTextProvider: RendererTypeProvider { func createImage(from image: LayerTree.Image) -> LayerTree.Image? { return image } - + + func createSize(from image: LayerTree.Image) -> LayerTree.Size { + LayerTree.Size(image.width ?? 0, image.height ?? 0) + } + func getBounds(from shape: LayerTree.Shape) -> LayerTree.Rect { #if canImport(CoreGraphics) return CGProvider().getBounds(from: shape) @@ -556,7 +560,7 @@ public final class CGTextRenderer: Renderer { } } - func draw(image: LayerTree.Image) { + func draw(image: LayerTree.Image, in rect: String) { lines.append("ctx.saveGState()") lines.append("ctx.translateBy(x: 0, y: image.height)") lines.append("ctx.scaleBy(x: 1, y: -1)") diff --git a/SwiftDraw/Renderer.CoreGraphics.swift b/SwiftDraw/Renderer.CoreGraphics.swift index e09ca19..f239ef7 100644 --- a/SwiftDraw/Renderer.CoreGraphics.swift +++ b/SwiftDraw/Renderer.CoreGraphics.swift @@ -268,14 +268,21 @@ struct CGProvider: RendererTypeProvider { } func createImage(from image: LayerTree.Image) -> CGImage? { - switch image { - case .jpeg(data: let d): + switch image.bitmap { + case .jpeg(let d): return CGImage.from(data: d) - case .png(data: let d): + case .png(let d): return CGImage.from(data: d) } } + func createSize(from image: CGImage) -> LayerTree.Size { + LayerTree.Size( + LayerTree.Float(image.width), + LayerTree.Float(image.height) + ) + } + func getBounds(from shape: LayerTree.Shape) -> LayerTree.Rect { let bounds = createPath(from: shape).boundingBoxOfPath return LayerTree.Rect(x: LayerTree.Float(bounds.origin.x), @@ -412,12 +419,13 @@ struct CGRenderer: Renderer { ctx.fillPath(using: rule) } - func draw(image: CGImage) { - let rect = CGRect(x: 0, y: 0, width: image.width, height: image.height) + func draw(image: CGImage, in rect: CGRect) { pushState() - translate(tx: 0, ty: rect.height) + translate(tx: rect.minX, ty: rect.maxY) scale(sx: 1, sy: -1) - ctx.draw(image, in: rect) + pushState() + ctx.draw(image, in: CGRect(origin: .zero, size: rect.size)) + popState() popState() } diff --git a/SwiftDraw/Renderer.LayerTree.swift b/SwiftDraw/Renderer.LayerTree.swift index 7a8af89..0258ab5 100644 --- a/SwiftDraw/Renderer.LayerTree.swift +++ b/SwiftDraw/Renderer.LayerTree.swift @@ -115,7 +115,14 @@ struct LayerTreeProvider: RendererTypeProvider { func createImage(from image: LayerTree.Image) -> LayerTree.Image? { return image } - + + func createSize(from image: LayerTree.Image) -> LayerTree.Size { + LayerTree.Size( + image.width ?? 0, + image.height ?? 0 + ) + } + func getBounds(from shape: LayerTree.Shape) -> LayerTree.Rect { return LayerTree.Rect(x: 0, y: 0, width: 0, height: 0) } diff --git a/SwiftDraw/Renderer.swift b/SwiftDraw/Renderer.swift index ff54a0a..cc75882 100644 --- a/SwiftDraw/Renderer.swift +++ b/SwiftDraw/Renderer.swift @@ -67,6 +67,7 @@ protocol RendererTypeProvider { func createLineCap(from cap: LayerTree.LineCap) -> Types.LineCap func createLineJoin(from join: LayerTree.LineJoin) -> Types.LineJoin func createImage(from image: LayerTree.Image) -> Types.Image? + func createSize(from image: Types.Image) -> LayerTree.Size func getBounds(from shape: LayerTree.Shape) -> LayerTree.Rect } @@ -99,7 +100,7 @@ protocol Renderer { func stroke(path: Types.Path) func clipStrokeOutline(path: Types.Path) func fill(path: Types.Path, rule: Types.FillRule) - func draw(image: Types.Image) + func draw(image: Types.Image, in rect: Types.Rect) func draw(linear gradient: Types.Gradient, from start: Types.Point, to end: Types.Point) func draw(radial gradient: Types.Gradient, startCenter: Types.Point, startRadius: Types.Float, endCenter: Types.Point, endRadius: Types.Float) } @@ -151,8 +152,8 @@ extension Renderer { clipStrokeOutline(path: p) case .fill(let p, let r): fill(path: p, rule: r) - case .draw(image: let i): - draw(image: i) + case .draw(image: let i, in: let r): + draw(image: i, in: r) case .drawLinearGradient(let g, let start, let end): draw(linear: g, from: start, to: end) case let .drawRadialGradient(g, startCenter, startRadius, endCenter, endRadius): @@ -192,7 +193,7 @@ enum RendererCommand { case clipStrokeOutline(Types.Path) case fill(Types.Path, rule: Types.FillRule) - case draw(image: Types.Image) + case draw(image: Types.Image, in: Types.Rect) case drawLinearGradient(Types.Gradient, from: Types.Point, to: Types.Point) case drawRadialGradient(Types.Gradient, startCenter: Types.Point, startRadius: Types.Float, endCenter: Types.Point, endRadius: Types.Float) diff --git a/SwiftDrawTests/LayerTree.Builder.LayerTests.swift b/SwiftDrawTests/LayerTree.Builder.LayerTests.swift index 2358e94..5d5e6b9 100644 --- a/SwiftDrawTests/LayerTree.Builder.LayerTests.swift +++ b/SwiftDrawTests/LayerTree.Builder.LayerTests.swift @@ -43,14 +43,12 @@ final class LayerTreeBuilderLayerTests: XCTestCase { } func testMakeImageContentsFromDOM() throws { - let image = DOM.Image(href: URL(maybeData: "data:image/png;base64,f00d")!, - width: 50, - height: 50) + let image = DOM.Image(href: URL(maybeData: "data:image/png;base64,f00d")!) let contents = try LayerTree.Builder.makeImageContents(from: image) XCTAssertEqual(contents, .image(.png(data: Data(base64Encoded: "f00d")!))) - let invalid = DOM.Image(href: URL(string: "aa")!, width: 10, height: 20) + let invalid = DOM.Image(href: URL(string: "aa")!) XCTAssertThrowsError(try LayerTree.Builder.makeImageContents(from: invalid)) } @@ -86,3 +84,14 @@ final class LayerTreeBuilderLayerTests: XCTestCase { XCTAssertEqual(l2.transform, [.translate(tx: 0, ty: 20)]) } } + +extension LayerTree.Image { + + static func png(data: Data) -> Self { + Self(bitmap: .png(data)) + } + + static func jpeg(data: Data) -> Self { + Self(bitmap: .jpeg(data)) + } +} diff --git a/SwiftDrawTests/MockRenderer.swift b/SwiftDrawTests/MockRenderer.swift index 03a039e..c0a4440 100644 --- a/SwiftDrawTests/MockRenderer.swift +++ b/SwiftDrawTests/MockRenderer.swift @@ -126,10 +126,10 @@ final class MockRenderer: Renderer { operations.append("fillPath") } - func draw(image: LayerTree.Image) { + func draw(image: LayerTree.Image, in rect: LayerTree.Rect) { operations.append("drawImage") } - + func draw(linear gradient: LayerTree.Gradient, from start: LayerTree.Point, to end: LayerTree.Point) { operations.append("drawLinearGradient") } diff --git a/SwiftDrawTests/RendererTests.swift b/SwiftDrawTests/RendererTests.swift index fdbe45b..126fa1c 100644 --- a/SwiftDrawTests/RendererTests.swift +++ b/SwiftDrawTests/RendererTests.swift @@ -59,7 +59,7 @@ final class RendererTests: XCTestCase { .clipStrokeOutline(.mock), .setAlpha(0.5), .setBlend(mode: .sourceIn), - .draw(image: .mock), + .draw(image: .mock, in: .zero), .drawLinearGradient(.mock, from: .zero, to: .zero), .drawRadialGradient(.mock, startCenter: .zero, startRadius: 0, endCenter: .zero, endRadius: 0) ])