Skip to content

Commit

Permalink
Added blob pixel style
Browse files Browse the repository at this point in the history
  • Loading branch information
dagronf committed Jul 23, 2024
1 parent e3859a9 commit 1f988a0
Show file tree
Hide file tree
Showing 8 changed files with 239 additions and 36 deletions.
Binary file added Art/images/data_blob.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Art/paths.pcvd
Binary file not shown.
2 changes: 1 addition & 1 deletion DSF_QRCode.podspec
Original file line number Diff line number Diff line change
@@ -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' }
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,7 @@ however you can supply a `PixelShape` object to custom-draw the data. There are

| Preview | Name | Class | Description |
|---|---|---|---|
|<img src="./Art/images/data_blob.png" width="60"/> |"blob"|`QRCode.PixelShape.Blob`|A blobby style|
|<img src="./Art/images/data_circle.png" width="60"/> |"circle"|`QRCode.PixelShape.Circle`|A basic circle pixel|
|<img src="./Art/images/data_crt.png" width="60"/> | "crt" |`QRCode.PixelShape.CRT`| A CRT shape |
|<img src="./Art/images/data_curvePixel.png" width="60"/> |"curvePixel"|`QRCode.PixelShape.CurvePixel`|A pixel that curves to follow paths|
Expand Down
71 changes: 36 additions & 35 deletions Sources/QRCode/QRCode+Drawing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
Expand All @@ -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 {
Expand Down
182 changes: 182 additions & 0 deletions Sources/QRCode/styles/data/QRCodePixelShapeBlob.swift
Original file line number Diff line number Diff line change
@@ -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()
}
}
1 change: 1 addition & 0 deletions Sources/QRCode/styles/data/QRCodePixelShapeFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions Tests/QRCodeTests/QRCodeDocGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 1f988a0

Please sign in to comment.