From abac0bb2fc69ce869de2b1125edbd64d8c3aa6d9 Mon Sep 17 00:00:00 2001 From: Heiko Westermann Date: Sat, 20 Jul 2019 14:24:24 +0200 Subject: [PATCH 1/4] support for photon v2 format. untested, needs tests. --- pyphotonfile/photonfile.py | 68 ++++++++++++++++++++++++++++++++------ 1 file changed, 58 insertions(+), 10 deletions(-) diff --git a/pyphotonfile/photonfile.py b/pyphotonfile/photonfile.py index d13ae8b..1916051 100644 --- a/pyphotonfile/photonfile.py +++ b/pyphotonfile/photonfile.py @@ -128,7 +128,8 @@ def _open(self, filepath=None): data = f.read() # embed() f = io.BytesIO(data) - self._header = f.read(8) + self._header = f.read(4) + self._version = struct.unpack('i', f.read(4))[0] self._bed_x = struct.unpack('f', f.read(4))[0] self._bed_y = struct.unpack('f', f.read(4))[0] self._bed_z = struct.unpack('f', f.read(4))[0] @@ -142,18 +143,30 @@ def _open(self, filepath=None): self._resolution_y = struct.unpack('i', f.read(4))[0] self._preview_highres_header_address = struct.unpack('i', f.read(4))[0] self._layer_def_address = struct.unpack('i', f.read(4))[0] - n_layers = struct.unpack('i', f.read(4))[0] + self.n_layers = struct.unpack('i', f.read(4))[0] self._preview_lowres_header_address = struct.unpack('i', f.read(4))[0] - f.seek(4, os.SEEK_CUR) # padding + if self._version > 1: + self._print_time = struct.unpack('i', f.read(4))[0] + else: + f.seek(4, os.SEEK_CUR) # padding self._projection_type = struct.unpack('i', f.read(4))[0] - f.seek(6*4, os.SEEK_CUR) # padding + self.layer_levels = 1 + if self._version > 1: + self._print_properties_address = struct.unpack('i', f.read(4))[0] + self._print_properties_length = struct.unpack('i', f.read(4))[0] + self._anti_aliasing_level = struct.unpack('i', f.read(4))[0] + self.layer_levels = self._anti_aliasing_level + self.light_pwm = struct.unpack('h', f.read(2))[0] + self.light_pwm_bottom = struct.unpack('h', f.read(2))[0] + # else: + # f.seek(6*4, os.SEEK_CUR) # padding f.seek(self._preview_highres_header_address, os.SEEK_SET) self._preview_highres_resolution_x = struct.unpack('i', f.read(4))[0] self._preview_highres_resolution_y = struct.unpack('i', f.read(4))[0] self._preview_highres_data_address = struct.unpack('i', f.read(4))[0] self._preview_highres_data_length = struct.unpack('i', f.read(4))[0] - f.seek(4*4, os.SEEK_CUR) # padding + # f.seek(4*4, os.SEEK_CUR) # padding self._preview_highres_data = f.read(self._preview_highres_data_length) f.seek(self._preview_lowres_header_address, os.SEEK_SET) @@ -161,13 +174,30 @@ def _open(self, filepath=None): self._preview_lowres_resolution_y = struct.unpack('i', f.read(4))[0] self._preview_lowres_data_address = struct.unpack('i', f.read(4))[0] self._preview_lowres_data_length = struct.unpack('i', f.read(4))[0] - f.seek(4*4, os.SEEK_CUR) # padding + # f.seek(4*4, os.SEEK_CUR) # padding self._preview_lowres_data = f.read(self._preview_lowres_data_length) - f.seek(self._layer_def_address, os.SEEK_SET) + if self._version > 1: + f.seek(self._print_properties_address, os.SEEK_SET) + self._bottom_lift_distance = struct.unpack('f', f.read(4))[0] + self._bottom_lift_speed = struct.unpack('f', f.read(4))[0] + self._lifting_distance = struct.unpack('f', f.read(4))[0] + self._lifting_speed = struct.unpack('f', f.read(4))[0] + self._retract_speed = struct.unpack('f', f.read(4))[0] + self._volume_ml = struct.unpack('f', f.read(4))[0] + self._weight_g = struct.unpack('f', f.read(4))[0] + self._cost_dollars = struct.unpack('f', f.read(4))[0] + self._bottom_light_off_delay = struct.unpack('f', f.read(4))[0] + self._light_off_delay = struct.unpack('f', f.read(4))[0] + self._bottom_layer_count = struct.unpack('i', f.read(4))[0] + self._p1 = struct.unpack('f', f.read(4))[0] + self._p2 = struct.unpack('f', f.read(4))[0] + self._p3 = struct.unpack('f', f.read(4))[0] + self._p4 = struct.unpack('f', f.read(4))[0] + f.seek(self._layer_def_address, os.SEEK_SET) self.layers = [] - for i in range(n_layers): + for i in range(self.n_layers * self.layer_levels): layer_height = struct.unpack('f', f.read(4))[0] try: layer_thickness = layer_height - previous_layer_height @@ -194,6 +224,7 @@ def write(self, filepath): self._update() with open(filepath, 'wb') as f: f.write(self._header) + f.write(struct.pack('i', self._version)) f.write(struct.pack('f', self._bed_x)) f.write(struct.pack('f', self._bed_y)) f.write(struct.pack('f', self._bed_z)) @@ -210,9 +241,26 @@ def write(self, filepath): f.write(struct.pack('i', self._layer_def_address)) f.write(struct.pack('i', len(self.layers))) f.write(struct.pack('i', self._preview_lowres_header_address)) - f.write(b'\x00' * 4) # padding + if self._version > 1: + f.write(struct.pack('i', self._print_time)) + else: + f.write(b'\x00' * 4) # padding f.write(struct.pack('i', self._projection_type)) - f.write(b'\x00' * 6 * 4) # padding + if self._version > 1: + # self._print_properties_address = struct.unpack('i', f.read(4))[0] + f.write(struct.pack('i', self._print_properties_address)) + # self._print_properties_length = struct.unpack('i', f.read(4))[0] + f.write(struct.pack('i', self._print_properties_length)) + # self._anti_aliasing_level = struct.unpack('i', f.read(4))[0] + f.write(struct.pack('i', self._anti_aliasing_level)) + # self.layer_levels = self._anti_aliasing_level + # self.light_pwm = struct.unpack('h', f.read(2))[0] + f.write(struct.pack('h', self.light_pwm)) + # self.light_pwm_bottom = struct.unpack('h', f.read(2))[0] + f.write(struct.pack('h', self.light_pwm_bottom)) + f.write(b'\x00' * 2 * 4) + else: + f.write(b'\x00' * 6 * 4) # padding # f.seek(self._preview_highres_header_address, os.SEEK_SET) f.write(struct.pack('i', self._preview_highres_resolution_x)) f.write(struct.pack('i', self._preview_highres_resolution_y)) From 997e6bc93d7dcd203140cda55a85cc351236ce14 Mon Sep 17 00:00:00 2001 From: Heiko Westermann Date: Fri, 6 Sep 2019 16:00:53 +0200 Subject: [PATCH 2/4] photon v2 / cbddlp implemented --- examples/example_usage.py | 64 +++++ pyphotonfile/photonfile.py | 478 +++++++++++++++++++++---------------- 2 files changed, 330 insertions(+), 212 deletions(-) create mode 100644 examples/example_usage.py diff --git a/examples/example_usage.py b/examples/example_usage.py new file mode 100644 index 0000000..6d88ed3 --- /dev/null +++ b/examples/example_usage.py @@ -0,0 +1,64 @@ +from pyphotonfile import Photon + +in_filepath = "input.photon" # .photon or compatible cbddlp-file +out_filepath = "output.photon" + +# export images +photon = Photon(in_filepath) +photon.export_images("tempdir") + +# import images +photon.delete_layers() # we want to clear the layers first +photon.append_layers("tempdir") # reimport previously exported images. the files need to have the same filenaming scheme +photon.write(out_filepath) + +# info +print(photon.layers) # list containing all layers with sublayers +print(photon.layers[0].sublayers[0]) # first sublayer of the first layer. anti-aliasing files contian multiple sublayers, otherwise their is only one sublayer +print(photon.layers[0].sublayers[0].layer_thickness) # layers have individual properties which might not be recognized by the firmware. feel free to play around +print(photon.layers[0].sublayers[0].exposure_time) +print(photon.layers[0].sublayers[0].off_time) + +# the shown variables below are the only meaningfull ones. Be aware that these are not protected. change those values at your own risk! +print(photon.header) +print(photon.version) +print(photon.bed_x) +print(photon.bed_y) +print(photon.bed_z) +print(photon.layer_height) +print(photon.exposure_time) +print(photon.exposure_time_bottom) +print(photon.off_time) +print(photon.bottom_layers) +print(photon.resolution_x) +print(photon.resolution_y) +print(photon.n_layers) +print(photon.projection_type) +print(photon.preview_highres_resolution_x) +print(photon.preview_highres_resolution_y) +print(photon.preview_lowres_resolution_x) +print(photon.preview_lowres_resolution_y) +print(len(photon.preview_highres_data)) # don't print the raw data. the preview images are not yet parsed. interested in implementing something? +print(len(photon.preview_lowres_data)) # don't print the raw data. the preview images are not yet parsed. interested in implementing something? + +if photon.version > 1: + print(photon.anti_aliasing_level) + print(photon.layer_levels) + print(photon.light_pwm) + print(photon.light_pwm_bottom) + print(photon.print_time) + print(photon.bottom_lift_distance) + print(photon.bottom_lift_speed) + print(photon.lifting_distance) + print(photon.lifting_speed) + print(photon.retract_speed) + print(photon.volume_ml) + print(photon.weight_g) + print(photon.cost_dollars) + print(photon.bottom_light_off_delay) + print(photon.light_off_delay) + print(photon.bottom_layer_count) + print(photon.p1) + print(photon.p2) + print(photon.p3) + print(photon.p4) \ No newline at end of file diff --git a/pyphotonfile/photonfile.py b/pyphotonfile/photonfile.py index 1916051..83c62a8 100644 --- a/pyphotonfile/photonfile.py +++ b/pyphotonfile/photonfile.py @@ -5,6 +5,9 @@ from PIL import Image import pkgutil import io +import glob + +# from IPython import embed def rle_to_imgarray(data): """ @@ -80,6 +83,32 @@ def image_to_imgarr(filepath): return imgarr class Layer: + """ + Represents a layer with one (no anti-aliasing) or more SubLayers (anti-aliasing) in the Photon-file. + """ + def __init__(self): + self.sublayers = [] + + def append_sublayer(self, sublayer): + self.sublayers.append(sublayer) + + def __eq__(self, other): + if not isinstance(other, Layer): + # don't attempt to compare against unrelated types + return NotImplemented + + if len(self.sublayers) != len(other.sublayers): + return False + + comparisons = [] + for a, b in zip(self.sublayers, other.sublayers): + comparisons.append(a == b) + return all(comparisons) + + # def __repr__(self): + # return 'Layer(%r, %r, %r)' % (self.layer_thickness, self.exposure_time, self.off_time) + +class SubLayer: """ Represents a single layer in the Photon-file. """ @@ -91,7 +120,7 @@ def __init__(self, data, layer_thickness=None, exposure_time=None, off_time=None self._data_length = len(self._data) def __eq__(self, other): - if not isinstance(other, Layer): + if not isinstance(other, SubLayer): # don't attempt to compare against unrelated types return NotImplemented @@ -103,7 +132,7 @@ def __eq__(self, other): return all(comparisons) def __repr__(self): - return 'Layer(%r, %r, %r)' % (self.layer_thickness, self.exposure_time, self.off_time) + return 'SubLayer(%r, %r, %r)' % (self.layer_thickness, self.exposure_time, self.off_time) class Photon: @@ -112,9 +141,6 @@ class Photon: """ def __init__(self, filepath=None): if filepath is None: - # filepath = os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', 'tests', 'testfiles', 'newfile.photon') - # filepath = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data', 'newfile.photon') - # self._open(filepath) self._open() self.delete_layers() else: @@ -126,192 +152,221 @@ def _open(self, filepath=None): else: with open(filepath, 'rb') as f: data = f.read() - # embed() f = io.BytesIO(data) - self._header = f.read(4) - self._version = struct.unpack('i', f.read(4))[0] - self._bed_x = struct.unpack('f', f.read(4))[0] - self._bed_y = struct.unpack('f', f.read(4))[0] - self._bed_z = struct.unpack('f', f.read(4))[0] + self.header = f.read(4) + self.version = struct.unpack('i', f.read(4))[0] + self.bed_x = struct.unpack('f', f.read(4))[0] + self.bed_y = struct.unpack('f', f.read(4))[0] + self.bed_z = struct.unpack('f', f.read(4))[0] f.seek(3*4, os.SEEK_CUR) # padding self.layer_height = struct.unpack('f', f.read(4))[0] self.exposure_time = struct.unpack('f', f.read(4))[0] self.exposure_time_bottom = struct.unpack('f', f.read(4))[0] self.off_time = struct.unpack('f', f.read(4))[0] self.bottom_layers = struct.unpack('i', f.read(4))[0] - self._resolution_x = struct.unpack('i', f.read(4))[0] - self._resolution_y = struct.unpack('i', f.read(4))[0] - self._preview_highres_header_address = struct.unpack('i', f.read(4))[0] - self._layer_def_address = struct.unpack('i', f.read(4))[0] + self.resolution_x = struct.unpack('i', f.read(4))[0] + self.resolution_y = struct.unpack('i', f.read(4))[0] + self.preview_highres_header_address = struct.unpack('i', f.read(4))[0] + self.layer_def_address = struct.unpack('i', f.read(4))[0] self.n_layers = struct.unpack('i', f.read(4))[0] - self._preview_lowres_header_address = struct.unpack('i', f.read(4))[0] - if self._version > 1: - self._print_time = struct.unpack('i', f.read(4))[0] + self.preview_lowres_header_address = struct.unpack('i', f.read(4))[0] + if self.version > 1: + self.print_time = struct.unpack('i', f.read(4))[0] else: f.seek(4, os.SEEK_CUR) # padding - self._projection_type = struct.unpack('i', f.read(4))[0] + self.projection_type = struct.unpack('i', f.read(4))[0] self.layer_levels = 1 - if self._version > 1: - self._print_properties_address = struct.unpack('i', f.read(4))[0] - self._print_properties_length = struct.unpack('i', f.read(4))[0] - self._anti_aliasing_level = struct.unpack('i', f.read(4))[0] - self.layer_levels = self._anti_aliasing_level + if self.version > 1: + self.print_properties_address = struct.unpack('i', f.read(4))[0] + self.print_properties_length = struct.unpack('i', f.read(4))[0] + self.anti_aliasing_level = struct.unpack('i', f.read(4))[0] + self.layer_levels = self.anti_aliasing_level self.light_pwm = struct.unpack('h', f.read(2))[0] self.light_pwm_bottom = struct.unpack('h', f.read(2))[0] - # else: - # f.seek(6*4, os.SEEK_CUR) # padding - - f.seek(self._preview_highres_header_address, os.SEEK_SET) - self._preview_highres_resolution_x = struct.unpack('i', f.read(4))[0] - self._preview_highres_resolution_y = struct.unpack('i', f.read(4))[0] - self._preview_highres_data_address = struct.unpack('i', f.read(4))[0] - self._preview_highres_data_length = struct.unpack('i', f.read(4))[0] - # f.seek(4*4, os.SEEK_CUR) # padding - self._preview_highres_data = f.read(self._preview_highres_data_length) - - f.seek(self._preview_lowres_header_address, os.SEEK_SET) - self._preview_lowres_resolution_x = struct.unpack('i', f.read(4))[0] - self._preview_lowres_resolution_y = struct.unpack('i', f.read(4))[0] - self._preview_lowres_data_address = struct.unpack('i', f.read(4))[0] - self._preview_lowres_data_length = struct.unpack('i', f.read(4))[0] - # f.seek(4*4, os.SEEK_CUR) # padding - self._preview_lowres_data = f.read(self._preview_lowres_data_length) - - if self._version > 1: - f.seek(self._print_properties_address, os.SEEK_SET) - self._bottom_lift_distance = struct.unpack('f', f.read(4))[0] - self._bottom_lift_speed = struct.unpack('f', f.read(4))[0] - self._lifting_distance = struct.unpack('f', f.read(4))[0] - self._lifting_speed = struct.unpack('f', f.read(4))[0] - self._retract_speed = struct.unpack('f', f.read(4))[0] - self._volume_ml = struct.unpack('f', f.read(4))[0] - self._weight_g = struct.unpack('f', f.read(4))[0] - self._cost_dollars = struct.unpack('f', f.read(4))[0] - self._bottom_light_off_delay = struct.unpack('f', f.read(4))[0] - self._light_off_delay = struct.unpack('f', f.read(4))[0] - self._bottom_layer_count = struct.unpack('i', f.read(4))[0] - self._p1 = struct.unpack('f', f.read(4))[0] - self._p2 = struct.unpack('f', f.read(4))[0] - self._p3 = struct.unpack('f', f.read(4))[0] - self._p4 = struct.unpack('f', f.read(4))[0] - - f.seek(self._layer_def_address, os.SEEK_SET) + + f.seek(self.preview_highres_header_address, os.SEEK_SET) + self.preview_highres_resolution_x = struct.unpack('i', f.read(4))[0] + self.preview_highres_resolution_y = struct.unpack('i', f.read(4))[0] + self.preview_highres_data_address = struct.unpack('i', f.read(4))[0] + self.preview_highres_data_length = struct.unpack('i', f.read(4))[0] + + f.seek(self.preview_highres_data_address, os.SEEK_SET) + self.preview_highres_data = f.read(self.preview_highres_data_length) + + f.seek(self.preview_lowres_header_address, os.SEEK_SET) + self.preview_lowres_resolution_x = struct.unpack('i', f.read(4))[0] + self.preview_lowres_resolution_y = struct.unpack('i', f.read(4))[0] + self.preview_lowres_data_address = struct.unpack('i', f.read(4))[0] + self.preview_lowres_data_length = struct.unpack('i', f.read(4))[0] + + f.seek(self.preview_lowres_data_address, os.SEEK_SET) + self.preview_lowres_data = f.read(self.preview_lowres_data_length) + + if self.version > 1: + f.seek(self.print_properties_address, os.SEEK_SET) + self.bottom_lift_distance = struct.unpack('f', f.read(4))[0] + self.bottom_lift_speed = struct.unpack('f', f.read(4))[0] + self.lifting_distance = struct.unpack('f', f.read(4))[0] + self.lifting_speed = struct.unpack('f', f.read(4))[0] + self.retract_speed = struct.unpack('f', f.read(4))[0] + self.volume_ml = struct.unpack('f', f.read(4))[0] + self.weight_g = struct.unpack('f', f.read(4))[0] + self.cost_dollars = struct.unpack('f', f.read(4))[0] + self.bottom_light_off_delay = struct.unpack('f', f.read(4))[0] + self.light_off_delay = struct.unpack('f', f.read(4))[0] + self.bottom_layer_count = struct.unpack('i', f.read(4))[0] + self.p1 = struct.unpack('f', f.read(4))[0] + self.p2 = struct.unpack('f', f.read(4))[0] + self.p3 = struct.unpack('f', f.read(4))[0] + self.p4 = struct.unpack('f', f.read(4))[0] + + f.seek(self.layer_def_address, os.SEEK_SET) self.layers = [] - for i in range(self.n_layers * self.layer_levels): - layer_height = struct.unpack('f', f.read(4))[0] - try: - layer_thickness = layer_height - previous_layer_height - except UnboundLocalError: - layer_thickness = self.layer_height - previous_layer_height = layer_thickness - previous_layer_height = layer_height - exposure_time = struct.unpack('f', f.read(4))[0] - off_time = struct.unpack('f', f.read(4))[0] - address = struct.unpack('i', f.read(4))[0] - data_length = struct.unpack('i', f.read(4))[0] - f.seek(4*4, os.SEEK_CUR) # padding - curpos = f.tell() - f.seek(address, os.SEEK_SET) - data = f.read(data_length) - self.layers.append(Layer(data, layer_thickness, exposure_time, off_time)) - f.seek(curpos, os.SEEK_SET) + sublayers = [] + for level in range(self.layer_levels): + sublayers.append([]) + for i in range(self.n_layers): + layer_height = struct.unpack('f', f.read(4))[0] + try: + layer_thickness = layer_height - previous_layer_height + except UnboundLocalError: + layer_thickness = self.layer_height + previous_layer_height = layer_thickness + previous_layer_height = layer_height + exposure_time = struct.unpack('f', f.read(4))[0] + off_time = struct.unpack('f', f.read(4))[0] + address = struct.unpack('i', f.read(4))[0] + data_length = struct.unpack('i', f.read(4))[0] + f.seek(4*4, os.SEEK_CUR) # padding + curpos = f.tell() + f.seek(address, os.SEEK_SET) + data = f.read(data_length) + sublayers[-1].append(SubLayer(data, layer_thickness, exposure_time, off_time)) + f.seek(curpos, os.SEEK_SET) + del previous_layer_height # dirty hack to rest thickness calculation for multiple levels / anti aliasing. does anyone actually need the thickness? f.close() + for i in range(self.n_layers): # create layer objects with sorted sublayers for easier manipulation. could be done more elegantly. + layer = Layer() + for level in range(self.layer_levels): + layer.append_sublayer(sublayers[level][i]) + self.layers.append(layer) + def write(self, filepath): """ Writes the Photon-file to disk. """ - self._update() + offsets = {} + addresses = {} + with open(filepath, 'wb') as f: - f.write(self._header) - f.write(struct.pack('i', self._version)) - f.write(struct.pack('f', self._bed_x)) - f.write(struct.pack('f', self._bed_y)) - f.write(struct.pack('f', self._bed_z)) + f.write(self.header) + f.write(struct.pack('i', self.version)) + f.write(struct.pack('f', self.bed_x)) + f.write(struct.pack('f', self.bed_y)) + f.write(struct.pack('f', self.bed_z)) f.write(b'\x00' * 3 * 4) # padding f.write(struct.pack('f', self.layer_height)) f.write(struct.pack('f', self.exposure_time)) f.write(struct.pack('f', self.exposure_time_bottom)) f.write(struct.pack('f', self.off_time)) f.write(struct.pack('i', self.bottom_layers)) - f.write(struct.pack('i', self._resolution_x)) - f.write(struct.pack('i', self._resolution_y)) - f.write(struct.pack('i', self._preview_highres_header_address)) - # print(f.tell()) - f.write(struct.pack('i', self._layer_def_address)) + f.write(struct.pack('i', self.resolution_x)) + f.write(struct.pack('i', self.resolution_y)) + f.write(struct.pack('i', self.preview_highres_header_address)) + offsets['layer_def_address'] = f.tell() # remember position in file to later add the correct address + f.write(struct.pack('i', 0x0 )) f.write(struct.pack('i', len(self.layers))) - f.write(struct.pack('i', self._preview_lowres_header_address)) - if self._version > 1: - f.write(struct.pack('i', self._print_time)) + offsets['preview_lowres_header_address'] = f.tell() # remember position in file to later add the correct address + f.write(struct.pack('i', 0x0 )) + + if self.version > 1: + f.write(struct.pack('i', self.print_time)) else: f.write(b'\x00' * 4) # padding - f.write(struct.pack('i', self._projection_type)) - if self._version > 1: - # self._print_properties_address = struct.unpack('i', f.read(4))[0] - f.write(struct.pack('i', self._print_properties_address)) - # self._print_properties_length = struct.unpack('i', f.read(4))[0] - f.write(struct.pack('i', self._print_properties_length)) - # self._anti_aliasing_level = struct.unpack('i', f.read(4))[0] - f.write(struct.pack('i', self._anti_aliasing_level)) - # self.layer_levels = self._anti_aliasing_level - # self.light_pwm = struct.unpack('h', f.read(2))[0] + f.write(struct.pack('i', self.projection_type)) + if self.version > 1: + offsets['print_properties_address'] = f.tell() # remember position in file to later add the correct address + f.write(struct.pack('i', 0x0)) + f.write(struct.pack('i', self.print_properties_length)) + f.write(struct.pack('i', self.anti_aliasing_level)) f.write(struct.pack('h', self.light_pwm)) - # self.light_pwm_bottom = struct.unpack('h', f.read(2))[0] f.write(struct.pack('h', self.light_pwm_bottom)) - f.write(b'\x00' * 2 * 4) + f.write(b'\x00' * 3 * 4) else: f.write(b'\x00' * 6 * 4) # padding - # f.seek(self._preview_highres_header_address, os.SEEK_SET) - f.write(struct.pack('i', self._preview_highres_resolution_x)) - f.write(struct.pack('i', self._preview_highres_resolution_y)) - f.write(struct.pack('i', self._preview_highres_data_address)) - f.write(struct.pack('i', self._preview_highres_data_length)) - # f.seek(4*4, os.SEEK_CUR) # padding + f.write(struct.pack('i', self.preview_highres_resolution_x)) + f.write(struct.pack('i', self.preview_highres_resolution_y)) + f.write(struct.pack('i', self.preview_highres_data_address)) + f.write(struct.pack('i', self.preview_highres_data_length)) f.write(b'\x00' * 4 * 4) - f.write(self._preview_highres_data) - - # f.seek(self._preview_lowres_header_address, os.SEEK_SET) - f.write(struct.pack('i', self._preview_lowres_resolution_x)) - f.write(struct.pack('i', self._preview_lowres_resolution_y)) - f.write(struct.pack('i', self._preview_lowres_data_address)) - f.write(struct.pack('i', self._preview_lowres_data_length)) - # f.seek(4*4, os.SEEK_CUR) # padding + f.write(self.preview_highres_data) + addresses['preview_lowres_header_address'] = f.tell() # remember position in file to later add the correct address + f.write(struct.pack('i', self.preview_lowres_resolution_x)) + f.write(struct.pack('i', self.preview_lowres_resolution_y)) + f.write(struct.pack('i', self.preview_lowres_data_address)) + f.write(struct.pack('i', self.preview_lowres_data_length)) f.write(b'\x00' * 4 * 4) - f.write(self._preview_lowres_data) - - layer_height = 0 - layer_data_pos = f.tell() + len(self.layers) * (9 * 4) - layer_data_offset = 0 - - for layer in self.layers: - try: - f.write(struct.pack('f', layer.layer_thickness + previous_layer_height)) - previous_layer_height += layer.layer_thickness - except UnboundLocalError: - f.write(struct.pack('f', 0.0)) - previous_layer_height = 0 - f.write(struct.pack('f', layer.exposure_time)) - f.write(struct.pack('f', layer.off_time)) - f.write(struct.pack('i', layer_data_pos + layer_data_offset)) - f.write(struct.pack('i', len(layer._data))) - f.write(b'\x00' * 4 * 4) - layer_height += layer.layer_thickness - layer_data_offset += len(layer._data) - for layer in self.layers: - f.write(layer._data) - - def _update(self): - """ - Updates all internal address. Should not be called directly. - """ - self._preview_lowres_header_address = self._preview_highres_header_address + 8 * 4 + self._preview_highres_data_length - self._layer_def_address = self._preview_lowres_header_address + 8 * 4 + self._preview_lowres_data_length - layer_data_offset = self._layer_def_address + len(self.layers) * (9 * 4) - self.layers[0]._address = layer_data_offset - layer_data_offset += self.layers[0]._data_length - for i, layer in enumerate(self.layers[1:]): - layer._address = layer_data_offset - layer_data_offset += layer._data_length + f.write(self.preview_lowres_data) + if self.version > 1: + addresses['print_properties_address'] = f.tell() # remember position in file to later add the correct address + f.write(struct.pack('f', self.bottom_lift_distance)) + f.write(struct.pack('f', self.bottom_lift_speed)) + f.write(struct.pack('f', self.lifting_distance)) + f.write(struct.pack('f', self.lifting_speed)) + f.write(struct.pack('f', self.retract_speed)) + f.write(struct.pack('f', self.volume_ml)) + f.write(struct.pack('f', self.weight_g)) + f.write(struct.pack('f', self.cost_dollars)) + f.write(struct.pack('f', self.bottom_light_off_delay)) + f.write(struct.pack('f', self.light_off_delay)) + f.write(struct.pack('i', self.bottom_layer_count)) + f.write(struct.pack('f', self.p1)) + f.write(struct.pack('f', self.p2)) + f.write(struct.pack('f', self.p3)) + f.write(struct.pack('f', self.p4)) + + addresses['layer_def_address'] = f.tell() # remember position in file to later add the correct address + + layer_data_pos = f.tell() + len(self.layers) * self.layer_levels * (9 * 4) + layer_data_offsets = {} + layer_data_addresses = {} + for i, level in enumerate(range(self.layer_levels)): # layers in header are sorted: Layer 1 Sublayer 1, L2 S1, L3 S1, ... L1 S4, L2 S4 + layer_data_offsets[i] = {} + for j, layer in enumerate(self.layers): + layer = layer.sublayers[level] + try: + f.write(struct.pack('f', layer.layer_thickness + previous_layer_height)) + previous_layer_height += layer.layer_thickness + except UnboundLocalError: + f.write(struct.pack('f', 0.0)) + previous_layer_height = 0 + f.write(struct.pack('f', layer.exposure_time)) + f.write(struct.pack('f', layer.off_time)) + layer_data_offsets[i][j] = f.tell() # remember position in file to later add the correct address + f.write(struct.pack('i', 0x0)) + f.write(struct.pack('i', len(layer._data))) + f.write(b'\x00' * 4 * 4) + del previous_layer_height # dirty hack to rest thickness calculation for multiple levels / anti aliasing. does anyone actually need the thickness? + for j, layer in enumerate(self.layers): # layers in data are sorted: Layer 1 Sublayer 1, L1 S2, L1 S3, ... LX S1, LX S2. Different than in the header! + for i, sublayer in enumerate(layer.sublayers): + try: + layer_data_addresses[i][j] = f.tell() + except KeyError: + layer_data_addresses[i] = {} + layer_data_addresses[i][j] = f.tell() + f.write(sublayer._data) + + # update addresses + # header + for key, value in offsets.items(): + f.seek(value) + f.write(struct.pack('i', addresses[key])) + # layers + for level, value in layer_data_offsets.items(): + for layer, address in layer_data_addresses[level].items(): + f.seek(layer_data_offsets[level][layer]) + f.write(struct.pack('i', layer_data_addresses[level][layer])) def export_images(self, dirpath): """ @@ -322,34 +377,46 @@ def export_images(self, dirpath): except OSError: pass for i, layer in enumerate(self.layers): - self.export_image(i, os.path.join(dirpath, '{0:05d}.png'.format(i))) + for j, sublayer in enumerate(layer.sublayers): + self.export_image(sublayer, os.path.join(dirpath, '{:05d}_{:02d}.png'.format(i, j))) - def export_image(self, idx, filepath): + def export_image(self, sublayer, filepath): """ Exports layer image at idx to the supplied filename. """ - img = rle_to_imgarray(self.layers[idx]._data) * 255 + img = rle_to_imgarray(sublayer._data) * 255 Image.fromarray(img).convert('RGB').save(filepath) - def append_layer(self, image, layer_thickness=None, exposure_time=None, off_time=None): + + def create_layer(self, images, layer_thickness=None, exposure_time=None, off_time=None): + if not isinstance(images, list): + images = [images] + if len(images) != self.layer_levels: + raise ValueError('supplied number of images must equal to number of levels in file') + layer = Layer() + for image in images: + if not isinstance(image, bytes): + data = imgarr_to_rle(image_to_imgarr(image)) + else: + data = image + if layer_thickness is None: + layer_thickness = self.layer_height + if exposure_time is None: + if len(self.layers) < self.bottom_layers: + exposure_time = self.exposure_time_bottom + else: + exposure_time = self.exposure_time + if off_time is None: + off_time = self.off_time + layer.append_sublayer(SubLayer(data, layer_thickness, exposure_time, off_time)) + return layer + + def append_layer(self, images, layer_thickness=None, exposure_time=None, off_time=None): """ - Appends a new layer. image should be a path or already rle encoded bytes object. Argument exposure_time + Appends a new layer. In case of multiple levels, the correct number of images must be supplied as a list. images should be a path or already rle encoded bytes object. Argument exposure_time seems to be not used by the firmware. If keyword args are ommited, falls back to global values. """ - if not isinstance(image, bytes): - data = imgarr_to_rle(image_to_imgarr(image)) - else: - data = image - if layer_thickness is None: - layer_thickness = self.layer_height - if exposure_time is None: - if len(self.layers) < self.bottom_layers: - exposure_time = self.exposure_time_bottom - else: - exposure_time = self.exposure_time - if off_time is None: - off_time = self.off_time - layer = Layer(data, layer_thickness, exposure_time, off_time) + layer = self.create_layer(images, layer_thickness, exposure_time, off_time) self.layers.append(layer) def append_layers(self, dirpath, layer_thickness=None, exposure_time=None, off_time=None): @@ -357,25 +424,33 @@ def append_layers(self, dirpath, layer_thickness=None, exposure_time=None, off_t Appends multiple new layers. dirpath should be an existing directory. Argument exposure_time seems to be not used by the firmware. If keyword args are ommited, falls back to global values. """ - for filepath in os.listdir(dirpath): - self.append_layer(os.path.join(dirpath, filepath), layer_thickness, exposure_time, off_time) + files = glob.glob(os.path.join(dirpath, "[0-9][0-9][0-9][0-9][0-9]_[0-9][0-9].png")) + layers = [] + last_file_id = '' + for filepath in files: + file_id = os.path.basename(filepath)[:5] + if file_id == last_file_id: + layers[-1].append(filepath) + else: + layers.append([filepath]) + last_file_id = file_id + for layer in layers: + self.append_layer(layer, layer_thickness, exposure_time, off_time) - def insert_layer(self, filepath, idx, layer_thickness=None, exposure_time=None, off_time=None): + def insert_layer(self, images, idx, layer_thickness=None, exposure_time=None, off_time=None): """ Insert a new layer at idx. Not tested. Argument exposure_time seems to be not used by the firmware. If keyword args are ommited, falls back to global values. """ - data = imgarr_to_rle(image_to_imgarr(filepath)) - layer = Layer(data, layer_thickness, exposure_time, off_time) + layer = self.create_layer(images, layer_thickness, exposure_time, off_time) self.layers.insert(idx, layer) - def replace_layer(self, filepath, idx, layer_thickness=None, exposure_time=None, off_time=None): + def replace_layer(self, images, idx, layer_thickness=None, exposure_time=None, off_time=None): """ Replaces a layer a layer at idx with a nw layer. Not tested. Argument exposure_time seems to be not used by the firmware. If keyword args are ommited, falls back to global values. """ - data = imgarr_to_rle(image_to_imgarr(filepath)) - layer = Layer(data, layer_thickness, exposure_time, off_time) + layer = self.create_layer(images, layer_thickness, exposure_time, off_time) self.delete_layer(idx) self.insert_layer(idx, layer) @@ -394,36 +469,15 @@ def delete_layers(self): def overwrite_layer_parameters(self, layer_thickness=None, exposure_time=None, off_time=None): """ Used to overwrite all layer parameters. Usefull for quick testing of a print without resin. Please - note taht exposure time seems to be ignored on a per layer basis. If keyword args are ommited, falls + note that exposure time seems to be ignored on a per layer basis. If keyword args are ommited, falls back to global values. """ for layer in self.layers: - if layer_thickness: - layer.layer_thickness = layer_thickness - if exposure_time: - layer.exposure_time = exposure_time - if off_time: - layer.off_time = off_time - -if __name__ == '__main__': - import shutil - photon = Photon(sys.argv[1]) - for layer in photon.layers: - imgarr = rle_to_imgarray(layer._data) - rle_heikos = imgarr_to_rle(imgarr) - try: - shutil.rmtree('tempdir') - except FileNotFoundError: - pass - try: - shutil.rmtree('tempdir2') - except FileNotFoundError: - pass - photon.export_images('tempdir') - photon.delete_layers() - paths = [] - for filepath in os.listdir('tempdir'): - paths.append(os.path.join('tempdir', filepath)) - photon.append_layers(paths) - photon.write('testwrite.photon') - photon.export_images('tempdir2') \ No newline at end of file + for sublayer in layer.sublayers: + if layer_thickness: + sublayer.layer_thickness = layer_thickness + if exposure_time: + sublayer.exposure_time = exposure_time + if off_time: + sublayer.off_time = off_time + From deeecb35cf6abebfb61c5da249a872ba61c4791e Mon Sep 17 00:00:00 2001 From: Heiko Date: Fri, 6 Sep 2019 16:15:46 +0200 Subject: [PATCH 3/4] Update README.md --- README.md | 88 ++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 74 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 11fc201..fe623b4 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,93 @@ -pyPhotonfile is a library used for manipulating Photon-files created for the Anycubic Photon 3D-Printer. Currently it supports removing and adding new layers as well as changing global parameters like the exposure time, etc. +pyPhotonfile is a library used for manipulating Photon- and cbddlp-files created for the Anycubic Photon 3D-Printer and compatibles (e.g. Elegoo Mars, etc.). Currently it supports removing and adding new layers as well as changing global parameters like the exposure time, etc. You are free to play around with all exposed variables, but be aware that changes might not be supported by the printer firmwares. Let me know if you crash your printer so that I can implement safety measures. A general description of the fileformat can be found in folder 'fileformat' including fancy graphs. It is based on the work done by [PhotonFileEditor](https://github.com/Photonsters/PhotonFileEditor). While PhotonFileEditor works, I was in need of a clean library which is why I refactored most of the code. pyPhotonfile is the backbone of [SL1toPhoton](https://github.com/fookatchu/SL1toPhoton). Friendly Reminder ================= - Use at your own risk. Please verify that what you are doing will not break your printer. + Use at your own risk. Please verify that what you are doing will not break your printer. A good test is to load the resulting files into ChituBox to see if everything is at its right place. Installation ================= ``` pip install pyphotonfile ``` +or for the bleeding edge version from the master repo: +``` +pip install git+https://github.com/fookatchu/pyphotonfile@master +``` Example Usage ======================================== ```python - from pyphotonfile import Photon - - photon = Photon('in_file.Photon') - for layer in photon.layers: - print(layer) - photon.export_images('tempdir') - photon.delete_layers() - for filepath in os.listdir('tempdir'): - photon.append_layer(filepath) - photon.exposure_time = 10 - photon.bottom_layers = 3 - photon.write('out_file.Photon') +from pyphotonfile import Photon + +in_filepath = "input.photon" # .photon or compatible cbddlp-file +out_filepath = "output.photon" + +# export images +photon = Photon(in_filepath) +photon.export_images("tempdir") + +# import images +photon.delete_layers() # we want to clear the layers first +photon.append_layers("tempdir") # reimport previously exported images. the files need to have the same filenaming scheme +photon.write(out_filepath) + +# info +print(photon.layers) # list containing all layers with sublayers +print(photon.layers[0].sublayers[0]) # first sublayer of the first layer. anti-aliasing files contian multiple sublayers, otherwise their is only one sublayer +print(photon.layers[0].sublayers[0].layer_thickness) # layers have individual properties which might not be recognized by the firmware. feel free to play around +print(photon.layers[0].sublayers[0].exposure_time) +print(photon.layers[0].sublayers[0].off_time) + +# modification of global parameters +photon.exposure_time = 10 +photon.bottom_layers = 3 + +# the shown variables below are the only meaningfull ones. Be aware that these are not protected. change those values at your own risk! +print(photon.header) +print(photon.version) +print(photon.bed_x) +print(photon.bed_y) +print(photon.bed_z) +print(photon.layer_height) +print(photon.exposure_time) +print(photon.exposure_time_bottom) +print(photon.off_time) +print(photon.bottom_layers) +print(photon.resolution_x) +print(photon.resolution_y) +print(photon.n_layers) +print(photon.projection_type) +print(photon.preview_highres_resolution_x) +print(photon.preview_highres_resolution_y) +print(photon.preview_lowres_resolution_x) +print(photon.preview_lowres_resolution_y) +print(len(photon.preview_highres_data)) # don't print the raw data. the preview images are not yet parsed. interested in implementing something? +print(len(photon.preview_lowres_data)) # don't print the raw data. the preview images are not yet parsed. interested in implementing something? + +if photon.version > 1: + print(photon.anti_aliasing_level) + print(photon.layer_levels) + print(photon.light_pwm) + print(photon.light_pwm_bottom) + print(photon.print_time) + print(photon.bottom_lift_distance) + print(photon.bottom_lift_speed) + print(photon.lifting_distance) + print(photon.lifting_speed) + print(photon.retract_speed) + print(photon.volume_ml) + print(photon.weight_g) + print(photon.cost_dollars) + print(photon.bottom_light_off_delay) + print(photon.light_off_delay) + print(photon.bottom_layer_count) + print(photon.p1) + print(photon.p2) + print(photon.p3) + print(photon.p4) ``` TODO / Roadmap (contributions welcome) From 33f05eff451f22522d9a6fb006b58cda90c96ac1 Mon Sep 17 00:00:00 2001 From: Heiko Westermann Date: Fri, 6 Sep 2019 16:39:37 +0200 Subject: [PATCH 4/4] version bump --- examples/example_usage.py | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/example_usage.py b/examples/example_usage.py index 6d88ed3..f1607d9 100644 --- a/examples/example_usage.py +++ b/examples/example_usage.py @@ -19,6 +19,10 @@ print(photon.layers[0].sublayers[0].exposure_time) print(photon.layers[0].sublayers[0].off_time) +# modification of global parameters +photon.exposure_time = 10 +photon.bottom_layers = 3 + # the shown variables below are the only meaningfull ones. Be aware that these are not protected. change those values at your own risk! print(photon.header) print(photon.version) diff --git a/setup.py b/setup.py index 2e74299..1b9d3f4 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ long_description = fh.read() setup(name='pyphotonfile', - version='0.1.1', + version='0.2.0', # packages=['pyphotonfile'], author="Heiko Westermann", author_email="heiko+pyphotonfile@orgizm.net",