2

I need to make a call to a 3rd party SOAP web service from Apex that accepts images encoded in base85 rather than the more standard base64. I don't see that this is supported in Salesforce, any ideas on how to implement this?

1 Answer 1

5

Unfortunately for you, there's not much that can be done here aside from checking to see if someone else has already implemented a Base85Encode method (a cursory google reveals that nobody has done this...yet) or implementing your own.

Given that you're working with image data, rather than a string, your data in Salesforce is probably a Base64 encoded Blob (Binary Large OBject). It's very likely that non-text data will not be a valid UTF-8 string, so trying to use EncodingUtil.base64Decode() is not going to work.

So, that means the order of operations here is going to look something like this...

  • Decode from Base64

    1. Iterate over your base64 encoded string
    2. Turn each base64 character into its equivalent 6-bit integer value
    3. Take 4 6-bit numbers, and turn them into a 24-bit number (using some simple bit shifting and addition)
    4. Take each 24-bit number, and break them into 3, 8-bit numbers (more bit shifting, and bitwise-and)
  • Encode to Base85

    1. Take 4, 8-bit numbers, and combine them into a single 32-bit number (more bit shifting and adding)
    2. Convert the 32-bit number into 5 Base85 numbers
    3. Add 33 to eachBase85 number, and then add that to a List<Integer>
    4. Convert the List<Integer> into an ascii string

That algorithm can probably be improved upon, more directly taking the 6-bit numbers from Base64 to 32-bit numbers, but I think that'd make the inner workings more difficult to reason about.

This seemed like an interesting diversion for me, so I had a look at the wikipedia pages for Base64 and Base85 and came up with the following:

// We can't just take the ascii value of each Base64 value.
// We need this map for building the 24-bit values that later get broken up
//   into 8-bit values
Map<String, Integer> b64To6bit = new Map<String, Integer>{
  'A' => 0, 'B' => 1, 'C' => 2, 'D' => 3, 'E' => 4, 'F' => 5, 'G' => 6, 'H' => 7, 'I' => 8, 'J' => 9, 'K' => 10, 'L' => 11, 'M' => 12, 'N' => 13, 'O' => 14, 'P' => 15, 'Q' => 16, 'R' => 17, 'S' => 18, 'T' => 19, 'U' => 20, 'V' => 21, 'W' => 22, 'X' => 23, 'Y' => 24, 'Z' => 25,
  'a' => 26, 'b' => 27, 'c' => 28, 'd' => 29, 'e' => 30, 'f' => 31, 'g' => 32, 'h' => 33, 'i' => 34, 'j' => 35, 'k' => 36, 'l' => 37, 'm' => 38, 'n' => 39, 'o' => 40, 'p' => 41, 'q' => 42, 'r' => 43, 's' => 44, 't' => 45, 'u' => 46, 'v' => 47, 'w' => 48, 'x' => 49, 'y' => 50, 'z' => 51,
  '0' => 52, '1' => 53, '2' => 54, '3' => 55, '4' => 56, '5' => 57, '6' => 58, '7' => 59, '8' => 60, '9' => 61, '+' => 62, '/' => 63
};

// The common test string for this type of work.
// I needed a known Base85 string to test this implementation against
String testStr = 'Man is distinguished, not only by his reason, but by this singular passion from other animals, which is a lust of the mind, that by a perseverance of delight in the continued and indefatigable generation of knowledge, exceeds the short vehemence of any carnal pleasure.';
String b64 = EncodingUtil.base64Encode(Blob.valueOf(testStr));

List<Integer> decoded8bit = new List<Integer>();

// Every 4 characters combines to give us a 24-bit value from Base64
for(Integer index = 0; index < b64.length(); index += 4){
    Integer workingInt = 0;
    Integer endingBitShift = 0;

    for(Integer i = 0; i < 4; i++){
        // Shifting left by 6 bits + adding a 6-bit number means that we don't
        //   interact with the previous 6-bit number (i.e. the addition only
        //   sets the LSBs)

        // I chose to shift first, then add so that I don't need to worry about
        //   shifting right after we exit this for loop.
        // Since the initial value of workingInt is 0, and shifting left adds
        //   0s to the LSBs, doing this does not adversely affect us
        workingInt <<= 6;

        if(b64.charAt(index + i) != 61){
            workingInt += b64To6bit.get(b64.subString(index + i, index + i + 1));
        }else{
            // for the padding, we simply want to shift bits

            // each padding character at the end decreases our number of 
            //   8-bit int outputs by 1
            endingBitShift += 8;
        }
    }

    // Take our 24-bit int, and divide it into 3, 8-bit integers
    //   (unless we're at the end of the base64 string, where we can
    //    expect fewer 8-bit integers)

    // We want to add 8-bit numbers to the list from left-to-right (MSB to LSB)
    // So, we start by shifting 16 bits, then 8, then 0
    for(Integer i = 16; i >= endingBitShift; i -= 8){
        // Shifting by 8 and 0 bits means we can have non-zero bits beyond bit # 8
        // Bitwise-and with 255 ensures we clear out the higher-order bits
        // Since we need to reuse workingInt, I use the "shift right" operator
        //   instead of the "shift right and assign" operator
        Integer workingInt2 = (workingInt >> i) & 255;
        decoded8bit.add(workingInt2);
    }
}

// Bog-standard Base85 introduces an offset of 33 to get us past the non-printable
//   ascii characters
Integer base85Offset = 33;

// Each 32-bit number is going to give us a 5 'digit' number in Base85
// We offset-ascii encode each 'digit', so to get each 'digit' we'll need
//   to divide by the relevant power of 85.
// Store this in a list since Math.pow() takes a good bit of time to run, and
//   we'll be re-using these numbers over and over
List<Integer> powersOf85 = new List<Integer>{Integer.valueOf(Math.pow(85, 4)), Integer.valueOf(Math.pow(85, 3)), Integer.valueOf(Math.pow(85, 2)), 85};

List<Integer> outputCharsList = new List<Integer>();
Integer numPads = 0;
for(Integer i = 0; i < decoded8bit.size(); i += 4){
    Integer runningTotal = 0;
    Integer full32BitNum = 0;

    // Again, shifting + adding to turn 4 8-bit integers into a single 32-bit
    //   integer
    for(Integer j = 0; j < 4; j++){
        full32BitNum <<= 8;
        if(i + j >= decoded8bit.size()){
            numPads++;
        }else{
            full32BitNum += decoded8Bit[i + j];
        }
    }

    // Base85 specifies a 32-bit 0 be encoded to 'z' instead of '!!!!!'
    if(full32BitNum == 0){
        outputCharsList.add(122);
        continue;
    }

    // Converting a 32-bit number to a Base85 representation means figuring out
    //   how many times (X) 85^5 can evenly divide that number, subtracting
    //   X * 85^5 from the 32-bit number, decreasing the exponent, and 
    //   repeating until we get to 85^0
    for(Integer j :powersOf85){
        Integer flooredMultiple = Integer.valueOf(Math.floor((full32BitNum - runningTotal) / j));
        runningTotal += flooredMultiple * j;

        outputCharsList.add(flooredMultiple + base85Offset);
    }

    // The last output character of each group is the difference between the full 32 bit number
    //   and our running total
    outputCharsList.add(full32BitNum - runningTotal + base85Offset);
}

// Thankfully, Salesforce does provide a way for us to get a string out of a List<Integer>
String base85Result = String.fromCharArray(outputCharsList).subString(0, outputCharsList.size() - numPads);
system.debug(base85Result);

The variable names are...not great, and I haven't rigorously tested the code, but it should serve as a good base.

1
  • Thanks for this. I will update my experiences if we go ahead and implement this.
    – scottrm
    Oct 29, 2018 at 11:41

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .