UNPKG

@onesy/huffman-code

Version:
441 lines (360 loc) 15.1 kB
import _defineProperty from "@babel/runtime/helpers/defineProperty"; import is from '@onesy/utils/is'; import merge from '@onesy/utils/merge'; import copy from '@onesy/utils/copy'; import to from '@onesy/utils/to'; import binaryStringToHexadecimal from '@onesy/utils/binaryStringToHexadecimal'; import hexadecimalStringToBinary from '@onesy/utils/hexadecimalStringToBinary'; import OnesyDate from '@onesy/date/OnesyDate'; import duration from '@onesy/date/duration'; export class OnesyHuffmanCodeResponse { constructor() { let value = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; let values = arguments.length > 1 ? arguments[1] : undefined; let values_encoded = arguments.length > 2 ? arguments[2] : undefined; let probabilities = arguments.length > 3 ? arguments[3] : undefined; let efficiency = arguments.length > 4 ? arguments[4] : undefined; let redundency = arguments.length > 5 ? arguments[5] : undefined; let entropy = arguments.length > 6 ? arguments[6] : undefined; let original_byte_size = arguments.length > 7 ? arguments[7] : undefined; let values_byte_size = arguments.length > 8 ? arguments[8] : undefined; let value_byte_size = arguments.length > 9 ? arguments[9] : undefined; let encoded_byte_size = arguments.length > 10 ? arguments[10] : undefined; let compression_ratio = arguments.length > 11 ? arguments[11] : undefined; let compression_percentage = arguments.length > 12 ? arguments[12] : undefined; let positive = arguments.length > 13 ? arguments[13] : undefined; let average_code_word_length = arguments.length > 14 ? arguments[14] : undefined; let performance_milliseconds = arguments.length > 15 ? arguments[15] : undefined; let performance = arguments.length > 16 ? arguments[16] : undefined; this.value = value; this.values = values; this.values_encoded = values_encoded; this.probabilities = probabilities; this.efficiency = efficiency; this.redundency = redundency; this.entropy = entropy; this.original_byte_size = original_byte_size; this.values_byte_size = values_byte_size; this.value_byte_size = value_byte_size; this.encoded_byte_size = encoded_byte_size; this.compression_ratio = compression_ratio; this.compression_percentage = compression_percentage; this.positive = positive; this.average_code_word_length = average_code_word_length; this.performance_milliseconds = performance_milliseconds; this.performance = performance; } } export class OnesyNode { constructor() { let value = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 1; let word = arguments.length > 1 ? arguments[1] : undefined; this.value = value; this.word = word; _defineProperty(this, "left", void 0); _defineProperty(this, "right", void 0); this.value = Number(value.toFixed(3)); } get leaf() { return !(this.left || this.right); } get maxDepth() { const maxDepthMethod = value => { if (value === undefined) return 0; return Math.max(1 + maxDepthMethod(value.left), 1 + maxDepthMethod(value.right)); }; return maxDepthMethod(this); } } export class OnesyHuffmanTree { constructor() { _defineProperty(this, "root", void 0); } static make(value) { return new OnesyHuffmanTree().make(value); } get array() { const value = []; this.preorder(this.root, value_ => { var _value_$path; value.push(value_.word ? value_.word : value_ === this.root ? 0 : (_value_$path = value_.path) === null || _value_$path === void 0 ? void 0 : _value_$path.slice(-1)); }); return value; } isRoot(value) { return value === this.root; } preorder(value, method) { if (value !== undefined && is('function', method)) { method(value, value.left, value.right); this.preorder(value.left, method); this.preorder(value.right, method); } } make(value_) { const items = copy(value_); const onesyHuffmanTree = new OnesyHuffmanTree(); onesyHuffmanTree.root = new OnesyNode(); onesyHuffmanTree.root.index = 0; function arrayToOnesyHuffmanTree(value) { if (items[0] === '0' && !value.left) { value.left = new OnesyNode(); value.left.index = 2 * value.index + 1; value.left.path = '0'; items.splice(0, 1); arrayToOnesyHuffmanTree(value.left); } if (items[0] === '1' && !value.right) { value.right = new OnesyNode(); value.right.index = 2 * value.index + 2; value.right.path = '1'; items.splice(0, 1); arrayToOnesyHuffmanTree(value.right); } if (is('array', items[0])) { if (items[0].length) { if (!value.left) { value.left = new OnesyNode(1, items[0][0]); value.left.index = 2 * value.index + 1; value.left.path = '0'; items[0].splice(0, 1); } if (!value.right && items[0].length) { value.right = new OnesyNode(1, items[0][0]); value.right.index = 2 * value.index + 2; value.right.path = '1'; items[0].splice(0, 1); } if (!items[0].length) items.splice(0, 1); } else items.splice(0, 1); } if (items[0] === '1' && !value.right) arrayToOnesyHuffmanTree(value); } arrayToOnesyHuffmanTree(onesyHuffmanTree.root); return onesyHuffmanTree; } } export const optionsDefault = { encode_values: true, base64: true }; class OnesyHuffmanCode { static get OnesyHuffmanCodeResponse() { return OnesyHuffmanCodeResponse; } static get OnesyNode() { return OnesyNode; } static get OnesyHuffmanTree() { return OnesyHuffmanTree; } static encodeValue(value) { if (!(is('string', value) && value.length)) return ''; // Add 1 at the start of every 3 characters // it's more data, but there will be no bugs // with padded 0s, a bug fix for now return binaryStringToHexadecimal((value.match(/.{1,3}/g) || []).map(item => 1 + item).join('')).match(/.{1,2}/g).flatMap(item => { if (item[0] === '0') return item.split('').map(item_ => String.fromCharCode(parseInt(item_, 16))); return String.fromCharCode(parseInt(item, 16)); }).join(''); } static decodeValue(value_) { if (!(is('string', value_) && value_.length)) return ''; const value = value_.split('').map(item => item.charCodeAt(0).toString(16)).join(''); return (hexadecimalStringToBinary(value).match(/.{1,4}/g) || []).map(item => item.slice(1)).join(''); } static encodeValues(values) { let encodeValues = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; if (values) { let result = ''; const keys = Object.keys(values); keys.forEach((item, index) => result += "".concat(item).concat(encodeValues ? this.encodeValue(values[item]) : values[item]).concat(index < keys.length - 1 ? ' ' : '')); return "".concat(encodeValues ? 1 : 0).concat(result); } } static decodeValues(value) { const result = {}; const values = []; if (value) { const encodeValues = value[0] === '1'; const values_ = value.slice(1).split(' '); values_.forEach((item, index) => { if (!item) values_[index + 1] = " ".concat(values_[index + 1]);else values.push(item); }); values.forEach(item => result[item[0]] = encodeValues ? this.decodeValue(item.slice(1)) : item.slice(1)); } return result; } static getValues(onesyHuffmanTree) { const values = {}; const leafs = []; if (onesyHuffmanTree) { onesyHuffmanTree.preorder(onesyHuffmanTree.root, (value, left, right) => { if (onesyHuffmanTree.isRoot(value)) { value.path = value.maxDepth === 1 ? '0' : ''; if (value.leaf) leafs.push(value); } if (left) { left.path = value.path + 0; if (left.leaf) leafs.push(left); } if (right) { right.path = value.path + 1; if (right.leaf) leafs.push(right); } }); } leafs.filter(leaf => leaf.word).forEach(leaf => values[leaf.word] = leaf.path); return values; } static decode(value, values) { const instance = new OnesyHuffmanCode(); instance.values = values; return instance.decode(value); } static encodeBase64(value) { return to(value, 'base64'); } static decodeBase64(value) { return to(value, 'string'); } get encoded() { return this.response; } get entropy() { const output = Object.keys(this.probabilities).reduce((result, key) => result += this.probabilities[key] * Math.log2(this.probabilities[key]), 0); return Math.abs(Number(output.toFixed(3))); } get averageCodeWordLength() { const output = Object.keys(this.probabilities).reduce((result, key) => { var _this$values$key; return result += this.probabilities[key] * (((_this$values$key = this.values[key]) === null || _this$values$key === void 0 ? void 0 : _this$values$key.length) || 8); }, 0); return Number(output.toFixed(3)); } get redundency() { return Number(Math.abs(this.entropy - this.averageCodeWordLength).toFixed(3)); } get efficiency() { return Number((this.entropy / this.averageCodeWordLength || 0).toFixed(3)); } constructor(value) { let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : optionsDefault; this.value = value; _defineProperty(this, "options", void 0); _defineProperty(this, "huffmanTree", void 0); _defineProperty(this, "probabilities", {}); _defineProperty(this, "values", {}); _defineProperty(this, "response", new OnesyHuffmanCodeResponse()); _defineProperty(this, "startTime", void 0); this.options = merge(options, optionsDefault); if (this.value !== undefined) this.init(); } init() { this.startTime = OnesyDate.milliseconds; if (!Object.keys(this.probabilities).length && is('string', this.value)) { // Frequencies this.getProbabilities(); } if (!Object.keys(this.values).length && Object.keys(this.probabilities).length) { // Normalize probabilities this.normalizeProbabilities(); // Make huffman tree this.makeHuffmanTree(); // Values this.values = OnesyHuffmanCode.getValues(this.huffmanTree); } // Encode this.encode(); } encode() { const response = new OnesyHuffmanCodeResponse(); if (Object.keys(this.values).length && is('string', this.value)) { let value = Array.from(this.value).reduce((result, item) => result += this.values[item] || item, ''); value = OnesyHuffmanCode.encodeValue(value); if (this.options.base64) value = OnesyHuffmanCode.encodeBase64(value); response.value = value; response.performance_milliseconds = OnesyDate.milliseconds - this.startTime; response.performance = duration(response.performance_milliseconds) || '0 milliseconds'; response.values = this.values; response.values_encoded = OnesyHuffmanCode.encodeValues(this.values, this.options.encode_values); response.probabilities = this.probabilities; response.efficiency = this.efficiency; response.redundency = this.redundency; response.entropy = this.entropy; response.average_code_word_length = this.averageCodeWordLength; response.original_byte_size = to(this.value, 'byte-size'); response.values_byte_size = to(response.values_encoded, 'byte-size'); response.value_byte_size = to(value, 'byte-size'); response.encoded_byte_size = response.values_byte_size + response.value_byte_size; response.compression_ratio = Number(((response.encoded_byte_size + response.original_byte_size) / response.encoded_byte_size - 1).toFixed(2)); response.compression_percentage = response.original_byte_size === 0 ? response.value_byte_size === 0 ? 0 : response.value_byte_size * -100 : Number(((response.original_byte_size - response.encoded_byte_size) / response.original_byte_size * 100).toFixed(2)); response.positive = response.compression_ratio > 1; this.response = response; } return response; } decode(value_) { if (!value_) return new OnesyHuffmanCodeResponse(value_); const response = new OnesyHuffmanCodeResponse(value_); const startTime = OnesyDate.milliseconds; const value = OnesyHuffmanCode.decodeValue(OnesyHuffmanCode.decodeBase64(value_)); if (is('string', value) && Object.keys(this.values).length) { let input = value; let output = ''; while (input.length) { let valueWord = Object.keys(this.values).find(key => input.indexOf(this.values[key]) === 0); if (!valueWord) { // bug valueWord = Object.keys(this.values).find(key => ('0' + input).indexOf(this.values[key]) === 0) || Object.keys(this.values).find(key => ('00' + input).indexOf(this.values[key]) === 0); if (!valueWord) break; } output += valueWord; input = input.slice(this.values[valueWord].length); } response.value = output; response.performance_milliseconds = OnesyDate.milliseconds - startTime; response.performance = duration(response.performance_milliseconds) || '0 milliseconds'; response.original_byte_size = to(output, 'byte-size'); response.value_byte_size = to(value_, 'byte-size'); } return response; } getProbabilities() { const value = this.value || ''; for (let i = 0; i < value.length; i++) { this.probabilities[value[i]] = ~~this.probabilities[value[i]] + 1; } return this.probabilities; } normalizeProbabilities() { const sum = Object.keys(this.probabilities).reduce((result, item) => result += this.probabilities[item], 0); Object.keys(this.probabilities).forEach(key => this.probabilities[key] = Number((this.probabilities[key] / sum).toFixed(4))); return this.probabilities; } makeHuffmanTree() { let trees = []; Object.keys(this.probabilities).forEach(key => { const onesyNode = new OnesyNode(this.probabilities[key], key); trees.push(onesyNode); }); trees.sort((a, b) => a.value - b.value); while (trees.length > 1) { const first = trees[0]; const second = trees[1]; const newNode = new OnesyNode(first.value + second.value); const children = [first, second].sort((a, b) => { const aMaxDepth = a.maxDepth; const bMaxDepth = b.maxDepth; if (a.leaf && b.leaf || aMaxDepth === b.maxDepth) return b.value - a.value; if (a.leaf || b.leaf) return a.leaf ? -1 : 1; return aMaxDepth - bMaxDepth; }); newNode.left = children[0]; newNode.right = children[1]; trees.push(newNode); trees = trees.slice(2); trees.sort((a, b) => a.value - b.value); } this.huffmanTree = new OnesyHuffmanTree(); this.huffmanTree.root = trees[0]; return this.huffmanTree; } } export default OnesyHuffmanCode;