├── LICENSE ├── README.md └── psdw.js /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014, JongChan Choi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # psdw 2 | simple javascript psd writer 3 | 4 | ## usage 5 | ```js 6 | var psdBlob = psdw({ 7 | width/* required */: 1, // pixel unit 8 | height/* required */: 1, // pixel unit 9 | flattenedImageData/* required */: new Uint8ClampedArray([0, 0, 0, 0]), 10 | layers: [ 11 | { // bottom layer (background) 12 | name/* required */: 'psdw layer', 13 | imageData/* required */: new Uint8ClampedArray([0, 0, 0, 0]), 14 | // like html5 canvas image data 15 | width/* required */: 1, // pixel unit 16 | height/* required */: 1, // pixel unit 17 | x/* optional */: 0, // pixel unit 18 | y/* optional */: 0, // pixel unit 19 | opacity/* optional */: 1, // 0(transparent) ~ 1(opaque) 20 | blendMode/* optional */: 'normal' // see below (blend modes) 21 | }, { // middle layer 22 | imageData: new Uint8ClampedArray([0, 0, 0, 0]), 23 | width: 1, 24 | height: 1 25 | // other properties... 26 | }, { // top layer (foreground) 27 | imageData: new Uint8ClampedArray([0, 0, 0, 0]), 28 | width: 1, 29 | height: 1 30 | // other properties... 31 | } 32 | ] 33 | }).blob; 34 | ``` 35 | 36 | ## blend modes 37 | * pass through 38 | * normal 39 | * dissolve 40 | * darken 41 | * multiply 42 | * color burn 43 | * linear burn 44 | * darker color 45 | * lighten 46 | * screen 47 | * color dodge 48 | * linear dodge 49 | * lighter color 50 | * overlay 51 | * soft light 52 | * hard light 53 | * vivid light 54 | * linear light 55 | * pin light 56 | * hard mix 57 | * difference 58 | * exclusion 59 | * subtract 60 | * divide 61 | * hue 62 | * saturation 63 | * color 64 | * luminosity 65 | -------------------------------------------------------------------------------- /psdw.js: -------------------------------------------------------------------------------- 1 | if (typeof module !== 'undefined') 2 | module.exports = psdw; 3 | 4 | function psdw(option) { 5 | var writer = new psdw.PsdWriter(); 6 | writer.width = option.width; 7 | writer.height = option.height; 8 | writer.backgroundColor = option.backgroundColor; 9 | writer.layers = option.layers; 10 | writer.flattenedImageData = option.flattenedImageData; 11 | return writer; 12 | } 13 | psdw.PsdWriter = function PsdWriter() { 14 | this.layers = []; 15 | }; 16 | psdw.blendMode = { 17 | 'pass through': 'pass', 18 | 'normal': 'norm', 19 | 'dissolve': 'diss', 20 | 'darken': 'dark', 21 | 'multiply': 'mul ', 22 | 'color burn': 'idiv', 23 | 'linear burn': 'lbrn', 24 | 'darker color': 'dkCl', 25 | 'lighten': 'lite', 26 | 'screen': 'scrn', 27 | 'color dodge': 'div ', 28 | 'linear dodge': 'lddg', 29 | 'lighter color': 'lgCl', 30 | 'overlay': 'over', 31 | 'soft light': 'sLit', 32 | 'hard light': 'hLit', 33 | 'vivid light': 'vLit', 34 | 'linear light': 'lLit', 35 | 'pin light': 'pLit', 36 | 'hard mix': 'hMix', 37 | 'difference': 'diff', 38 | 'exclusion': 'smud', 39 | 'subtract': 'fsub', 40 | 'divide': 'fdiv', 41 | 'hue': 'hue ', 42 | 'saturation': 'sat ', 43 | 'color': 'colr', 44 | 'luminosity': 'lum ' 45 | }; 46 | psdw.compression = { 47 | 'raw': 0, 48 | 'rle': 1, 49 | 'zip without prediction': 2, 50 | 'zip with prediction': 3 51 | }; 52 | psdw.PsdWriter.pad2 = function pad2(len) { 53 | return len % 2; 54 | }; 55 | psdw.PsdWriter.pad4 = function pad4(len) { 56 | return (4 - (len % 4)) % 4; 57 | }; 58 | psdw.PsdWriter.pascalString = function pascalString(value) { // 1 byte length + iso-8859-1 59 | var charCodeArray = value.substr(0, 0xff).split('').map(function (char) { 60 | var charCode = char.charCodeAt(); 61 | var alternativeCode = '?'.charCodeAt(); 62 | return charCode > 0xff ? alternativeCode : charCode; 63 | }); 64 | return new Uint8Array( 65 | [charCodeArray.length].concat(charCodeArray) 66 | ); 67 | }; 68 | psdw.PsdWriter.unicodeString = function unicodeString(value) { // 4 byte length + ucs2 69 | var buffer = new ArrayBuffer(4 + (value.length * 2)); 70 | var view = new DataView(buffer); 71 | view.setUint32(0, value.length); 72 | value.split('').forEach(function (char, index) { 73 | view.setUint16(4 + (index * 2), char.charCodeAt()); 74 | }); 75 | return new Uint8Array(buffer); 76 | }; 77 | psdw.PsdWriter.writeSignature = function writeSignature(view, byteOffset, value) { 78 | view.setUint8(byteOffset, value[0].charCodeAt()); 79 | view.setUint8(byteOffset + 1, value[1].charCodeAt()); 80 | view.setUint8(byteOffset + 2, value[2].charCodeAt()); 81 | view.setUint8(byteOffset + 3, value[3].charCodeAt()); 82 | }; 83 | psdw.PsdWriter.imageResource = function imageResource(resourceId, resourceData) { 84 | if (!(resourceData instanceof Uint8Array)) 85 | resourceData = new Uint8Array(resourceData); 86 | var buffer = new ArrayBuffer(12 + resourceData.length); 87 | var view = new DataView(buffer); 88 | psdw.PsdWriter.writeSignature(view, 0, '8BIM'); 89 | view.setUint16(4, resourceId | 0); 90 | // two zero byte for null name 91 | view.setUint32(8, resourceData.length); 92 | var uint8array = new Uint8Array(buffer, 12); 93 | uint8array.set(resourceData); 94 | return buffer; 95 | }; 96 | psdw.PsdWriter.channelImageData = function channelImageData(imageData, channelCount, channelOffset, compression) { 97 | if (compression !== psdw.compression['raw']) 98 | compression = psdw.compression['raw']; // TODO: support rle 99 | var len = (imageData.length / channelCount) | 0; 100 | var uint8array = new Uint8Array(2 + len); 101 | uint8array[1] = compression; 102 | for (var i = 2, j = channelOffset; j < imageData.length; ++i, j += channelCount) 103 | uint8array[i] = imageData[j]; 104 | return uint8array; 105 | }; 106 | psdw.PsdWriter.additionalLayerInformation = function additionalLayerInformation(key, data) { 107 | if (!(data instanceof Uint8Array)) 108 | data = new Uint8Array(data); 109 | var dataLength = data.length + psdw.PsdWriter.pad2(data.length); // length of data with padding 110 | var buffer = new ArrayBuffer( 111 | 4 + // signature 112 | 4 + // key 113 | 4 + // length of data 114 | dataLength 115 | ); 116 | var view = new DataView(buffer); 117 | psdw.PsdWriter.writeSignature(view, 0, '8BIM'); 118 | psdw.PsdWriter.writeSignature(view, 4, key); 119 | view.setUint32(8, dataLength); 120 | var uint8array = new Uint8Array(buffer, 12); 121 | uint8array.set(data); 122 | return buffer; 123 | }; 124 | psdw.PsdWriter.layerRecord = function layerRecord(top, left, bottom, right, 125 | name, opacity, blendMode, 126 | rLen, gLen, bLen, aLen, 127 | additionalLayerInformationArray) { 128 | var nameUint8Array = psdw.PsdWriter.pascalString(name); 129 | var padding = psdw.PsdWriter.pad4(nameUint8Array.length); 130 | var additionalLayerInformationArrayByteLength = additionalLayerInformationArray.reduce(function (prev, curr) { 131 | return prev + curr.byteLength; 132 | }, 0); 133 | var extraDataFieldLength = 4 + // no mask data 134 | 44 + // 4 byte for length + src, dst blending ranges per channel (gray + g + b + a) 135 | nameUint8Array.length + padding + // name 136 | additionalLayerInformationArrayByteLength; 137 | var buffer = new ArrayBuffer( 138 | 16 + // top, left, bottom, right 139 | 2 + // number of channels. 4 140 | 24 + // channel information(6) * number of channels(4) 141 | 4 + // signature 142 | 4 + // blend mode key 143 | 1 + // opacity 144 | 1 + // clipping 145 | 1 + // flags 146 | 1 + // filler 147 | 4 + // length of extra data field 148 | extraDataFieldLength 149 | ); 150 | var view = new DataView(buffer); 151 | view.setUint32(0, top); 152 | view.setUint32(4, left); 153 | view.setUint32(8, bottom); 154 | view.setUint32(12, right); 155 | view.setUint16(16, 4); // number of channels 156 | view.setUint16(18, 0); // red 157 | view.setUint32(20, rLen); // red channel data length 158 | view.setUint16(24, 1); // green 159 | view.setUint32(26, gLen); // green channel data length 160 | view.setUint16(30, 2); // blue 161 | view.setUint32(32, bLen); // blue channel data length 162 | view.setUint16(36, -1); // alpha 163 | view.setUint32(38, aLen); // alpha channel data length 164 | psdw.PsdWriter.writeSignature(view, 42, '8BIM'); 165 | psdw.PsdWriter.writeSignature(view, 46, psdw.blendMode[blendMode]); 166 | view.setUint8(50, (opacity * 0xff) | 0); // opacity 167 | view.setUint8(51, 0); // clipping 168 | view.setUint8(52, 8); // flags. TODO: visibility 169 | view.setUint8(53, 0); // filler 170 | view.setUint32(54, extraDataFieldLength); 171 | view.setUint32(58, 0); // no mask data 172 | view.setUint32(62, 40); // length of layer blending ranges data 173 | view.setUint32(66, 0x0000ffff); // gray src range 174 | view.setUint32(70, 0x0000ffff); // gray dst range 175 | view.setUint32(74, 0x0000ffff); // red src range 176 | view.setUint32(78, 0x0000ffff); // red dst range 177 | view.setUint32(82, 0x0000ffff); // green src range 178 | view.setUint32(86, 0x0000ffff); // green dst range 179 | view.setUint32(90, 0x0000ffff); // blue src range 180 | view.setUint32(94, 0x0000ffff); // blue dst range 181 | view.setUint32(98, 0x0000ffff); // alpha src range 182 | view.setUint32(102, 0x0000ffff); // alpha dst range 183 | var uint8array = new Uint8Array(buffer); 184 | uint8array.set(nameUint8Array, 106); 185 | var byteOffset = 106 + nameUint8Array.length + padding; 186 | additionalLayerInformationArray.forEach(function (additionalLayerInformation) { 187 | var additionalLayerInformationUint8Array = new Uint8Array(additionalLayerInformation); 188 | uint8array.set(additionalLayerInformationUint8Array, byteOffset); 189 | byteOffset += additionalLayerInformationUint8Array.length; 190 | }); 191 | return buffer; 192 | }; 193 | psdw.PsdWriter.layerInfo = function layerInfo(layers) { 194 | var channelImageDataArray = []; 195 | var layerRecordArray = layers.map(function (layer) { 196 | var top = layer.y | 0; 197 | var left = layer.x | 0; 198 | var bottom = top + (layer.height | 0); 199 | var right = left + (layer.width | 0); 200 | var name = layer.name + ''; 201 | var opacity = layer.opacity !== undefined ? layer.opacity : 1; 202 | var blendMode = layer.blendMode || 'normal'; 203 | var additionalLayerInformationArray = []; 204 | var imageData = layer.imageData; 205 | var compression = psdw.compression['raw']; 206 | var rImageData = psdw.PsdWriter.channelImageData(imageData, 4, 0, compression); 207 | var gImageData = psdw.PsdWriter.channelImageData(imageData, 4, 1, compression); 208 | var bImageData = psdw.PsdWriter.channelImageData(imageData, 4, 2, compression); 209 | var aImageData = psdw.PsdWriter.channelImageData(imageData, 4, 3, compression); 210 | channelImageDataArray.push(rImageData, gImageData, bImageData, aImageData); 211 | (function () { 212 | var data = psdw.PsdWriter.unicodeString(name); 213 | additionalLayerInformationArray.push( 214 | psdw.PsdWriter.additionalLayerInformation('luni', data) 215 | ); 216 | })(); 217 | // TODO? fx: drop shadow, glow, bevel... 218 | return psdw.PsdWriter.layerRecord( 219 | top, left, bottom, right, 220 | name, opacity, blendMode, 221 | rImageData.length, gImageData.length, bImageData.length, aImageData.length, 222 | additionalLayerInformationArray 223 | ); 224 | }); 225 | var layerRecordArrayByteLength = layerRecordArray.reduce(function (prev, curr) { 226 | return prev + curr.byteLength; 227 | }, 0); 228 | var channelImageDataArrayByteLength = channelImageDataArray.reduce(function (prev, curr) { 229 | return prev + curr.length; 230 | }, 0); 231 | var layerInfoLength = 2 + // layer count 232 | layerRecordArrayByteLength + 233 | channelImageDataArrayByteLength + 234 | psdw.PsdWriter.pad2(channelImageDataArrayByteLength); // padding 235 | var buffer = new ArrayBuffer( 236 | 4 + // length 237 | layerInfoLength 238 | ); 239 | var view = new DataView(buffer); 240 | view.setUint32(0, layerInfoLength); 241 | view.setUint16(4, layers.length); 242 | var uint8array = new Uint8Array(buffer); 243 | var byteOffset = 6; 244 | layerRecordArray.forEach(function (layerRecord) { 245 | var layerRecordUint8Array = new Uint8Array(layerRecord); 246 | uint8array.set(layerRecordUint8Array, byteOffset); 247 | byteOffset += layerRecordUint8Array.length; 248 | }); 249 | channelImageDataArray.forEach(function (channelImageData) { 250 | uint8array.set(channelImageData, byteOffset); 251 | byteOffset += channelImageData.length; 252 | }); 253 | return buffer; 254 | }; 255 | Object.defineProperty(psdw.PsdWriter.prototype, 'fileHeader', { 256 | get: function () { 257 | var buffer = new ArrayBuffer(4 + 2 + 6 + 2 + 4 + 4 + 2 + 2); 258 | var view = new DataView(buffer); 259 | psdw.PsdWriter.writeSignature(view, 0, '8BPS'); // 0 Signature 260 | view.setUint16(4, 1); // version 261 | view.setUint16(12, 4); // number of channels, RGBA 262 | view.setUint32(14, this.height | 0); // height of the image in pixels 263 | view.setUint32(18, this.width | 0); // width of the image in pixels 264 | view.setUint16(22, 8); // depth, 1 byte per channel 265 | view.setUint16(24, 3); // color mode, RGB 266 | return buffer; 267 | } 268 | }); 269 | Object.defineProperty(psdw.PsdWriter.prototype, 'colorModeData', { 270 | get: function () { 271 | var buffer = new ArrayBuffer(4); 272 | return buffer; 273 | } 274 | }); 275 | Object.defineProperty(psdw.PsdWriter.prototype, 'imageResources', { 276 | get: function () { 277 | var self = this; 278 | var imageResourceArray = []; 279 | // if (self.backgroundColor !== undefined) { 280 | // (function () { 281 | // var buffer = new ArrayBuffer(8); 282 | // var view = new DataView(buffer); 283 | // view.setUint16(0, 0); // RGB 284 | // view.setUint16(2, (self.backgroundColor.r * 0xffff) | 0); 285 | // view.setUint16(4, (self.backgroundColor.g * 0xffff) | 0); 286 | // view.setUint16(6, (self.backgroundColor.b * 0xffff) | 0); 287 | // imageResourceArray.push(psdw.PsdWriter.imageResource(0x03f2, buffer)); 288 | // })(); 289 | // } 290 | var totalLength = imageResourceArray.reduce(function (prev, curr) { 291 | return prev + curr.byteLength; 292 | }, 0); 293 | var buffer = new ArrayBuffer(totalLength + 4); 294 | var view = new DataView(buffer); 295 | view.setUint32(0, totalLength); 296 | var uint8array = new Uint8Array(buffer, 4); 297 | var byteOffset = 0; 298 | imageResourceArray.forEach(function (imageResource) { 299 | uint8array.set(new Uint8Array(imageResource), byteOffset); 300 | byteOffset += imageResource.byteLength; 301 | }); 302 | return buffer; 303 | } 304 | }); 305 | Object.defineProperty(psdw.PsdWriter.prototype, 'layerAndMaskInformation', { 306 | get: function () { 307 | var layerInfo = psdw.PsdWriter.layerInfo(this.layers); 308 | var layerAndMaskInformationByteLength = layerInfo.byteLength + 309 | 4; // no global layer mask information 310 | var buffer = new ArrayBuffer( 311 | 4 + // length of the layer and mask information section 312 | layerAndMaskInformationByteLength 313 | ); 314 | var view = new DataView(buffer); 315 | view.setUint32(0, layerAndMaskInformationByteLength); 316 | var uint8array = new Uint8Array(buffer); 317 | uint8array.set(new Uint8Array(layerInfo), 4); 318 | return buffer; 319 | } 320 | }); 321 | Object.defineProperty(psdw.PsdWriter.prototype, 'imageData', { 322 | get: function () { 323 | var flattenedImageData = this.flattenedImageData; 324 | var compression = psdw.compression['raw']; 325 | var i, len = flattenedImageData.length; 326 | var buffer = new ArrayBuffer( 327 | 2 + // compression 328 | len 329 | ); 330 | var view = new DataView(buffer); 331 | view.setUint16(0, compression); 332 | var uint8array = new Uint8Array(buffer); 333 | var byteOffset = 2; 334 | for (i = 0; i < len; i += 4) uint8array[byteOffset++] = flattenedImageData[i]; 335 | for (i = 1; i < len; i += 4) uint8array[byteOffset++] = flattenedImageData[i]; 336 | for (i = 2; i < len; i += 4) uint8array[byteOffset++] = flattenedImageData[i]; 337 | for (i = 3; i < len; i += 4) uint8array[byteOffset++] = flattenedImageData[i]; 338 | return buffer; 339 | } 340 | }); 341 | Object.defineProperty(psdw.PsdWriter.prototype, 'blob', { 342 | get: function () { 343 | return new Blob([ 344 | this.fileHeader, 345 | this.colorModeData, 346 | this.imageResources, 347 | this.layerAndMaskInformation, 348 | this.imageData 349 | ], { 350 | type: 'image/vnd.adobe.photoshop' 351 | }); 352 | } 353 | }); 354 | --------------------------------------------------------------------------------