diff --git a/Sources/Internal/RCBitGroup.swift b/Sources/Internal/Extensions.swift similarity index 77% rename from Sources/Internal/RCBitGroup.swift rename to Sources/Internal/Extensions.swift index e8181a6..e4c88c9 100644 --- a/Sources/Internal/RCBitGroup.swift +++ b/Sources/Internal/Extensions.swift @@ -22,22 +22,10 @@ import Foundation -final class RCBitGroup { - - var bit: RCBit - var count: Int - var offset: Int - - init(bit: RCBit, count: Int, offset: Int) { - self.bit = bit - self.count = count - self.offset = offset +extension Array { + func chunked(into size: Int) -> [[Element]] { + return stride(from: 0, to: count, by: size).map { + Array(self[$0 ..< Swift.min($0 + size, count)]) + } } } - -extension RCBitGroup: CustomDebugStringConvertible { - var debugDescription: String { - "bit: \(bit), count: \(count), offset: \(offset) \n" - } -} - diff --git a/Sources/Internal/RCBit.swift b/Sources/Internal/RCBit.swift index a3566bb..f91dcae 100644 --- a/Sources/Internal/RCBit.swift +++ b/Sources/Internal/RCBit.swift @@ -27,10 +27,6 @@ enum RCBit: Int { case zero case one - var boolValue: Bool { - return self == .zero ? false : true - } - init?(_ character: Character) { switch character { case "0": self = .zero @@ -39,9 +35,3 @@ enum RCBit: Int { } } } - -extension RCBit: CustomDebugStringConvertible { - var debugDescription: String { - String(rawValue) - } -} diff --git a/Sources/Internal/RCBitCoder.swift b/Sources/Internal/RCBitCoder.swift index 0b5786e..459fa0d 100644 --- a/Sources/Internal/RCBitCoder.swift +++ b/Sources/Internal/RCBitCoder.swift @@ -29,61 +29,62 @@ final class RCBitCoder { init(configuration: RCCoderConfiguration) { self.configuration = configuration } - - func encode(message: String) throws -> [[[RCBitGroup]]] { - guard message.trimmingCharacters(in: configuration.characterSet).isEmpty else { +} + +extension RCBitCoder { + func encode(message: String) throws -> RCData { + var specialCharacters = RCConstants.emptyCharacters + specialCharacters.append(RCConstants.startingCharacter) + guard message.map({$0}).allSatisfy({configuration.characters.contains($0) && !specialCharacters.contains($0)}) else { throw RCError.invalidCharacter } guard message.count <= configuration.maxMessageCount else { throw RCError.longText } - let dataBytes = message.map({configuration.symbols.firstIndex(of: $0)!}) - let emptyIndexes = configuration.emptySymbolsIndex() - var randomBytes = (0.. [RCBit] in + var dataBytes = message.map({configuration.characters.firstIndex(of: $0)!}) + dataBytes.insert(configuration.characters.firstIndex(of: RCConstants.startingCharacter)!, at: 0) + let emptyIndexes = RCConstants.emptyCharacters.map({configuration.characters.firstIndex(of: $0)!}) + let randomBytes = (0.. [RCBit] in var stringValue = String(repeating: "0", count: configuration.bitesPerSymbol) stringValue += String(byte, radix: 2) return stringValue.suffix(configuration.bitesPerSymbol).compactMap({RCBit($0)}) }.flatMap({$0}) - var totalBits = [RCBit](repeating: .zero, count: RCConstants.maxBites) + var totalBits = [RCBit](repeating: .zero, count: RCConstants.maxBitesPerSection * 3) totalBits.insert(contentsOf: encodedBits, at: 0) - totalBits = Array(totalBits.prefix(RCConstants.maxBites)) - let bitChunks = [RCConstants.level1BitesCount, RCConstants.level2BitesCount, RCConstants.level3BitesCount].map { count in - return (0...3).map { _ -> [RCBit] in // 4 groups - let bitChunk = Array(totalBits.prefix(count)) - totalBits = Array(totalBits.dropFirst(count)) - return bitChunk - } - } - let bitGroups = zip(bitChunks[0], zip(bitChunks[1], bitChunks[2])).map {[$0.0, $0.1.0, $0.1.1]}.map { group in - group.map { row -> [RCBitGroup] in - var bitGroupRow = [RCBitGroup(bit: row[0], count: 0, offset: 0)] - row.enumerated().forEach { value in - bitGroupRow.last!.bit == value.element ? bitGroupRow.last!.count += 1 : bitGroupRow.append(RCBitGroup(bit: value.element, count: 1, offset: value.offset)) - } - return bitGroupRow - } - } - return bitGroups + totalBits = Array(totalBits.prefix(RCConstants.maxBitesPerSection * 3)) + //reed solomon + return RCData(totalBits) } + func decode(_ bits: [RCBit]) throws -> String { guard bits.contains(.one) else { return "" } - let emptyIndexes = configuration.emptySymbolsIndex() - let bitChunks = stride(from: 0, to: bits.count, by: configuration.bitesPerSymbol).map { + //decode bits to indexes + let bytes = stride(from: 0, to: bits.count, by: configuration.bitesPerSymbol).map { Array(bits[$0 ..< min($0 + configuration.bitesPerSymbol, bits.count)]) } - var indexes = bitChunks.map({ bits in + var indexes = bytes.map({ bits in return bits.reduce(0) { accumulated, current in accumulated << 1 | current.rawValue } }) - indexes = Array(indexes.prefix(configuration.maxMessageCount)).filter({!emptyIndexes.contains($0)}) - guard (indexes.max() ?? 0) <= configuration.symbols.count else { + var sectionIndexes = indexes.chunked(into: 4) + let startingIndex = configuration.characters.firstIndex(of: RCConstants.startingCharacter)! + guard let firstSectionIndex = sectionIndexes.firstIndex(where: {$0.first == startingIndex}) else { + throw RCError.decoding + } + let previousSections = Array(sectionIndexes.prefix(firstSectionIndex)) + sectionIndexes = Array(sectionIndexes.dropFirst(firstSectionIndex)) + sectionIndexes.append(contentsOf: previousSections) + //reed solomon check + let emptyIndexes = RCConstants.emptyCharacters.map({configuration.characters.firstIndex(of: $0)!}) + indexes = Array(sectionIndexes.flatMap({$0}).prefix(configuration.maxMessageCount)).filter({!emptyIndexes.contains($0)}) + guard (indexes.max() ?? 0) <= configuration.characters.count else { throw RCError.wrongConfiguration } - return String(indexes.compactMap({configuration.symbols[$0]})) + return String(indexes.compactMap({configuration.characters[$0]})) } } diff --git a/Sources/Internal/RCConstants.swift b/Sources/Internal/RCConstants.swift index 27301b0..f3b84ec 100644 --- a/Sources/Internal/RCConstants.swift +++ b/Sources/Internal/RCConstants.swift @@ -23,14 +23,23 @@ import CoreGraphics struct RCConstants { - static let maxBites = 276 - static let level1BitesCount = 23 - static let level2BitesCount = 23 - static let level3BitesCount = 23 + static let maxBitesPerSection = 276 + static let topLevelBitesCount = 23 + static let middleLevelBitesCount = 23 + static let bottomLevelBitesCount = 23 static let imageScale: CGFloat = 0.8 static let dotSizeScale: CGFloat = 0.08 static let dotPatterns: [CGFloat] = [6, 4, 2] static let dotPointRange = (Float(1.6)...Float(2.2)) static let pixelThreshold = 180 - static let emptySymbols: [Character] = ["\u{0540}", "\u{0531}"] + static let emptyCharacters: [Character] = ["\u{0540}", "\u{0531}"] + static let startingCharacter: Character = "\u{058D}" + + + + + static let level1BitesCount = 23 + static let level2BitesCount = 23 + static let level3BitesCount = 23 + } diff --git a/Sources/Internal/RCData.swift b/Sources/Internal/RCData.swift new file mode 100644 index 0000000..ba29a86 --- /dev/null +++ b/Sources/Internal/RCData.swift @@ -0,0 +1,76 @@ +// MIT License + +// Copyright (c) 2020 Haik Aslanyan + +// 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 + +struct RCBitSection { + + var topLevel = [RCBit]() + var middleLevel = [RCBit]() + var bottomLevel = [RCBit]() +} + +struct RCData { + + var firstShard = RCBitSection() + var secondShard = RCBitSection() + var thirdShard = RCBitSection() + var parity = RCBitSection() + + init(_ bits: [RCBit]) { + let sections = bits.chunked(into: 4).map { sectionBits -> RCBitSection in + var data = sectionBits + var section = RCBitSection() + section.topLevel = Array(data.prefix(RCConstants.topLevelBitesCount)) + data = Array(data.dropFirst(RCConstants.topLevelBitesCount)) + section.middleLevel = Array(data.prefix(RCConstants.middleLevelBitesCount)) + data = Array(data.dropFirst(RCConstants.middleLevelBitesCount)) + section.bottomLevel = data + return section + } + firstShard = sections[0] + secondShard = sections[1] + thirdShard = sections[2] + parity = sections[3] + } +} + + +final class RCBitGroup1 { + + var bit: RCBit + var count: Int + var offset: Int + + init(bit: RCBit, count: Int, offset: Int) { + self.bit = bit + self.count = count + self.offset = offset + } +} + +extension RCBitGroup1: CustomDebugStringConvertible { + var debugDescription: String { + "bit: \(bit), count: \(count), offset: \(offset) \n" + } +} + diff --git a/Sources/Internal/RCImageDecoder.swift b/Sources/Internal/RCImageDecoder.swift index eaeeb0a..a3e0ad4 100644 --- a/Sources/Internal/RCImageDecoder.swift +++ b/Sources/Internal/RCImageDecoder.swift @@ -230,16 +230,11 @@ extension RCImageDecoder { extension RCImageDecoder { - struct RCPixelPattern: CustomStringConvertible { - + struct RCPixelPattern { let bit: RCBit let x: Int let y: Int var count: Float - - var description: String { - "\(bit.debugDescription), start: \(y) \(x), count: \(count)" - } } enum Side: CaseIterable { diff --git a/Sources/Internal/RCImageEncoder.swift b/Sources/Internal/RCImageEncoder.swift index 001dc9b..a2f3cd0 100644 --- a/Sources/Internal/RCImageEncoder.swift +++ b/Sources/Internal/RCImageEncoder.swift @@ -24,7 +24,7 @@ import UIKit final class RCImageEncoder { - func encode(_ image: RCImage, bits: [[[RCBitGroup]]]) -> UIImage { + func encode(_ image: RCImage, bits: [[[RCBitGroup1]]]) -> UIImage { let rect = CGRect(origin: .zero, size: CGSize(width: image.size + image.contentInsets.left + image.contentInsets.right, height: image.size + image.contentInsets.top + image.contentInsets.bottom)) let renderer = UIGraphicsImageRenderer(bounds: rect, format: .default()) let renderedImage = renderer.image { context in @@ -97,7 +97,7 @@ private extension RCImageEncoder { attachmentImage.draw(in: scaledImageRect, blendMode: .normal, alpha: 1) } - func drawMessage(image: RCImage, group: [[RCBitGroup]], angle: CGFloat, path: UIBezierPath) { + func drawMessage(image: RCImage, group: [[RCBitGroup1]], angle: CGFloat, path: UIBezierPath) { guard !image.message.isEmpty else { return } let lineWidth = image.size * RCConstants.dotSizeScale / 11 * 2 //number of lines including spaces let mainRadius = (image.size - lineWidth) / 2 @@ -110,7 +110,7 @@ private extension RCImageEncoder { let rowBitsGroup = $0.1 let radius = $0.0 rowBitsGroup.forEach { bit in - guard bit.bit.boolValue else { return } + //guard bit.bit.boolValue else { return } let startPosition = startAngle + distancePerBit * CGFloat(bit.offset) let endPosition = startPosition + distancePerBit * CGFloat(bit.count) let linePath = UIBezierPath(arcCenter: center, radius: radius, startAngle: startPosition, endAngle: endPosition, clockwise: true) diff --git a/Sources/Public/RCCoder.swift b/Sources/Public/RCCoder.swift index 9fbd597..b21d0b8 100644 --- a/Sources/Public/RCCoder.swift +++ b/Sources/Public/RCCoder.swift @@ -37,8 +37,8 @@ public final class RCCoder { public extension RCCoder { func encode(_ image: RCImage) throws -> UIImage { let bits = try bitCoder.encode(message: image.message) - let image = imageEncoder.encode(image, bits: bits) - return image +// let image = imageEncoder.encode(image, bits: bits) + return UIImage() } func decode(_ image: UIImage) throws -> String { @@ -49,7 +49,7 @@ public extension RCCoder { } func validate(_ text: String) -> Bool { - return text.trimmingCharacters(in: configuration.characterSet).isEmpty && text.count <= configuration.maxMessageCount + configuration.validate(text) } } diff --git a/Sources/Public/RCCoderConfiguration.swift b/Sources/Public/RCCoderConfiguration.swift index b93ca56..b23c05e 100644 --- a/Sources/Public/RCCoderConfiguration.swift +++ b/Sources/Public/RCCoderConfiguration.swift @@ -22,51 +22,48 @@ import Foundation -public final class RCCoderConfiguration { +public struct RCCoderConfiguration { + public let version: Version public let maxMessageCount: Int - public let symbols: [Character] public let bitesPerSymbol: Int - internal let emptySymbols: [Character] - internal let characterSet: CharacterSet + public let characters: [Character] - public init(symbols: String, shouldFillEmptySpace: Bool = true) { - self.characterSet = CharacterSet(charactersIn: symbols.map({String($0)}).reduce("", +)) - var symbolsArray = symbols.map({$0}) - if shouldFillEmptySpace { - self.emptySymbols = RCConstants.emptySymbols - symbolsArray.insert(emptySymbols[0], at: symbolsArray.count / 3) - symbolsArray.insert(emptySymbols[1], at: symbolsArray.count / 3 * 2) - } else { - self.emptySymbols = [RCConstants.emptySymbols[0]] - symbolsArray.insert(emptySymbols[0], at: 0) - } - self.symbols = symbolsArray - self.bitesPerSymbol = String(symbols.count, radix: 2).count - self.maxMessageCount = RCConstants.maxBites / bitesPerSymbol + public init(version: Version = .v1, characters: String) { + self.version = version + var charactersArray = characters.map({$0}) + charactersArray.append(RCConstants.startingCharacter) + charactersArray.append(contentsOf: RCConstants.emptyCharacters) + self.characters = charactersArray + self.bitesPerSymbol = String(charactersArray.count - 1, radix: 2).count + self.maxMessageCount = RCConstants.maxBitesPerSection * 3 / bitesPerSymbol } - func indexOf(symbol: Character) -> Int { - return symbols.firstIndex(of: symbol)! - } - - func emptySymbolsIndex() -> [Int] { - return emptySymbols.map({symbols.firstIndex(of: $0)!}) + func validate(_ text: String) -> Bool { + var specialCharacters = RCConstants.emptyCharacters + specialCharacters.append(RCConstants.startingCharacter) + return text.map({$0}).allSatisfy({characters.contains($0) && !specialCharacters.contains($0)}) && text.count <= maxMessageCount } public static var uuidConfiguration: RCCoderConfiguration { - return RCCoderConfiguration(symbols: "-ABCDEF0123456789") + return RCCoderConfiguration(characters: "-ABCDEF0123456789") } public static var numericConfiguration: RCCoderConfiguration { - return RCCoderConfiguration(symbols: ".,_0123456789") + return RCCoderConfiguration(characters: ".,_0123456789") } public static var shortConfiguration: RCCoderConfiguration { - return RCCoderConfiguration(symbols: " -abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + return RCCoderConfiguration(characters: " -abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") } public static var defaultConfiguration: RCCoderConfiguration { - return RCCoderConfiguration(symbols: ##"! "#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~"##) + return RCCoderConfiguration(characters: ##"! "#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~"##) + } +} + +public extension RCCoderConfiguration { + enum Version { + case v1 } }