15 Commits

Author SHA1 Message Date
Haik Aslanyan 63a209fb0c Update README.md 2020-06-23 21:41:58 +04:00
Haik Aslanyan 2fa33d0e88 updated readme 2020-05-09 13:30:36 +04:00
Haik Aslanyan 25d734d87d updated readme file 2020-05-07 21:19:32 +04:00
Haik Aslanyan a95fc1265c Merge pull request #7 from aslanyanhaik/develop
Develop
2020-05-07 21:11:29 +04:00
Haik Aslanyan e02e761c2e changed version number to 1.3.0 2020-05-07 21:10:48 +04:00
Haik Aslanyan 9e5ddf0082 updated example project 2020-05-07 21:10:04 +04:00
Haik Aslanyan aace0fd52d added ScanningMode parameter 2020-05-07 21:08:40 +04:00
Haik Aslanyan 63fd478b58 modified scanning logic for dark background images 2020-05-07 21:08:08 +04:00
Haik Aslanyan 783ac44550 moved decoding calculation into different queue 2020-05-07 17:07:24 +04:00
Haik Aslanyan 118dcd6752 Merge pull request #6 from aslanyanhaik/develop
Develop
2020-05-07 14:46:09 +04:00
Haik Aslanyan 05f71742a1 bumped to version 1.2.0 2020-05-07 14:45:25 +04:00
Haik Aslanyan d544551032 small change 2020-05-06 19:00:46 +04:00
Haik Aslanyan 255195d1b4 modified scanning area logic to support non centered images 2020-05-06 18:56:59 +04:00
Haik Aslanyan cbeea6ef44 removed redundant UIImagePickerControllerDelegate 2020-05-01 12:30:00 +04:00
Haik Aslanyan 3d584f7b23 updated example project 2020-04-30 21:50:34 +04:00
11 changed files with 147 additions and 110 deletions
+12 -5
View File
@@ -84,11 +84,18 @@
<viewLayoutGuide key="safeArea" id="aXm-cj-1eD"/>
</view>
<navigationItem key="navigationItem" id="XgG-ZM-i9d">
<barButtonItem key="leftBarButtonItem" title="Share" id="so2-d3-VCF">
<connections>
<action selector="share:" destination="0zP-ee-W0J" id="uNU-bV-PaS"/>
</connections>
</barButtonItem>
<leftBarButtonItems>
<barButtonItem title="Share" id="so2-d3-VCF">
<connections>
<action selector="share:" destination="0zP-ee-W0J" id="uNU-bV-PaS"/>
</connections>
</barButtonItem>
<barButtonItem title="Config" id="33c-Ng-bhx">
<connections>
<action selector="changeConfig:" destination="0zP-ee-W0J" id="izA-qU-8it"/>
</connections>
</barButtonItem>
</leftBarButtonItems>
<rightBarButtonItems>
<barButtonItem title="Camera" id="Ckh-RB-Kad">
<connections>
+4 -2
View File
@@ -22,8 +22,8 @@
<true/>
<key>NSAppleMusicUsageDescription</key>
<string>To scan roundcodes</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>To save RoundCode images</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>To save RoundCode images</string>
<key>NSCameraUsageDescription</key>
<string>To scan roundcodes</string>
<key>UIApplicationSceneManifest</key>
@@ -64,5 +64,7 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIUserInterfaceStyle</key>
<string>Light</string>
</dict>
</plist>
+29 -2
View File
@@ -36,6 +36,33 @@ class ViewController: UIViewController {
extension ViewController {
@IBAction func changeConfig(_ sender: Any) {
textField.text = ""
image.message = ""
imageView.image = try? coder.encode(image)
let vc = UIAlertController(title: "Select Configuration", message: nil, preferredStyle: .actionSheet)
let uuidAction = UIAlertAction(title: "UUID", style: .default) { _ in
self.coder = RCCoder(configuration: .uuidConfiguration)
}
let numericAction = UIAlertAction(title: "Numeric", style: .default) { _ in
self.coder = RCCoder(configuration: .numericConfiguration)
}
let shortAction = UIAlertAction(title: "Short", style: .default) { _ in
self.coder = RCCoder(configuration: .shortConfiguration)
}
let defaultAction = UIAlertAction(title: "Default", style: .default) { _ in
self.coder = RCCoder(configuration: .defaultConfiguration)
}
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)
vc.addAction(uuidAction)
vc.addAction(numericAction)
vc.addAction(shortAction)
vc.addAction(defaultAction)
vc.addAction(cancelAction)
present(vc, animated: true)
}
@IBAction func scanImage(_ sender: Any) {
isScanningFromLibrary = true
let vc = UIImagePickerController()
@@ -48,18 +75,18 @@ extension ViewController {
@IBAction func scan(_ sender: Any) {
let vc = RCCameraViewController()
coder.scanningMode = .lightBackground
vc.coder = coder
vc.delegate = self
present(vc, animated: true)
}
@IBAction func share(_ sender: Any) {
image.size = 1000
image.isTransparent = false
image.contentInsets = UIEdgeInsets(top: 50, left: 50, bottom: 50, right: 50)
let uiImage = try? coder.encode(image)
image.size = 300
image.contentInsets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
image.isTransparent = true
let vc = UIActivityViewController.init(activityItems: [uiImage!], applicationActivities: nil)
present(vc, animated: true)
}
+14 -12
View File
@@ -5,10 +5,8 @@
[![Twitter: @aslanyanhaik](https://img.shields.io/badge/Contact-Twitter-blue.svg?style=flat)](https://twitter.com/aslanyanhaik)
RoundCode is a Facebook messenger like custom QR code with lots of customization.
In addition to encoder the RoundCode also includes convenient camera scanner and decoder.
RoundCode is custom circular QR code with lots of customization.
Similar to Facebook messenger and Apple's App Clip Codes the RoundCode includes convenient camera scanner and decoder.
<h3 align="center">
<img src="appearance.png" alt="Different styles of RoundCode for iOS"/>
@@ -81,14 +79,14 @@ You can also decode a UIImage like this
```swift
let coder = RCCoder()
do {
messageLabel.text = try coder.decode(UIImage(named: code)!)
} catch {
//handle errors
}
do {
messageLabel.text = try coder.decode(UIImage(named: code)!)
} catch {
//handle errors
}
```
### Appearance
## Appearance
You can change the appearance like this
@@ -100,10 +98,14 @@ image.size = 300
image.gradientType = .linear(angle: CGFloat.pi)
image.tintColors = [.red, .black]
```
If image is on dark background you should change scanning mode to `darkBackground`
⚠️ When choosing colors or transparent background you should keep in mind that decoder will detect the code on light background with dark colors ⚠️
```swift
let coder = RCCoder()
coder.scanningMode = .darkBackground
```
### Advanced
## Advanced coding configuration
You can provide custom coding configuration in order to encode long text by reducing number of characters
+1 -1
View File
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'RoundCode'
s.version = '1.1.1'
s.version = '1.3.0'
s.summary = 'Facebook messenger style custom barcode.'
s.description = <<-DESC
Encode and decode data into custom stylish barcode.
+2 -1
View File
@@ -28,5 +28,6 @@ struct RCConstants {
static let dotSizeScale: CGFloat = 0.08
static let dotPatterns: [CGFloat] = [6, 4, 2]
static let dotPointRange = (Float(1.3)...Float(2.5))
static let pixelThreshold = (0...180)
static let lightBackgroundRange = UInt8(0)...UInt8(180)
static let darkBackgroundRange = UInt8(100)...UInt8(255)
}
-3
View File
@@ -27,7 +27,6 @@ public enum RCError: String, LocalizedError {
case longText
case decoding
case wrongImageSize
case wrongColors
public var errorDescription: String? {
switch self {
@@ -39,8 +38,6 @@ public enum RCError: String, LocalizedError {
return "Error decoding"
case .wrongImageSize:
return "Error decoding. Image width and height must be a equal"
case .wrongColors:
return "The tint colors should be in a range of \(RCConstants.pixelThreshold) grayscale with 1.0 alpha"
}
}
}
+66 -61
View File
@@ -27,37 +27,18 @@ struct RCImageDecoder {
internal let configuration: RCCoderConfiguration
internal var size = 720
internal var bytesPerRow = 720
internal var padding = 0
private var sectionSize: Int {
self.size / 5
}
internal var pixelThreshold = RCConstants.lightBackgroundRange
}
extension RCImageDecoder {
func process(pointer: UnsafeMutablePointer<UInt8>) throws -> [RCBit] {
let bufferData = UnsafeMutableBufferPointer<UInt8>(start: pointer, count: size * bytesPerRow)
let data = PixelContainer(rows: bytesPerRow, items: bufferData)
var points = [CGPoint]()
for side in Side.allCases {
switch side {
case .left:
let point = try scanControlPoint(for: data, region: (padding, (size - sectionSize) / 2), side: side)
points.append(point)
case .top:
let point = try scanControlPoint(for: data, region: ((size - sectionSize) / 2, padding), side: side)
points.append(point)
case .right:
let point = try scanControlPoint(for: data, region: (size - sectionSize - padding, (size - sectionSize) / 2), side: side)
points.append(point)
case .bottom:
let point = try scanControlPoint(for: data, region: ((size - sectionSize) / 2, size - sectionSize - padding), side: side)
points.append(point)
}
}
let points = try scanControlPoints(for: data)
let transform = calculateTransform(from: points)
let mapper = RCPointMapper(transform: transform, size: size)
let locations = mapper.map(points: calculateBitLocations())
let bits = locations.map { RCConstants.pixelThreshold.contains(Int(data[Int($0.x), Int($0.y)])) ? RCBit.one : RCBit.zero }
let bits = locations.map { pixelThreshold.contains(data[Int($0.x), Int($0.y)]) ? RCBit.one : RCBit.zero }
return bits
}
@@ -72,47 +53,66 @@ extension RCImageDecoder {
}
extension RCImageDecoder {
private func scanControlPoint(for data: PixelContainer, region: (x: Int, y: Int), side: Side) throws -> CGPoint {
func scan(region: (x: Int, y: Int, size: Int), data: PixelContainer, coordinate: (Int) -> (x: Int, y: Int), comparison: (PixelPattern, (x: Int, y: Int)) -> Bool) -> [PixelPattern] {
var lastPattern = PixelPattern(bit: RCConstants.pixelThreshold.contains(Int(data[region.x, region.y])) ? RCBit.one : RCBit.zero, x: region.x, y: region.y, count: 0)
var pixelPatterns = [lastPattern]
var count = 0
let maxSize = region.size * region.size
while count < maxSize {
let coordinate = coordinate(count)
let bit = RCConstants.pixelThreshold.contains(Int(data[coordinate.x, coordinate.y])) ? RCBit.one : RCBit.zero
if comparison(lastPattern, coordinate), lastPattern.bit == bit {
lastPattern.count += 1
pixelPatterns[pixelPatterns.count - 1] = lastPattern
} else {
lastPattern = PixelPattern(bit: bit, x: coordinate.x, y: coordinate.y, count: 1)
pixelPatterns.append(lastPattern)
}
count += 1
private func scanControlPoints(for data: PixelContainer) throws -> [CGPoint] {
var horizontalPatterns = [PixelPattern]()
var verticalPatterns = [PixelPattern]()
var points = [CGPoint]()
for side in Side.allCases {
switch side {
case .left:
horizontalPatterns = scanPixelPattern(for: .horizontal, data: data)
points.append(try controlPoint(for: horizontalPatterns, side: side))
case .right:
points.append(try controlPoint(for: horizontalPatterns, side: side))
case .top:
verticalPatterns = scanPixelPattern(for: .vertical, data: data)
points.append(try controlPoint(for: verticalPatterns, side: side))
case .bottom:
points.append(try controlPoint(for: verticalPatterns, side: side))
}
return pixelPatterns
}
let pixelPatterns: [PixelPattern]
switch side {
case .left, .right:
pixelPatterns = scan(region: (region.x, region.y, sectionSize), data: data, coordinate: { count in
let x = count % sectionSize + region.x
let y = count / sectionSize + region.y
return (x, y)
}, comparison: { (pattern, coordinate) in
return pattern.y == coordinate.y
})
case .top, .bottom:
pixelPatterns = scan(region: (region.x, region.y, sectionSize), data: data, coordinate: { count in
let x = count / sectionSize + region.x
let y = count % sectionSize + region.y
return (x, y)
}, comparison: { (pattern, coordinate) in
return pattern.x == coordinate.x
})
return points
}
private func scanPixelPattern(for mode: ScanMode, data: PixelContainer) -> [PixelPattern] {
var lastPattern = PixelPattern(bit: pixelThreshold.contains((data[0, 0])) ? RCBit.one : RCBit.zero, x: 0, y: 0, count: 0)
var pixelPatterns = [lastPattern]
var count = 0
let maxSize = size * size
switch mode {
case .horizontal:
while count < maxSize {
let x = count % size
let y = count / size
let bit = pixelThreshold.contains(data[x, y]) ? RCBit.one : RCBit.zero
if lastPattern.y == y, lastPattern.bit == bit {
lastPattern.count += 1
pixelPatterns[pixelPatterns.count - 1] = lastPattern
} else {
lastPattern = PixelPattern(bit: bit, x: x, y: y, count: 1)
pixelPatterns.append(lastPattern)
}
count += 1
}
case .vertical:
while count < maxSize {
let x = count / size
let y = count % size
let bit = pixelThreshold.contains(data[x, y]) ? RCBit.one : RCBit.zero
if lastPattern.x == x, lastPattern.bit == bit {
lastPattern.count += 1
pixelPatterns[pixelPatterns.count - 1] = lastPattern
} else {
lastPattern = PixelPattern(bit: bit, x: x, y: y, count: 1)
pixelPatterns.append(lastPattern)
}
count += 1
}
}
return pixelPatterns
}
private func controlPoint(for pixelPatterns: [PixelPattern], side: Side) throws -> CGPoint {
let controlPoints = try pixelPatterns.withUnsafeBufferPointer { (pixelPatternsBuffer) -> [CGPoint] in
var points = [CGPoint]()
guard pixelPatternsBuffer.count >= 5 else { throw RCError.decoding }
@@ -164,7 +164,7 @@ extension RCImageDecoder {
}
}.first!
}
private func calculateTransform(from points: [CGPoint]) -> CATransform3D {
let perspective = RCTransformation(points)
let middleRect = CGRect(origin: .zero, size: CGSize(width: CGFloat(size), height: CGFloat(size)))
@@ -215,6 +215,11 @@ extension RCImageDecoder {
case right
}
enum ScanMode {
case horizontal
case vertical
}
final class PixelContainer {
let rows: Int
var columns: Int { data.count / rows }
+6 -12
View File
@@ -23,7 +23,7 @@
import UIKit
import AVFoundation
public final class RCCameraViewController: UIViewController, UIImagePickerControllerDelegate {
public final class RCCameraViewController: UIViewController {
//MARK: Public properties
public weak var delegate: RCCameraViewControllerDelegate?
@@ -113,7 +113,6 @@ public extension RCCameraViewController {
super.viewDidLayoutSubviews()
configureMaskLayer()
videoPreviewLayer?.frame = view.bounds
calculateScanArea()
}
}
@@ -157,12 +156,11 @@ extension RCCameraViewController {
guard let captureDevice = AVCaptureDevice.default(for: .video) else { return }
do {
captureSession.sessionPreset = .hd1280x720
calculateScanArea()
let input = try AVCaptureDeviceInput(device: captureDevice)
input.device.activeVideoMaxFrameDuration = CMTimeMake(value: 1, timescale: 30)
captureSession.addInput(input)
let videoOutput = AVCaptureVideoDataOutput()
videoOutput.setSampleBufferDelegate(self, queue: .main)
videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.global(qos: .userInteractive))
videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: NSNumber(value: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)]
captureSession.addOutput(videoOutput)
} catch {
@@ -170,12 +168,6 @@ extension RCCameraViewController {
}
}
private func calculateScanArea() {
let actualWidth = view.frame.height / 16 * 9
let sideArea = (actualWidth - view.frame.width * 0.9) / 2
coder.imageDecoder.padding = Int(sideArea / actualWidth * 720)
}
private func configureVideoPreview(orientation: AVCaptureVideoOrientation = .portrait) {
videoPreviewLayer?.removeFromSuperlayer()
videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
@@ -205,8 +197,10 @@ extension RCCameraViewController: AVCaptureVideoDataOutputSampleBufferDelegate {
coder.imageDecoder.bytesPerRow = bytesPerRow
if let message = try? coder.decode(buffer: lumaCopy.assumingMemoryBound(to: UInt8.self)) {
captureSession.stopRunning()
delegate?.cameraViewController(didFinishScanning: message)
dismiss(animated: true)
DispatchQueue.main.async {[weak self] in
self?.delegate?.cameraViewController(didFinishScanning: message)
self?.dismiss(animated: true)
}
}
lumaCopy.deallocate()
CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly)
+12 -10
View File
@@ -25,6 +25,11 @@ import UIKit
public final class RCCoder {
public let configuration: RCCoderConfiguration
public var scanningMode = ScanningMode.lightBackground {
didSet {
imageDecoder.pixelThreshold = scanningMode == .lightBackground ? RCConstants.lightBackgroundRange : RCConstants.darkBackgroundRange
}
}
internal lazy var imageDecoder = RCImageDecoder(configuration: self.configuration)
internal lazy var imageEncoder = RCImageEncoder(configuration: self.configuration)
internal lazy var bitCoder = RCBitCoder(configuration: self.configuration)
@@ -44,7 +49,6 @@ public extension RCCoder {
func decode(_ image: UIImage) throws -> String {
guard image.size.width == image.size.height else { throw RCError.wrongImageSize }
imageDecoder.size = image.cgImage!.height
imageDecoder.padding = 0
imageDecoder.bytesPerRow = image.cgImage!.height
let bits = try imageDecoder.decode(image)
let message = try bitCoder.decode(bits)
@@ -54,15 +58,12 @@ public extension RCCoder {
func validate(_ text: String) -> Bool {
configuration.validate(text)
}
func validateForBlackBackground(colors: [UIColor]) -> Bool {
colors.allSatisfy { color in
var white: CGFloat = 0
var alpha: CGFloat = 0
guard color.getWhite(&white, alpha: &alpha) else { return false }
guard alpha == 1.0 else { return false }
return RCConstants.pixelThreshold.contains(Int(white * 255))
}
}
public extension RCCoder {
enum ScanningMode {
case lightBackground
case darkBackground
}
}
@@ -74,3 +75,4 @@ extension RCCoder {
return message
}
}
+1 -1
View File
@@ -28,7 +28,7 @@ public struct RCImage {
public var size = CGFloat(300)
public var tintColors: [UIColor] = [.orange, .magenta]
public var attachmentImage: UIImage?
public var isTransparent = true
public var isTransparent = false
public var gradientType = GradientType.linear(angle: CGFloat.pi / 4)
public var contentInsets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)