UNPKG

dpi-tools

Version:

Quickly change the dpi for an image without re-enconding

291 lines (230 loc) 8.86 kB
function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); } function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); } function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && Symbol.iterator in Object(iter)) return Array.from(iter); } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var PNG = 'image/png'; var JPEG = 'image/jpeg'; // those are 3 possible signature of the physBlock in base64. // the pHYs signature block is preceed by the 4 bytes of lenght. The length of // the block is always 9 bytes. So a phys block has always this signature: // 0 0 0 9 p H Y s. // However the data64 encoding aligns we will always find one of those 3 strings. // this allow us to find this particular occurence of the pHYs block without // converting from b64 back to string var B64_PHYS_SIGNATURE_1 = 'AAlwSFlz'; var B64_PHYS_SIGNATURE_2 = 'AAAJcEhZ'; var B64_PHYS_SIGNATURE_3 = 'AAAACXBI'; var P_CHAR_CODE = 'p'.charCodeAt(0); var H_CHAR_CODE = 'H'.charCodeAt(0); var Y_CHAR_CODE = 'Y'.charCodeAt(0); var S_CHAR_CODE = 's'.charCodeAt(0); var createPngDataTable = function createPngDataTable() { /* Table of CRCs of all 8-bit messages. */ var crcTable = new Int32Array(256); for (var n = 0; n < 256; n += 1) { var c = n; for (var k = 0; k < 8; k += 1) { c = c & 1 ? 0xedb88320 ^ c >>> 1 : c >>> 1; } crcTable[n] = c; } return crcTable; }; var pngDataTable = createPngDataTable(); var calcCrc = function calcCrc(buf) { var c = -1; for (var n = 0; n < buf.length; n += 1) { c = pngDataTable[(c ^ buf[n]) & 0xff] ^ c >>> 8; } return c ^ -1; }; var searchStartOfPhys = function searchStartOfPhys(data) { var length = data.length - 1; // we check from the end since we cut the string in proximity of the header // the header is within 21 bytes from the end. for (var i = length; i >= 4; i -= 1) { if (data[i - 4] === 9 && data[i - 3] === P_CHAR_CODE && data[i - 2] === H_CHAR_CODE && data[i - 1] === Y_CHAR_CODE && data[i] === S_CHAR_CODE) { return i - 3; } } return -1; }; var changeDpiOnArray = function changeDpiOnArray(dataArray, sourceDpi, format) { var overwritePhys = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false; var dpi = sourceDpi; if (format === JPEG) { dataArray[13] = 1; // 1 pixel per inch or 2 pixel per cm dataArray[14] = dpi >> 8; // dpiX high byte dataArray[15] = dpi & 0xff; // dpiX low byte dataArray[16] = dpi >> 8; // dpiY high byte dataArray[17] = dpi & 0xff; // dpiY low byte return dataArray; } if (format === PNG) { var physChunk = new Uint8Array(13); // chunk header pHYs // 9 bytes of data // 4 bytes of crc // this multiplication is because the standard is dpi per meter. dpi *= 39.3701; physChunk[0] = P_CHAR_CODE; physChunk[1] = H_CHAR_CODE; physChunk[2] = Y_CHAR_CODE; physChunk[3] = S_CHAR_CODE; physChunk[4] = dpi >>> 24; // dpiX highest byte physChunk[5] = dpi >>> 16; // dpiX veryhigh byte physChunk[6] = dpi >>> 8; // dpiX high byte physChunk[7] = dpi & 0xff; // dpiX low byte physChunk[8] = physChunk[4]; // dpiY highest byte physChunk[9] = physChunk[5]; // dpiY veryhigh byte physChunk[10] = physChunk[6]; // dpiY high byte physChunk[11] = physChunk[7]; // dpiY low byte physChunk[12] = 1; // dot per meter.... var crc = calcCrc(physChunk); var crcChunk = new Uint8Array(4); crcChunk[0] = crc >>> 24; crcChunk[1] = crc >>> 16; crcChunk[2] = crc >>> 8; crcChunk[3] = crc & 0xff; if (overwritePhys) { var startingIndex = searchStartOfPhys(dataArray); dataArray.set(physChunk, startingIndex); dataArray.set(crcChunk, startingIndex + 13); return dataArray; } // i need to give back an array of data that is divisible by 3 so that // dataurl encoding gives me integers, for luck this chunk is 17 + 4 = 21 // if it was we could add a text chunk containing some info, until desired // length is met. // chunk structure 4 bytes for length is 9 var chunkLength = new Uint8Array(4); chunkLength[0] = 0; chunkLength[1] = 0; chunkLength[2] = 0; chunkLength[3] = 9; var finalHeader = new Uint8Array(54); finalHeader.set(dataArray, 0); finalHeader.set(chunkLength, 33); finalHeader.set(physChunk, 37); finalHeader.set(crcChunk, 50); return finalHeader; } return null; }; var detectPhysChunkFromDataUrl = function detectPhysChunkFromDataUrl(data) { var b64index = data.indexOf(B64_PHYS_SIGNATURE_1); if (b64index === -1) { b64index = data.indexOf(B64_PHYS_SIGNATURE_2); } if (b64index === -1) { b64index = data.indexOf(B64_PHYS_SIGNATURE_3); } // if b64index === -1 chunk is not found return b64index; }; var isBufferPng = function isBufferPng(buffer) { if (buffer.length < 8) { return null; } return buffer[0] === 137 && buffer[1] === 80 && buffer[2] === 78 && buffer[3] === 71 && buffer[4] === 13 && buffer[5] === 10 && buffer[6] === 26 && buffer[7] === 10; }; var isBufferJpeg = function isBufferJpeg(buffer) { if (buffer.length < 3) { return null; } return buffer[0] === 255 && buffer[1] === 216 && buffer[2] === 255; }; var getType = function getType(buffer) { if (isBufferJpeg(buffer)) { return JPEG; } if (isBufferPng(buffer)) { return PNG; } return ''; }; var mergeTypedArrays = function mergeTypedArrays(a, b) { var c = new a.constructor(a.length + b.length); c.set(a); c.set(b, a.length); return c; }; var changeDpiDataUrl = function changeDpiDataUrl(base64Image, dpi) { var dataSplitted = base64Image.split(','); var format = dataSplitted[0]; var body = dataSplitted[1]; var type; var headerLength; var overwritePhys = false; if (format.indexOf(PNG) !== -1) { type = PNG; var b64Index = detectPhysChunkFromDataUrl(body); // 28 bytes in dataUrl are 21bytes, length of phys chunk with everything inside. if (b64Index >= 0) { headerLength = Math.ceil((b64Index + 28) / 3) * 4; overwritePhys = true; } else { headerLength = 33 / 3 * 4; } } else if (format.indexOf(JPEG) !== -1) { type = JPEG; headerLength = 18 / 3 * 4; } // 33 bytes are ok for pngs and jpegs // to contain the information. var stringHeader = body.substring(0, headerLength); var restOfData = body.substring(headerLength); var headerBytes = atob(stringHeader); var dataArray = new Uint8Array(headerBytes.length); for (var i = 0; i < dataArray.length; i += 1) { dataArray[i] = headerBytes.charCodeAt(i); } var finalArray = changeDpiOnArray(dataArray, dpi, type, overwritePhys); var base64Header = btoa(String.fromCharCode.apply(String, _toConsumableArray(finalArray))); return [format, ',', base64Header, restOfData].join(''); }; var changeDpiBlob = function changeDpiBlob(blob, dpi) { // 33 bytes are ok for pngs and jpegs // to contain the information. var headerChunk = blob.slice(0, 33); return new Promise(function (resolve) { var fileReader = new FileReader(); fileReader.onload = function () { var dataArray = new Uint8Array(fileReader.result); var tail = blob.slice(33); var changedArray = changeDpiOnArray(dataArray, dpi, blob.type); resolve(new Blob([changedArray, tail], { type: blob.type })); }; fileReader.readAsArrayBuffer(headerChunk); }); }; var changeDpiBuffer = function changeDpiBuffer(buffer, dpi) { var headerChunk = new Uint8Array(buffer.slice(0, 33)); var rest = buffer.slice(33); var type = getType(buffer); var changedHeader = changeDpiOnArray(headerChunk, dpi, type); return mergeTypedArrays(changedHeader, rest); }; var dpiTools = { changeDpiDataUrl: changeDpiDataUrl, changeDpiBlob: changeDpiBlob, changeDpiBuffer: changeDpiBuffer }; export default dpiTools; export { changeDpiBlob, changeDpiBuffer, changeDpiDataUrl };