added RCData, refactored bit coding
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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..<configuration.maxMessageCount).map({_ in emptyIndexes[Int.random(in: 0..<emptyIndexes.count)]})
|
||||
randomBytes.insert(contentsOf: dataBytes, at: 0)
|
||||
randomBytes = Array(randomBytes.prefix(configuration.maxMessageCount))
|
||||
let encodedBits = randomBytes.map { byte -> [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..<configuration.maxMessageCount).map({_ in emptyIndexes[Int.random(in: 0..<emptyIndexes.count)]})
|
||||
dataBytes.append(contentsOf: randomBytes)
|
||||
dataBytes = Array(dataBytes.prefix(configuration.maxMessageCount))
|
||||
let encodedBits = dataBytes.map { byte -> [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]}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user