blob: 20bb5d02af282807ae6c722e4f8acaf17c13792b [file] [log] [blame] [edit]
/*
* Copyright 2006 Jeremias Maerki in part, and ZXing Authors in part
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "ZXErrors.h"
#import "ZXPDF417HighLevelEncoder.h"
/**
* code for Text compaction
*/
const int TEXT_COMPACTION = 0;
/**
* code for Byte compaction
*/
const int BYTE_COMPACTION = 1;
/**
* code for Numeric compaction
*/
const int NUMERIC_COMPACTION = 2;
/**
* Text compaction submode Alpha
*/
const int SUBMODE_ALPHA = 0;
/**
* Text compaction submode Lower
*/
const int SUBMODE_LOWER = 1;
/**
* Text compaction submode Mixed
*/
const int SUBMODE_MIXED = 2;
/**
* Text compaction submode Punctuation
*/
const int SUBMODE_PUNCTUATION = 3;
/**
* mode latch to Text Compaction mode
*/
const int LATCH_TO_TEXT = 900;
/**
* mode latch to Byte Compaction mode (number of characters NOT a multiple of 6)
*/
const int LATCH_TO_BYTE_PADDED = 901;
/**
* mode latch to Numeric Compaction mode
*/
const int LATCH_TO_NUMERIC = 902;
/**
* mode shift to Byte Compaction mode
*/
const int SHIFT_TO_BYTE = 913;
/**
* mode latch to Byte Compaction mode (number of characters a multiple of 6)
*/
const int LATCH_TO_BYTE = 924;
/**
* Raw code table for text compaction Mixed sub-mode
*/
const int TEXT_MIXED_RAW_LEN = 30;
const int8_t TEXT_MIXED_RAW[TEXT_MIXED_RAW_LEN] = {
48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 38, 13, 9, 44, 58,
35, 45, 46, 36, 47, 43, 37, 42, 61, 94, 0, 32, 0, 0, 0};
/**
* Raw code table for text compaction: Punctuation sub-mode
*/
const int TEXT_PUNCTUATION_RAW_LEN = 30;
const int8_t TEXT_PUNCTUATION_RAW[TEXT_PUNCTUATION_RAW_LEN] = {
59, 60, 62, 64, 91, 92, 93, 95, 96, 126, 33, 13, 9, 44, 58,
10, 45, 46, 36, 47, 34, 124, 42, 40, 41, 63, 123, 125, 39, 0};
const int MIXED_TABLE_LEN = 128;
unichar MIXED_TABLE[MIXED_TABLE_LEN];
const int PUNCTUATION_LEN = 128;
unichar PUNCTUATION[PUNCTUATION_LEN];
@implementation ZXPDF417HighLevelEncoder
+ (void)initialize {
//Construct inverse lookups
for (int i = 0; i < MIXED_TABLE_LEN; i++) {
MIXED_TABLE[i] = 0xFF;
}
for (int8_t i = 0; i < TEXT_MIXED_RAW_LEN; i++) {
int8_t b = TEXT_MIXED_RAW[i];
if (b > 0) {
MIXED_TABLE[b] = i;
}
}
for (int i = 0; i < PUNCTUATION_LEN; i++) {
PUNCTUATION[i] = 0xFF;
}
for (int8_t i = 0; i < TEXT_PUNCTUATION_RAW_LEN; i++) {
int8_t b = TEXT_PUNCTUATION_RAW[i];
if (b > 0) {
PUNCTUATION[b] = i;
}
}
}
/**
* Converts the message to a byte array using the default encoding (cp437) as defined by the
* specification
*/
+ (int8_t *)bytesForMessage:(NSString *)msg {
return (int8_t *)[[msg dataUsingEncoding:(NSStringEncoding) 0x80000400] bytes];
}
/**
* Performs high-level encoding of a PDF417 message using the algorithm described in annex P
* of ISO/IEC 15438:2001(E). If byte compaction has been selected, then only byte compaction
* is used.
*/
+ (NSString *)encodeHighLevel:(NSString *)msg compaction:(ZXCompaction)compaction error:(NSError **)error {
int8_t *bytes = NULL; //Fill later and only if needed
//the codewords 0..928 are encoded as Unicode characters
NSMutableString *sb = [NSMutableString stringWithCapacity:msg.length];
NSUInteger len = msg.length;
int p = 0;
int textSubMode = SUBMODE_ALPHA;
// User selected encoding mode
if (compaction == ZX_COMPACTION_TEXT) {
[self encodeText:msg startpos:p count:(int)len buffer:sb initialSubmode:textSubMode];
} else if (compaction == ZX_COMPACTION_BYTE) {
bytes = [self bytesForMessage:msg];
[self encodeBinary:bytes startpos:p count:(int)msg.length startmode:BYTE_COMPACTION buffer:sb];
} else if (compaction == ZX_COMPACTION_NUMERIC) {
[sb appendFormat:@"%C", (unichar) LATCH_TO_NUMERIC];
[self encodeNumeric:msg startpos:p count:(int)len buffer:sb];
} else {
int encodingMode = TEXT_COMPACTION; //Default mode, see 4.4.2.1
while (p < len) {
int n = [self determineConsecutiveDigitCount:msg startpos:p];
if (n >= 13) {
[sb appendFormat:@"%C", (unichar) LATCH_TO_NUMERIC];
encodingMode = NUMERIC_COMPACTION;
textSubMode = SUBMODE_ALPHA; //Reset after latch
[self encodeNumeric:msg startpos:p count:n buffer:sb];
p += n;
} else {
int t = [self determineConsecutiveTextCount:msg startpos:p];
if (t >= 5 || n == len) {
if (encodingMode != TEXT_COMPACTION) {
[sb appendFormat:@"%C", (unichar) LATCH_TO_TEXT];
encodingMode = TEXT_COMPACTION;
textSubMode = SUBMODE_ALPHA; //start with submode alpha after latch
}
textSubMode = [self encodeText:msg startpos:p count:t buffer:sb initialSubmode:textSubMode];
p += t;
} else {
if (bytes == NULL) {
bytes = [self bytesForMessage:msg];
}
int b = [self determineConsecutiveBinaryCount:msg bytes:bytes startpos:p error:error];
if (b == -1) {
return nil;
} else if (b == 0) {
b = 1;
}
if (b == 1 && encodingMode == TEXT_COMPACTION) {
//Switch for one byte (instead of latch)
[self encodeBinary:bytes startpos:p count:1 startmode:TEXT_COMPACTION buffer:sb];
} else {
//Mode latch performed by encodeBinary
[self encodeBinary:bytes startpos:p count:b startmode:encodingMode buffer:sb];
encodingMode = BYTE_COMPACTION;
textSubMode = SUBMODE_ALPHA; //Reset after latch
}
p += b;
}
}
}
}
return sb;
}
/**
* Encode parts of the message using Text Compaction as described in ISO/IEC 15438:2001(E),
* chapter 4.4.2.
*/
+ (int)encodeText:(NSString *)msg startpos:(int)startpos count:(int)count buffer:(NSMutableString *)sb initialSubmode:(int)initialSubmode {
NSMutableString *tmp = [NSMutableString stringWithCapacity:count];
int submode = initialSubmode;
int idx = 0;
while (true) {
unichar ch = [msg characterAtIndex:startpos + idx];
switch (submode) {
case SUBMODE_ALPHA:
if ([self isAlphaUpper:ch]) {
if (ch == ' ') {
[tmp appendFormat:@"%C", (unichar) 26]; //space
} else {
[tmp appendFormat:@"%C", (unichar) (ch - 65)];
}
} else {
if ([self isAlphaLower:ch]) {
submode = SUBMODE_LOWER;
[tmp appendFormat:@"%C", (unichar) 27]; //ll
continue;
} else if ([self isMixed:ch]) {
submode = SUBMODE_MIXED;
[tmp appendFormat:@"%C", (unichar) 28]; //ml
continue;
} else {
[tmp appendFormat:@"%C", (unichar) 29]; //ps
[tmp appendFormat:@"%C", PUNCTUATION[ch]];
break;
}
}
break;
case SUBMODE_LOWER:
if ([self isAlphaLower:ch]) {
if (ch == ' ') {
[tmp appendFormat:@"%C", (unichar) 26]; //space
} else {
[tmp appendFormat:@"%C", (unichar) (ch - 97)];
}
} else {
if ([self isAlphaUpper:ch]) {
[tmp appendFormat:@"%C", (unichar) 27]; //as
[tmp appendFormat:@"%C", (unichar) (ch - 65)];
//space cannot happen here, it is also in "Lower"
break;
} else if ([self isMixed:ch]) {
submode = SUBMODE_MIXED;
[tmp appendFormat:@"%C", (unichar) 28]; //ml
continue;
} else {
[tmp appendFormat:@"%C", (unichar) 29]; //ps
[tmp appendFormat:@"%C", PUNCTUATION[ch]];
break;
}
}
break;
case SUBMODE_MIXED:
if ([self isMixed:ch]) {
[tmp appendFormat:@"%C", MIXED_TABLE[ch]]; //as
} else {
if ([self isAlphaUpper:ch]) {
submode = SUBMODE_ALPHA;
[tmp appendFormat:@"%C", (unichar) 28]; //al
continue;
} else if ([self isAlphaLower:ch]) {
submode = SUBMODE_LOWER;
[tmp appendFormat:@"%C", (unichar) 27]; //ll
continue;
} else {
if (startpos + idx + 1 < count) {
char next = [msg characterAtIndex:startpos + idx + 1];
if ([self isPunctuation:next]) {
submode = SUBMODE_PUNCTUATION;
[tmp appendFormat:@"%C", (unichar) 25]; //pl
continue;
}
}
[tmp appendFormat:@"%C", (unichar) 29]; //ps
[tmp appendFormat:@"%C", PUNCTUATION[ch]];
}
}
break;
default: //SUBMODE_PUNCTUATION
if ([self isPunctuation:ch]) {
[tmp appendFormat:@"%C", PUNCTUATION[ch]];
} else {
submode = SUBMODE_ALPHA;
[tmp appendFormat:@"%C", (unichar) 29]; //al
continue;
}
}
idx++;
if (idx >= count) {
break;
}
}
unichar h = 0;
NSUInteger len = tmp.length;
for (int i = 0; i < len; i++) {
BOOL odd = (i % 2) != 0;
if (odd) {
h = (unichar) ((h * 30) + [tmp characterAtIndex:i]);
[sb appendFormat:@"%C", h];
} else {
h = [tmp characterAtIndex:i];
}
}
if ((len % 2) != 0) {
[sb appendFormat:@"%C", (unichar) ((h * 30) + 29)]; //ps
}
return submode;
}
/**
* Encode parts of the message using Byte Compaction as described in ISO/IEC 15438:2001(E),
* chapter 4.4.3. The Unicode characters will be converted to binary using the cp437
* codepage.
*/
+ (void)encodeBinary:(int8_t *)bytes startpos:(int)startpos count:(int)count startmode:(int)startmode buffer:(NSMutableString *)sb {
if (count == 1 && startmode == TEXT_COMPACTION) {
[sb appendFormat:@"%C", (unichar) SHIFT_TO_BYTE];
}
int idx = startpos;
// Encode sixpacks
if (count >= 6) {
[sb appendFormat:@"%C", (unichar) LATCH_TO_BYTE];
const int charsLen = 5;
unichar chars[charsLen];
memset(chars, 0, charsLen * sizeof(unichar));
while ((startpos + count - idx) >= 6) {
long t = 0;
for (int i = 0; i < 6; i++) {
t <<= 8;
t += bytes[idx + i] & 0xff;
}
for (int i = 0; i < 5; i++) {
chars[i] = (unichar) (t % 900);
t /= 900;
}
for (int i = charsLen - 1; i >= 0; i--) {
[sb appendFormat:@"%C", chars[i]];
}
idx += 6;
}
}
//Encode rest (remaining n<5 bytes if any)
if (idx < startpos + count) {
[sb appendFormat:@"%C", (unichar) LATCH_TO_BYTE_PADDED];
}
for (int i = idx; i < startpos + count; i++) {
int ch = bytes[i] & 0xff;
[sb appendFormat:@"%C", (unichar)ch];
}
}
+ (void)encodeNumeric:(NSString *)msg startpos:(int)startpos count:(int)count buffer:(NSMutableString *)sb {
int idx = 0;
NSMutableString *tmp = [NSMutableString stringWithCapacity:count / 3 + 1];
while (idx < count - 1) {
[tmp setString:@""];
int len = MIN(44, count - idx);
NSString *part = [@"1" stringByAppendingString:[msg substringWithRange:NSMakeRange(startpos + idx, len)]];
long long bigint = [part longLongValue];
do {
long c = bigint % 900;
[tmp appendFormat:@"%C", (unichar) c];
bigint /= 900;
} while (bigint != 0);
//Reverse temporary string
for (int i = (int)tmp.length - 1; i >= 0; i--) {
[sb appendFormat:@"%C", [tmp characterAtIndex:i]];
}
idx += len;
}
}
+ (BOOL)isDigit:(char)ch {
return ch >= '0' && ch <= '9';
}
+ (BOOL)isAlphaUpper:(char)ch {
return ch == ' ' || (ch >= 'A' && ch <= 'Z');
}
+ (BOOL)isAlphaLower:(char)ch {
return ch == ' ' || (ch >= 'a' && ch <= 'z');
}
+ (BOOL)isMixed:(char)ch {
return MIXED_TABLE[ch] != 0xFF;
}
+ (BOOL)isPunctuation:(char)ch {
return PUNCTUATION[ch] != 0xFF;
}
+ (BOOL)isText:(char)ch {
return ch == '\t' || ch == '\n' || ch == '\r' || (ch >= 32 && ch <= 126);
}
/**
* Determines the number of consecutive characters that are encodable using numeric compaction.
*/
+ (int)determineConsecutiveDigitCount:(NSString *)msg startpos:(int)startpos {
int count = 0;
NSUInteger len = msg.length;
int idx = startpos;
if (idx < len) {
char ch = [msg characterAtIndex:idx];
while ([self isDigit:ch] && idx < len) {
count++;
idx++;
if (idx < len) {
ch = [msg characterAtIndex:idx];
}
}
}
return count;
}
/**
* Determines the number of consecutive characters that are encodable using text compaction.
*
* @param msg the message
* @param startpos the start position within the message
* @return the requested character count
*/
+ (int)determineConsecutiveTextCount:(NSString *)msg startpos:(int)startpos {
NSUInteger len = msg.length;
int idx = startpos;
while (idx < len) {
char ch = [msg characterAtIndex:idx];
int numericCount = 0;
while (numericCount < 13 && [self isDigit:ch] && idx < len) {
numericCount++;
idx++;
if (idx < len) {
ch = [msg characterAtIndex:idx];
}
}
if (numericCount >= 13) {
return idx - startpos - numericCount;
}
if (numericCount > 0) {
//Heuristic: All text-encodable chars or digits are binary encodable
continue;
}
ch = [msg characterAtIndex:idx];
//Check if character is encodable
if (![self isText:ch]) {
break;
}
idx++;
}
return idx - startpos;
}
/**
* Determines the number of consecutive characters that are encodable using binary compaction.
*/
+ (int)determineConsecutiveBinaryCount:(NSString *)msg bytes:(int8_t *)bytes startpos:(int)startpos error:(NSError **)error {
NSUInteger len = msg.length;
int idx = startpos;
while (idx < len) {
char ch = [msg characterAtIndex:idx];
int numericCount = 0;
while (numericCount < 13 && [self isDigit:ch]) {
numericCount++;
//textCount++;
int i = idx + numericCount;
if (i >= len) {
break;
}
ch = [msg characterAtIndex:i];
}
if (numericCount >= 13) {
return idx - startpos;
}
int textCount = 0;
while (textCount < 5 && [self isText:ch]) {
textCount++;
int i = idx + textCount;
if (i >= len) {
break;
}
ch = [msg characterAtIndex:i];
}
if (textCount >= 5) {
return idx - startpos;
}
ch = [msg characterAtIndex:idx];
//Check if character is encodable
//Sun returns a ASCII 63 (?) for a character that cannot be mapped. Let's hope all
//other VMs do the same
if (bytes[idx] == 63 && ch != '?') {
NSDictionary *userInfo = @{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Non-encodable character detected: %c (Unicode: %C)", ch, (unichar)ch]};
if (error) *error = [[NSError alloc] initWithDomain:ZXErrorDomain code:ZXWriterError userInfo:userInfo];
return -1;
}
idx++;
}
return idx - startpos;
}
@end