how to make .ase color palettes with javascript
For the last year or so, I've been working on a color tool called onyx to replace an old tool I made called emerald. one of the primary functions of onyx is to allow the creation of color palettes for use in other software. however, there are just so many color palette formats floating around in the world...so I just decided to pick seven for onyx's initial release.
.sass, .css, .hex, .pal, .gpl, .txt, and .png are the seven formats. the one I had the most trouble with was definitely .ase, so I figured I'd write down what I did in case someone else needed to know. let's get right into it!
one: ase file format specification.
the ase file format, which stands for adobe swatch exchange, can be used to import and export color palettes between most products in the adobe suite. here's what one of the files looks like, inspected in a hex editor:

side note: the hex editor I'm using is imhex — which, as far as I'm concerned, is the best hex editor ever created.
the file format is quite simple. I read a few other posts elsewhere about this format, but it seems that many of them are now out of date. so here's how it works: the entire palette is just a header, followed by however many color entries you want. I wrote some imhex patterns to help me understand the file structure. if you can read some typed language like c, you can probably figure out what these mean. the header goes as follows:
struct ASEHeader {
char sig[4]; // constant ASEF (adobe swatch exchange format)
u32 version; // constant 0x0001 0x0000
be u32 color_count; // amount of colors in the palette
};
that's it. just two constants followed by the number of colors that are in the palette. to construct the color entries, we'll use this structure:
struct ColorBlock {
be u16 group_start;
be u32 block_length;
be u16 color_name_length;
be u96 color_name; // hex color, zeroes between characters
be u16 null_term;
be char color_space[4];
be float red;
be float green;
be float blue;
be u16 mode; // 0x0000: global, 0x0010: spot, 0x0020: normal
};
two: translation.
in order to translate this into javascript, we'll need a color palette. here's the color palette we'll be using (neo-5 by munche):

for this, I used hsl colors. here are the five colors from the palette above, in order from left to right: hsl(0.00, 0.00%, 5.49%), hsl(254.24, 57.68%, 47.25%), hsl(317.01, 79.51%, 52.16%), hsl(175.21, 94.00%, 60.78%), and hsl(180.00, 52.38%, 95.88%)
here's how you convert your array of colors into a hexadecimal version of the final binary .ase file. this intermediate step might not be necessary, but it sure made things easier for me.
make two copies of your array, one in which the colors are converted to rgb, and another one in which the colors are converted to hexadecimal. I won't show the code for this (because it would be too long), but there are plenty of color conversion algorithms available online.
second, create the file header. here's a utility function to convert a number to a zero-padded hexadecimal string:
const zeroPaddedHex = d => (+d).toString(16).padStart(2, '0')
and here's how the header is built:
const fileHeader = `41 53 45 46 00 01 00 00 00 00 00 ${zeroPaddedHex(palette.length)}`
then, create a mapping function to convert your hexadecimal colors to color blocks in the manner described by the struct above:
const generateColorBlock = (color, index) => {
const GROUP_START = '00 01'
const BLOCK_LENGTH = '00 00 00 22'
const COLOR_NAME_LENGTH = `00 ${zeroPaddedHex(color.length)}`
const COLOR_NAME = color
.replace('#', '')
.split('')
.map(f => `00 ${f.charCodeAt(0).toString(16).padStart(2, 0)}`)
.join(' ')
const NULL_TERM = '00 00'
const COLOR_SPACE = '52 47 42 20'
const RED = floatToHex(rgbColorsindex.r / 255)
const GREEN = floatToHex(rgbColorsindex.g / 255)
const BLUE = floatToHex(rgbColorsindex.b / 255)
const MODE = '00 00'
return `${GROUP_START} ${BLOCK_LENGTH} ${COLOR_NAME_LENGTH} ${COLOR_NAME} ${NULL_TERM} ${COLOR_SPACE} ${RED} ${GREEN} ${BLUE} ${MODE}`
}
first, note the BLOCK_LENGTH variable. you'll need to change this depending on the format you decide to use and the names of your colors. keep the struct from earlier in mind — you have to change around some lengths here and there if you want to change the color name. this doesn't matter if you just use the hexadecimal representation of the color as the color name, which is what I did.
second of all, you might have to change the COLOR_SPACE string. I'm using rgb here, which is the string "RGB ". the trailing space is significant because you have four color space options: RGB, LAB, CMYK, or gray. no matter what you pick, the COLOR_SPACE string has to be four characters. rgb in hex is 52 47 42 20, cmyk is 63 6D 79 6B, and so forth.
lastly, look at the RED, GREEN, and BLUE variables. I formatted my rgb colors into objects like { r, g, b } with r, g, and b in range [0, 255]. keep that in mind so you can adapt this code for your use. also, my hex color array is an array of six-digit hex color strings like '#ffffff', '#000000', etc. by the way, if you're not even using rgb, you'll have a different amount of color values here. cmyk requires four thirty-two bit floats, gray requires only one, lab and rgb require three.
use the generateColorBlock function to map your color array to hexadecimal strings and join the blocks together with spaces. if you're using the same color palette as I am, and you're using hsl colors, you should have close to this exact string:
00 01 00 00 00 22 00 07 00 30 00 65 00 30 00 65 00 30 00 65 00 00 52 47 42 20 3D 60 E0 E1 3D 60 E0 E1 3D 60 E0 E1 00 00 00 01 00 00 00 22 00 07 00 35 00 34 00 33 00 33 00 62 00 65 00 00 52 47 42 20 3E A8 A8 A9 3E 4C CC CD 3F 3E BE BF 00 00 00 01 00 00 00 22 00 07 00 65 00 36 00 32 00 34 00 61 00 66 00 00 52 47 42 20 3F 66 E6 E7 3E 10 90 91 3F 2F AF B0 00 00 00 01 00 00 00 22 00 07 00 33 00 64 00 66 00 39 00 65 00 61 00 00 52 47 42 20 3E 74 F4 F5 3F 79 F9 FA 3F 6A EA EB 00 00 00 01 00 00 00 22 00 07 00 65 00 66 00 66 00 61 00 66 00 61 00 00 52 47 42 20 3F 6F EF F0 3F 7A FA FB 3F 7A FA FB 00 00
then, just stick the header from earlier on the front:
41 53 45 46 00 01 00 00 00 00 00 05 00 01 00 00 00 22 00 07 00 30 00 65 00 30 00 65 00 30 00 65 00 00 52 47 42 20 3D 60 E0 E1 3D 60 E0 E1 3D 60 E0 E1 00 00 00 01 00 00 00 22 00 07 00 35 00 34 00 33 00 33 00 62 00 65 00 00 52 47 42 20 3E A8 A8 A9 3E 4C CC CD 3F 3E BE BF 00 00 00 01 00 00 00 22 00 07 00 65 00 36 00 32 00 34 00 61 00 66 00 00 52 47 42 20 3F 66 E6 E7 3E 10 90 91 3F 2F AF B0 00 00 00 01 00 00 00 22 00 07 00 33 00 64 00 66 00 39 00 65 00 61 00 00 52 47 42 20 3E 74 F4 F5 3F 79 F9 FA 3F 6A EA EB 00 00 00 01 00 00 00 22 00 07 00 65 00 66 00 66 00 61 00 66 00 61 00 00 52 47 42 20 3F 6F EF F0 3F 7A FA FB 3F 7A FA FB 00 00
if you look back at the hex editor screenshot from earlier, you'll see that this hexadecimal is the same as the data in the screenshot. now, all we have to do is convert the hexadecimal into a binary file that we can download as an .ase.
three: downloading.
creating a binary file out of hexadecimal is pretty straightforward in javascript. here's the code I used to create a url to the file:
const downloadHexadecimalData = (data, filename, extension) => {
const hexData = data.replaceAll(' ', '')
let byteArray = new Uint8Array(hexData.length / 2)
for (let i = 0; i < byteArray.length; i++) {
byteArrayi = parseInt(hexData.substr(i * 2, 2), 16)
}
const blob = new Blob(byteArray, { type: 'application/octet-stream' })
const url = URL.createObjectURL(blob)
return {
href: url,
download: `${filename}.${extension}`,
}
}
then, call this function with three arguments: the hexadecimal data, your desired filename, and the string 'ase'. you'll get back an object URL and a download filename that you can attach to a link. if you don't want to download the file, just use this function up to the creation of the Blob and do whatever you want with it. the end!
see also:
http://www.selapa.net/swatches/colors/fileformats.php: this is a great reference for many different kinds of color file formats!
currently listening to:
- Holy Waterfall by Aesop Rock
- The Gates by Aesop Rock