Skip to content

Commit

Permalink
update swatch to write .ase files in both py2/3
Browse files Browse the repository at this point in the history
due to my misremembering of how bytes and strings work in python 3,
the previous commit never really worked in python3.x for a variety
of reasons, mostly due to my loosey-goosey handling of string and
bytes. this makes it more consistent.

updates lots of documentation and also documents part of the
writer's internals, even though those are far more obvious and the
code is pretty clear on that front.

adds in two more test cases which the previous versions missed:

1. a swatch file with only one color. this catches two important
issues: the left padded string being written out incorrectly and
the color mode/rgb/etc not matching the keys in the mode index.
2. a swatch with a folder and one color, mostly here so that it
can be tested to see if the folder-writing code is broken in a
small test-case.

the other tests are sort of ridiculous test harnesses since they're
really overkill. maybe i'll change that, but it's nice to have some
test on real legitimate style swatches.
  • Loading branch information
nsfmc committed Apr 16, 2014
1 parent 1161495 commit 8654edf
Show file tree
Hide file tree
Showing 9 changed files with 75 additions and 9 deletions.
1 change: 1 addition & 0 deletions CHANGES
@@ -1,2 +1,3 @@
v0.4.0, 04/16/2014 -- more tests, docs and module writes under py2.7 and py3.4
v0.2.9, 03/23/2014 -- swatch can write json files, round trip from ase->json->ase
v0.2.3, 12/30/2012 -- Unit tests added under swatch/tests, CHANGES file added
2 changes: 1 addition & 1 deletion README.rst
Expand Up @@ -12,7 +12,7 @@ the ase generator written for colourlovers by
`Chris Williams <http://www.colourlovers.com/ase.phps>`_

``swatch.write(lst, filename)`` reads in a list, as described below
and outputs a .ase file.
and outputs a .ase file. (new in v0.4.0)

``swatch.parse(filename)`` reads in an ase file and converts it to a
list of colors and palettes. colors are simple dicts of the form::
Expand Down
6 changes: 3 additions & 3 deletions swatch/__init__.py
Expand Up @@ -13,7 +13,7 @@


__title__ = 'swatch'
__version__ = '0.2.9'
__version__ = '0.4.0'
__author__ = 'Marcos Ojeda'
__license__ = 'MIT'
__copyright__ = 'Copyright 2014 Marcos A Ojeda'
Expand Down Expand Up @@ -111,8 +111,8 @@ def dumps(obj):
v_major, v_minor = 1, 0
chunk_count = writer.chunk_count(obj)

head = struct.pack("!4sHHI", header, v_major, v_minor, chunk_count)
body = "".join([writer.chunk_for_object(c) for c in obj])
head = struct.pack('!4sHHI', header, v_major, v_minor, chunk_count)
body = b''.join([writer.chunk_for_object(c) for c in obj])
return head + body

def dump(obj, fp):
Expand Down
57 changes: 52 additions & 5 deletions swatch/writer.py
Expand Up @@ -16,6 +16,11 @@


def chunk_count(swatch):
"""return the number of byte-chunks in a swatch object
this recursively walks the swatch list, returning 1 for a single color &
returns 2 for each folder plus 1 for each color it contains
"""
if type(swatch) is dict:
if 'data' in swatch:
return 1
Expand All @@ -32,20 +37,41 @@ def chunk_for_object(obj):
return chunk_for_color(obj)

def chunk_for_color(obj):
# will prepend the total length of the chunk as a 4 byte int
"""builds up a byte-chunk for a color
the format for this is
b'\x00\x01' +
Big-Endian Unsigned Int == len(bytes that follow in this block)
• Big-Endian Unsigned Short == len(color_name)
in practice, because utf-16 takes up 2 bytes per letter
this will be 2 * (len(name) + 1)
so a color named 'foo' would be 8 bytes long
• UTF-16BE Encoded color_name terminated with '\0'
using 'foo', this yields '\x00f\x00o\x00o\x00\x00'
• A 4-byte char for Color mode ('RGB ', 'Gray', 'CMYK', 'LAB ')
note the trailing spaces
• a variable-length number of 4-byte length floats
this depends entirely on the color mode of the color.
• A Big-Endian short int for either a global, spot, or process color
global == 0, spot == 1, process == 2
title = (obj['name']+'\0')
the chunk has no terminating string although other sites have indicated
that the global/spot/process short is a terminator, it's actually used
to indicate how illustrator should deal with the color.
"""
title = obj['name'] + '\0'
title_length = len(title)
chunk = struct.pack('>H', title_length)
chunk += title.encode('utf-16be')

mode = obj['data']['mode']
mode = obj['data']['mode'].encode()
values = obj['data']['values']
color_type = obj['type']

fmt = {b'RGB': '!fff', b'Gray': '!f', b'CMYK': '!ffff', b'LAB': '!fff'}
if mode in fmt:
chunk += struct.pack('!4s', str(mode).ljust(4)) # color name
padded_mode = mode.decode().ljust(4).encode()
chunk += struct.pack('!4s', padded_mode) # the color mode
chunk += struct.pack(fmt[mode], *values) # the color values

color_types = ['Global', 'Spot', 'Process']
Expand All @@ -57,6 +83,27 @@ def chunk_for_color(obj):
return b'\x00\x01' + chunk # swatch color header

def chunk_for_folder(obj):
"""produce a byte-chunk for a folder of colors
the structure is very similar to a color's data:
• Header
b'\xC0\x01' +
Big Endian Unsigned Int == len(Bytes in the Header Block)
note _only_ the header, this doesn't include the length of color data
• Big Endian Unsigned Short == len(Folder Name + '\0')
Note that Folder Name is assumed to be utf-16be so this
will always be an even number
• Folder Name + '\0', encoded UTF-16BE
• body
chunks for each color, see chunk_for_color
• folder terminator
b'\xC0\x02' +
b'\x00\x00\x00\x00'
Perhaps the four null bytes represent something, but i'm pretty sure
they're just a terminating string, but there's something nice about
how the b'\xC0\x02' matches with the folder's header
"""
title = obj['name'] + '\0'
title_length = len(title)
chunk_body = struct.pack('>H', title_length) # title length
Expand All @@ -67,7 +114,7 @@ def chunk_for_folder(obj):
# precede entire chunk by folder header and size of folder
chunk = chunk_head + chunk_body

chunk += "".join([chunk_for_color(c) for c in obj['swatches']])
chunk += b''.join([chunk_for_color(c) for c in obj['swatches']])

chunk += b'\xC0\x02' # folder terminator chunk
chunk += b'\x00\x00\x00\x00' # folder terminator
Expand Down
16 changes: 16 additions & 0 deletions tests/__init__.py
Expand Up @@ -27,6 +27,10 @@ def compare_with_json(self, basepath):
js = json.load(f)
return js, ase

def test_single_swatch(self):
js, ase = self.compare_with_json("white swatch no folder")
self.assertEqual(js, ase, "single swatch no longer parses")

def test_LAB(self):
js, ase = self.compare_with_json("solarized")
self.assertEqual(js, ase, "LAB test fails with solarized data")
Expand All @@ -35,6 +39,10 @@ def test_empty_file(self):
js, ase = self.compare_with_json("empty white folder")
self.assertEqual(js, ase, "empty named folder no longer parses")

def test_single_swatch_in_folder(self):
js, ase = self.compare_with_json("single white swatch in folder")
self.assertEqual(js, ase, "folder with one swatch no longer parses")

def test_RGB(self):
js, ase = self.compare_with_json("sampler")
self.assertEqual(js, ase, "RGB test fails with sampler")
Expand All @@ -50,6 +58,10 @@ def compare_with_ase(self, basepath):
with open(base + ".ase", 'rb') as raw_ase:
return raw_ase.read(), generated_ase

def test_single_swatch(self):
raw, generated = self.compare_with_ase("white swatch no folder")
self.assertEqual(raw, generated, "single swatch has broken")

def test_LAB(self):
raw, generated = self.compare_with_ase("solarized")
self.assertEqual(raw, generated, "LAB test fails with solarized data")
Expand All @@ -58,6 +70,10 @@ def test_empty_file(self):
raw, generated = self.compare_with_ase("empty white folder")
self.assertEqual(raw, generated, "empty named folder no longer parses")

def test_single_swatch_in_folder(self):
raw, generated = self.compare_with_ase("single white swatch in folder")
self.assertEqual(raw, generated, "folder with one swatch no longer parses")

def test_RGB(self):
raw, generated = self.compare_with_ase("sampler")
self.assertEqual(raw, generated, "RGB test fails with sampler")
Expand Down
Binary file added tests/single white swatch in folder.ase
Binary file not shown.
1 change: 1 addition & 0 deletions tests/single white swatch in folder.json
@@ -0,0 +1 @@
[{"swatches": [{"data": {"values": [1.0, 1.0, 1.0], "mode": "RGB"}, "type": "Process", "name": "White"}], "type": "Color Group", "name": "Whitefolder"}]
Binary file added tests/white swatch no folder.ase
Binary file not shown.
1 change: 1 addition & 0 deletions tests/white swatch no folder.json
@@ -0,0 +1 @@
[{"data": {"values": [1.0, 1.0, 1.0], "mode": "RGB"}, "type": "Process", "name": "White"}]

0 comments on commit 8654edf

Please sign in to comment.