diff --git a/Art/images/data_crt.png b/Art/images/data_crt.png new file mode 100644 index 0000000..05db06d Binary files /dev/null and b/Art/images/data_crt.png differ diff --git a/Art/images/eye_crt.png b/Art/images/eye_crt.png new file mode 100644 index 0000000..b983a15 Binary files /dev/null and b/Art/images/eye_crt.png differ diff --git a/Art/images/pupil_crt.png b/Art/images/pupil_crt.png new file mode 100644 index 0000000..5dca1bd Binary files /dev/null and b/Art/images/pupil_crt.png differ diff --git a/Art/paths.pcvd b/Art/paths.pcvd index d1aa673..1eda498 100644 Binary files a/Art/paths.pcvd and b/Art/paths.pcvd differ diff --git a/DSF_QRCode.podspec b/DSF_QRCode.podspec index c7c653c..fbc6b16 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.1.0' +s.version = '20.2.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/Demo/Cocoapods Test/Podfile.lock b/Demo/Cocoapods Test/Podfile.lock index 46c63cf..067bf1c 100644 --- a/Demo/Cocoapods Test/Podfile.lock +++ b/Demo/Cocoapods Test/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - DSF_QRCode (20.0.0): + - DSF_QRCode (20.2.0): - SwiftImageReadWrite (~> 1.7.2) - SwiftQRCodeGenerator (~> 2.0.2) - SwiftImageReadWrite (1.7.2) @@ -18,7 +18,7 @@ EXTERNAL SOURCES: :path: "../../" SPEC CHECKSUMS: - DSF_QRCode: 6558f9b71f29ddbedd546e524f6d5e505841c5c6 + DSF_QRCode: 694d68eb0572f0a6d16c05ef5ffceae82d1554d5 SwiftImageReadWrite: ef6dfb4a78a0142a62c89cacf549623b26961fd3 SwiftQRCodeGenerator: cc02fed209335064d0b0dd61b2a0874b9bc6bd5a diff --git a/README.md b/README.md index 4a7451f..6a7607e 100644 --- a/README.md +++ b/README.md @@ -361,6 +361,7 @@ square, circle, rounded rectangle, and more. | | "barsVertical" |`QRCode.EyeShape.BarsVertical`| Simple rounded rect with three vertical bars as the pupil| | | "circle" |`QRCode.EyeShape.Circle`|Simple circle| | | "corneredPixels" |`QRCode.EyeShape.CorneredPixels`| A simple collection of pixels with configurable corner radius for the entire shape| +| | "crt" |`QRCode.EyeShape.CRT`| A CRT shape | | | "edges" |`QRCode.EyeShape.Edges`| Simple bordered bars with a configurable corner radius | | | "fireball" |`QRCode.EyeShape.Fireball` | A fireball shape | | | "leaf" |`QRCode.EyeShape.Leaf`|An eye that look like a leaf| @@ -394,6 +395,7 @@ If you don't override the pupil shape, it defaults to the eye shape's pupil shap | |"corneredPixels"|`QRCode.PupilShape.CorneredPixels`| A simple collection of pixels with configurable corner radius for the entire shape | | |"cross"|`QRCode.PupilShape.Cross`| A cross | | |"crossCurved"|`QRCode.PupilShape.CrossCurved`| A cross with curved insets | +| | "crt" |`QRCode.PupilShape.CRT`| A CRT shape | | |"hexagonLeaf"|`QRCode.PixelShape.HexagonLeaf` | A hexagonal leaf shape | | |"leaf"|`QRCode.PupilShape.Leaf`|An eye that look like a leaf| | |"pinch"|`QRCode.PupilShape.Pinch`| A square pinched in at the sides | @@ -435,6 +437,7 @@ however you can supply a `PixelShape` object to custom-draw the data. There are | Preview | Name | Class | Description | |---|---|---|---| | |"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| | |"flower"|`QRCode.PixelShape.Flower`|A 'flower' style| | |"horizontal"|`QRCode.PixelShape.Horizontal`|The pixels are horizonally joined to make continuous horizontal bars| @@ -1144,7 +1147,7 @@ OPTIONS: --all-pixel-shapes Print all the available pixel shapes. -d, --on-pixel-shape - The onPixels shape to use. Available shapes are circle, curvePixel, flower, horizontal, + The onPixels shape to use. Available shapes are crt, circle, curvePixel, flower, horizontal, pointy, razor, roundedEndIndent, roundedPath, roundedRect, sharp, shiny, square, squircle, star, vertical. -n, --on-pixel-inset-fraction @@ -1156,7 +1159,7 @@ OPTIONS: --all-eye-shapes Print all the available eye shapes. -e, --eye-shape - The eye shape to use. Available shapes are circle, corneredPixels, edges, fireball, + The eye shape to use. Available shapes are crt, circle, corneredPixels, edges, fireball, barsHorizontal, leaf, peacock, pinch, pixels, roundedOuter, roundedPointingIn, roundedPointingOut, roundedRect, shield, square, squircle, teardrop, ufo, usePixelShape, barsVertical. @@ -1165,7 +1168,7 @@ OPTIONS: --all-pupil-shapes Print all the available pupil shapes. -p, --pupil-shape - The pupil shape to use. Available shapes are blobby, circle, corneredPixels, cross, + The pupil shape to use. Available shapes are blobby, crt, circle, corneredPixels, cross, crossCurved, edges, hexagonLeaf, barsHorizontal, leaf, pinch, pixels, roundedOuter, roundedPointingIn, roundedPointingOut, roundedRect, seal, shield, square, barsHorizontalSquare, barsVerticalSquare, squircle, teardrop, ufo, usePixelShape, diff --git a/Sources/QRCode/QRCode+Drawing.swift b/Sources/QRCode/QRCode+Drawing.swift index 0e3df36..7c0594b 100644 --- a/Sources/QRCode/QRCode+Drawing.swift +++ b/Sources/QRCode/QRCode+Drawing.swift @@ -34,26 +34,26 @@ public extension QRCode { ) { // Only works with a 1:1 rect let sz = min(rect.width, rect.height) - + /// The size of each pixel in the output let additionalQuietSpacePixels = CGFloat(design.additionalQuietZonePixels) let dm: CGFloat = CGFloat(sz) / (CGFloat(self.cellDimension) + (2.0 * additionalQuietSpacePixels)) let additionalQuietSpace = dm * additionalQuietSpacePixels - + // Factor in the additional quiet space in the result guard (2 * additionalQuietSpace) < sz else { Swift.print("additionalQuietSpace too large") return } - + let xoff = additionalQuietSpace + (rect.width - (CGFloat(self.cellDimension) * dm)) / 2.0 let yoff = additionalQuietSpace + (rect.height - (CGFloat(self.cellDimension) * dm)) / 2.0 - + // This is the final position for the generated qr code, inset within the final image let finalRect = rect.insetBy(dx: additionalQuietSpace, dy: additionalQuietSpace) - + let style = design.style - + // // Special case handling for the 'use pixel shape' eye and pupil types // @@ -63,7 +63,7 @@ public extension QRCode { if let pupilPixelShape = design.shape.pupil as? QRCode.PupilShape.UsePixelShape { pupilPixelShape.pixelShape = design.shape.onPixels } - + // Fill the background first let backgroundStyle = style.background ?? QRCode.FillStyle.clear ctx.usingGState { context in @@ -77,13 +77,13 @@ public extension QRCode { } backgroundStyle.fill(ctx: context, rect: rect) } - + if design.shape.negatedOnPixelsOnly { var negatedMatrix = self.boolMatrix.inverted() if let logoTemplate = logoTemplate { negatedMatrix = logoTemplate.applyingMask(matrix: negatedMatrix, dimension: sz) } - + if let c = design.style.onPixelsBackground { let negatedPath = self.path( finalRect.size, @@ -96,7 +96,7 @@ public extension QRCode { QRCode.FillStyle.Solid(c).fill(ctx: context, rect: finalRect, path: negatedPath) } } - + let negatedPath = self.path( finalRect.size, components: .negative, @@ -104,7 +104,7 @@ public extension QRCode { logoTemplate: logoTemplate, additionalQuietSpace: additionalQuietSpace ) - + ctx.usingGState { context in style.onPixels.fill(ctx: context, rect: finalRect, path: negatedPath) } @@ -125,7 +125,7 @@ public extension QRCode { ctx.fillPath() } } - + // Draw the outer eye let eyeOuterPath = self.path( finalRect.size, @@ -137,7 +137,7 @@ public extension QRCode { ctx.usingGState { context in style.actualEyeStyle.fill(ctx: context, rect: finalRect, path: eyeOuterPath) } - + // Draw the eye 'pupil' let eyePupilPath = self.path( finalRect.size, @@ -149,7 +149,7 @@ 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 = { @@ -158,7 +158,7 @@ public extension QRCode { d.style.onPixels = QRCode.FillStyle.Solid(c) return d }() - + let qrPath2 = self.path( finalRect.size, components: .onPixels, @@ -170,7 +170,7 @@ public extension QRCode { design.style.onPixels.fill(ctx: context, rect: finalRect, path: qrPath2) } } - + // Now, the 'on' pixels let qrPath = self.path( finalRect.size, @@ -182,7 +182,7 @@ public extension QRCode { 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. if let s = style.offPixels, let _ = design.shape.offPixels { // Draw the 'off' pixels background IF the caller has set a color @@ -204,7 +204,7 @@ public extension QRCode { design.style.offPixels?.fill(ctx: context, rect: finalRect, path: qrPath2) } } - + let qrPath = self.path( finalRect.size, components: .offPixels, @@ -217,26 +217,26 @@ public extension QRCode { } } } - + if let logoTemplate = logoTemplate { - ctx.saveGState() - // Get the absolute rect within the generated image of the mask path - let absMask = logoTemplate.absolutePathForMaskPath( - dimension: min(finalRect.width, finalRect.height), - flipped: true - ) - - // logo drawing is flipped. - ctx.scaleBy(x: 1, y: -1) - ctx.translateBy(x: xoff - additionalQuietSpace, y: yoff - rect.height - additionalQuietSpace) - - // Clip to the mask path. - ctx.addPath(absMask) - ctx.clip() - - // Draw the logo image into the mask bounds - ctx.draw(logoTemplate.image, in: absMask.boundingBoxOfPath.insetBy(dx: logoTemplate.inset, dy: logoTemplate.inset)) - ctx.restoreGState() + ctx.usingGState { context in + // Get the absolute rect within the generated image of the mask path + let absMask = logoTemplate.absolutePathForMaskPath( + dimension: min(finalRect.width, finalRect.height), + flipped: true + ) + + // logo drawing is flipped. + context.scaleBy(x: 1, y: -1) + context.translateBy(x: xoff - additionalQuietSpace, y: yoff - rect.height - additionalQuietSpace) + + // Clip to the mask path. + context.addPath(absMask) + context.clip() + + // Draw the logo image into the mask bounds + context.draw(logoTemplate.image, in: absMask.boundingBoxOfPath.insetBy(dx: logoTemplate.inset, dy: logoTemplate.inset)) + } } } } diff --git a/Sources/QRCode/styles/data/QRCodePixelShapeFactory.swift b/Sources/QRCode/styles/data/QRCodePixelShapeFactory.swift index e071179..9d9ecea 100644 --- a/Sources/QRCode/styles/data/QRCodePixelShapeFactory.swift +++ b/Sources/QRCode/styles/data/QRCodePixelShapeFactory.swift @@ -78,6 +78,7 @@ import Foundation QRCode.PixelShape.Sharp.self, QRCode.PixelShape.Star.self, QRCode.PixelShape.Shiny.self, + QRCode.PixelShape.CRT.self, ].sorted(by: { a, b in a.Title < b.Title }) /// The default matrix to use when generating pixel sample images diff --git a/Sources/QRCode/styles/data/pixel/QRCodePixelShapeCRT.swift b/Sources/QRCode/styles/data/pixel/QRCodePixelShapeCRT.swift new file mode 100644 index 0000000..c59eb0e --- /dev/null +++ b/Sources/QRCode/styles/data/pixel/QRCodePixelShapeCRT.swift @@ -0,0 +1,154 @@ +// +// QRCodePixelShapeCRT.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 CoreGraphics +import Foundation + +public extension QRCode.PixelShape { + /// A squircle pixel shape + @objc(QRCodePixelShapeCRT) class CRT: NSObject, QRCodePixelShapeGenerator { + /// The generator name + @objc public static let Name: String = "crt" + /// The generator title + @objc public static var Title: String { "CRT" } + + /// The fractional inset for the pixel (0.0 -> 1.0) + @objc public var insetFraction: CGFloat { self.common.insetFraction } + /// If true, randomly sets the inset to create a "wobble" + @objc public var useRandomInset: Bool { self.common.useRandomInset } + /// The rotation for each pixel (0.0 -> 1.0) + @objc public var rotationFraction: CGFloat { self.common.rotationFraction } + /// If true, randomly chooses a rotation for each pixel + @objc public var useRandomRotation: Bool { self.common.useRandomRotation } + + /// Create + /// - Parameters: + /// - insetFraction: The inset between each pixel + /// - useRandomInset: If true, randomly sets the inset of each pixel within the range `0 ... insetFraction` + /// - rotationFraction: A rotation factor (0 -> 1) to apply to the rotation of each pixel + /// - useRandomRotation: If true, randomly sets the rotation of each pixel within the range `0 ... rotationFraction` + @objc public init( + insetFraction: CGFloat = 0, + useRandomInset: Bool = false, + rotationFraction: CGFloat = 0, + useRandomRotation: Bool = false + ) { + self.common = CommonPixelGenerator( + pixelType: .crt, + insetFraction: insetFraction, + useRandomInset: useRandomInset, + rotationFraction: rotationFraction, + useRandomRotation: useRandomRotation + ) + super.init() + } + + /// Create an instance of this path generator with the specified settings + @objc public static func Create(_ settings: [String: Any]?) -> any QRCodePixelShapeGenerator { + let insetFraction = DoubleValue(settings?[QRCode.SettingsKey.insetFraction, default: 0]) ?? 0 + let useRandomInset = BoolValue(settings?[QRCode.SettingsKey.useRandomInset]) ?? false + let rotationFraction = CGFloatValue(settings?[QRCode.SettingsKey.rotationFraction]) ?? 0.0 + let useRandomRotation = BoolValue(settings?[QRCode.SettingsKey.useRandomRotation]) ?? false + return CRT( + insetFraction: insetFraction, + useRandomInset: useRandomInset, + rotationFraction: rotationFraction, + useRandomRotation: useRandomRotation + ) + } + + /// Make a copy of the object + @objc public func copyShape() -> any QRCodePixelShapeGenerator { + return CRT( + insetFraction: self.common.insetFraction, + useRandomInset: self.common.useRandomInset, + rotationFraction: self.common.rotationFraction, + useRandomRotation: self.common.useRandomRotation + ) + } + + /// Generate a CGPath from the matrix contents + /// - Parameters: + /// - matrix: The matrix to generate + /// - size: The size of the resulting CGPath + /// - Returns: A path + public func generatePath(from matrix: BoolMatrix, size: CGSize) -> CGPath { + common.generatePath(from: matrix, size: size) + } + + // A 10x10 'pixel' representation of a squircle + internal static func crtPixel10x10() -> CGPath { + let crt_pixelPath = CGMutablePath() + crt_pixelPath.move(to: CGPoint(x: 0, y: 5)) + crt_pixelPath.curve(to: CGPoint(x: 0.6, y: 0.6), controlPoint1: CGPoint(x: 0, y: 2.5), controlPoint2: CGPoint(x: 0.6, y: 0.6)) + crt_pixelPath.curve(to: CGPoint(x: 5, y: 0), controlPoint1: CGPoint(x: 0.6, y: 0.6), controlPoint2: CGPoint(x: 2.5, y: 0)) + crt_pixelPath.curve(to: CGPoint(x: 9.4, y: 0.6), controlPoint1: CGPoint(x: 7.5, y: 0), controlPoint2: CGPoint(x: 9.4, y: 0.6)) + crt_pixelPath.curve(to: CGPoint(x: 10, y: 5), controlPoint1: CGPoint(x: 9.4, y: 0.6), controlPoint2: CGPoint(x: 10, y: 2.5)) + crt_pixelPath.curve(to: CGPoint(x: 9.4, y: 9.4), controlPoint1: CGPoint(x: 10, y: 7.5), controlPoint2: CGPoint(x: 9.4, y: 9.4)) + crt_pixelPath.curve(to: CGPoint(x: 5, y: 10), controlPoint1: CGPoint(x: 9.4, y: 9.4), controlPoint2: CGPoint(x: 7.5, y: 10)) + crt_pixelPath.curve(to: CGPoint(x: 0.6, y: 9.4), controlPoint1: CGPoint(x: 2.5, y: 10), controlPoint2: CGPoint(x: 0.6, y: 9.4)) + crt_pixelPath.curve(to: CGPoint(x: 0, y: 5), controlPoint1: CGPoint(x: 0.6, y: 9.4), controlPoint2: CGPoint(x: 0, y: 7.5)) + crt_pixelPath.close() + return crt_pixelPath + } + + private let common: CommonPixelGenerator + } +} + +// MARK: - Settings + +public extension QRCode.PixelShape.CRT { + /// Returns true if the shape supports setting a value for the specified key, false otherwise + @objc func supportsSettingValue(forKey key: String) -> Bool { + return key == QRCode.SettingsKey.insetFraction + || key == QRCode.SettingsKey.useRandomInset + || key == QRCode.SettingsKey.rotationFraction + || key == QRCode.SettingsKey.useRandomRotation + } + + /// Returns the current settings for the shape + @objc func settings() -> [String: Any] { + return [ + QRCode.SettingsKey.insetFraction: self.common.insetFraction, + QRCode.SettingsKey.useRandomInset: self.common.useRandomInset, + QRCode.SettingsKey.rotationFraction: self.common.rotationFraction, + QRCode.SettingsKey.useRandomRotation: self.common.useRandomRotation, + ] + } + + /// Set a configuration value for a particular setting string + @objc func setSettingValue(_ value: Any?, forKey key: String) -> Bool { + if key == QRCode.SettingsKey.insetFraction { + return self.common.setInsetFractionValue(value) + } + else if key == QRCode.SettingsKey.useRandomInset { + return self.common.setUsesRandomInset(value) + } + else if key == QRCode.SettingsKey.rotationFraction { + return self.common.setRotationFraction(value) + } + else if key == QRCode.SettingsKey.useRandomRotation { + return self.common.setUsesRandomRotation(value) + } + return false + } +} diff --git a/Sources/QRCode/styles/data/pixel/private/QRCodePixelShapePixel.swift b/Sources/QRCode/styles/data/pixel/private/QRCodePixelShapePixel.swift index 410e293..684a3b5 100644 --- a/Sources/QRCode/styles/data/pixel/private/QRCodePixelShapePixel.swift +++ b/Sources/QRCode/styles/data/pixel/private/QRCodePixelShapePixel.swift @@ -28,6 +28,7 @@ internal extension QRCode.PixelShape { enum PixelType: String, CaseIterable { case square case circle + case crt case roundedRect case squircle case sharp @@ -151,6 +152,17 @@ internal extension QRCode.PixelShape { let sq = Squircle.squircle10x10() path.addPath(sq, transform: transform) } + else if self.pixelType == .crt { + let transform = CGAffineTransform(scaleX: ri.width / 10, y: ri.width / 10) + .concatenating(CGAffineTransform( + translationX: xoff + (CGFloat(col) * dm) + insetValue, + y: yoff + (CGFloat(row) * dm) + insetValue + )) + .concatenating(rotateTransform) + + let sq = CRT.crtPixel10x10() + path.addPath(sq, transform: transform) + } else if self.pixelType == .sharp { let transform = CGAffineTransform(scaleX: ri.width / 10, y: ri.width / 10) .concatenating(CGAffineTransform( diff --git a/Sources/QRCode/styles/eye/QRCodeEyeShapeCRT.swift b/Sources/QRCode/styles/eye/QRCodeEyeShapeCRT.swift new file mode 100644 index 0000000..27d5d0e --- /dev/null +++ b/Sources/QRCode/styles/eye/QRCodeEyeShapeCRT.swift @@ -0,0 +1,91 @@ +// +// QRCodeEyeShapeCRT.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 CoreGraphics +import Foundation + +public extension QRCode.EyeShape { + /// A 'pinch' style eye design + @objc(QRCodeEyeShapeCrt) class CRT: NSObject, QRCodeEyeShapeGenerator { + @objc public static let Name = "crt" + @objc public static var Title: String { "CRT" } + @objc public static func Create(_ settings: [String: Any]?) -> any QRCodeEyeShapeGenerator { + return QRCode.EyeShape.CRT() + } + + @objc public func settings() -> [String: Any] { return [:] } + @objc public func supportsSettingValue(forKey key: String) -> Bool { false } + @objc public func setSettingValue(_ value: Any?, forKey key: String) -> Bool { false } + + /// Make a copy of the object + @objc public func copyShape() -> any QRCodeEyeShapeGenerator { + return Self.Create(self.settings()) + } + + private static let _defaultPupil = QRCode.PupilShape.CRT() + public func defaultPupil() -> any QRCodePupilShapeGenerator { Self._defaultPupil } + } +} + +public extension QRCode.EyeShape.CRT { + func eyePath() -> CGPath { + let crt_eyePath = CGMutablePath() + crt_eyePath.move(to: CGPoint(x: 45, y: 70)) + crt_eyePath.curve(to: CGPoint(x: 22.14, y: 67.86), controlPoint1: CGPoint(x: 32.5, y: 70), controlPoint2: CGPoint(x: 22.14, y: 67.86)) + crt_eyePath.curve(to: CGPoint(x: 20, y: 45), controlPoint1: CGPoint(x: 22.14, y: 67.86), controlPoint2: CGPoint(x: 20, y: 57.5)) + crt_eyePath.curve(to: CGPoint(x: 22.14, y: 22.14), controlPoint1: CGPoint(x: 20, y: 32.5), controlPoint2: CGPoint(x: 22.14, y: 22.14)) + crt_eyePath.curve(to: CGPoint(x: 45, y: 20), controlPoint1: CGPoint(x: 22.14, y: 22.14), controlPoint2: CGPoint(x: 32.5, y: 20)) + crt_eyePath.curve(to: CGPoint(x: 67.86, y: 22.14), controlPoint1: CGPoint(x: 57.5, y: 20), controlPoint2: CGPoint(x: 67.86, y: 22.14)) + crt_eyePath.curve(to: CGPoint(x: 70, y: 45), controlPoint1: CGPoint(x: 67.86, y: 22.14), controlPoint2: CGPoint(x: 70, y: 32.5)) + crt_eyePath.curve(to: CGPoint(x: 67.86, y: 67.86), controlPoint1: CGPoint(x: 70, y: 57.5), controlPoint2: CGPoint(x: 67.86, y: 67.86)) + crt_eyePath.curve(to: CGPoint(x: 45, y: 70), controlPoint1: CGPoint(x: 67.86, y: 67.86), controlPoint2: CGPoint(x: 57.5, y: 70)) + crt_eyePath.close() + crt_eyePath.move(to: CGPoint(x: 77, y: 77)) + crt_eyePath.curve(to: CGPoint(x: 80, y: 45), controlPoint1: CGPoint(x: 77, y: 77), controlPoint2: CGPoint(x: 80, y: 62.5)) + crt_eyePath.curve(to: CGPoint(x: 77, y: 13), controlPoint1: CGPoint(x: 80, y: 27.5), controlPoint2: CGPoint(x: 77, y: 13)) + crt_eyePath.curve(to: CGPoint(x: 45, y: 10), controlPoint1: CGPoint(x: 77, y: 13), controlPoint2: CGPoint(x: 62.5, y: 10)) + crt_eyePath.curve(to: CGPoint(x: 13, y: 13), controlPoint1: CGPoint(x: 27.5, y: 10), controlPoint2: CGPoint(x: 13, y: 13)) + crt_eyePath.curve(to: CGPoint(x: 10, y: 45), controlPoint1: CGPoint(x: 13, y: 13), controlPoint2: CGPoint(x: 10, y: 27.5)) + crt_eyePath.curve(to: CGPoint(x: 13, y: 77), controlPoint1: CGPoint(x: 10, y: 62.5), controlPoint2: CGPoint(x: 13, y: 77)) + crt_eyePath.curve(to: CGPoint(x: 45, y: 80), controlPoint1: CGPoint(x: 13, y: 77), controlPoint2: CGPoint(x: 27.5, y: 80)) + crt_eyePath.curve(to: CGPoint(x: 77, y: 77), controlPoint1: CGPoint(x: 62.5, y: 80), controlPoint2: CGPoint(x: 77, y: 77)) + crt_eyePath.line(to: CGPoint(x: 77, y: 77)) + crt_eyePath.close() + return crt_eyePath + } +} + +public extension QRCode.EyeShape.CRT { + func eyeBackgroundPath() -> CGPath { + let crt_backgroundPath = CGMutablePath() + crt_backgroundPath.move(to: CGPoint(x: 0, y: 45)) + crt_backgroundPath.curve(to: CGPoint(x: 3.86, y: 3.86), controlPoint1: CGPoint(x: 0, y: 22.5), controlPoint2: CGPoint(x: 3.86, y: 3.86)) + crt_backgroundPath.curve(to: CGPoint(x: 45, y: 0), controlPoint1: CGPoint(x: 3.86, y: 3.86), controlPoint2: CGPoint(x: 22.5, y: 0)) + crt_backgroundPath.curve(to: CGPoint(x: 86.14, y: 3.86), controlPoint1: CGPoint(x: 67.5, y: 0), controlPoint2: CGPoint(x: 86.14, y: 3.86)) + crt_backgroundPath.curve(to: CGPoint(x: 90, y: 45), controlPoint1: CGPoint(x: 86.14, y: 3.86), controlPoint2: CGPoint(x: 90, y: 22.5)) + crt_backgroundPath.curve(to: CGPoint(x: 86.14, y: 86.14), controlPoint1: CGPoint(x: 90, y: 67.5), controlPoint2: CGPoint(x: 86.14, y: 86.14)) + crt_backgroundPath.curve(to: CGPoint(x: 45, y: 90), controlPoint1: CGPoint(x: 86.14, y: 86.14), controlPoint2: CGPoint(x: 67.5, y: 90)) + crt_backgroundPath.curve(to: CGPoint(x: 3.86, y: 86.14), controlPoint1: CGPoint(x: 22.5, y: 90), controlPoint2: CGPoint(x: 3.86, y: 86.14)) + crt_backgroundPath.curve(to: CGPoint(x: 0, y: 45), controlPoint1: CGPoint(x: 3.86, y: 86.14), controlPoint2: CGPoint(x: 0, y: 67.5)) + crt_backgroundPath.close() + return crt_backgroundPath + } +} diff --git a/Sources/QRCode/styles/eye/QRCodeEyeShapeFactory.swift b/Sources/QRCode/styles/eye/QRCodeEyeShapeFactory.swift index 20ffd3a..5fe7a06 100644 --- a/Sources/QRCode/styles/eye/QRCodeEyeShapeFactory.swift +++ b/Sources/QRCode/styles/eye/QRCodeEyeShapeFactory.swift @@ -83,6 +83,7 @@ import Foundation QRCode.EyeShape.Peacock.self, QRCode.EyeShape.UFO.self, QRCode.EyeShape.Pinch.self, + QRCode.EyeShape.CRT.self, ].sorted(by: { a, b in a.Title < b.Title }) } diff --git a/Sources/QRCode/styles/pupil/QRCodePupilShapeCRT.swift b/Sources/QRCode/styles/pupil/QRCodePupilShapeCRT.swift new file mode 100644 index 0000000..18deebd --- /dev/null +++ b/Sources/QRCode/styles/pupil/QRCodePupilShapeCRT.swift @@ -0,0 +1,59 @@ +// +// QRCodePupilShapeCRT.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 CoreGraphics +import Foundation + +extension QRCode.PupilShape { + /// A pinch leaf pupil design + @objc(QRCodePupilShapeCRT) public class CRT: NSObject, QRCodePupilShapeGenerator { + /// Generator name + @objc public static var Name: String { "crt" } + /// Generator title + @objc public static var Title: String { "CRT" } + /// Create a hexagon leaf pupil shape, using the specified settings + @objc public static func Create(_ settings: [String : Any]?) -> any QRCodePupilShapeGenerator { CRT() } + /// Make a copy of the object + @objc public func copyShape() -> any QRCodePupilShapeGenerator { CRT() } + + @objc public func settings() -> [String: Any] { [:] } + @objc public func supportsSettingValue(forKey key: String) -> Bool { false } + @objc public func setSettingValue(_: Any?, forKey _: String) -> Bool { false } + + /// The pupil centered in the 90x90 square + @objc public func pupilPath() -> CGPath { _path() } + } +} + +private func _path() -> CGPath { + let crt_pupilPath = CGMutablePath() + crt_pupilPath.move(to: CGPoint(x: 30, y: 45)) + crt_pupilPath.curve(to: CGPoint(x: 31.29, y: 31.29), controlPoint1: CGPoint(x: 30, y: 37.5), controlPoint2: CGPoint(x: 31.29, y: 31.29)) + crt_pupilPath.curve(to: CGPoint(x: 45, y: 30), controlPoint1: CGPoint(x: 31.29, y: 31.29), controlPoint2: CGPoint(x: 37.5, y: 30)) + crt_pupilPath.curve(to: CGPoint(x: 58.71, y: 31.29), controlPoint1: CGPoint(x: 52.5, y: 30), controlPoint2: CGPoint(x: 58.71, y: 31.29)) + crt_pupilPath.curve(to: CGPoint(x: 60, y: 45), controlPoint1: CGPoint(x: 58.71, y: 31.29), controlPoint2: CGPoint(x: 60, y: 37.5)) + crt_pupilPath.curve(to: CGPoint(x: 58.71, y: 58.71), controlPoint1: CGPoint(x: 60, y: 52.5), controlPoint2: CGPoint(x: 58.71, y: 58.71)) + crt_pupilPath.curve(to: CGPoint(x: 45, y: 60), controlPoint1: CGPoint(x: 58.71, y: 58.71), controlPoint2: CGPoint(x: 52.5, y: 60)) + crt_pupilPath.curve(to: CGPoint(x: 31.29, y: 58.71), controlPoint1: CGPoint(x: 37.5, y: 60), controlPoint2: CGPoint(x: 31.29, y: 58.71)) + crt_pupilPath.curve(to: CGPoint(x: 30, y: 45), controlPoint1: CGPoint(x: 31.29, y: 58.71), controlPoint2: CGPoint(x: 30, y: 52.5)) + crt_pupilPath.close() + return crt_pupilPath +} diff --git a/Sources/QRCode/styles/pupil/QRCodePupilShapeFactory.swift b/Sources/QRCode/styles/pupil/QRCodePupilShapeFactory.swift index a79df40..7edb076 100644 --- a/Sources/QRCode/styles/pupil/QRCodePupilShapeFactory.swift +++ b/Sources/QRCode/styles/pupil/QRCodePupilShapeFactory.swift @@ -86,6 +86,7 @@ import CoreGraphics QRCode.PupilShape.Teardrop.self, QRCode.PupilShape.UFO.self, QRCode.PupilShape.Pinch.self, + QRCode.PupilShape.CRT.self, QRCode.PupilShape.SquareBarsHorizontal.self, QRCode.PupilShape.SquareBarsVertical.self, ].sorted(by: { a, b in a.Title < b.Title }) diff --git a/Sources/QRCode/styles/pupil/QRCodePupilShapePinch.swift b/Sources/QRCode/styles/pupil/QRCodePupilShapePinch.swift index ddf9506..e9e3e8b 100644 --- a/Sources/QRCode/styles/pupil/QRCodePupilShapePinch.swift +++ b/Sources/QRCode/styles/pupil/QRCodePupilShapePinch.swift @@ -24,7 +24,7 @@ import Foundation extension QRCode.PupilShape { /// A pinch leaf pupil design - @objc(QRCodePupilShapePinchh) public class Pinch: NSObject, QRCodePupilShapeGenerator { + @objc(QRCodePupilShapePinch) public class Pinch: NSObject, QRCodePupilShapeGenerator { /// Generator name @objc public static var Name: String { "pinch" } /// Generator title diff --git a/Tests/QRCodeTests/QRCodeDocGenerator.swift b/Tests/QRCodeTests/QRCodeDocGenerator.swift index 5bfd672..476c315 100644 --- a/Tests/QRCodeTests/QRCodeDocGenerator.swift +++ b/Tests/QRCodeTests/QRCodeDocGenerator.swift @@ -958,6 +958,22 @@ final class QRCodeDocGeneratorTests: XCTestCase { doc.design.style.offPixels = QRCode.FillStyle.Solid(gray: 0, alpha: 0.1) return ("design-landscape", doc) }(), + + // ------------------- + try { + let doc = try QRCode.build + .text("CRT-Style") + .errorCorrection(.high) + .onPixels.shape(QRCode.PixelShape.CRT(insetFraction: 0.1, rotationFraction: 0.1)) + .onPixels.style(QRCode.FillStyle.Solid(0, 0, 1)) + .offPixels.shape(QRCode.PixelShape.CRT(insetFraction: 0.4, rotationFraction: 0.3, useRandomRotation: true)) + .offPixels.style(QRCode.FillStyle.Solid(gray: 0, alpha: 0.1)) + .eye.shape(QRCode.EyeShape.CRT()) + .document + + return ("crt-style-shapes", doc) + }(), + // ------------------- try { let doc = try QRCode.Document(utf8String: "QRCode stylish design with quiet space - landscape", errorCorrection: .quantize)