1

I want to implement the PackBits algorithm.

The background is that I am writing some code for an ONVIF camera. I want to compress a pattern/string of 1's and 0's with PackBits, and I also want to decode an existing packed string.

JavaScript has my preference, but C, PHP or similar will do too.

I have been looking for some examples, but couldn't find any.

How can I implement the PackBits algorithm?

trincot
  • 317,000
  • 35
  • 244
  • 286
Ron
  • 51
  • 3
  • 1
    There is a [wikipedia entry](https://en.wikipedia.org/wiki/PackBits) with a similar name, is that the same algorithm? Maybe you have some example data? – harold Aug 28 '20 at 19:37
  • well, the data I have is very short. it's a packed string that is used for what cells are acive in an onvif camera. The OnVif programmers manual is pretty vague about it, but it seems that a cell is active "1" or not "0" 8 'bit's form a byte, and the resulting 'string' is packed with the "packbit" algorithm. That is pretty much what it says. The string I see in a camera is "zwA" (packed) probably 22x18 '1' assuming all cells are active – Ron Aug 28 '20 at 20:35
  • That doens't check out actually, ASCII `z` would be 122 so if it's the format we thought it was, that would mean 123 literal bytes follow, but they don't and there should be only 50 (maybe? that's from 22x18/8, there might be padding) encoded bytes – harold Aug 28 '20 at 20:51
  • that is one of the things I was wondering about. I use the firefox restclient to send soap/xml and it returns this: " " I might try the "onvifconfigtool" to see what it says – Ron Aug 28 '20 at 20:54
  • OK I think I have it: that's base64-encoded packbits. zwA decodes first to cf 00 (two bytes in hex), and they mean "50 times literal" and the literal is zero – harold Aug 28 '20 at 21:04
  • Hmm ok, here is another one I found in a camera: "0P8A8A==" "base64 encode packbits" is that the 'regular' 'run of the mill base64" ? Because I can just use base64encode and base 64 decode for that – Ron Aug 28 '20 at 21:07
  • Yes it decodes to d0 ff 00 f0 (which "unpackbits" to 49 times FF and one F0 so it would have the first 22x18 bits set), but some online decoders cannot show binary output – harold Aug 28 '20 at 21:08
  • cool, glad that's figured out, thank you for that. Basically what I am doing is make an interactive "page" to activate/de-activate cells. and also, write something that can de-ativate depending on wind or so (waving trees). If it is "regular" base64, that is easy to find (I think) if not, I have some C code somewhere that does. thanks! – Ron Aug 28 '20 at 21:17
  • so.. how does "d0 ff 00 f0 " 'unpack' to 49 times FF and 1 F0 ? is that still the "packbits" algorithm, in this case the unpack? – Ron Aug 29 '20 at 23:28
  • Yes they're both used. Packbits for compression, then base64-encoding is used to get a valid XML string (XML can't embed binary data directly). – harold Aug 29 '20 at 23:34
  • ah.. ok, now I see. So I still need to find a packbits and unpackbits function I think I saw something mentioned "below". – Ron Aug 30 '20 at 16:00

2 Answers2

0

The Wikipedia page for PackBits algorithm has a JS implementation example and other links. It has the following comment which includes a JSFiddle as well:

/**
 * Helper functions to create readable input and output
 * 
 * Also, see this fiddle for interactive PackBits decoder:
 * https://jsfiddle.net/volter9/tj04ejdt/
 */

A PackBits data stream consists of packets with a one-byte header followed by data. The header is a signed byte; the data can be signed, unsigned, or packed (such as MacPaint pixels).

In the following table, n is the value of the header byte as a signed integer.

Header byte   Data following the header byte
 0 to 127     (1 + n) literal bytes of data
-1 to -127    One byte of data, repeated (1 – n) times in the decompressed output
  -128        No operation (skip and treat next byte as a header byte)

It is a fairly simple runlength encoding algorithm.

See: https://github.com/fiahfy/packbits for an implementation in Typescript.

https://en.wikipedia.org/wiki/PackBits

vvg
  • 1,010
  • 7
  • 25
  • I actually found an unpack algorithm through there... I am looking for the pack algorithm itself too – Ron Aug 28 '20 at 20:15
  • I think I misread some Onvif info, it said it was "packbits" but earlier, up here, Harold figured out that it was base64. What I am trying to do is: A camera has 22x18 so called "active cells", for detecting motion. The cells can be turned on and off. by either a 1 or a 0, a 'string' of them, in 4 bytes. Then the onvif documentation says those 4 bytes are 'packed' with a"packbits" algorithm. The encoded string I get from the camera is "0P8A8A==" which should 'decode' into a string with 22x18 times a 1, since it is safe to assume all cells are active. – Ron Aug 29 '20 at 22:57
  • @Ron You mentioned it's "safe to assume all cells are active" That would only be true if the there's movement in all of the camera image. For an 8x8 grid there would be 64 cells going left to right one row at a time. Note that if there are insufficient cells then the remaining cells would be inactive. – Andy Gee Jan 18 '22 at 08:09
0

You can use the following pack/unpack functions. I added a demo with the example data on Wikipedia:

const pack = s =>
    s.match(/(.)\1{1,127}|(?:(.)(?!\2)){1,128}/gs).map(s =>
        s[0] === s[1] ? String.fromCharCode(257-s.length) + s[0]
                      : String.fromCharCode(s.length-1) + s
    ).join("");

function unpack(s) {
    let res = "";
    let i = 0;
    while (i < s.length) {
        let hdr = s.charCodeAt(i++);
        res += hdr > 128 ? s[i++].repeat(257 - hdr)
             : hdr < 128 ? s.slice(i, i += hdr+1)
             : "";
    }
    return res;
}

const toHex = s => Array.from(s, c => c.charCodeAt().toString(16).padStart(2, "0")).join(" ");

// Demo
let s = "\xAA\xAA\xAA\x80\x00\x2A\xAA\xAA\xAA\xAA\x80\x00\x2A\x22\xAA\xAA\xAA\xAA\xAA\xAA\xAA\xAA\xAA\xAA";
console.log(toHex(s));

let packed = pack(s);
console.log(toHex(packed));

let unpacked = unpack(packed);
console.log(toHex(unpacked));

console.log(s === unpacked ? "OK" : "Mismatch!");

Note that the code on Wikipedia does not deal with the header value (128) according to the specification. A header value of 128, which as a signed byte is -128, should be skipped -- it has no payload.

As you mentioned base64 encoding in comments: JavaScript has atob and btoa functions for that purpose.

Without s flag, with a base64 encoded example

If you don't have support for the s flag for regexes, then in the regex replace each . with [^].

In this version I use that regular expression, and include an example that uses a base64 encoded string (the one you mention in comments):

const pack = s =>
    s.match(/([^])\1{1,127}|(?:([^])(?!\2)){1,128}/g).map(s =>
        s[0] === s[1] ? String.fromCharCode(257-s.length) + s[0]
                      : String.fromCharCode(s.length-1) + s
    ).join("");

function unpack(s) {
    let res = "";
    let i = 0;
    while (i < s.length) {
        let hdr = s.charCodeAt(i++);
        res += hdr > 128 ? s[i++].repeat(257 - hdr)
             : hdr < 128 ? s.slice(i, i += hdr+1)
             : "";
    }
    return res;
}

const toHex = s => Array.from(s, c => c.charCodeAt().toString(16).padStart(2, "0")).join(" ");

// Demo
let base64encoded = "0P8A8A==";
console.log("packed, base64 encoded input: ", base64encoded);

let s = atob(base64encoded);
console.log("base64 decoded: ", toHex(s));

let unpacked = unpack(s);
console.log("unpacked: ", toHex(unpacked));
trincot
  • 317,000
  • 35
  • 244
  • 286
  • That is helpful, thank you. I tried running it by using "run code snippet" but it says: "message": "SyntaxError: invalid regular expression flag s", – Ron Aug 30 '20 at 16:20
  • Flag `s` is supported in EcmaScript2020, and latest versions of Chrome, Firefox, and Edge support this flag. If you don't have that support, replace each `.` in the regular expression with `[^]`, and then you don't need the `s` flag. – trincot Aug 30 '20 at 16:33
  • Also added as a snippet in my answer now. – trincot Aug 30 '20 at 16:43
  • Are you there?? – trincot Aug 30 '20 at 17:39
  • I am here now... Just tried it, that seems to work. I am running the browser mostly on linux.. thanks. I'll try that code and see if I can stitch it all together. – Ron Aug 30 '20 at 20:57
  • I combine the base64 encode and decode and start with "0P8A8A==" which is a value that comes from a camera. (it's the active cells for motion detection, there are 22x18 cells.) "0P8A8A==" --base64decode--> "d0 ff 00 f0" --unpack--> 0 ff 00 f0. what I'd expect something that upacks into being interpreted as 22x18 bits, set to 1 (at this point all cells are active, represented as a 1, that is compressed with packbits into "d0 ff 00 f0" which is base64 encoded to "0P8A8A==". What is not clear to me is how to interpret what comes out of the unpacking. – Ron Aug 30 '20 at 21:49
  • You are right that base64 decoding gives `D0 FF 00 D0`, but unpacking that does not give `0 FF 00 F0` (how did you arrive at that?). Unpacking gives 50 bytes, which all except the last byte are FF. The last byte is F0. If you count the 1-bits in those 50 bytes, you get 49x8 + 4 = 396. This happens to be exactly 22x18. – trincot Aug 30 '20 at 21:58
  • I updated the final snippet in my answer to run on this input. You can see where you have a different result. – trincot Aug 30 '20 at 22:06
  • I used the unpack function, from the code snippet. I do something like value = unpack(packedValue); which is actually value = unpack("D0 FF 00 D0"); and if I display that I see "0 FF 00", but yes I did expect a 'string' of 50 bytes, the first 49 being FF all bits set, except for the last byte. – Ron Aug 30 '20 at 22:12
  • ok, I have it now. I had a 'toHex' in the wrong spot, only need that for printing/logging the string. Thanks!! seems it is working now – Ron Aug 30 '20 at 22:19
  • Yes I found it, I was using a toHex in the wrong spot. it is working now. I also changed/added a toBin, by adapting the toHex, that way it is easier to loop through an array of these cells used for motion detection. Thanks!! I really appreciate it. – Ron Aug 30 '20 at 22:28