Permalink
Browse files

update swatch to write .ase files in both py2/3

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...
1 parent 1161495 commit 8654edf4f1aeef37d42211ff3fe6a3e9e4325859 @nsfmc committed Apr 16, 2014
View
@@ -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
View
@@ -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::
View
@@ -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'
@@ -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):
View
@@ -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
@@ -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']
@@ -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
@@ -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
View
@@ -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")
@@ -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")
@@ -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")
@@ -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")
Binary file not shown.
@@ -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 not shown.
@@ -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.