9

I have an NSData object. I need to convert its bytes to a string and send as JSON. description returns hex and is unreliable (according to various SO posters). So I'm looking at code like this:

NSUInteger len = [imageData length];
Byte *byteData = (Byte*)malloc(len);
[imageData getBytes:&byteData length:len];

How do I then send byteData as JSON? I want to send the raw bytes.

CODE:

NSString *jsonBase64 = [imageData base64EncodedString];
NSLog(@"BASE 64 FINGERPRINT: %@", jsonBase64);
NSData *b64 = [NSData dataFromBase64String:jsonBase64];
NSLog(@"Equal: %d", [imageData isEqualToData:b64]);
NSLog(@"b64: %@", b64);
NSLog(@"original: %@", imageData);
NSString *decoded = [[NSString alloc] initWithData:b64 encoding:NSUTF8StringEncoding];
NSLog(@"decoded: %@", decoded);

I get values for everything except for the last line - decoded. Which would indicate to me that the raw bytes are not formatted in NSUTF8encoding?

quantumpotato
  • 9,637
  • 14
  • 70
  • 146

4 Answers4

6

Have you tried using something like this:

@implementation NSData (Base64)
- (NSString *)base64EncodedString
{
    return [self base64EncodedStringWithWrapWidth:0];
}

This will turn your NSData in a base64 string, and on the other side you just need to decode it.

EDIT: @Lucas said you can do something like this:

NSString *myString = [[NSString alloc] initWithData:myData encoding:NSUTF8StringEncoding];

but i had some problem with this method because of some special characters, and because of that i started using base64 strings for communication.

EDIT3: Trys this method base64EncodedString

    @implementation NSData (Base64)

    - (NSString *)base64EncodedString
    {
        return [self base64EncodedStringWithWrapWidth:0];
    }

    //Helper Method
    - (NSString *)base64EncodedStringWithWrapWidth:(NSUInteger)wrapWidth
    {
        //ensure wrapWidth is a multiple of 4
        wrapWidth = (wrapWidth / 4) * 4;

        const char lookup[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

        long long inputLength = [self length];
        const unsigned char *inputBytes = [self bytes];

        long long maxOutputLength = (inputLength / 3 + 1) * 4;
        maxOutputLength += wrapWidth? (maxOutputLength / wrapWidth) * 2: 0;
        unsigned char *outputBytes = (unsigned char *)malloc((NSUInteger)maxOutputLength);

        long long i;
        long long outputLength = 0;
        for (i = 0; i < inputLength - 2; i += 3)
        {
            outputBytes[outputLength++] = lookup[(inputBytes[i] & 0xFC) >> 2];
            outputBytes[outputLength++] = lookup[((inputBytes[i] & 0x03) << 4) | ((inputBytes[i + 1] & 0xF0) >> 4)];
            outputBytes[outputLength++] = lookup[((inputBytes[i + 1] & 0x0F) << 2) | ((inputBytes[i + 2] & 0xC0) >> 6)];
            outputBytes[outputLength++] = lookup[inputBytes[i + 2] & 0x3F];

            //add line break
            if (wrapWidth && (outputLength + 2) % (wrapWidth + 2) == 0)
            {
                outputBytes[outputLength++] = '\r';
                outputBytes[outputLength++] = '\n';
            }
        }

        //handle left-over data
        if (i == inputLength - 2)
        {
            // = terminator
            outputBytes[outputLength++] = lookup[(inputBytes[i] & 0xFC) >> 2];
            outputBytes[outputLength++] = lookup[((inputBytes[i] & 0x03) << 4) | ((inputBytes[i + 1] & 0xF0) >> 4)];
            outputBytes[outputLength++] = lookup[(inputBytes[i + 1] & 0x0F) << 2];
            outputBytes[outputLength++] =   '=';
        }
        else if (i == inputLength - 1)
        {
            // == terminator
            outputBytes[outputLength++] = lookup[(inputBytes[i] & 0xFC) >> 2];
            outputBytes[outputLength++] = lookup[(inputBytes[i] & 0x03) << 4];
            outputBytes[outputLength++] = '=';
            outputBytes[outputLength++] = '=';
        }

        if (outputLength >= 4)
        {
            //truncate data to match actual output length
            outputBytes = realloc(outputBytes, (NSUInteger)outputLength);
            return [[NSString alloc] initWithBytesNoCopy:outputBytes
                                                  length:(NSUInteger)outputLength
                                                encoding:NSASCIIStringEncoding
                                            freeWhenDone:YES];
        }
        else if (outputBytes)
        {
            free(outputBytes);
        }
        return nil;
    }
rob180
  • 901
  • 1
  • 9
  • 29
6
  1. The reason the String is being considered 'unreliable' in previous Stack posts is because they too were attempting to use NSData objects where the ending bytes aren't properly terminated with NULL :

    NSString *jsonString = [NSString stringWithUTF8String:[nsDataObj bytes]];
    // This is unreliable because it may result in NULL string values
    
  2. Whereas the example below should give you your desired results because the NSData byte string will terminate correctly:

    NSString *jsonString = [[NSString alloc]  initWithBytes:[nsDataObj bytes] length:[nsDataObj length] encoding: NSUTF8StringEncoding];
    

You were on the right track and hopefully this is able to help you solve your current problem. Best of luck!

~ EDIT ~

Make sure you are declaring your NSData Object from an image like so:

NSData *imageData = [[NSData alloc] init];
imageData = UIImagePNGRepresentation(yourImage);
ChrisHaze
  • 2,800
  • 16
  • 20
  • 1
    I will give this a shot. – quantumpotato Aug 11 '15 at 13:54
  • 1
    Hey @chrishaze, I get a nil string from this :( – quantumpotato Aug 14 '15 at 16:03
  • 1
    are you converting your image to an NSData object? Check my newest edit for details. – ChrisHaze Aug 14 '15 at 16:26
  • 1
    Hey, I'm actually getting it directly from a hardware API callback: -(void)fingerprintImageEvent:(NSData *)imageData imageType:(int)imageType numRows:(int)numRows numColumns:(int)numColumns { I actually am able to view an image by doing UIImage *image = [[GRGrabba sharedGrabba].fingerprint getUIImageFromImageData:imageData imageType:imageType numRows:numRows numColumns:numColumns];. But the API requires that I send the raw data up as the request body. So I'm trying to get that from the NSData *imageData – quantumpotato Aug 14 '15 at 16:41
  • I am not a registered dev with Grabba, but their Cordova plugin has a method: `convertImageToBase64` that seems to be built in. Check to see if there are any related methods for the iOS sdk? – ChrisHaze Aug 14 '15 at 17:07
3

Null termination is not the only problem when converting from NSData to NSString.

NSString is not designed to hold arbitrary binary data. It expects an encoding.

If your NSData contains an invalid UTF-8 sequence, initializing the NSString will fail.

The documentation isn't completely clear on this point, but for initWithData it says:

Returns nil if the initialization fails for some reason (for example if data does not represent valid data for encoding).

Also: The JSON specification defines a string as a sequence of Unicode characters.

That means even if you're able to get your raw data into a JSON string, parsing could fail on the receiving end if the code performs UTF-8 validation.

If you don't want to use Base64, take a look at the answers here.

Community
  • 1
  • 1
3

All code in this answer is pseudo-code fragments, you need to convert the algorithms into Objective-C or other language yourself.

Your question raises many questions... You start with:

I have an NSData object. I need to convert its bytes to a string and send as JSON. description returns hex and is unreliable (according to various SO posters).

This appears to suggest you wish to encode the bytes as a string, ready to decode them back to bytes the other end. If this is the case you have a number of choices, such as Base-64 encoding etc. If you want something simple you can just encode each byte as its two character hex value, pseudo code outline:

NSMutableString *encodedString = @"".mutableCopy;
foreach aByte in byteData
   [encodedString appendFormat:@"%02x", aByte];

The format %02x means two hexadecimal digits with zero padding. This results in a string which can be sent as JSON and decoded easily the other end. The byte size over the wire will probably be twice the byte length as UTF-8 is the recommended encoding for JSON over the wire.

However in response to one of the answer you write:

But I need absolutely the raw bits.

What do you mean by this? Is your receiver going to interpret the JSON string it gets as a sequence of raw bytes? If so you have a number of problems to address. JSON strings are a subset of JavaScript strings and are stored as UCS-2 or UTF-16, that is they are sequences of 16-bit values not 8-bit values. If you encode each byte into a character in a string then it will be represented using 16-bits, if your receiver can access the byte stream it has to skip ever other byte. Of course if you receiver accesses the strings a character at a time each 16-bit character can be truncated back to an 8-bit byte. Now you might think if you take this approach then each 8-bit byte can just be output as a character as part of a string, but that won't work. While all values 1-255 are valid Unicode character code points, and JavaScript/JSON allow NULs (0 value) in strings, not all those values are printable, you cannot put a double quote " into a string without escaping it, and the escape character is \ - all these will need to be encoded into the string. You'd end up with something like:

NSMutableString *encodedString = @"".mutableCopy;
foreach aByte in byteData
   if (isprint(aByte) && aByte != '"' && aByte != '\\')
       [encodedString appendFormat:@"%c", aByte];
   otherwise
       [encodedString appendFormat:@"\\u00%02x", aByte]; // JSON unicode escape sequence

This will produce a string which when parsed by a JSON decoder will give you one character (16-bits) for each byte, the top 8-bits being zero. However if you pass this string to a JSON encoder it will encode the unicode escape sequences, which are already encoded... So you really need to send this string over the wire yourself to avoid this...

Confused? Getting complicated? Well why are you trying to send binary byte data as a string? You never say what your high-level goal is or what, if anything, is known about the byte data (e.g. does it represent character in some encoding)

If this is really just an array of bytes then why not send it as JSON array of numbers - a byte is just a number in the range 0-255. To do this you would use code along the lines of:

NSMutableArray *encodedBytes = [NSMutableArray new];
foreach aByte in byteData
   [encodedBytes addObject:@(aByte)]; // add aByte as an NSNumber object

Now pass encodedBytes to NSJSONSerialisation and it will send a JSON array of numbers over the wire, the receiver will reverse the process packing each byte back into a byte buffer and you have you bytes back.

This method avoids all issues of valid strings, encodings and escapes.

HTH

CRD
  • 52,522
  • 5
  • 70
  • 86
  • I'm trying to send binary data because that is what the API requires. It's sent as the request body. – quantumpotato Aug 14 '15 at 16:44
  • Which API requires the binary data? If it is one on the receiver and you are trying to invoke it by passing the data as JSON then you simply need to encode the binary data *in some form* while it travels over the wire as part of the communication. You *do not need* to encode it as a string, but you may. Either the first solution, encoding as a hex or base-64 string, or the last solution, encoding as an array of numbers, will meet your needs. If the data is large you may want to pick base-64 as the simplest most compact method, but otherwise any will do. HTH – CRD Aug 14 '15 at 16:54
  • I note you say "sent as the request body", this suggests you are trying to communicate with a web service and have some specification of what it expects, as opposed to commincate with your own server based code which is calling some API. If this is the case you need to give more details in the question about the API and what it expects - no API should ask for binary data in JSON without specifying how to encode it - JSON does not have binary data as a basic data type. – CRD Aug 14 '15 at 16:59