320 lines
14 KiB
Python
320 lines
14 KiB
Python
#!/usr/bin/python3
|
|
# -*- coding: utf-8 -*-
|
|
# -*- mode: Python -*-
|
|
|
|
'''
|
|
|
|
fromild
|
|
v0.1.0
|
|
|
|
Read/display once an .ild animation file and quit ??
|
|
Licensed under GNU GPLv3
|
|
|
|
by cocoa and Sam Neurohack
|
|
|
|
Heavy u-se of :
|
|
|
|
ILDA.py
|
|
|
|
Python module for dealing with the ILDA Image Data Transfer Format,
|
|
an interchange format for laser image frames.
|
|
|
|
Copyright (c) 2008 Micah Dowty
|
|
|
|
Permission is hereby granted, free of charge, to any person
|
|
obtaining a copy of this software and associated documentation
|
|
files (the "Software"), to deal in the Software without
|
|
restriction, including without limitation the rights to use, copy,
|
|
modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be
|
|
included in all copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
|
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
SOFTWARE.
|
|
|
|
'''
|
|
|
|
from __future__ import print_function
|
|
import time
|
|
import struct
|
|
import argparse
|
|
import sys
|
|
|
|
name="generator::fromild"
|
|
|
|
|
|
def debug(*args, **kwargs):
|
|
if( verbose == False ):
|
|
return
|
|
print(*args, file=sys.stderr, **kwargs)
|
|
|
|
argsparser = argparse.ArgumentParser(description=".ild file frame generator")
|
|
argsparser.add_argument("-f","--fps",help="Frame Per Second",default=30,type=int)
|
|
argsparser.add_argument("-v","--verbose",action="store_true",help="Verbose output")
|
|
argsparser.add_argument("-i","--ild",help=".ild file",default="book2.ild",type=str)
|
|
args = argsparser.parse_args()
|
|
|
|
fps=args.fps
|
|
verbose=args.verbose
|
|
|
|
optimal_looptime = 1 / fps
|
|
debug(name+" optimal looptime "+str(optimal_looptime))
|
|
|
|
# Format codes
|
|
FORMAT_3D = 0
|
|
FORMAT_2D = 1
|
|
FORMAT_COLOR_TABLE = 2
|
|
|
|
# Mapping from FORMAT_* codes to struct format strings
|
|
formatTable = (
|
|
'>hhhH',
|
|
'>hhH',
|
|
'>BBB',
|
|
)
|
|
|
|
# Header values
|
|
HEADER_MAGIC = b"ILDA\0\0\0"
|
|
HEADER_RESERVED = 0
|
|
HEADER_FORMAT = ">7sB16sHHHBB"
|
|
HEADER_LEN = struct.calcsize(HEADER_FORMAT)
|
|
|
|
# 64 default colors table : use rgb2int(colors64[ildacolor])
|
|
colors64 = [[255, 0, 0], [255, 17, 0], [255, 34, 0], [255, 51, 0], [255, 68, 0], [255, 85, 0], [255, 102, 0], [255, 119, 0], [255, 136, 0], [255, 153, 0], [255, 170, 0], [255, 187, 0], [255, 204, 0], [255, 221, 0], [255, 238, 0], [255, 255, 0], [255, 255, 0], [238, 255, 0], [204, 255, 0], [170, 255, 0], [136, 255, 0], [102, 255, 0], [68, 255, 0], [34, 255, 0], [0, 255, 0], [0, 255, 34], [0, 255, 68], [0, 255, 102], [0, 255, 136], [0, 255, 170], [0, 255, 204], [0, 255, 238], [0, 136, 255], [0, 119, 255], [0, 102, 255], [0, 102, 255], [0, 85, 255], [0, 68, 255], [0, 68, 255], [0, 34, 255], [0, 0, 255], [34, 0, 255], [68, 0, 255], [102, 0, 255], [136, 0, 255], [170, 0, 255], [204, 0, 255], [238, 0, 255], [255, 0, 255], [255, 34, 255], [255, 68, 255], [255, 102, 255], [255, 136, 255], [255, 170, 255], [255, 204, 255], [255, 238, 255], [255, 255, 255], [255, 238, 238], [255, 204, 204], [255, 170, 170], [255, 136, 136], [255, 102, 102], [255, 68, 68], [0, 34, 34]]
|
|
|
|
|
|
# 256 default colors table
|
|
colors256 = [[0, 0, 0], [255, 255, 255], [255, 0, 0], [255, 255, 0], [0, 255, 0], [0, 255, 255], [0, 0, 255], [255, 0, 255], [255, 128, 128], [255, 140, 128], [255, 151, 128], [255, 163, 128], [255, 174, 128], [255, 186, 128], [255, 197, 128], [255, 209, 128], [255, 220, 128], [255, 232, 128], [255, 243, 128], [255, 255, 128], [243, 255, 128], [232, 255, 128], [220, 255, 128], [209, 255, 128], [197, 255, 128], [186, 255, 128], [174, 255, 128], [163, 255, 128], [151, 255, 128], [140, 255, 128], [128, 255, 128], [128, 255, 140], [128, 255, 151], [128, 255, 163], [128, 255, 174], [128, 255, 186], [128, 255, 197], [128, 255, 209], [128, 255, 220], [128, 255, 232], [128, 255, 243], [128, 255, 255], [128, 243, 255], [128, 232, 255], [128, 220, 255], [128, 209, 255], [128, 197, 255], [128, 186, 255], [128, 174, 255], [128, 163, 255], [128, 151, 255], [128, 140, 255], [128, 128, 255], [140, 128, 255], [151, 128, 255], [163, 128, 255], [174, 128, 255], [186, 128, 255], [197, 128, 255], [209, 128, 255], [220, 128, 255], [232, 128, 255], [243, 128, 255], [255, 128, 255], [255, 128, 243], [255, 128, 232], [255, 128, 220], [255, 128, 209], [255, 128, 197], [255, 128, 186], [255, 128, 174], [255, 128, 163], [255, 128, 151], [255, 128, 140], [255, 0, 0], [255, 23, 0], [255, 46, 0], [255, 70, 0], [255, 93, 0], [255, 116, 0], [255, 139, 0], [255, 162, 0], [255, 185, 0], [255, 209, 0], [255, 232, 0], [255, 255, 0], [232, 255, 0], [209, 255, 0], [185, 255, 0], [162, 255, 0], [139, 255, 0], [116, 255, 0], [93, 255, 0], [70, 255, 0], [46, 255, 0], [23, 255, 0], [0, 255, 0], [0, 255, 23], [0, 255, 46], [0, 255, 70], [0, 255, 93], [0, 255, 116], [0, 255, 139], [0, 255, 162], [0, 255, 185], [0, 255, 209], [0, 255, 232], [0, 255, 255], [0, 232, 255], [0, 209, 255], [0, 185, 255], [0, 162, 255], [0, 139, 255], [0, 116, 255], [0, 93, 255], [0, 70, 255], [0, 46, 255], [0, 23, 255], [0, 0, 255], [23, 0, 255], [46, 0, 255], [70, 0, 255], [93, 0, 255], [116, 0, 255], [139, 0, 255], [162, 0, 255], [185, 0, 255], [209, 0, 255], [232, 0, 255], [255, 0, 255], [255, 0, 232], [255, 0, 209], [255, 0, 185], [255, 0, 162], [255, 0, 139], [255, 0, 116], [255, 0, 93], [255, 0, 70], [255, 0, 46], [255, 0, 23], [128, 0, 0], [128, 12, 0], [128, 23, 0], [128, 35, 0], [128, 47, 0], [128, 58, 0], [128, 70, 0], [128, 81, 0], [128, 93, 0], [128, 105, 0], [128, 116, 0], [128, 128, 0], [116, 128, 0], [105, 128, 0], [93, 128, 0], [81, 128, 0], [70, 128, 0], [58, 128, 0], [47, 128, 0], [35, 128, 0], [23, 128, 0], [12, 128, 0], [0, 128, 0], [0, 128, 12], [0, 128, 23], [0, 128, 35], [0, 128, 47], [0, 128, 58], [0, 128, 70], [0, 128, 81], [0, 128, 93], [0, 128, 105], [0, 128, 116], [0, 128, 128], [0, 116, 128], [0, 105, 128], [0, 93, 128], [0, 81, 128], [0, 70, 128], [0, 58, 128], [0, 47, 128], [0, 35, 128], [0, 23, 128], [0, 12, 128], [0, 0, 128], [12, 0, 128], [23, 0, 128], [35, 0, 128], [47, 0, 128], [58, 0, 128], [70, 0, 128], [81, 0, 128], [93, 0, 128], [105, 0, 128], [116, 0, 128], [128, 0, 128], [128, 0, 116], [128, 0, 105], [128, 0, 93], [128, 0, 81], [128, 0, 70], [128, 0, 58], [128, 0, 47], [128, 0, 35], [128, 0, 23], [128, 0, 12], [255, 192, 192], [255, 64, 64], [192, 0, 0], [64, 0, 0], [255, 255, 192], [255, 255, 64], [192, 192, 0], [64, 64, 0], [192, 255, 192], [64, 255, 64], [0, 192, 0], [0, 64, 0], [192, 255, 255], [64, 255, 255], [0, 192, 192], [0, 64, 64], [192, 192, 255], [64, 64, 255], [0, 0, 192], [0, 0, 64], [255, 192, 255], [255, 64, 255], [192, 0, 192], [64, 0, 64], [255, 96, 96], [255, 255, 255], [245, 245, 245], [235, 235, 235], [224, 224, 224], [213, 213, 213], [203, 203, 203], [192, 192, 192], [181, 181, 181], [171, 171, 171], [160, 160, 160], [149, 149, 149], [139, 139, 139], [128, 128, 128], [117, 117, 117], [107, 107, 107], [96, 96, 96], [85, 85, 85], [75, 75, 75], [64, 64, 64], [53, 53, 53], [43, 43, 43], [32, 32, 32], [21, 21, 21], [11, 11, 11], [0, 0, 0]]
|
|
|
|
def rgb2int(rgb):
|
|
return int('0x%02x%02x%02x' % tuple(rgb),0)
|
|
|
|
class Table(object):
|
|
"""Container object for one ILDA table: either a frame (table of points)
|
|
or a palette (table of colors).
|
|
|
|
The 'items' list contains the data within this table. Each item
|
|
is a tuple, corresponding to the raw values within that row of the
|
|
table.
|
|
|
|
2D frame: (x, y, status)
|
|
3D frame: (x, y, z, status)
|
|
Color: (r, g, b)
|
|
|
|
"""
|
|
def __init__(self, format=FORMAT_2D, name="",
|
|
length=0, number=0, total=0, scanHead=0):
|
|
self.__dict__.update(locals())
|
|
self.items = []
|
|
self.itemsproducer = None
|
|
|
|
def __repr__(self):
|
|
return ("<ILDA.Table format=%d name=%r "
|
|
"length=%d number=%d total=%d scanHead=%d>" %
|
|
(self.format, self.name, self.length, self.number,
|
|
self.total, self.scanHead))
|
|
|
|
def unpackHeader(self, data):
|
|
magic, self.format, self.name, self.length, \
|
|
self.number, self.total, self.scanHead, \
|
|
reserved = struct.unpack(HEADER_FORMAT, data)
|
|
print(magic, HEADER_MAGIC)
|
|
if magic != HEADER_MAGIC:
|
|
raise ValueError("Bad ILDA header magic. Not an ILDA file?")
|
|
if reserved != HEADER_RESERVED:
|
|
raise ValueError("Reserved ILDA field is not zero.")
|
|
|
|
def packHeader(self):
|
|
return struct.pack(HEADER_FORMAT, HEADER_MAGIC, self.format,
|
|
self.name, self.length, self.number,
|
|
self.total, self.scanHead, HEADER_RESERVED)
|
|
|
|
def readHeader(self, stream):
|
|
self.unpackHeader(stream.read(HEADER_LEN))
|
|
|
|
def writeHeader(self, stream):
|
|
stream.write(self.packHeader())
|
|
|
|
def _getItemFormat(self):
|
|
try:
|
|
return formatTable[self.format]
|
|
except IndexError:
|
|
raise ValueError("Unsupported format code")
|
|
|
|
def read_stream(self, stream):
|
|
"""Read the header, then read all items in this table."""
|
|
self.readHeader(stream)
|
|
if self.length:
|
|
fmt = self._getItemFormat()
|
|
itemSize = struct.calcsize(fmt)
|
|
self.items = [struct.unpack(fmt, stream.read(itemSize))
|
|
for i in range(self.length)]
|
|
self.itemsproducer = self.produce()
|
|
|
|
def write(self, stream):
|
|
"""Write the header, then write all items in this table."""
|
|
self.writeHeader(stream)
|
|
if self.length:
|
|
fmt = self._getItemFormat()
|
|
itemSize = struct.calcsize(fmt)
|
|
stream.write(''.join([struct.pack(fmt, *item)
|
|
for item in self.items]))
|
|
|
|
def iterPoints(self):
|
|
"""Iterate over Point instances for each item in this table.
|
|
Only makes sense if this is a 2D or 3D point table.
|
|
"""
|
|
for item in self.items:
|
|
p = Point()
|
|
p.decode(item)
|
|
yield p
|
|
|
|
|
|
def produce(self):
|
|
"""Iterate over Point instances for each item in this table.
|
|
Only makes sense if this is a 2D or 3D point table.
|
|
"""
|
|
while True:
|
|
for item in self.items:
|
|
p = Point()
|
|
p.decode(item)
|
|
yield p.encode()
|
|
#yield (p.x, p.y, p.z, p.color, p.blanking)
|
|
|
|
def read(self, cap):
|
|
"""yields what dac.play_stream() needs (x, y, z, ?, ?)
|
|
"""
|
|
return [next(self.itemsproducer) for i in range(cap)]
|
|
|
|
|
|
class Point:
|
|
"""Abstraction for one vector point. The Table object, for
|
|
completeness and efficiency, stores raw tuples for each
|
|
point. This is a higher level interface that decodes the status
|
|
bits and represents coordinates in floating point.
|
|
"""
|
|
def __init__(self, x=0.0, y=0.0, z=0.0, color=0, blanking=False):
|
|
self.__dict__.update(locals())
|
|
|
|
def __repr__(self):
|
|
|
|
return "%s, %s, %s, %s, %s" % (
|
|
self.x, self.y, self.z, self.color, self.blanking)
|
|
#return "<ILDA.Point (%s, %s, %s) color=%s blanking=%s>" % (
|
|
# self.x, self.y, self.z, self.color, self.blanking)
|
|
|
|
def encode(self):
|
|
status = self.color & 0xFF
|
|
if self.blanking:
|
|
status |= 1 << 14
|
|
|
|
return (
|
|
int( min(0x7FFF, max(-0x7FFF, self.x * 0x7FFF)) ),
|
|
int( min(0x7FFF, max(-0x7FFF, self.y * 0x7FFF)) ),
|
|
int( min(0x7FFF, max(-0x7FFF, self.z * 0x7FFF)) ),
|
|
int( min(0x7FFF, max(-0x7FFF, self.color * 0x7FFF)) ),
|
|
int( min(0x7FFF, max(-0x7FFF, self.blanking * 0x7FFF)) )
|
|
)
|
|
|
|
def decode(self, t):
|
|
#print "~~ Decoding, t of len "+ str(len(t)) +" is: " + str(t)
|
|
self.x = t[0] / 0x7FFF
|
|
self.y = t[1] / 0x7FFF
|
|
if len(t) > 3:
|
|
self.z = t[2] / 0x7FFF
|
|
# self.color = t[3] & 0xFF
|
|
# self.blanking = (t[3] & (1 << 14)) != 0
|
|
else:
|
|
self.z = 0.0
|
|
|
|
self.color = t[-1] & 0xFF
|
|
self.blanking = (t[-1] & (1 << 14)) != 0
|
|
|
|
|
|
def read(stream):
|
|
"""Read ILDA data from a stream until we hit the
|
|
end-of-stream marker. Yields a sequence of Table objects.
|
|
"""
|
|
while True:
|
|
t = Table()
|
|
t.read_stream(stream)
|
|
if not t.length:
|
|
# End-of-stream
|
|
break
|
|
yield t
|
|
|
|
|
|
def write(stream, tables):
|
|
"""Write a sequence of tables in ILDA format,
|
|
terminated by an end-of-stream marker.
|
|
"""
|
|
for t in tables:
|
|
t.write(stream)
|
|
Table().write(stream)
|
|
|
|
|
|
def readFrames(stream):
|
|
"""Read ILDA data from a stream, and ignore
|
|
all non-frame tables. Yields only 2D or 3D
|
|
point tables.
|
|
"""
|
|
for t in read(stream):
|
|
if t.format in (FORMAT_2D, FORMAT_3D):
|
|
yield t
|
|
|
|
|
|
def readFirstFrame(stream):
|
|
"""Read only a single frame from an ILDA stream."""
|
|
for frame in readFrames(stream):
|
|
return frame
|
|
|
|
|
|
#
|
|
f = open(args.ild, 'rb')
|
|
myframe = readFirstFrame(f)
|
|
|
|
while myframe.number +1< myframe.total:
|
|
|
|
start = time.time()
|
|
shape =[]
|
|
|
|
if myframe is None:
|
|
f.close()
|
|
break
|
|
|
|
debug(name,"Frame", myframe.number, "/",myframe.total, "length", myframe.length)
|
|
|
|
for p in myframe.iterPoints():
|
|
p2 = str(p)
|
|
point = p2.split(',')
|
|
|
|
x = float(point[0])
|
|
y = float(point[1])
|
|
z = float(point[2])
|
|
color = int(point[3])
|
|
blanking = point[4][1:]
|
|
|
|
if blanking == "True":
|
|
shape.append([300+(x*300),300+(-y*300),0])
|
|
else:
|
|
shape.append([300+(x*300),300+(-y*300),rgb2int(colors64[color])])
|
|
|
|
print(shape, flush=True);
|
|
myframe = readFirstFrame(f)
|
|
|
|
looptime = time.time() - start
|
|
if( looptime < optimal_looptime ):
|
|
time.sleep( optimal_looptime - looptime)
|
|
debug(name+" micro sleep:"+str( optimal_looptime - looptime))
|
|
|
|
f.close()
|
|
debug(name + " end of .ild animation")
|