diff --git a/Art/images/data_blob.png b/Art/images/data_blob.png new file mode 100644 index 0000000..ea958c5 Binary files /dev/null and b/Art/images/data_blob.png differ diff --git a/Art/paths.pcvd b/Art/paths.pcvd index 1eda498..39c7fb9 100644 Binary files a/Art/paths.pcvd and b/Art/paths.pcvd differ diff --git a/DSF_QRCode.podspec b/DSF_QRCode.podspec index fbc6b16..f533457 100644 --- a/DSF_QRCode.podspec +++ b/DSF_QRCode.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = 'DSF_QRCode' -s.version = '20.2.0' +s.version = '20.3.0' s.summary = 'A simple drop-in macOS/iOS/tvOS/watchOS QR Code generator view for Swift, Objective-C and SwiftUI.' s.homepage = 'https://github.com/dagronf/QRCode' s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/README.md b/README.md index 6a7607e..956502f 100644 --- a/README.md +++ b/README.md @@ -436,6 +436,7 @@ however you can supply a `PixelShape` object to custom-draw the data. There are | Preview | Name | Class | Description | |---|---|---|---| +| |"blob"|`QRCode.PixelShape.Blob`|A blobby style| | |"circle"|`QRCode.PixelShape.Circle`|A basic circle pixel| | | "crt" |`QRCode.PixelShape.CRT`| A CRT shape | | |"curvePixel"|`QRCode.PixelShape.CurvePixel`|A pixel that curves to follow paths| diff --git a/Sources/QRCode/QRCode+Drawing.swift b/Sources/QRCode/QRCode+Drawing.swift index 7c0594b..cac932f 100644 --- a/Sources/QRCode/QRCode+Drawing.swift +++ b/Sources/QRCode/QRCode+Drawing.swift @@ -149,41 +149,9 @@ public extension QRCode { ctx.usingGState { context in style.actualPupilStyle.fill(ctx: context, rect: finalRect, path: eyePupilPath) } - - // Now, the 'on' pixels background - if let c = design.style.onPixelsBackground { - let design: QRCode.Design = { - let d = QRCode.Design() - d.shape.onPixels = QRCode.PixelShape.Square() - d.style.onPixels = QRCode.FillStyle.Solid(c) - return d - }() - - let qrPath2 = self.path( - finalRect.size, - components: .onPixels, - shape: design.shape, - logoTemplate: logoTemplate, - additionalQuietSpace: additionalQuietSpace - ) - ctx.usingGState { context in - design.style.onPixels.fill(ctx: context, rect: finalRect, path: qrPath2) - } - } - - // Now, the 'on' pixels - let qrPath = self.path( - finalRect.size, - components: .onPixels, - shape: design.shape, - logoTemplate: logoTemplate, - additionalQuietSpace: additionalQuietSpace - ) - ctx.usingGState { context in - style.onPixels.fill(ctx: context, rect: finalRect, path: qrPath) - } - + // The 'off' pixels ONLY IF the user specifies both a offPixels shape AND an offPixels style. + // Draw this first so it's guaranteed to be drawn behind the on-pixels if let s = style.offPixels, let _ = design.shape.offPixels { // Draw the 'off' pixels background IF the caller has set a color if let c = design.style.offPixelsBackground { @@ -204,7 +172,7 @@ public extension QRCode { design.style.offPixels?.fill(ctx: context, rect: finalRect, path: qrPath2) } } - + let qrPath = self.path( finalRect.size, components: .offPixels, @@ -216,6 +184,39 @@ public extension QRCode { s.fill(ctx: context, rect: finalRect, path: qrPath) } } + + // Now, the 'on' pixels background + if let c = design.style.onPixelsBackground { + let design: QRCode.Design = { + let d = QRCode.Design() + d.shape.onPixels = QRCode.PixelShape.Square() + d.style.onPixels = QRCode.FillStyle.Solid(c) + return d + }() + + let qrPath2 = self.path( + finalRect.size, + components: .onPixels, + shape: design.shape, + logoTemplate: logoTemplate, + additionalQuietSpace: additionalQuietSpace + ) + ctx.usingGState { context in + design.style.onPixels.fill(ctx: context, rect: finalRect, path: qrPath2) + } + } + + // Now, the 'on' pixels + let qrPath = self.path( + finalRect.size, + components: .onPixels, + shape: design.shape, + logoTemplate: logoTemplate, + additionalQuietSpace: additionalQuietSpace + ) + ctx.usingGState { context in + style.onPixels.fill(ctx: context, rect: finalRect, path: qrPath) + } } if let logoTemplate = logoTemplate { diff --git a/Sources/QRCode/styles/data/QRCodePixelShapeBlob.swift b/Sources/QRCode/styles/data/QRCodePixelShapeBlob.swift new file mode 100644 index 0000000..5e64f95 --- /dev/null +++ b/Sources/QRCode/styles/data/QRCodePixelShapeBlob.swift @@ -0,0 +1,182 @@ +// +// QRCodePixelShapeBlob.swift +// +// Copyright © 2024 Darren Ford. All rights reserved. +// +// MIT license +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial +// portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +// OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation +import CoreGraphics + +public extension QRCode.PixelShape { + /// A generator for a rounded path that links diagonal pixels with a 'sticky' region + @objc(QRCodePixelShapeBlob) class Blob: NSObject, QRCodePixelShapeGenerator { + /// The generator name + @objc public static let Name: String = "blob" + /// The generator title + @objc public static var Title: String { "Blob" } + /// Create an instance of this path generator with the specified settings + @objc public static func Create(_ settings: [String: Any]?) -> any QRCodePixelShapeGenerator { Blob() } + + /// Make a copy of the object + @objc public func copyShape() -> any QRCodePixelShapeGenerator { Blob() } + } +} + +public extension QRCode.PixelShape.Blob { + /// Generate a CGPath from the matrix contents + /// - Parameters: + /// - matrix: The matrix to generate + /// - size: The size of the resulting CGPath + /// - Returns: A path + @objc func generatePath(from matrix: BoolMatrix, size: CGSize) -> CGPath { + let dx = size.width / CGFloat(matrix.dimension) + let dy = size.height / CGFloat(matrix.dimension) + let dm = min(dx, dy) + + let xoff = (size.width - (CGFloat(matrix.dimension) * dm)) / 2.0 + let yoff = (size.height - (CGFloat(matrix.dimension) * dm)) / 2.0 + + // The scale required to convert our template paths to output path size + let w = QRCode.PixelShape.RoundedPath.DefaultSize.width + let scaleTransform = CGAffineTransform(scaleX: dm / w, y: dm / w) + + let path = CGMutablePath() + + let blobbyness = CGSize(dimension: 3) + + for row in 0 ..< matrix.dimension { + for col in 0 ..< matrix.dimension { + let translate = CGAffineTransform(translationX: CGFloat(col) * dm + xoff, y: CGFloat(row) * dm + yoff) + let ne = Neighbours(matrix: matrix, row: row, col: col) + if matrix[row, col] == false { + // Attach inner corners if needed + if ne.leading, ne.top { + path.addPath( + Self.templateRoundBottomLeft, + transform: scaleTransform.concatenating(translate) + ) + } + if ne.trailing, ne.top { + path.addPath( + Self.templateRoundBottomRight, + transform: scaleTransform.concatenating(translate) + ) + } + if ne.leading, ne.bottom { + path.addPath( + Self.templateRoundTopLeft, + transform: scaleTransform.concatenating(translate) + ) + } + if ne.trailing, ne.bottom { + path.addPath( + Self.templateRoundTopRight, + transform: scaleTransform.concatenating(translate) + ) + } + } + else { + var tl = CGSize(width: 0, height: 0) + var tr = CGSize(width: 0, height: 0) + var bl = CGSize(width: 0, height: 0) + var br = CGSize(width: 0, height: 0) + + if !ne.leading, !ne.top, !ne.topLeading { + tl = blobbyness + } + if !ne.leading, !ne.bottom, !ne.bottomLeading { + bl = blobbyness + } + if !ne.trailing, !ne.top, !ne.topTrailing { + tr = blobbyness + } + if !ne.trailing, !ne.bottom, !ne.bottomTrailing { + br = blobbyness + } + + let pth = CGPath.RoundedRect( + rect: CGRect(x: 0, y: 0, width: 10, height: 10), + topLeftRadius: tl, + topRightRadius: tr, + bottomLeftRadius: bl, + bottomRightRadius: br + ) + path.addPath(pth, transform: scaleTransform.concatenating(translate)) + } + } + } + return path + } +} + +// MARK: - Settings + +public extension QRCode.PixelShape.Blob { + /// Does the shape generator support setting values for a particular key? + @objc func supportsSettingValue(forKey key: String) -> Bool { false } + /// Returns a storable representation of the shape handler + @objc func settings() -> [String: Any] { return [:] } + /// Set a configuration value for a particular setting string + @objc func setSettingValue(_ value: Any?, forKey key: String) -> Bool { false } +} + +// MARK: - Shapes + +// Inner corner templates + +private extension QRCode.PixelShape.Blob { + static let templateRoundTopLeft = + CGPath.make { + $0.move(to: CGPoint(x: 0, y: 5)) + $0.curve(to: CGPoint(x: 1.1, y: 8.8), controlPoint1: CGPoint(x: 0, y: 5), controlPoint2: CGPoint(x: 0, y: 7.6)) + $0.curve(to: CGPoint(x: 5, y: 10), controlPoint1: CGPoint(x: 2.2, y: 10), controlPoint2: CGPoint(x: 5, y: 10)) + $0.line(to: CGPoint(x: 0, y: 10)) + $0.line(to: CGPoint(x: 0, y: 5)) + $0.closeSubpath() + } + + static let templateRoundTopRight = + CGPath.make { + $0.move(to: CGPoint(x: 10, y: 5)) + $0.curve(to: CGPoint(x: 8.9, y: 8.8), controlPoint1: CGPoint(x: 10, y: 5), controlPoint2: CGPoint(x: 10, y: 7.6)) + $0.curve(to: CGPoint(x: 5, y: 10), controlPoint1: CGPoint(x: 7.8, y: 10), controlPoint2: CGPoint(x: 5, y: 10)) + $0.line(to: CGPoint(x: 10, y: 10)) + $0.line(to: CGPoint(x: 10, y: 5)) + $0.closeSubpath() + } + + static let templateRoundBottomLeft = + CGPath.make { + $0.move(to: CGPoint(x: 0, y: 5)) + $0.curve(to: CGPoint(x: 1.1, y: 1.2), controlPoint1: CGPoint(x: 0, y: 5), controlPoint2: CGPoint(x: 0, y: 2.4)) + $0.curve(to: CGPoint(x: 5, y: 0), controlPoint1: CGPoint(x: 2.2, y: 0), controlPoint2: CGPoint(x: 5, y: 0)) + $0.line(to: CGPoint(x: 0, y: 0)) + $0.line(to: CGPoint(x: 0, y: 5)) + $0.closeSubpath() + } + + static let templateRoundBottomRight = + CGPath.make { + $0.move(to: CGPoint(x: 10, y: 5)) + $0.curve(to: CGPoint(x: 8.9, y: 1.2), controlPoint1: CGPoint(x: 10, y: 5), controlPoint2: CGPoint(x: 10, y: 2.4)) + $0.curve(to: CGPoint(x: 5, y: 0), controlPoint1: CGPoint(x: 7.8, y: 0), controlPoint2: CGPoint(x: 5, y: 0)) + $0.line(to: CGPoint(x: 10, y: 0)) + $0.line(to: CGPoint(x: 10, y: 5)) + $0.closeSubpath() + } +} diff --git a/Sources/QRCode/styles/data/QRCodePixelShapeFactory.swift b/Sources/QRCode/styles/data/QRCodePixelShapeFactory.swift index 9d9ecea..1ac1da9 100644 --- a/Sources/QRCode/styles/data/QRCodePixelShapeFactory.swift +++ b/Sources/QRCode/styles/data/QRCodePixelShapeFactory.swift @@ -79,6 +79,7 @@ import Foundation QRCode.PixelShape.Star.self, QRCode.PixelShape.Shiny.self, QRCode.PixelShape.CRT.self, + QRCode.PixelShape.Blob.self, ].sorted(by: { a, b in a.Title < b.Title }) /// The default matrix to use when generating pixel sample images diff --git a/Tests/QRCodeTests/QRCodeDocGenerator.swift b/Tests/QRCodeTests/QRCodeDocGenerator.swift index 476c315..4856347 100644 --- a/Tests/QRCodeTests/QRCodeDocGenerator.swift +++ b/Tests/QRCodeTests/QRCodeDocGenerator.swift @@ -974,6 +974,24 @@ final class QRCodeDocGeneratorTests: XCTestCase { return ("crt-style-shapes", doc) }(), + // ------------------- + try { + let doc = try QRCode.Document(utf8String: "Blobby pixel style", errorCorrection: .medium) + doc.design.shape.eye = QRCode.EyeShape.RoundedRect() + doc.design.shape.onPixels = QRCode.PixelShape.Blob() + doc.design.style.onPixels = QRCode.FillStyle.LinearGradient( + try DSFGradient(pins: [ + DSFGradient.Pin(CGColor.RGBA(1, 0.589, 0, 1), 0), + DSFGradient.Pin(CGColor.RGBA(1, 0, 0.3, 1), 1), + ]), + startPoint: CGPoint(x: 0, y: 1), + endPoint: CGPoint(x: 0, y: 0) + ) + doc.design.shape.offPixels = QRCode.PixelShape.Circle(insetFraction: 0.3) + doc.design.style.offPixels = QRCode.FillStyle.Solid(0, 0, 0, alpha: 0.1) + return ("blobby-pixel-style", doc) + }(), + // ------------------- try { let doc = try QRCode.Document(utf8String: "QRCode stylish design with quiet space - landscape", errorCorrection: .quantize)