Skip to content

Commit

Permalink
fix sl parsing to use config.json and to return correct keys (#43)
Browse files Browse the repository at this point in the history
  • Loading branch information
sarkafa authored Aug 15, 2024
1 parent 7a5dd19 commit 1adcc22
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 66 deletions.
120 changes: 80 additions & 40 deletions gcode_metadata/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
from logging import getLogger
from importlib.metadata import version

# pylint: disable=too-many-lines

GCODE_EXTENSIONS = (".gcode", ".gc", ".g", ".gco")
SLA_EXTENSIONS = ("sl1", "sl1s")
CHARS_TO_REMOVE = ["/", "\\", "\"", "(", ")", "[", "]", "'"]
Expand All @@ -23,10 +25,10 @@
r"((?P<seconds>[0-9]+)s)?")

PRINTERS = [
'MK4IS', 'MK4MMU3', 'MK4', 'MK3SMMU3', 'MK3MMU3', 'MK3SMMU2S', 'MK3MMU2',
'MK3S', 'MK3', 'MK2.5SMMU2S', 'MK2.5MMU2', 'MK2.5S', 'MK2.5', 'MK3.5',
'MK3.9', 'MINI', 'MINIIS', 'XL5', 'XL4', 'XL3', 'XL2', 'XL', 'iX', 'SL1',
'SHELF', 'EXTRACTOR', 'HARVESTER'
'MK4IS', 'MK4MMU3', 'MK4', 'MK4S', 'MK4SMMU3', 'MK3SMMU3', 'MK3MMU3',
'MK3SMMU2S', 'MK3MMU2', 'MK3S', 'MK3', 'MK2.5SMMU2S', 'MK2.5MMU2',
'MK2.5S', 'MK2.5', 'MK3.5', 'MK3.9', 'MINI', 'MINIIS', 'XL5', 'XL4', 'XL3',
'XL2', 'XL', 'iX', 'SL1', 'SHELF', 'EXTRACTOR', 'HARVESTER'
]

PRINTERS.sort(key=len, reverse=True)
Expand Down Expand Up @@ -379,19 +381,15 @@ def set_data(self, data: Dict):
"""Helper function to save all items from `data` that
match `self.Attr` in `self.data`.
"""
for attr, conv in self.Attrs.items():
val = data.get(attr)
if not val:
continue
try:
self.data[attr] = conv(val)
except ValueError:
log.warning("Could not convert using %s: %s", conv, val)
for key, value in data.items():
self.set_attr(key, value)

def set_attr(self, name, value):
"""A helper function that saves attributes to `self.data`"""
if name not in self.Attrs:
return
if value is None:
return
conv = self.Attrs[name]
try:
self.data[name] = conv(value)
Expand Down Expand Up @@ -422,15 +420,21 @@ def __init__(self,
self.value_type = value_type
self.conversion: Callable[[List[Any]], Any] = conversion

def from_string(self, raw_value):
"""Parses the value from string, returns the list value as well as a
value for a single tool info compatibility"""
parsed = []
for value in raw_value.split(self.separator):
def parse_tools(self, raw_value: Any) -> tuple[list[Any], Any]:
"""Parses the value from raw_value, returns the list value as well as a
value for a single tool info compatibility
"""
try:
values = raw_value.split(self.separator)
except AttributeError:
values = [raw_value]

parsed: list[Any] = []
for value in values:
try:
parsed.append(self.value_type(value))
except ValueError:
return None, None
return [], None
try:
single_value = self.conversion(parsed)
except ValueError:
Expand All @@ -447,10 +451,12 @@ def set_attr(self, name, value):
"""Set an attribute, but add support for mmu list attributes"""
if value == '""': # e.g. when no extruder_colour
return
if value is None:
return
if name in self.MMUAttrs:
value_list, single_value = self.MMUAttrs[name].from_string(value)
value_list, single_value = self.MMUAttrs[name].parse_tools(value)
mmu_name = get_mmu_name(name)
if value_list is not None:
if len(value_list) > 1:
super().set_attr(mmu_name, value_list)
if single_value is not None:
super().set_attr(name, single_value)
Expand Down Expand Up @@ -763,6 +769,34 @@ def percent_of_m73_data(self):
return (present / count) * 100


class SLKeys:
"""Class that maps keys from config.json to
more readable names"""

def __init__(self, key_to_parse: str):
self.key_to_parse = key_to_parse

KeyMapping = {
"printTime": "estimated_print_time",
"expTime": "exposure_time",
"expTimeFirst": "exposure_time_first",
"layerHeight": "layer_height",
"materialName": "material",
"printerModel": "printer_model"
}

@staticmethod
def keys():
"""Returns all keys"""
return list(SLKeys.KeyMapping.keys()) + list(
SLKeys.KeyMapping.values())

@property
def key(self):
"""Returns correct key"""
return self.KeyMapping.get(self.key_to_parse, self.key_to_parse)


class SLMetaData(MetaData):
"""Class that can extract available metadata and thumbnails from
ziparchives used by SL1 slicers"""
Expand All @@ -771,23 +805,33 @@ class SLMetaData(MetaData):
# thumbnails!

Attrs = {
"printer_model": str,
"printTime": int,
"faded_layers": int,
"exposure_time": float,
"initial_exposure_time": float,
"max_initial_exposure_time": float,
"max_exposure_time": float,
"min_initial_exposure_time": float,
"min_exposure_time": float,
"estimated_print_time": int,
"layer_height": float,
"materialName": str,
"fileCreationTimestamp": str,
"material": str,
"exposure_time": int,
"exposure_time_first": int,
"total_layers": int,
"total_height": int,
"resin_used_ml": int,
"printer_model": str,
}

THUMBNAIL_NAME_PAT = re.compile(
r".*?(?P<dim>\d+x\d+)\.(?P<format>qoi|jpg|png)")

def set_attr(self, name, value):
"""A helper function that saves attributes to `self.data`"""
if value is None:
return
correct_name = SLKeys(name).key
if correct_name not in self.Attrs:
return
conv = self.Attrs[correct_name]
try:
self.data[correct_name] = conv(value)
except ValueError:
log.warning("Could not convert using %s: %s", conv, value)

def load(self, save_cache=True):
"""Load metadata"""
try:
Expand Down Expand Up @@ -816,16 +860,12 @@ def extract_metadata(path: str) -> Dict[str, str]:
value as value
"""
# pylint: disable=invalid-name
data = {}
data: dict = {}
file_name = "config.json"
with zipfile.ZipFile(path, "r") as zip_file:
for fn in ("config.ini", "prusaslicer.ini"):
config_file = zip_file.read(fn).decode("utf-8")
for line in config_file.splitlines():
key, value = line.split(" = ")
try:
data[key] = json.loads(value)
except json.decoder.JSONDecodeError:
data[key] = value
if file_name not in zip_file.namelist():
return data
data = json.loads(zip_file.read(file_name))
return data

@staticmethod
Expand Down
Binary file added tests/gcodes/Shape-Box.sl1
Binary file not shown.
Binary file removed tests/gcodes/pentagonal-hexecontahedron-1.sl1
Binary file not shown.
52 changes: 26 additions & 26 deletions tests/test_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,27 +203,19 @@ def test_full(self):
meta = get_metadata(fname, False)
assert meta.data == {
'bed_temperature': 90,
'bed_temperature per tool': [90],
'brim_width': 0,
'estimated printing time (normal mode)': '2h 6m 5s',
'filament cost': 0.41,
'filament cost per tool': [0.41],
'filament used [cm3]': 10.65,
'filament used [cm3] per tool': [10.65],
'filament used [g]': 13.52,
'filament used [g] per tool': [13.52],
'filament used [mm]': 4427.38,
'filament used [mm] per tool': [4427.38],
'filament_type': 'PETG',
'filament_type per tool': ['PETG'],
'fill_density': '20%',
'nozzle_diameter': 0.4,
'nozzle_diameter per tool': [0.4],
'printer_model': 'MK3S',
'layer_height': 0.15,
'support_material': 0,
'temperature': 250,
'temperature per tool': [250],
'ironing': 0,
'layer_info_present': True,
'normal_left_present': True,
Expand Down Expand Up @@ -258,14 +250,15 @@ def test_m73_and_layer_info(self):

def test_only_path(self):
"""Only the filename contains metadata. There are no thumbnails."""
fname = os.path.join(gcodes_dir,
"fdn_only_filename_0.25mm_PETG_MK3S_2h9m.gcode")
fname = os.path.join(
gcodes_dir, "fdn_only_filename_0.4n_0.25mm_PETG_MK3S_2h9m.gcode")
meta = get_metadata(fname, False)
assert meta.data == {
'estimated printing time (normal mode)': '2h9m',
'filament_type': 'PETG',
'printer_model': 'MK3S',
'layer_height': 0.25,
'nozzle_diameter': 0.4,
}
assert not meta.thumbnails

Expand Down Expand Up @@ -302,8 +295,8 @@ def test_from_chunks_fake_data(self):

def test_from_chunks_meta_only_path(self):
"""Test chunks from file with no metadata."""
fname = os.path.join(gcodes_dir,
"fdn_only_filename_0.25mm_PETG_MK3S_2h9m.gcode")
fname = os.path.join(
gcodes_dir, "fdn_only_filename_0.4n_0.25mm_PETG_MK3S_2h9m.gcode")
chunk_meta = get_meta_class(fname)
chunk_read_file(chunk_meta, fname)
assert chunk_meta.data == {}
Expand All @@ -313,6 +306,7 @@ def test_from_chunks_meta_only_path(self):
'filament_type': 'PETG',
'printer_model': 'MK3S',
'layer_height': 0.25,
'nozzle_diameter': 0.4,
}
assert not chunk_meta.thumbnails

Expand All @@ -335,26 +329,20 @@ class TestSLMetaData:

def test_sl(self):
"""Basic test."""
fname = os.path.join(gcodes_dir, "pentagonal-hexecontahedron-1.sl1")
fname = os.path.join(gcodes_dir, "Shape-Box.sl1")
meta = get_metadata(fname, False)

assert meta.data == {
'printer_model': 'SL1',
'printTime': 8720,
'faded_layers': 10,
'exposure_time': 7.5,
'initial_exposure_time': 35.0,
'max_initial_exposure_time': 300.0,
'max_exposure_time': 120.0,
'min_initial_exposure_time': 1.0,
'min_exposure_time': 1.0,
'estimated_print_time': 2722,
'exposure_time': 2,
'exposure_time_first': 25,
'layer_height': 0.05,
'materialName': 'Prusa Orange Tough @0.05',
'fileCreationTimestamp': '2020-09-17 at 13:53:21 UTC'
'material': 'Prusament Resin Tough Prusa Orange @0.05 SL1S',
'printer_model': 'SL1S'
}

assert len(meta.thumbnails["400x400_PNG"]) == 19688
assert len(meta.thumbnails["800x480_PNG"]) == 64524
assert len(meta.thumbnails["400x400_PNG"]) == 26948
assert len(meta.thumbnails["800x480_PNG"]) == 77796

def test_sl_empty_file(self):
"""Test a file that is empty"""
Expand All @@ -370,3 +358,15 @@ def test_sl1_chunk_read(self):
chunk_meta = get_meta_class(fname)
chunk_read_file(chunk_meta, fname)
assert chunk_meta.data == {}

def test_set_attr_same_as_set_data(self):
"""Test that loading meta through set_attr and set_data
gives the same output"""
fname = os.path.join(gcodes_dir, "Shape-Box.sl1")
meta_from_set_data = get_metadata(fname, False)
# read from set_attr
meta_cls_for_set_attr = get_meta_class(fname, "Shape-Box.sl1")
raw_meta = meta_cls_for_set_attr.extract_metadata(fname)
for attr, value in raw_meta.items():
meta_cls_for_set_attr.set_attr(attr, value)
assert meta_cls_for_set_attr.data == meta_from_set_data.data

0 comments on commit 1adcc22

Please sign in to comment.