Skip to content

Instantly share code, notes, and snippets.

@akabekobeko
Created November 20, 2017 12:12
Show Gist options
  • Save akabekobeko/3358ca68150c0318d53095ae622e2f0c to your computer and use it in GitHub Desktop.
Save akabekobeko/3358ca68150c0318d53095ae622e2f0c to your computer and use it in GitHub Desktop.
RLE for ICNS.
/**
* @file rle-icns.js
* @author akabeko
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
export default class RunLengthEncodingICNS {
/**
* Compress binary with ICNS RLE.
* I transplanted https://github.com/pornel/libicns/blob/master/src/icns_rle24.c to Node.
*
* @param {Array.<Number>} src Source binary (RGBA).
*
* @return {Array.<Number>} Compressed binary.
*
* @see http://icns.sourceforge.net/
* @see https://github.com/pornel/libicns/
*/
static pack (src) {
// Assumptions of what icns rle data is all about:
// A) Each channel is encoded indepenent of the next.
// B) An encoded channel looks like this:
// 0xRL 0xCV 0xCV 0xRL 0xCV - RL is run-length and CV is color value.
// C) There are two types of runs
// 1) Run of same value - high bit of RL is set
// 2) Run of differing values - high bit of RL is NOT set
// D) 0xRL also has two ranges
// 1) for set high bit RL, 3 to 130
// 2) for clr high bit RL, 1 to 128
// E) 0xRL byte is therefore set as follows:
// 1) for same values, RL = RL - 1
// 2) different values, RL = RL + 125
// 3) both methods will automatically set the high bit appropriately
// F) 0xCV byte are set accordingly
// 1) for differing values, run of all differing values
// 2) for same values, only one byte of that values
// Estimations put the absolute worst case scenario as the
// final compressed data being slightly LARGER. So we need to be
// careful about allocating memory. (Did I miss something?)
// tests seem to indicate it will never be larger than the original
const dataTemp = (new Array(src.length + (src.length / 4))).fill(0)
const dataRun = (new Array(140)).fill(0)
const dataInChanSize = src.length / 4
let dataTempCount = 65536 <= src.length ? 4 : 0
// Data is stored in red run, green run,blue run
// So we compress from pixel format RGBA
// RED: byte[0], byte[4], byte[8] ...
// GREEN: byte[1], byte[5], byte[9] ...
// BLUE: byte[2], byte[6], byte[10] ...
// ALPHA: byte[3], byte[7], byte[11] do nothing with these bytes
for (let colorOffset = 0; colorOffset < 3; colorOffset++) {
dataRun[0] = src[colorOffset]
// Start with a runlength of 1 for the first byte
let runLength = 1
// Assume that the run will be different for now... We can change this later
// 0 for low bit (different), 1 for high bit (same)
let runType = 0
// Start one byte ahead
for (let dataInCount = 1; dataInCount < dataInChanSize; dataInCount++) {
const dataByte = src[colorOffset + (dataInCount * 4)]
if (runLength < 2) {
// Simply append to the current run
dataRun[runLength++] = dataByte
} else if (runLength === 2) {
// Decide here if the run should be same values or different values
// If the last three values were the same, we can change to a same-type run
if ((dataByte === dataRun[runLength - 1]) && (dataByte === dataRun[runLength - 2])) {
runType = 1
} else {
runType = 0
}
dataRun[runLength++] = dataByte
} else {
// Greater than or equal to 2
if (runType === 0 && runLength < 128) {
// Different type run
// If the new value matches both of the last two values, we have a new
// same-type run starting with the previous two bytes
if ((dataByte === dataRun[runLength - 1]) && (dataByte === dataRun[runLength - 2])) {
// Set the RL byte
dataTemp[dataTempCount] = runLength - 3
dataTempCount++
// Copy 0 to runLength-2 bytes to the RLE data here
PackBits._arrayCopy(dataRun, 0, dataTemp, dataTempCount, runLength - 2)
dataTempCount = dataTempCount + (runLength - 2)
// Set up the new same-type run
dataRun[0] = dataRun[runLength - 2]
dataRun[1] = dataRun[runLength - 1]
dataRun[2] = dataByte
runLength = 3
runType = 1
} else {
// They don't match, so we can proceed
dataRun[runLength++] = dataByte
}
} else if (runType === 1 && runLength < 130) {
// Same type run
// If the new value matches both of the last two values, we
// can safely continue
if ((dataByte === dataRun[runLength - 1]) && (dataByte === dataRun[runLength - 2])) {
dataRun[runLength++] = dataByte
} else {
// They don't match, so we need to start a new run
// Set the RL byte
dataTemp[dataTempCount] = runLength + 125
dataTempCount++
// Only copy the first byte, since all the remaining values are identical
dataTemp[dataTempCount] = dataRun[0]
dataTempCount++
// Copy 0 to runLength bytes to the RLE data here
dataRun[0] = dataByte
runLength = 1
runType = 0
}
} else {
// Exceeded run limit, need to start a new one
if (runType === 0) {
// Set the RL byte low
dataTemp[dataTempCount] = runLength - 1
dataTempCount++
// Copy 0 to runLength bytes to the RLE data here
PackBits._arrayCopy(dataRun, 0, dataTemp, dataTempCount, runLength)
dataTempCount = dataTempCount + runLength
} else if (runType === 1) {
// Set the RL byte high
dataTemp[dataTempCount] = runLength + 125
dataTempCount++
// Only copy the first byte, since all the remaining values are identical
dataTemp[dataTempCount] = dataRun[0]
dataTempCount++
}
// Copy 0 to runLength bytes to the RLE data here
dataRun[0] = dataByte
runLength = 1
runType = 0
}
}
}
// Copy the end of the last run
if (runLength > 0) {
if (runType === 0) {
// Set the RL byte low
dataTemp[dataTempCount] = runLength - 1
dataTempCount++
// Copy 0 to runLength bytes to the RLE data here
PackBits._arrayCopy(dataRun, 0, dataTemp, dataTempCount, runLength)
dataTempCount = dataTempCount + runLength
} else if (runType === 1) {
// Set the RL byte high
dataTemp[dataTempCount] = runLength + 125
dataTempCount++
// Only copy the first byte, since all the remaining values are identical
dataTemp[dataTempCount] = dataRun[0]
dataTempCount++
}
}
}
const dest = new Array(dataTempCount)
PackBits._arrayCopy(dataTemp, 0, dest, 0, dataTempCount)
return dest
}
/**
* Copies the array to the target array at the specified position and size.
*
* @param {Array.<Number>} src Byte array of copy source.
* @param {Number} srcBegin Copying start position of source.
* @param {Array.<Number>} dest Bayte array of copy destination.
* @param {Number} destBegin Writing start position of destinnation.
* @param {Number} size Size of copy bytes.
*/
static _arrayCopy (src, srcBegin, dest, destBegin, size) {
if (src.length <= srcBegin || src.length < size || dest.length <= destBegin || dest.length < size) {
return
}
for (let i = srcBegin, j = destBegin, k = 0; k < size; ++i, ++j, ++k) {
dest[j] = src[i]
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment