mirror of
https://github.com/zxing/zxing.git
synced 2024-11-11 13:34:08 -08:00
711e746900
git-svn-id: https://zxing.googlecode.com/svn/trunk@1869 59b500cc-1b3d-0410-9834-0bbf25fbcc57
436 lines
15 KiB
Objective-C
436 lines
15 KiB
Objective-C
//
|
|
// VCardResultParser.m
|
|
// ZXing
|
|
//
|
|
// Ported to Objective-C by George Nachman on 7/19/2011.
|
|
/*
|
|
* Copyright 2008 ZXing authors
|
|
*
|
|
* 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 "BusinessCardParsedResult.h"
|
|
#import "CBarcodeFormat.h"
|
|
#import "ResultParser.h"
|
|
#import "AddressBookAUResultParser.h"
|
|
#import "VCardResultParser.h"
|
|
#import "ArrayAndStringCategories.h"
|
|
|
|
@interface NSString (VCardResultParser)
|
|
|
|
// Extract a single VCard value from this with a given prefix.
|
|
- (NSString *)vcardValueForFieldWithPrefix:(NSString *)prefix;
|
|
|
|
// Extracts an array of VCard values from this string with a given prefix.
|
|
- (NSArray *)vcardValuesForFieldWithPrefix:(NSString *)prefix;
|
|
|
|
// Returns true if the string's value is a well-formed date.
|
|
- (BOOL)isVCardDate;
|
|
|
|
// Returns the starting index (or NSNotFound) of the given substring not
|
|
// before index |offset|.
|
|
- (NSUInteger)vcardIndexOf:(NSString*)substr startingAt:(int)offset;
|
|
|
|
// Assuming this string is in quoted printable in the character set |charset|,
|
|
// decode the QP encoding and convert to NSString's native character set.
|
|
- (NSString *)vcardStringFromQuotedPrintableWithCharset:(NSString *)charset;
|
|
|
|
// Strip out \r. Also strip out \n plus one character following it if possible.
|
|
- (NSString *)vcardStringWithoutContinuationCRLF;
|
|
|
|
// Split up key-value strings on = sign.
|
|
- (NSString *)vcardKeyComponent;
|
|
- (NSString *)vcardValueComponent;
|
|
@end
|
|
|
|
@interface NSArray (VCardResultParser)
|
|
|
|
// Reformat VCard names from a semicolon-delimited list to a human-readable
|
|
// name.
|
|
- (NSArray *)vcardArrayWithFormattedNames;
|
|
// Reformat VCard addresses from a semicolon-delimited list to a human-readable
|
|
// name.
|
|
- (NSArray *)vcardArrayWithFormattedAddresses;
|
|
|
|
@end
|
|
|
|
// Parses contact information formatted according to the VCard (2.1) format.
|
|
// This is not a complete implementation but should parse information as
|
|
// commonly encoded in 2D barcodes.
|
|
//
|
|
// Originally by Sean Owen. Adapted to Objective-C by George Nachman.
|
|
//
|
|
@implementation VCardResultParser
|
|
|
|
+ (void)load {
|
|
[ResultParser registerResultParserClass:self];
|
|
}
|
|
|
|
+ (ParsedResult *)parsedResultForString:(NSString *)rawText
|
|
format:(BarcodeFormat)format {
|
|
// Although we should insist on the raw text ending with "END:VCARD",
|
|
// there's no reason to throw out everything else we parsed just because
|
|
// this was omitted. In fact, Eclair is doing just that, and we can't parse
|
|
// its contacts without this leniency.
|
|
if (rawText == nil || ![rawText hasPrefix:@"BEGIN:VCARD"]) {
|
|
return nil;
|
|
}
|
|
NSArray *names = [[rawText vcardValuesForFieldWithPrefix:@"FN"]
|
|
stringArrayWithTrimmedWhitespace];
|
|
if ([names count] == 0) {
|
|
// If no display names found, look for regular name fields and format them
|
|
names = [[rawText vcardValuesForFieldWithPrefix:@"N"]
|
|
stringArrayWithTrimmedWhitespace];
|
|
}
|
|
names = [names vcardArrayWithFormattedNames];
|
|
NSArray *phoneNumbers = [[rawText vcardValuesForFieldWithPrefix:@"TEL"] stringArrayWithTrimmedWhitespace];
|
|
NSArray *emails = [[rawText vcardValuesForFieldWithPrefix:@"EMAIL"] stringArrayWithTrimmedWhitespace];
|
|
NSString *note = [rawText vcardValueForFieldWithPrefix:@"NOTE"];
|
|
NSArray *addresses = [[[rawText vcardValuesForFieldWithPrefix:@"ADR"] stringArrayWithTrimmedWhitespace]
|
|
vcardArrayWithFormattedAddresses];
|
|
NSString *org = [[rawText vcardValueForFieldWithPrefix:@"ORG"] stringWithTrimmedWhitespace];
|
|
NSString *birthday = [[rawText vcardValueForFieldWithPrefix:@"BDAY"]stringWithTrimmedWhitespace];
|
|
if (![birthday isVCardDate]) {
|
|
birthday = nil;
|
|
}
|
|
NSString *title = [[rawText vcardValueForFieldWithPrefix:@"TITLE"]stringWithTrimmedWhitespace];
|
|
NSString *url = [[rawText vcardValueForFieldWithPrefix:@"URL"] stringWithTrimmedWhitespace];
|
|
|
|
BusinessCardParsedResult *result =
|
|
[[[BusinessCardParsedResult alloc] init] autorelease];
|
|
|
|
if ([names count]) {
|
|
result.names = names;
|
|
}
|
|
if ([phoneNumbers count]) {
|
|
result.phoneNumbers = phoneNumbers;
|
|
}
|
|
if ([emails count]) {
|
|
result.emails = emails;
|
|
}
|
|
if ([note length]) {
|
|
result.note = note;
|
|
}
|
|
if ([addresses count]) {
|
|
result.addresses = addresses;
|
|
}
|
|
if ([org length]) {
|
|
result.organization = org;
|
|
}
|
|
if (birthday) {
|
|
result.birthday = birthday;
|
|
}
|
|
if ([title length]) {
|
|
result.jobTitle = title;
|
|
}
|
|
if ([url length]) {
|
|
result.url = url;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation NSString (VCardResultParser)
|
|
|
|
- (NSUInteger)vcardIndexOf:(NSString*)substr startingAt:(int)offset {
|
|
NSRange temp = NSMakeRange(offset, [self length] - offset);
|
|
NSRange r = [self rangeOfString:substr
|
|
options:0
|
|
range:temp];
|
|
return r.location;
|
|
}
|
|
|
|
- (NSString *)vcardKeyComponent {
|
|
NSUInteger equals = [self vcardIndexOf:@"=" startingAt:0];
|
|
if (equals != NSNotFound) {
|
|
return [self substringWithRange:NSMakeRange(0, equals)];
|
|
} else {
|
|
return nil;
|
|
}
|
|
}
|
|
|
|
- (NSString *)vcardValueComponent {
|
|
NSUInteger equals = [self vcardIndexOf:@"=" startingAt:0];
|
|
if (equals != NSNotFound) {
|
|
return [self substringWithRange:NSMakeRange(equals + 1,
|
|
[self length] - equals - 1)];
|
|
} else {
|
|
return nil;
|
|
}
|
|
}
|
|
|
|
- (NSDictionary*)parsedFieldMetadata {
|
|
NSArray *parts = [self componentsSeparatedByCharactersInSet:
|
|
[NSCharacterSet characterSetWithCharactersInString:@";:"]];
|
|
NSMutableDictionary *result = [NSMutableDictionary dictionary];
|
|
for (NSString *part in parts) {
|
|
NSString *key = [part vcardKeyComponent];
|
|
NSString *value = [part vcardValueComponent];
|
|
if (key && value) {
|
|
[result setObject:value forKey:key];
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
- (NSArray *)vcardValuesForFieldWithPrefix:(NSString *)prefix {
|
|
NSMutableArray *matches = [NSMutableArray array];
|
|
NSUInteger i = 0;
|
|
NSUInteger myLength = [self length];
|
|
unichar c;
|
|
while (i < myLength) {
|
|
i = [self vcardIndexOf:prefix startingAt:i];
|
|
if (i == NSNotFound) {
|
|
break;
|
|
}
|
|
|
|
if (i > 0 && [self characterAtIndex:i - 1] != '\n') {
|
|
// This didn't start a new token: we matched in the middle of
|
|
// something.
|
|
i++;
|
|
continue;
|
|
}
|
|
i += [prefix length]; // Skip past the prefix.
|
|
c = [self characterAtIndex:i];
|
|
if (c != ':' && c != ';') {
|
|
// What we found wasn't actually a prefix.
|
|
continue;
|
|
}
|
|
|
|
const NSUInteger metadataStart = i;
|
|
// Skip until we find a colon.
|
|
while ([self characterAtIndex:i] != ':') {
|
|
i++;
|
|
}
|
|
|
|
// Extract key-value metadata fields between the ; and the : after the
|
|
// prefix.
|
|
BOOL quotedPrintable = NO;
|
|
NSString *quotedPrintableCharset = @"ASCII";
|
|
if (i >= metadataStart + 1) {
|
|
NSString *metaData = [self substringWithRange:
|
|
NSMakeRange(metadataStart + 1,
|
|
i - (metadataStart + 1))];
|
|
NSDictionary *metaDataDict = [metaData parsedFieldMetadata];
|
|
NSString *encoding = [metaDataDict objectForKey:@"ENCODING"];
|
|
NSString *charset = [metaDataDict objectForKey:@"CHARSET"];
|
|
if ([encoding isEqualToString:@"QUOTED-PRINTABLE"]) {
|
|
quotedPrintable = YES;
|
|
if (charset) {
|
|
quotedPrintableCharset = charset;
|
|
}
|
|
}
|
|
}
|
|
|
|
i++; // Skip the colon.
|
|
|
|
const NSUInteger matchStart = i; // Found the start of a match here.
|
|
|
|
while ((i = [self vcardIndexOf:@"\n" startingAt:i]) != NSNotFound) {
|
|
if (i + 1 < [self length] &&
|
|
([self characterAtIndex:i + 1] == ' ' ||
|
|
[self characterAtIndex:i + 1] == '\t')) {
|
|
// If it's followed by a tab or space, ignore them.
|
|
i += 2;
|
|
} else if (quotedPrintable &&
|
|
i >= 2 &&
|
|
([self characterAtIndex:i-1] == '=' ||
|
|
[self characterAtIndex:i-2] == '=')) {
|
|
// Indicates this is a quoted-printable continuation so
|
|
// ignore the newline.
|
|
i++;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (i == NSNotFound) {
|
|
// No terminating character.
|
|
break;
|
|
} else if (i > matchStart) {
|
|
// Found a legal line. Add it to the output. i must be greater than
|
|
// 0 because matchStart is unsigned.
|
|
if ([self characterAtIndex:i-1] == '\r') {
|
|
i--; // Back up over \r if present.
|
|
}
|
|
NSUInteger rangeLength;
|
|
if (i >= matchStart) {
|
|
rangeLength = i - matchStart;
|
|
} else {
|
|
rangeLength = 0;
|
|
}
|
|
NSString *element = [self substringWithRange:
|
|
NSMakeRange(matchStart, rangeLength)];
|
|
if (quotedPrintable) {
|
|
element = [element vcardStringFromQuotedPrintableWithCharset:
|
|
quotedPrintableCharset];
|
|
} else {
|
|
element = [element vcardStringWithoutContinuationCRLF];
|
|
}
|
|
[matches addObject:element];
|
|
i++;
|
|
} else {
|
|
// Zero-length line.
|
|
i++;
|
|
}
|
|
}
|
|
|
|
return matches;
|
|
}
|
|
|
|
- (NSString *)vcardStringWithoutContinuationCRLF {
|
|
int length = [self length];
|
|
NSMutableString *result = [NSMutableString stringWithCapacity:length];
|
|
BOOL lastWasLF = NO;
|
|
for (int i = 0; i < length; i++) {
|
|
if (lastWasLF) {
|
|
lastWasLF = NO;
|
|
continue;
|
|
}
|
|
unichar c = [self characterAtIndex:i];
|
|
lastWasLF = NO;
|
|
switch (c) {
|
|
case '\n':
|
|
lastWasLF = YES;
|
|
break;
|
|
case '\r':
|
|
break;
|
|
default:
|
|
[result appendString:[NSString stringWithCharacters:&c
|
|
length:1]];
|
|
break;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
- (NSString *)vcardStringFromQuotedPrintableWithCharset:(NSString *)charset {
|
|
int length = [self length];
|
|
NSMutableData *temp = [NSMutableData dataWithCapacity:length];
|
|
for (int i = 0; i < length; i++) {
|
|
unichar c = [self characterAtIndex:i];
|
|
switch (c) {
|
|
case '\r':
|
|
case '\n':
|
|
break;
|
|
case '=':
|
|
if (i < length - 2) {
|
|
unichar nextChar = [self characterAtIndex:i+1];
|
|
if (nextChar == '\r' || nextChar == '\n') {
|
|
// Ignore, it's just a continuation symbol.
|
|
} else {
|
|
NSString *hexstr =
|
|
[self substringWithRange:NSMakeRange(i+1, 2)];
|
|
NSScanner *scanner =
|
|
[NSScanner scannerWithString:hexstr];
|
|
unsigned result;
|
|
if ([scanner scanHexInt:&result]) {
|
|
unsigned char parsedChar = result;
|
|
[temp appendBytes:&parsedChar length:1];
|
|
}
|
|
|
|
i += 2;
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
[temp appendBytes:&c length:1];
|
|
}
|
|
}
|
|
|
|
NSStringEncoding encoding;
|
|
encoding = CFStringConvertEncodingToNSStringEncoding(
|
|
CFStringConvertIANACharSetNameToEncoding((CFStringRef) charset));
|
|
return [[[NSString alloc] initWithData:temp
|
|
encoding:encoding] autorelease];
|
|
}
|
|
|
|
- (NSString *)vcardValueForFieldWithPrefix:(NSString *)prefix {
|
|
NSArray *values = [self vcardValuesForFieldWithPrefix:prefix];
|
|
return [values count] ? [values objectAtIndex:0] : @"";
|
|
}
|
|
|
|
- (BOOL)rangeIsDigits:(NSRange)range {
|
|
for (NSUInteger i = range.location;
|
|
i < range.location + range.length;
|
|
i++) {
|
|
unichar c = [self characterAtIndex:i];
|
|
if (c < '0' || c > '9') {
|
|
return NO;
|
|
}
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)isVCardDate {
|
|
// Not really sure this is true but matches practice
|
|
if ([self length] == 8 && [self rangeIsDigits:NSMakeRange(0, 8)]) {
|
|
// Matches YYYYMMDD
|
|
return YES;
|
|
} else if ([self length] == 10 &&
|
|
[self characterAtIndex:4] == '-' &&
|
|
[self characterAtIndex:7] == '-' &&
|
|
[self rangeIsDigits:NSMakeRange(0, 4)] &&
|
|
[self rangeIsDigits:NSMakeRange(5, 2)] &&
|
|
[self rangeIsDigits:NSMakeRange(8, 2)]) {
|
|
// Matches YYYY-MM-DD
|
|
return YES;
|
|
} else {
|
|
return NO;
|
|
}
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation NSArray (VCardResultParser)
|
|
|
|
- (NSArray *)vcardArrayWithFormattedAddresses {
|
|
NSMutableArray* result = [NSMutableArray array];
|
|
for (NSString* address in self) {
|
|
[result addObject:
|
|
[[address stringByReplacingOccurrencesOfString:@";"
|
|
withString:@" "]
|
|
stringWithTrimmedWhitespace]];
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// Formats name fields of the form "Public;John;Q.;Reverend;III" into a form
|
|
// like "Reverend John Q. Public III".
|
|
- (NSArray *)vcardArrayWithFormattedNames {
|
|
NSMutableArray *result = [NSMutableArray array];
|
|
for (NSString *name in self) {
|
|
NSArray *components = [name componentsSeparatedByString:@";"];
|
|
int newOrder[] = { 3, 1, 2, 0, 4 };
|
|
int numReorderItems = sizeof(newOrder) / sizeof(int);
|
|
NSMutableString *formattedName =
|
|
[NSMutableString stringWithCapacity:[name length]];
|
|
int n = [components count];
|
|
for (int i = 0; i < numReorderItems; i++) {
|
|
int j = newOrder[i];
|
|
if (n > j) {
|
|
[formattedName appendString:@" "];
|
|
[formattedName appendString:[components objectAtIndex:j]];
|
|
}
|
|
}
|
|
[result addObject:[formattedName stringWithTrimmedWhitespace]];
|
|
}
|
|
return result;
|
|
}
|
|
|
|
|
|
@end
|