-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathilda_format.py
More file actions
167 lines (134 loc) · 5.71 KB
/
Copy pathilda_format.py
File metadata and controls
167 lines (134 loc) · 5.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# -*- coding: utf-8 -*-
"""
ilda_format.py -- Pure-Python reader/writer for the ILDA Image Data Transfer Format.
Compatible with Python 2.7 (Cinema 4D R21) *and* Python 3 (R23+). No third-party
dependencies. Deliberately decoupled from Cinema 4D so the byte encoding can be
unit-tested on its own and reused elsewhere (e.g. inside laser-synth).
Supports the two true-colour formats (explicit RGB, no palette needed):
* Format 5 -- 2D true colour (X, Y) -> 8 bytes / record
* Format 4 -- 3D true colour (X, Y, Z) -> 10 bytes / record
Header layout (32 bytes; multi-byte numeric fields are big-endian):
0 "ILDA" 4s 16 company name 8s
4 reserved (0) 3s 24 number of records H (0 => EOF)
7 format code B 26 frame number H
8 frame / palette name 8s 28 total frames H
30 projector number B
31 reserved (0) B
Per-point status byte: bit 7 (0x80) = last point of frame,
bit 6 (0x40) = blanking (1 = laser off).
Reference: ILDA Technical Standard, "ILDA Image Data Transfer Format", rev 011.
"""
from __future__ import division, print_function
__version__ = "1.0.0"
import struct
ILDA_MAGIC = b"ILDA"
COORD_MIN = -32768
COORD_MAX = 32767
STATUS_LAST_POINT = 0x80 # bit 7: final point of a frame
STATUS_BLANKING = 0x40 # bit 6: 1 = blanked (laser off)
_HEADER = struct.Struct(">4s3sB8s8sHHHBB")
assert _HEADER.size == 32, "ILDA header must be 32 bytes"
_REC_2D = struct.Struct(">hhBBBB") # X, Y, status, B, G, R
_REC_3D = struct.Struct(">hhhBBBB") # X, Y, Z, status, B, G, R
class IldaPoint(object):
"""A single galvo target. Coordinates are already in ILDA device space.
Plain class (not a dataclass) so it works under Python 2.7 in C4D R21.
"""
__slots__ = ("x", "y", "z", "r", "g", "b", "blank")
def __init__(self, x, y, z=0, r=255, g=255, b=255, blank=False):
self.x = x
self.y = y
self.z = z
self.r = r
self.g = g
self.b = b
self.blank = blank
def __repr__(self):
return "IldaPoint(%r,%r,%r,rgb=(%r,%r,%r),blank=%r)" % (
self.x, self.y, self.z, self.r, self.g, self.b, self.blank)
def _clamp(v, lo, hi):
return lo if v < lo else hi if v > hi else v
def _fit8(s):
raw = s.encode("ascii", "replace")[:8]
return raw + b"\x00" * (8 - len(raw))
def _pack_header(fmt, name, company, records, frame_no, total, projector=0):
return _HEADER.pack(
ILDA_MAGIC, b"\x00\x00\x00", fmt,
_fit8(name), _fit8(company),
records & 0xFFFF, frame_no & 0xFFFF, total & 0xFFFF,
projector & 0xFF, 0,
)
def save(frames, path, true_color_3d=False, name="C4D", company="ILDAEXP"):
"""Write a list of frames (each a list of IldaPoint) to an .ild file.
A trailing zero-record header is appended as the end-of-file marker, per spec.
Returns the number of frames written.
"""
fmt = 4 if true_color_3d else 5
rec = _REC_3D if true_color_3d else _REC_2D
total = len(frames)
f = open(path, "wb")
try:
for fi, pts in enumerate(frames):
n = len(pts)
if n > 0xFFFF:
raise ValueError(
"Frame %d has %d points; ILDA record count is a uint16 "
"(max 65535). Reduce sampling density." % (fi, n))
f.write(_pack_header(fmt, name, company, n, fi, total))
for i, p in enumerate(pts):
status = 0
if p.blank:
status |= STATUS_BLANKING
if i == n - 1:
status |= STATUS_LAST_POINT
x = _clamp(int(round(p.x)), COORD_MIN, COORD_MAX)
y = _clamp(int(round(p.y)), COORD_MIN, COORD_MAX)
r = _clamp(int(p.r), 0, 255)
g = _clamp(int(p.g), 0, 255)
b = _clamp(int(p.b), 0, 255)
if true_color_3d:
z = _clamp(int(round(p.z)), COORD_MIN, COORD_MAX)
f.write(rec.pack(x, y, z, status, b, g, r))
else:
f.write(rec.pack(x, y, status, b, g, r))
# End-of-file marker: a header carrying zero records.
f.write(_pack_header(fmt, name, company, 0, total, total))
finally:
f.close()
return total
def read(path):
"""Read an .ild file (formats 4 and 5) into a list of frames of IldaPoint.
Provided for round-trip testing and reuse; ignores palette formats.
"""
f = open(path, "rb")
try:
data = f.read()
finally:
f.close()
frames = []
off = 0
while off + _HEADER.size <= len(data):
unpacked = _HEADER.unpack_from(data, off)
magic = unpacked[0]
fmt = unpacked[2]
records = unpacked[5]
off += _HEADER.size
if magic != ILDA_MAGIC:
raise ValueError("Bad ILDA magic at offset %d" % (off - _HEADER.size))
if records == 0:
break # EOF marker
pts = []
if fmt == 5:
for _ in range(records):
x, y, status, b, g, r = _REC_2D.unpack_from(data, off)
off += _REC_2D.size
pts.append(IldaPoint(x, y, 0, r, g, b, bool(status & STATUS_BLANKING)))
elif fmt == 4:
for _ in range(records):
x, y, z, status, b, g, r = _REC_3D.unpack_from(data, off)
off += _REC_3D.size
pts.append(IldaPoint(x, y, z, r, g, b, bool(status & STATUS_BLANKING)))
else:
raise ValueError("Reader supports formats 4 and 5 only (got %d)" % fmt)
frames.append(pts)
return frames