/* * 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 "ZXByteArray.h" #import "ZXCharacterSetECI.h" #import "ZXErrors.h" #import "ZXPDF417HighLevelEncoder.h" /** * code for Text compaction */ const int ZX_PDF417_TEXT_COMPACTION = 0; /** * code for Byte compaction */ const int ZX_PDF417_BYTE_COMPACTION = 1; /** * code for Numeric compaction */ const int ZX_PDF417_NUMERIC_COMPACTION = 2; /** * Text compaction submode Alpha */ const int ZX_PDF417_SUBMODE_ALPHA = 0; /** * Text compaction submode Lower */ const int ZX_PDF417_SUBMODE_LOWER = 1; /** * Text compaction submode Mixed */ const int ZX_PDF417_SUBMODE_MIXED = 2; /** * Text compaction submode Punctuation */ const int ZX_PDF417_SUBMODE_PUNCTUATION = 3; /** * mode latch to Text Compaction mode */ const int ZX_PDF417_LATCH_TO_TEXT = 900; /** * mode latch to Byte Compaction mode (number of characters NOT a multiple of 6) */ const int ZX_PDF417_LATCH_TO_BYTE_PADDED = 901; /** * mode latch to Numeric Compaction mode */ const int ZX_PDF417_LATCH_TO_NUMERIC = 902; /** * mode shift to Byte Compaction mode */ const int ZX_PDF417_SHIFT_TO_BYTE = 913; /** * mode latch to Byte Compaction mode (number of characters a multiple of 6) */ const int ZX_PDF417_LATCH_TO_BYTE = 924; /** * identifier for a user defined Extended Channel Interpretation (ECI) */ const int ZX_PDF417_HIGH_LEVEL_ECI_USER_DEFINED = 925; /** * identifier for a general purpose ECO format */ const int ZX_PDF417_HIGH_LEVEL_ECI_GENERAL_PURPOSE = 926; /** * identifier for an ECI of a character set of code page */ const int ZX_PDF417_HIGH_LEVEL_ECI_CHARSET = 927; /** * Raw code table for text compaction Mixed sub-mode */ const int8_t ZX_PDF417_TEXT_MIXED_RAW[] = { 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 int8_t ZX_PDF417_TEXT_PUNCTUATION_RAW[] = { 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 ZX_PDF417_MIXED_TABLE_LEN = 128; unichar ZX_PDF417_MIXED_TABLE[ZX_PDF417_MIXED_TABLE_LEN]; const int ZX_PDF417_PUNCTUATION_LEN = 128; unichar ZX_PDF417_PUNCTUATION[ZX_PDF417_PUNCTUATION_LEN]; const NSStringEncoding ZX_PDF417_DEFAULT_ENCODING = NSISOLatin1StringEncoding; @implementation ZXPDF417HighLevelEncoder + (void)initialize { if ([self class] != [ZXPDF417HighLevelEncoder class]) return; //Construct inverse lookups for (int i = 0; i < ZX_PDF417_MIXED_TABLE_LEN; i++) { ZX_PDF417_MIXED_TABLE[i] = 0xFF; } for (int8_t i = 0; i < sizeof(ZX_PDF417_TEXT_MIXED_RAW) / sizeof(int8_t); i++) { int8_t b = ZX_PDF417_TEXT_MIXED_RAW[i]; if (b > 0) { ZX_PDF417_MIXED_TABLE[b] = i; } } for (int i = 0; i < ZX_PDF417_PUNCTUATION_LEN; i++) { ZX_PDF417_PUNCTUATION[i] = 0xFF; } for (int8_t i = 0; i < sizeof(ZX_PDF417_TEXT_PUNCTUATION_RAW) / sizeof(int8_t); i++) { int8_t b = ZX_PDF417_TEXT_PUNCTUATION_RAW[i]; if (b > 0) { ZX_PDF417_PUNCTUATION[b] = i; } } } + (NSString *)encodeHighLevel:(NSString *)msg compaction:(ZXPDF417Compaction)compaction encoding:(NSStringEncoding)encoding error:(NSError **)error { //the codewords 0..928 are encoded as Unicode characters NSMutableString *sb = [NSMutableString stringWithCapacity:msg.length]; if (encoding == 0) { encoding = ZX_PDF417_DEFAULT_ENCODING; } else if (ZX_PDF417_DEFAULT_ENCODING != encoding) { ZXCharacterSetECI *eci = [ZXCharacterSetECI characterSetECIByEncoding:encoding]; if (![self encodingECI:eci.value sb:sb error:error]) { return nil; } } NSUInteger len = msg.length; int p = 0; int textSubMode = ZX_PDF417_SUBMODE_ALPHA; // User selected encoding mode ZXByteArray *bytes = nil; //Fill later and only if needed if (compaction == ZXPDF417CompactionText) { [self encodeText:msg startpos:p count:(int)len buffer:sb initialSubmode:textSubMode]; } else if (compaction == ZXPDF417CompactionByte) { bytes = [self bytesForMessage:msg encoding:encoding]; [self encodeBinary:bytes startpos:p count:(int)msg.length startmode:ZX_PDF417_BYTE_COMPACTION buffer:sb]; } else if (compaction == ZXPDF417CompactionNumeric) { [sb appendFormat:@"%C", (unichar) ZX_PDF417_LATCH_TO_NUMERIC]; [self encodeNumeric:msg startpos:p count:(int)len buffer:sb]; } else { int encodingMode = ZX_PDF417_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) ZX_PDF417_LATCH_TO_NUMERIC]; encodingMode = ZX_PDF417_NUMERIC_COMPACTION; textSubMode = ZX_PDF417_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 != ZX_PDF417_TEXT_COMPACTION) { [sb appendFormat:@"%C", (unichar) ZX_PDF417_LATCH_TO_TEXT]; encodingMode = ZX_PDF417_TEXT_COMPACTION; textSubMode = ZX_PDF417_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 encoding:encoding]; } 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 == ZX_PDF417_TEXT_COMPACTION) { //Switch for one byte (instead of latch) [self encodeBinary:bytes startpos:p count:1 startmode:ZX_PDF417_TEXT_COMPACTION buffer:sb]; } else { //Mode latch performed by encodeBinary [self encodeBinary:bytes startpos:p count:b startmode:encodingMode buffer:sb]; encodingMode = ZX_PDF417_BYTE_COMPACTION; textSubMode = ZX_PDF417_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. * * @param msg the message * @param startpos the start position within the message * @param count the number of characters to encode * @param sb receives the encoded codewords * @param initialSubmode should normally be SUBMODE_ALPHA * @return the text submode in which this method ends */ + (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 ZX_PDF417_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 = ZX_PDF417_SUBMODE_LOWER; [tmp appendFormat:@"%C", (unichar) 27]; //ll continue; } else if ([self isMixed:ch]) { submode = ZX_PDF417_SUBMODE_MIXED; [tmp appendFormat:@"%C", (unichar) 28]; //ml continue; } else { [tmp appendFormat:@"%C", (unichar) 29]; //ps [tmp appendFormat:@"%C", ZX_PDF417_PUNCTUATION[ch]]; break; } } break; case ZX_PDF417_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 = ZX_PDF417_SUBMODE_MIXED; [tmp appendFormat:@"%C", (unichar) 28]; //ml continue; } else { [tmp appendFormat:@"%C", (unichar) 29]; //ps [tmp appendFormat:@"%C", ZX_PDF417_PUNCTUATION[ch]]; break; } } break; case ZX_PDF417_SUBMODE_MIXED: if ([self isMixed:ch]) { [tmp appendFormat:@"%C", ZX_PDF417_MIXED_TABLE[ch]]; //as } else { if ([self isAlphaUpper:ch]) { submode = ZX_PDF417_SUBMODE_ALPHA; [tmp appendFormat:@"%C", (unichar) 28]; //al continue; } else if ([self isAlphaLower:ch]) { submode = ZX_PDF417_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 = ZX_PDF417_SUBMODE_PUNCTUATION; [tmp appendFormat:@"%C", (unichar) 25]; //pl continue; } } [tmp appendFormat:@"%C", (unichar) 29]; //ps [tmp appendFormat:@"%C", ZX_PDF417_PUNCTUATION[ch]]; } } break; default: //ZX_PDF417_SUBMODE_PUNCTUATION if ([self isPunctuation:ch]) { [tmp appendFormat:@"%C", ZX_PDF417_PUNCTUATION[ch]]; } else { submode = ZX_PDF417_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. * * @param bytes the message converted to a byte array * @param startpos the start position within the message * @param count the number of bytes to encode * @param startmode the mode from which this method starts * @param sb receives the encoded codewords */ + (void)encodeBinary:(ZXByteArray *)bytes startpos:(int)startpos count:(int)count startmode:(int)startmode buffer:(NSMutableString *)sb { if (count == 1 && startmode == ZX_PDF417_TEXT_COMPACTION) { [sb appendFormat:@"%C", (unichar) ZX_PDF417_SHIFT_TO_BYTE]; } else { BOOL sixpack = ((count % 6) == 0); if (sixpack) { [sb appendFormat:@"%C", (unichar) ZX_PDF417_LATCH_TO_BYTE]; } else { [sb appendFormat:@"%C", (unichar) ZX_PDF417_LATCH_TO_BYTE_PADDED]; } } int idx = startpos; // Encode sixpacks if (count >= 6) { const int charsLen = 5; unichar chars[charsLen]; memset(chars, 0, charsLen * sizeof(unichar)); while ((startpos + count - idx) >= 6) { long long t = 0; for (int i = 0; i < 6; i++) { t <<= 8; t += bytes.array[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) for (int i = idx; i < startpos + count; i++) { int ch = bytes.array[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]; NSDecimalNumber *num900 = [NSDecimalNumber decimalNumberWithDecimal:[[NSNumber numberWithInt:900] decimalValue]]; NSDecimalNumber *num0 = [NSDecimalNumber decimalNumberWithDecimal:[[NSNumber numberWithInt:0] decimalValue]]; while (idx < count) { [tmp setString:@""]; int len = MIN(44, count - idx); NSString *part = [@"1" stringByAppendingString:[msg substringWithRange:NSMakeRange(startpos + idx, len)]]; NSDecimalNumber *bigint = [NSDecimalNumber decimalNumberWithString:part]; do { NSRoundingMode roundingMode = ((bigint.floatValue < 0) ^ (num900.floatValue < 0)) ? NSRoundUp : NSRoundDown; NSDecimalNumber *quotient = [bigint decimalNumberByDividingBy:num900 withBehavior:[NSDecimalNumberHandler decimalNumberHandlerWithRoundingMode:roundingMode scale:0 raiseOnExactness:NO raiseOnOverflow:NO raiseOnUnderflow:NO raiseOnDivideByZero:NO]]; NSDecimalNumber *subtractAmount = [quotient decimalNumberByMultiplyingBy:num900]; NSDecimalNumber *remainder = [bigint decimalNumberBySubtracting:subtractAmount]; [tmp appendFormat:@"%C", (unichar)[remainder longValue]]; bigint = quotient; } while (![bigint isEqualToNumber:num0]); //Reverse temporary string for (int i = (int)tmp.length - 1; i >= 0; i--) { [sb appendFormat:@"%C", [tmp characterAtIndex:i]]; } idx += len; } } + (BOOL)isDigit:(unichar)ch { return ch >= '0' && ch <= '9'; } + (BOOL)isAlphaUpper:(unichar)ch { return ch == ' ' || (ch >= 'A' && ch <= 'Z'); } + (BOOL)isAlphaLower:(unichar)ch { return ch == ' ' || (ch >= 'a' && ch <= 'z'); } + (BOOL)isMixed:(unichar)ch { return ZX_PDF417_MIXED_TABLE[ch] != 0xFF; } + (BOOL)isPunctuation:(unichar)ch { return ZX_PDF417_PUNCTUATION[ch] != 0xFF; } + (BOOL)isText:(unichar)ch { return ch == '\t' || ch == '\n' || ch == '\r' || (ch >= 32 && ch <= 126); } /** * Determines the number of consecutive characters that are encodable using numeric compaction. * * @param msg the message * @param startpos the start position within the message * @return the requested character count */ + (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. * * @param msg the message * @param bytes the message converted to a byte array * @param startpos the start position within the message * @return the requested character count */ + (int)determineConsecutiveBinaryCount:(NSString *)msg bytes:(ZXByteArray *)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; } 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.array[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; } + (BOOL)encodingECI:(int)eci sb:(NSMutableString *)sb error:(NSError **)error { if (eci >= 0 && eci < 900) { [sb appendFormat:@"%C", (unichar) ZX_PDF417_HIGH_LEVEL_ECI_CHARSET]; [sb appendFormat:@"%C", (unichar) eci]; } else if (eci < 810900) { [sb appendFormat:@"%C", (unichar) ZX_PDF417_HIGH_LEVEL_ECI_GENERAL_PURPOSE]; [sb appendFormat:@"%C", (unichar) (eci / 900 - 1)]; [sb appendFormat:@"%C", (unichar) (eci % 900)]; } else if (eci < 811800) { [sb appendFormat:@"%C", (unichar) ZX_PDF417_HIGH_LEVEL_ECI_USER_DEFINED]; [sb appendFormat:@"%C", (unichar) (810900 - eci)]; } else { NSDictionary *userInfo = @{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"ECI number not in valid range from 0..811799, but was %d", eci]}; if (error) *error = [[NSError alloc] initWithDomain:ZXErrorDomain code:ZXWriterError userInfo:userInfo]; return NO; } return YES; } + (ZXByteArray *)bytesForMessage:(NSString *)msg encoding:(NSStringEncoding)encoding { NSData *data = [msg dataUsingEncoding:encoding]; int8_t *bytes = (int8_t *)[data bytes]; ZXByteArray *byteArray = [[ZXByteArray alloc] initWithLength:(unsigned int)[data length]]; memcpy(byteArray.array, bytes, [data length] * sizeof(int8_t)); return byteArray; } @end