extract_metadata: add annotations

Signed-off-by:  Eric Callahan <arksine.code@gmail.com>
This commit is contained in:
Arksine 2021-05-16 14:58:08 -04:00
parent dbe28751df
commit c83d91eea7
1 changed files with 139 additions and 111 deletions

View File

@ -5,6 +5,7 @@
# #
# This file may be distributed under the terms of the GNU GPLv3 license. # This file may be distributed under the terms of the GNU GPLv3 license.
from __future__ import annotations
import json import json
import argparse import argparse
import re import re
@ -12,21 +13,35 @@ import os
import sys import sys
import base64 import base64
import traceback import traceback
import io
import tempfile import tempfile
import zipfile import zipfile
import shutil import shutil
from PIL import Image from PIL import Image
# Annotation imports
from typing import (
TYPE_CHECKING,
Any,
Optional,
Dict,
List,
Type,
)
if TYPE_CHECKING:
pass
UFP_MODEL_PATH = "/3D/model.gcode" UFP_MODEL_PATH = "/3D/model.gcode"
UFP_THUMB_PATH = "/Metadata/thumbnail.png" UFP_THUMB_PATH = "/Metadata/thumbnail.png"
def log_to_stderr(msg): def log_to_stderr(msg: str) -> None:
sys.stderr.write(f"{msg}\n") sys.stderr.write(f"{msg}\n")
sys.stderr.flush() sys.stderr.flush()
# regex helpers # regex helpers
def _regex_find_floats(pattern, data, strict=False): def _regex_find_floats(pattern: str,
data: str,
strict: bool = False
) -> List[float]:
# If strict is enabled, pattern requires a floating point # If strict is enabled, pattern requires a floating point
# value, otherwise it can be an integer value # value, otherwise it can be an integer value
fptrn = r'\d+\.\d*' if strict else r'\d+\.?\d*' fptrn = r'\d+\.\d*' if strict else r'\d+\.?\d*'
@ -35,121 +50,133 @@ def _regex_find_floats(pattern, data, strict=False):
# return the maximum height value found # return the maximum height value found
try: try:
return [float(h) for h in re.findall( return [float(h) for h in re.findall(
fptrn, " ".join(matches))] fptrn, " ".join(matches))]
except Exception: except Exception:
pass pass
return [] return []
def _regex_find_ints(pattern, data): def _regex_find_ints(pattern: str, data: str) -> List[int]:
matches = re.findall(pattern, data) matches = re.findall(pattern, data)
if matches: if matches:
# return the maximum height value found # return the maximum height value found
try: try:
return [int(h) for h in re.findall( return [int(h) for h in re.findall(
r'\d+', " ".join(matches))] r'\d+', " ".join(matches))]
except Exception: except Exception:
pass pass
return [] return []
def _regex_find_first(pattern, data, cast=float): def _regex_find_first(pattern: str, data: str) -> Optional[float]:
match = re.search(pattern, data) match = re.search(pattern, data)
val = None val: Optional[float] = None
if match: if match:
try: try:
val = cast(match.group(1)) val = float(match.group(1))
except Exception: except Exception:
return None return None
return val return val
# Slicer parsing implementations # Slicer parsing implementations
class BaseSlicer(object): class BaseSlicer(object):
def __init__(self, file_path): def __init__(self, file_path: str) -> None:
self.path = file_path self.path = file_path
self.header_data = self.footer_data = None self.header_data: str = ""
self.layer_height = None self.footer_data: str = ""
self.layer_height: Optional[float] = None
def set_data(self, header_data, footer_data, fsize): def set_data(self,
header_data: str,
footer_data: str,
fsize: int) -> None:
self.header_data = header_data self.header_data = header_data
self.footer_data = footer_data self.footer_data = footer_data
self.size = fsize self.size: int = fsize
def _parse_min_float(self, pattern, data, strict=False): def _parse_min_float(self,
pattern: str,
data: str,
strict: bool = False
) -> Optional[float]:
result = _regex_find_floats(pattern, data, strict) result = _regex_find_floats(pattern, data, strict)
if result: if result:
return min(result) return min(result)
else: else:
return None return None
def _parse_max_float(self, pattern, data, strict=False): def _parse_max_float(self,
pattern: str,
data: str,
strict: bool = False
) -> Optional[float]:
result = _regex_find_floats(pattern, data, strict) result = _regex_find_floats(pattern, data, strict)
if result: if result:
return max(result) return max(result)
else: else:
return None return None
def check_identity(self, data): def check_identity(self, data: str) -> Optional[Dict[str, str]]:
return None return None
def parse_gcode_start_byte(self): def parse_gcode_start_byte(self) -> Optional[int]:
m = re.search(r"\n[MG]\d+\s.*\n", self.header_data) m = re.search(r"\n[MG]\d+\s.*\n", self.header_data)
if m is None: if m is None:
return None return None
return m.start() return m.start()
def parse_gcode_end_byte(self): def parse_gcode_end_byte(self) -> Optional[int]:
rev_data = self.footer_data[::-1] rev_data = self.footer_data[::-1]
m = re.search(r"\n.*\s\d+[MG]\n", rev_data) m = re.search(r"\n.*\s\d+[MG]\n", rev_data)
if m is None: if m is None:
return None return None
return self.size - m.start() return self.size - m.start()
def parse_first_layer_height(self): def parse_first_layer_height(self) -> Optional[float]:
return None return None
def parse_layer_height(self): def parse_layer_height(self) -> Optional[float]:
return None return None
def parse_object_height(self): def parse_object_height(self) -> Optional[float]:
return None return None
def parse_filament_total(self): def parse_filament_total(self) -> Optional[float]:
return None return None
def parse_filament_weight_total(self): def parse_filament_weight_total(self) -> Optional[float]:
return None return None
def parse_estimated_time(self): def parse_estimated_time(self) -> Optional[float]:
return None return None
def parse_first_layer_bed_temp(self): def parse_first_layer_bed_temp(self) -> Optional[float]:
return None return None
def parse_first_layer_extr_temp(self): def parse_first_layer_extr_temp(self) -> Optional[float]:
return None return None
def parse_thumbnails(self): def parse_thumbnails(self) -> Optional[List[Dict[str, Any]]]:
return None return None
class UnknownSlicer(BaseSlicer): class UnknownSlicer(BaseSlicer):
def check_identity(self, data): def check_identity(self, data: str) -> Optional[Dict[str, str]]:
return {'slicer': "Unknown"} return {'slicer': "Unknown"}
def parse_first_layer_height(self): def parse_first_layer_height(self) -> Optional[float]:
return self._parse_min_float(r"G1\sZ\d+\.\d*", self.header_data) return self._parse_min_float(r"G1\sZ\d+\.\d*", self.header_data)
def parse_object_height(self): def parse_object_height(self) -> Optional[float]:
return self._parse_max_float(r"G1\sZ\d+\.\d*", self.footer_data) return self._parse_max_float(r"G1\sZ\d+\.\d*", self.footer_data)
def parse_first_layer_extr_temp(self): def parse_first_layer_extr_temp(self) -> Optional[float]:
return _regex_find_first( return _regex_find_first(
r"M109 S(\d+\.?\d*)", self.header_data) r"M109 S(\d+\.?\d*)", self.header_data)
def parse_first_layer_bed_temp(self): def parse_first_layer_bed_temp(self) -> Optional[float]:
return _regex_find_first( return _regex_find_first(
r"M190 S(\d+\.?\d*)", self.header_data) r"M190 S(\d+\.?\d*)", self.header_data)
class PrusaSlicer(BaseSlicer): class PrusaSlicer(BaseSlicer):
def check_identity(self, data): def check_identity(self, data: str) -> Optional[Dict[str, str]]:
match = re.search(r"PrusaSlicer\s(.*)\son", data) match = re.search(r"PrusaSlicer\s(.*)\son", data)
if match: if match:
return { return {
@ -158,7 +185,7 @@ class PrusaSlicer(BaseSlicer):
} }
return None return None
def parse_first_layer_height(self): def parse_first_layer_height(self) -> Optional[float]:
# Check percentage # Check percentage
pct = _regex_find_first( pct = _regex_find_first(
r"; first_layer_height = (\d+)%", self.footer_data) r"; first_layer_height = (\d+)%", self.footer_data)
@ -171,12 +198,12 @@ class PrusaSlicer(BaseSlicer):
return _regex_find_first( return _regex_find_first(
r"; first_layer_height = (\d+\.?\d*)", self.footer_data) r"; first_layer_height = (\d+\.?\d*)", self.footer_data)
def parse_layer_height(self): def parse_layer_height(self) -> Optional[float]:
self.layer_height = _regex_find_first( self.layer_height = _regex_find_first(
r"; layer_height = (\d+\.?\d*)", self.footer_data) r"; layer_height = (\d+\.?\d*)", self.footer_data)
return self.layer_height return self.layer_height
def parse_object_height(self): def parse_object_height(self) -> Optional[float]:
matches = re.findall( matches = re.findall(
r";BEFORE_LAYER_CHANGE\n(?:.*\n)?;(\d+\.?\d*)", self.footer_data) r";BEFORE_LAYER_CHANGE\n(?:.*\n)?;(\d+\.?\d*)", self.footer_data)
if matches: if matches:
@ -188,34 +215,34 @@ class PrusaSlicer(BaseSlicer):
return max(matches) return max(matches)
return self._parse_max_float(r"G1\sZ\d+\.\d*\sF", self.footer_data) return self._parse_max_float(r"G1\sZ\d+\.\d*\sF", self.footer_data)
def parse_filament_total(self): def parse_filament_total(self) -> Optional[float]:
return _regex_find_first( return _regex_find_first(
r"filament\sused\s\[mm\]\s=\s(\d+\.\d*)", self.footer_data) r"filament\sused\s\[mm\]\s=\s(\d+\.\d*)", self.footer_data)
def parse_filament_weight_total(self): def parse_filament_weight_total(self) -> Optional[float]:
return _regex_find_first( return _regex_find_first(
r"total\sfilament\sused\s\[g\]\s=\s(\d+\.\d*)", self.footer_data) r"total\sfilament\sused\s\[g\]\s=\s(\d+\.\d*)", self.footer_data)
def parse_estimated_time(self): def parse_estimated_time(self) -> Optional[float]:
time_match = re.search( time_match = re.search(
r';\sestimated\sprinting\stime.*', self.footer_data) r';\sestimated\sprinting\stime.*', self.footer_data)
if not time_match: if not time_match:
return None return None
total_time = 0 total_time = 0
time_match = time_match.group() time_group = time_match.group()
time_patterns = [(r"(\d+)d", 24*60*60), (r"(\d+)h", 60*60), time_patterns = [(r"(\d+)d", 24*60*60), (r"(\d+)h", 60*60),
(r"(\d+)m", 60), (r"(\d+)s", 1)] (r"(\d+)m", 60), (r"(\d+)s", 1)]
try: try:
for pattern, multiplier in time_patterns: for pattern, multiplier in time_patterns:
t = re.search(pattern, time_match) t = re.search(pattern, time_group)
if t: if t:
total_time += int(t.group(1)) * multiplier total_time += int(t.group(1)) * multiplier
except Exception: except Exception:
return None return None
return round(total_time, 2) return round(total_time, 2)
def parse_thumbnails(self): def parse_thumbnails(self) -> Optional[List[Dict[str, Any]]]:
thumb_matches = re.findall( thumb_matches: List[str] = re.findall(
r"; thumbnail begin[;/\+=\w\s]+?; thumbnail end", self.header_data) r"; thumbnail begin[;/\+=\w\s]+?; thumbnail end", self.header_data)
if not thumb_matches: if not thumb_matches:
return None return None
@ -225,9 +252,9 @@ class PrusaSlicer(BaseSlicer):
os.mkdir(thumb_dir) os.mkdir(thumb_dir)
except Exception: except Exception:
log_to_stderr(f"Unable to create thumb dir: {thumb_dir}") log_to_stderr(f"Unable to create thumb dir: {thumb_dir}")
return return None
thumb_base = os.path.splitext(os.path.basename(self.path))[0] thumb_base = os.path.splitext(os.path.basename(self.path))[0]
parsed_matches = [] parsed_matches: List[Dict[str, Any]] = []
for match in thumb_matches: for match in thumb_matches:
lines = re.split(r"\r?\n", match.replace('; ', '')) lines = re.split(r"\r?\n", match.replace('; ', ''))
info = _regex_find_ints(r".*", lines[0]) info = _regex_find_ints(r".*", lines[0])
@ -253,16 +280,16 @@ class PrusaSlicer(BaseSlicer):
'relative_path': rel_thumb_path}) 'relative_path': rel_thumb_path})
return parsed_matches return parsed_matches
def parse_first_layer_extr_temp(self): def parse_first_layer_extr_temp(self) -> Optional[float]:
return _regex_find_first( return _regex_find_first(
r"; first_layer_temperature = (\d+\.?\d*)", self.footer_data) r"; first_layer_temperature = (\d+\.?\d*)", self.footer_data)
def parse_first_layer_bed_temp(self): def parse_first_layer_bed_temp(self) -> Optional[float]:
return _regex_find_first( return _regex_find_first(
r"; first_layer_bed_temperature = (\d+\.?\d*)", self.footer_data) r"; first_layer_bed_temperature = (\d+\.?\d*)", self.footer_data)
class Slic3rPE(PrusaSlicer): class Slic3rPE(PrusaSlicer):
def check_identity(self, data): def check_identity(self, data: str) -> Optional[Dict[str, str]]:
match = re.search(r"Slic3r\sPrusa\sEdition\s(.*)\son", data) match = re.search(r"Slic3r\sPrusa\sEdition\s(.*)\son", data)
if match: if match:
return { return {
@ -271,15 +298,15 @@ class Slic3rPE(PrusaSlicer):
} }
return None return None
def parse_filament_total(self): def parse_filament_total(self) -> Optional[float]:
return _regex_find_first( return _regex_find_first(
r"filament\sused\s=\s(\d+\.\d+)mm", self.footer_data) r"filament\sused\s=\s(\d+\.\d+)mm", self.footer_data)
def parse_thumbnails(self): def parse_thumbnails(self) -> Optional[List[Dict[str, Any]]]:
return None return None
class Slic3r(Slic3rPE): class Slic3r(Slic3rPE):
def check_identity(self, data): def check_identity(self, data: str) -> Optional[Dict[str, str]]:
match = re.search(r"Slic3r\s(\d.*)\son", data) match = re.search(r"Slic3r\s(\d.*)\son", data)
if match: if match:
return { return {
@ -288,22 +315,22 @@ class Slic3r(Slic3rPE):
} }
return None return None
def parse_filament_total(self): def parse_filament_total(self) -> Optional[float]:
filament = _regex_find_first( filament = _regex_find_first(
r";\sfilament\_length\_m\s=\s(\d+\.\d*)", self.footer_data) r";\sfilament\_length\_m\s=\s(\d+\.\d*)", self.footer_data)
if filament is not None: if filament is not None:
filament *= 1000 filament *= 1000
return filament return filament
def parse_filament_weight_total(self): def parse_filament_weight_total(self) -> Optional[float]:
return _regex_find_first( return _regex_find_first(
r";\sfilament\smass\_g\s=\s(\d+\.\d*)", self.footer_data) r";\sfilament\smass\_g\s=\s(\d+\.\d*)", self.footer_data)
def parse_estimated_time(self): def parse_estimated_time(self) -> Optional[float]:
return None return None
class SuperSlicer(PrusaSlicer): class SuperSlicer(PrusaSlicer):
def check_identity(self, data): def check_identity(self, data: str) -> Optional[Dict[str, str]]:
match = re.search(r"SuperSlicer\s(.*)\son", data) match = re.search(r"SuperSlicer\s(.*)\son", data)
if match: if match:
return { return {
@ -313,7 +340,7 @@ class SuperSlicer(PrusaSlicer):
return None return None
class Cura(PrusaSlicer): class Cura(PrusaSlicer):
def check_identity(self, data): def check_identity(self, data: str) -> Optional[Dict[str, str]]:
match = re.search(r"Cura_SteamEngine\s(.*)", data) match = re.search(r"Cura_SteamEngine\s(.*)", data)
if match: if match:
return { return {
@ -322,40 +349,40 @@ class Cura(PrusaSlicer):
} }
return None return None
def parse_first_layer_height(self): def parse_first_layer_height(self) -> Optional[float]:
return _regex_find_first(r";MINZ:(\d+\.?\d*)", self.header_data) return _regex_find_first(r";MINZ:(\d+\.?\d*)", self.header_data)
def parse_layer_height(self): def parse_layer_height(self) -> Optional[float]:
self.layer_height = _regex_find_first( self.layer_height = _regex_find_first(
r";Layer\sheight:\s(\d+\.?\d*)", self.header_data) r";Layer\sheight:\s(\d+\.?\d*)", self.header_data)
return self.layer_height return self.layer_height
def parse_object_height(self): def parse_object_height(self) -> Optional[float]:
return _regex_find_first(r";MAXZ:(\d+\.?\d*)", self.header_data) return _regex_find_first(r";MAXZ:(\d+\.?\d*)", self.header_data)
def parse_filament_total(self): def parse_filament_total(self) -> Optional[float]:
filament = _regex_find_first( filament = _regex_find_first(
r";Filament\sused:\s(\d+\.?\d*)m", self.header_data) r";Filament\sused:\s(\d+\.?\d*)m", self.header_data)
if filament is not None: if filament is not None:
filament *= 1000 filament *= 1000
return filament return filament
def parse_filament_weight_total(self): def parse_filament_weight_total(self) -> Optional[float]:
return _regex_find_first( return _regex_find_first(
r";Filament\sweight\s=\s.(\d+\.\d+).", self.header_data) r";Filament\sweight\s=\s.(\d+\.\d+).", self.header_data)
def parse_estimated_time(self): def parse_estimated_time(self) -> Optional[float]:
return self._parse_max_float(r";TIME:.*", self.header_data) return self._parse_max_float(r";TIME:.*", self.header_data)
def parse_first_layer_extr_temp(self): def parse_first_layer_extr_temp(self) -> Optional[float]:
return _regex_find_first( return _regex_find_first(
r"M109 S(\d+\.?\d*)", self.header_data) r"M109 S(\d+\.?\d*)", self.header_data)
def parse_first_layer_bed_temp(self): def parse_first_layer_bed_temp(self) -> Optional[float]:
return _regex_find_first( return _regex_find_first(
r"M190 S(\d+\.?\d*)", self.header_data) r"M190 S(\d+\.?\d*)", self.header_data)
def parse_thumbnails(self): def parse_thumbnails(self) -> Optional[List[Dict[str, Any]]]:
# Attempt to parse thumbnails from file metadata # Attempt to parse thumbnails from file metadata
thumbs = super().parse_thumbnails() thumbs = super().parse_thumbnails()
if thumbs is not None: if thumbs is not None:
@ -392,7 +419,7 @@ class Cura(PrusaSlicer):
return thumbs return thumbs
class Simplify3D(BaseSlicer): class Simplify3D(BaseSlicer):
def check_identity(self, data): def check_identity(self, data: str) -> Optional[Dict[str, str]]:
match = re.search(r"Simplify3D\(R\)\sVersion\s(.*)", data) match = re.search(r"Simplify3D\(R\)\sVersion\s(.*)", data)
if match: if match:
return { return {
@ -401,50 +428,50 @@ class Simplify3D(BaseSlicer):
} }
return None return None
def parse_first_layer_height(self): def parse_first_layer_height(self) -> Optional[float]:
return self._parse_min_float(r"G1\sZ\d+\.\d*", self.header_data) return self._parse_min_float(r"G1\sZ\d+\.\d*", self.header_data)
def parse_layer_height(self): def parse_layer_height(self) -> Optional[float]:
self.layer_height = _regex_find_first( self.layer_height = _regex_find_first(
r";\s+layerHeight,(\d+\.?\d*)", self.header_data) r";\s+layerHeight,(\d+\.?\d*)", self.header_data)
return self.layer_height return self.layer_height
def parse_object_height(self): def parse_object_height(self) -> Optional[float]:
return self._parse_max_float(r"G1\sZ\d+\.\d*", self.footer_data) return self._parse_max_float(r"G1\sZ\d+\.\d*", self.footer_data)
def parse_filament_total(self): def parse_filament_total(self) -> Optional[float]:
return _regex_find_first( return _regex_find_first(
r";\s+Filament\slength:\s(\d+\.?\d*)\smm", self.footer_data) r";\s+Filament\slength:\s(\d+\.?\d*)\smm", self.footer_data)
def parse_filament_weight_total(self): def parse_filament_weight_total(self) -> Optional[float]:
return _regex_find_first( return _regex_find_first(
r";\s+Plastic\sweight:\s(\d+\.?\d*)\sg", self.footer_data) r";\s+Plastic\sweight:\s(\d+\.?\d*)\sg", self.footer_data)
def parse_estimated_time(self): def parse_estimated_time(self) -> Optional[float]:
time_match = re.search( time_match = re.search(
r';\s+Build time:.*', self.footer_data) r';\s+Build time:.*', self.footer_data)
if not time_match: if not time_match:
return None return None
total_time = 0 total_time = 0
time_match = time_match.group() time_group = time_match.group()
time_patterns = [(r"(\d+)\shours", 60*60), (r"(\d+)\smin", 60), time_patterns = [(r"(\d+)\shours", 60*60), (r"(\d+)\smin", 60),
(r"(\d+)\ssec", 1)] (r"(\d+)\ssec", 1)]
try: try:
for pattern, multiplier in time_patterns: for pattern, multiplier in time_patterns:
t = re.search(pattern, time_match) t = re.search(pattern, time_group)
if t: if t:
total_time += int(t.group(1)) * multiplier total_time += int(t.group(1)) * multiplier
except Exception: except Exception:
return None return None
return round(total_time, 2) return round(total_time, 2)
def _get_temp_items(self, pattern): def _get_temp_items(self, pattern: str) -> List[str]:
match = re.search(pattern, self.header_data) match = re.search(pattern, self.header_data)
if match is None: if match is None:
return [] return []
return match.group().split(",")[1:] return match.group().split(",")[1:]
def _get_first_layer_temp(self, heater): def _get_first_layer_temp(self, heater: str) -> Optional[float]:
heaters = self._get_temp_items(r"temperatureName.*") heaters = self._get_temp_items(r"temperatureName.*")
temps = self._get_temp_items(r"temperatureSetpointTemperatures.*") temps = self._get_temp_items(r"temperatureSetpointTemperatures.*")
for h, temp in zip(heaters, temps): for h, temp in zip(heaters, temps):
@ -455,14 +482,14 @@ class Simplify3D(BaseSlicer):
return None return None
return None return None
def parse_first_layer_extr_temp(self): def parse_first_layer_extr_temp(self) -> Optional[float]:
return self._get_first_layer_temp("Extruder 1") return self._get_first_layer_temp("Extruder 1")
def parse_first_layer_bed_temp(self): def parse_first_layer_bed_temp(self) -> Optional[float]:
return self._get_first_layer_temp("Heated Bed") return self._get_first_layer_temp("Heated Bed")
class KISSlicer(BaseSlicer): class KISSlicer(BaseSlicer):
def check_identity(self, data): def check_identity(self, data: str) -> Optional[Dict[str, Any]]:
match = re.search(r";\sKISSlicer", data) match = re.search(r";\sKISSlicer", data)
if match: if match:
ident = {'slicer': "KISSlicer"} ident = {'slicer': "KISSlicer"}
@ -473,27 +500,27 @@ class KISSlicer(BaseSlicer):
return ident return ident
return None return None
def parse_first_layer_height(self): def parse_first_layer_height(self) -> Optional[float]:
return _regex_find_first( return _regex_find_first(
r";\s+first_layer_thickness_mm\s=\s(\d+\.?\d*)", self.header_data) r";\s+first_layer_thickness_mm\s=\s(\d+\.?\d*)", self.header_data)
def parse_layer_height(self): def parse_layer_height(self) -> Optional[float]:
self.layer_height = _regex_find_first( self.layer_height = _regex_find_first(
r";\s+max_layer_thickness_mm\s=\s(\d+\.?\d*)", self.header_data) r";\s+max_layer_thickness_mm\s=\s(\d+\.?\d*)", self.header_data)
return self.layer_height return self.layer_height
def parse_object_height(self): def parse_object_height(self) -> Optional[float]:
return self._parse_max_float( return self._parse_max_float(
r";\sEND_LAYER_OBJECT\sz.*", self.footer_data) r";\sEND_LAYER_OBJECT\sz.*", self.footer_data)
def parse_filament_total(self): def parse_filament_total(self) -> Optional[float]:
filament = _regex_find_floats( filament = _regex_find_floats(
r";\s+Ext\s.*mm", self.footer_data, strict=True) r";\s+Ext\s.*mm", self.footer_data, strict=True)
if filament: if filament:
return sum(filament) return sum(filament)
return None return None
def parse_estimated_time(self): def parse_estimated_time(self) -> Optional[float]:
time = _regex_find_first( time = _regex_find_first(
r";\sCalculated.*Build\sTime:\s(\d+\.?\d*)\sminutes", r";\sCalculated.*Build\sTime:\s(\d+\.?\d*)\sminutes",
self.footer_data) self.footer_data)
@ -502,17 +529,17 @@ class KISSlicer(BaseSlicer):
return round(time, 2) return round(time, 2)
return None return None
def parse_first_layer_extr_temp(self): def parse_first_layer_extr_temp(self) -> Optional[float]:
return _regex_find_first( return _regex_find_first(
r"; first_layer_C = (\d+\.?\d*)", self.header_data) r"; first_layer_C = (\d+\.?\d*)", self.header_data)
def parse_first_layer_bed_temp(self): def parse_first_layer_bed_temp(self) -> Optional[float]:
return _regex_find_first( return _regex_find_first(
r"; bed_C = (\d+\.?\d*)", self.header_data) r"; bed_C = (\d+\.?\d*)", self.header_data)
class IdeaMaker(BaseSlicer): class IdeaMaker(BaseSlicer):
def check_identity(self, data): def check_identity(self, data: str) -> Optional[Dict[str, str]]:
match = re.search(r"\sideaMaker\s(.*),", data) match = re.search(r"\sideaMaker\s(.*),", data)
if match: if match:
return { return {
@ -521,14 +548,14 @@ class IdeaMaker(BaseSlicer):
} }
return None return None
def parse_first_layer_height(self): def parse_first_layer_height(self) -> Optional[float]:
layer_info = _regex_find_floats( layer_info = _regex_find_floats(
r";LAYER:0\s*.*\s*;HEIGHT.*", self.header_data) r";LAYER:0\s*.*\s*;HEIGHT.*", self.header_data)
if len(layer_info) >= 3: if len(layer_info) >= 3:
return layer_info[2] return layer_info[2]
return None return None
def parse_layer_height(self): def parse_layer_height(self) -> Optional[float]:
layer_info = _regex_find_floats( layer_info = _regex_find_floats(
r";LAYER:1\s*.*\s*;HEIGHT.*", self.header_data) r";LAYER:1\s*.*\s*;HEIGHT.*", self.header_data)
if len(layer_info) >= 3: if len(layer_info) >= 3:
@ -536,21 +563,21 @@ class IdeaMaker(BaseSlicer):
return self.layer_height return self.layer_height
return None return None
def parse_object_height(self): def parse_object_height(self) -> Optional[float]:
bounds = _regex_find_floats( bounds = _regex_find_floats(
r";Bounding Box:.*", self.header_data) r";Bounding Box:.*", self.header_data)
if len(bounds) >= 6: if len(bounds) >= 6:
return bounds[5] return bounds[5]
return None return None
def parse_filament_total(self): def parse_filament_total(self) -> Optional[float]:
filament = _regex_find_floats( filament = _regex_find_floats(
r";Material.\d\sUsed:.*", self.footer_data, strict=True) r";Material.\d\sUsed:.*", self.footer_data, strict=True)
if filament: if filament:
return sum(filament) return sum(filament)
return None return None
def parse_filament_weight_total(self): def parse_filament_weight_total(self) -> Optional[float]:
pi = 3.141592653589793 pi = 3.141592653589793
length = _regex_find_floats( length = _regex_find_floats(
r";Material.\d\sUsed:.*", self.footer_data, strict=True) r";Material.\d\sUsed:.*", self.footer_data, strict=True)
@ -565,51 +592,51 @@ class IdeaMaker(BaseSlicer):
return sum(weights) return sum(weights)
return None return None
def parse_estimated_time(self): def parse_estimated_time(self) -> Optional[float]:
return _regex_find_first( return _regex_find_first(
r";Print\sTime:\s(\d+\.?\d*)", self.footer_data) r";Print\sTime:\s(\d+\.?\d*)", self.footer_data)
def parse_first_layer_extr_temp(self): def parse_first_layer_extr_temp(self) -> Optional[float]:
return _regex_find_first( return _regex_find_first(
r"M109 T0 S(\d+\.?\d*)", self.header_data) r"M109 T0 S(\d+\.?\d*)", self.header_data)
def parse_first_layer_bed_temp(self): def parse_first_layer_bed_temp(self) -> Optional[float]:
return _regex_find_first( return _regex_find_first(
r"M190 S(\d+\.?\d*)", self.header_data) r"M190 S(\d+\.?\d*)", self.header_data)
class IceSL(BaseSlicer): class IceSL(BaseSlicer):
def check_identity(self, data): def check_identity(self, data) -> Optional[Dict[str, Any]]:
match = re.search(r"; <IceSL.*>", data) match = re.search(r"; <IceSL.*>", data)
if match: if match:
return {'slicer': "IceSL"} return {'slicer': "IceSL"}
return None return None
def parse_first_layer_height(self): def parse_first_layer_height(self) -> Optional[float]:
return _regex_find_first( return _regex_find_first(
r"; z_layer_height_first_layer_mm :\s+(\d+\.\d+)", r"; z_layer_height_first_layer_mm :\s+(\d+\.\d+)",
self.header_data, float) self.header_data)
def parse_layer_height(self): def parse_layer_height(self) -> Optional[float]:
self.layer_height = _regex_find_first( self.layer_height = _regex_find_first(
r"; z_layer_height_mm :\s+(\d+\.\d+)", r"; z_layer_height_mm :\s+(\d+\.\d+)",
self.header_data, float) self.header_data)
return self.layer_height return self.layer_height
def parse_object_height(self): def parse_object_height(self) -> Optional[float]:
return self._parse_max_float( return self._parse_max_float(
r"G0 F\d+ Z\d+\.\d+", self.footer_data, strict=True) r"G0 F\d+ Z\d+\.\d+", self.footer_data, strict=True)
def parse_first_layer_extr_temp(self): def parse_first_layer_extr_temp(self) -> Optional[float]:
return _regex_find_first( return _regex_find_first(
r"; extruder_temp_degree_c_0 :\s+(\d+\.?\d*)", self.header_data) r"; extruder_temp_degree_c_0 :\s+(\d+\.?\d*)", self.header_data)
def parse_first_layer_bed_temp(self): def parse_first_layer_bed_temp(self) -> Optional[float]:
return _regex_find_first( return _regex_find_first(
r"; bed_temp_degree_c :\s+(\d+\.?\d*)", self.header_data) r"; bed_temp_degree_c :\s+(\d+\.?\d*)", self.header_data)
READ_SIZE = 512 * 1024 READ_SIZE = 512 * 1024
SUPPORTED_SLICERS = [ SUPPORTED_SLICERS: List[Type[BaseSlicer]] = [
PrusaSlicer, Slic3rPE, Slic3r, SuperSlicer, PrusaSlicer, Slic3rPE, Slic3r, SuperSlicer,
Cura, Simplify3D, KISSlicer, IdeaMaker, IceSL] Cura, Simplify3D, KISSlicer, IdeaMaker, IceSL]
SUPPORTED_DATA = [ SUPPORTED_DATA = [
@ -618,10 +645,11 @@ SUPPORTED_DATA = [
'thumbnails', 'first_layer_bed_temp', 'first_layer_extr_temp', 'thumbnails', 'first_layer_bed_temp', 'first_layer_extr_temp',
'gcode_start_byte', 'gcode_end_byte'] 'gcode_start_byte', 'gcode_end_byte']
def extract_metadata(file_path): def extract_metadata(file_path: str) -> Dict[str, Any]:
metadata = {} metadata: Dict[str, Any] = {}
slicers = [s(file_path) for s in SUPPORTED_SLICERS] slicers = [s(file_path) for s in SUPPORTED_SLICERS]
header_data = footer_data = slicer = None header_data = footer_data = ""
slicer: Optional[BaseSlicer] = None
size = os.path.getsize(file_path) size = os.path.getsize(file_path)
metadata['size'] = size metadata['size'] = size
metadata['modified'] = os.path.getmtime(file_path) metadata['modified'] = os.path.getmtime(file_path)
@ -654,7 +682,7 @@ def extract_metadata(file_path):
metadata[key] = result metadata[key] = result
return metadata return metadata
def extract_ufp(ufp_path, dest_path): def extract_ufp(ufp_path: str, dest_path: str) -> None:
if not os.path.isfile(ufp_path): if not os.path.isfile(ufp_path):
log_to_stderr(f"UFP file Not Found: {ufp_path}") log_to_stderr(f"UFP file Not Found: {ufp_path}")
sys.exit(-1) sys.exit(-1)
@ -682,11 +710,11 @@ def extract_ufp(ufp_path, dest_path):
except Exception: except Exception:
log_to_stderr(f"Error removing ufp file: {ufp_path}") log_to_stderr(f"Error removing ufp file: {ufp_path}")
def main(path, filename, ufp): def main(path: str, filename: str, ufp: Optional[str]) -> None:
file_path = os.path.join(path, filename) file_path = os.path.join(path, filename)
if ufp is not None: if ufp is not None:
extract_ufp(ufp, file_path) extract_ufp(ufp, file_path)
metadata = {} metadata: Dict[str, Any] = {}
if not os.path.isfile(file_path): if not os.path.isfile(file_path):
log_to_stderr(f"File Not Found: {file_path}") log_to_stderr(f"File Not Found: {file_path}")
sys.exit(-1) sys.exit(-1)