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)