2021-10-23 02:32:35 +02:00
#!/usr/bin/env python3
import inkex
from inkex . bezier import csplength , csparea
from lxml import etree
import re
import math
class LaserCheck ( inkex . EffectExtension ) :
'''
check for old styles which should be upgraded
'''
2021-10-23 22:42:22 +02:00
def add_arguments ( self , pars ) :
pars . add_argument ( ' --tab ' )
pars . add_argument ( ' --checks ' , default = " check_all " )
pars . add_argument ( ' --bbox ' , type = inkex . Boolean , default = False )
pars . add_argument ( ' --bbox_offset ' , type = float , default = 5.000 )
pars . add_argument ( ' --groups_and_layers ' , type = inkex . Boolean , default = False )
pars . add_argument ( ' --clones ' , type = inkex . Boolean , default = False )
pars . add_argument ( ' --clippaths ' , type = inkex . Boolean , default = False )
pars . add_argument ( ' --images ' , type = inkex . Boolean , default = False )
pars . add_argument ( ' --texts ' , type = inkex . Boolean , default = False )
pars . add_argument ( ' --lowlevelstrokes ' , type = inkex . Boolean , default = False )
pars . add_argument ( ' --stroke_colors ' , type = inkex . Boolean , default = False )
pars . add_argument ( ' --stroke_widths ' , type = inkex . Boolean , default = False )
pars . add_argument ( ' --stroke_opacities ' , type = inkex . Boolean , default = False )
pars . add_argument ( ' --cosmestic_dashes ' , type = inkex . Boolean , default = False )
pars . add_argument ( ' --invisible_shapes ' , type = inkex . Boolean , default = False )
pars . add_argument ( ' --pointy_paths ' , type = inkex . Boolean , default = False )
pars . add_argument ( ' --transformations ' , type = inkex . Boolean , default = False )
pars . add_argument ( ' --short_paths ' , type = inkex . Boolean , default = False )
pars . add_argument ( ' --short_paths_min ' , type = float , default = 1.000 )
pars . add_argument ( ' --non_path_shapes ' , type = inkex . Boolean , default = False )
2021-10-23 02:32:35 +02:00
def effect ( self ) :
2021-10-23 22:42:22 +02:00
so = self . options
2021-10-23 02:32:35 +02:00
def parseChildren ( element ) :
if element not in selected :
selected . append ( element )
children = element . getchildren ( )
if children is not None :
for child in children :
if child not in selected :
selected . append ( child )
parseChildren ( child ) #go deeper and deeper
#check if we have selected elements or if we should parse the whole document instead
selected = [ ] #total list of elements to parse
if len ( self . svg . selected ) == 0 :
for element in self . document . getroot ( ) . iter ( tag = etree . Element ) :
if element != self . document . getroot ( ) :
selected . append ( element )
else :
for element in self . svg . selected . values ( ) :
2021-10-26 10:39:15 +02:00
parseChildren ( element )
2021-10-23 02:32:35 +02:00
namedView = self . document . getroot ( ) . find ( inkex . addNS ( ' namedview ' , ' sodipodi ' ) )
doc_units = namedView . get ( inkex . addNS ( ' document-units ' , ' inkscape ' ) )
2021-10-23 22:42:22 +02:00
pagecolor = namedView . get ( ' pagecolor ' )
inkex . utils . debug ( " ---------- Default checks " )
2021-10-23 02:32:35 +02:00
inkex . utils . debug ( " Document units: {} " . format ( doc_units ) )
2021-10-26 10:39:15 +02:00
'''
The SVG format is highly complex and offers a lot of possibilities . Most things of SVG we do not
need for a laser cutter . Usually we need svg : path and maybe svg : image ; we can drop a lot of stuff
like svg : defs , svg : desc , gradients , etc .
'''
2021-10-23 02:32:35 +02:00
nonShapes = [ ]
shapes = [ ]
for element in selected :
if not isinstance ( element , inkex . ShapeElement ) :
nonShapes . append ( element )
else :
2021-10-26 10:39:15 +02:00
shapes . append ( element )
2021-10-23 02:32:35 +02:00
inkex . utils . debug ( " {} shape elements in total " . format ( len ( shapes ) ) )
inkex . utils . debug ( " {} non-shape elements in total " . format ( len ( nonShapes ) ) )
for nonShape in nonShapes :
inkex . utils . debug ( " non-shape id= {} " . format ( nonShape . get ( ' id ' ) ) )
2021-10-23 22:42:22 +02:00
if so . checks == " check_all " or so . groups_and_layers is True :
inkex . utils . debug ( " \n ---------- Groups and layers " )
groups = [ ]
layers = [ ]
for element in selected :
if element . tag == inkex . addNS ( ' g ' , ' svg ' ) :
if element . get ( ' inkscape:groupmode ' ) == ' layer ' :
layers . append ( element )
else :
groups . append ( element )
inkex . utils . debug ( " {} groups in total " . format ( len ( groups ) ) )
inkex . utils . debug ( " {} layers in total " . format ( len ( layers ) ) )
#check for empty groups
for group in groups :
if len ( group ) == 0 :
inkex . utils . debug ( " id= {} is empty group " . format ( group . get ( ' id ' ) ) )
#check for empty layers
for layer in layers :
if len ( layer ) == 0 :
inkex . utils . debug ( " id= {} is empty layer " . format ( layer . get ( ' id ' ) ) )
2021-10-23 02:32:35 +02:00
2021-10-26 10:39:15 +02:00
'''
Clones should be unlinked because they cause similar issues like transformations
'''
2021-10-23 22:42:22 +02:00
if so . checks == " check_all " or so . clones is True :
inkex . utils . debug ( " \n ---------- Clones (svg:use) - maybe unlink " )
uses = [ ]
for element in selected :
if element . tag == inkex . addNS ( ' use ' , ' svg ' ) :
uses . append ( element )
inkex . utils . debug ( " {} svg:use clones in total " . format ( len ( uses ) ) )
for use in uses :
inkex . utils . debug ( " id= {} " . format ( use . get ( ' id ' ) ) )
2021-10-26 10:39:15 +02:00
'''
Clip paths are neat to visualize things , but they do not perform a real path cutting .
Please perform real intersections to have an intact target geometry .
'''
2021-10-23 22:42:22 +02:00
if so . checks == " check_all " or so . clippaths is True :
inkex . utils . debug ( " \n ---------- Clippings (svg:clipPath) - please replace with real cut paths " )
clipPaths = [ ]
for element in selected :
if element . tag == inkex . addNS ( ' clipPath ' , ' svg ' ) :
clipPaths . append ( element )
inkex . utils . debug ( " {} svg:clipPath in total " . format ( len ( clipPaths ) ) )
for clipPath in clipPaths :
inkex . utils . debug ( " id= {} " . format ( clipPath . get ( ' id ' ) ) )
2021-10-26 10:39:15 +02:00
'''
Sometimes images look like vector but they are ' nt. In case you dont want to perform engraving, either
check to drop or trace to vector paths
'''
2021-10-23 22:42:22 +02:00
if so . checks == " check_all " or so . images is True :
inkex . utils . debug ( " \n ---------- Images (svg:image) - maybe trace to svg " )
images = [ ]
for element in selected :
if element . tag == inkex . addNS ( ' image ' , ' svg ' ) :
images . append ( element )
inkex . utils . debug ( " {} svg:image in total " . format ( len ( images ) ) )
for image in images :
inkex . utils . debug ( " image id= {} " . format ( image . get ( ' id ' ) ) )
2021-10-26 10:39:15 +02:00
'''
Low level strokes cannot be properly edited in Inkscape ( no node handles ) . Converting helps
'''
2021-10-23 22:42:22 +02:00
if so . checks == " check_all " or so . lowlevelstrokes is True :
inkex . utils . debug ( " \n ---------- Low level strokes (svg:line/polyline/polygon) - maybe convert to path " )
lowlevels = [ ]
for element in selected :
if element . tag in ( inkex . addNS ( ' line ' , ' svg ' ) , inkex . addNS ( ' polyline ' , ' svg ' ) , inkex . addNS ( ' polygon ' , ' svg ' ) ) :
lowlevels . append ( element )
inkex . utils . debug ( " {} low level strokes in total " . format ( len ( lowlevels ) ) )
for lowlevel in lowlevels :
inkex . utils . debug ( " id= {} " . format ( lowlevel . get ( ' id ' ) ) )
2021-10-26 10:39:15 +02:00
'''
Texts cause problems when sharing with other people . You must ensure that everyone has the
font files installed you used . Convert to paths avoids this issue and guarantees same result
everywhere .
'''
2021-10-23 22:42:22 +02:00
if so . checks == " check_all " or so . texts is True :
inkex . utils . debug ( " \n ---------- Texts (should be converted to paths) " )
texts = [ ]
for element in selected :
if element . tag == inkex . addNS ( ' text ' , ' svg ' ) :
texts . append ( element )
inkex . utils . debug ( " {} svg:text in total " . format ( len ( texts ) ) )
for text in texts :
inkex . utils . debug ( " id= {} " . format ( text . get ( ' id ' ) ) )
2021-10-23 02:32:35 +02:00
2021-10-23 22:42:22 +02:00
2021-10-26 10:39:15 +02:00
'''
The more stroke colors the more laser job configuration is required . Reduce the SVG file
to a minimum of stroke colors to be quicker
'''
2021-10-23 22:42:22 +02:00
if so . checks == " check_all " or so . stroke_colors is True :
inkex . utils . debug ( " \n ---------- Stroke colors " )
strokeColors = [ ]
for element in shapes :
style = element . get ( ' style ' )
if style is not None :
stroke = re . search ( ' (;|^)stroke:(.*?)(;|$) ' , style )
if stroke is not None :
strokeColor = stroke [ 0 ] . split ( " stroke: " ) [ 1 ] . split ( " ; " ) [ 0 ]
if strokeColor not in strokeColors :
strokeColors . append ( strokeColor )
inkex . utils . debug ( " {} different stroke colors in total " . format ( len ( strokeColors ) ) )
for strokeColor in strokeColors :
inkex . utils . debug ( " stroke color {} " . format ( strokeColor ) )
2021-10-26 10:39:15 +02:00
'''
Nearly each laser job needs a bit of border to place the material inside the laser . Often
we have to fixate on vector grid , pin grid or task plate . Thus we need tapes or pins . So we
leave some borders off the actual part geometries .
'''
2021-10-23 22:42:22 +02:00
if so . checks == " check_all " or so . bbox is True :
inkex . utils . debug ( " \n ---------- Borders around all elements - minimum offset {} mm from each side " . format ( so . bbox_offset ) )
bbox = inkex . BoundingBox ( )
for element in self . document . getroot ( ) . iter ( tag = etree . Element ) :
if element != self . document . getroot ( ) and isinstance ( element , inkex . ShapeElement ) and element . tag != inkex . addNS ( ' use ' , ' svg ' ) and element . get ( ' inkscape:groupmode ' ) != ' layer ' : #bbox fails for svg:use elements and layers
transform = inkex . Transform ( )
parent = element . getparent ( )
if parent is not None and isinstance ( parent , inkex . ShapeElement ) :
transform = parent . composed_transform ( )
try :
bbox + = element . bounding_box ( transform )
except Exception :
transform = element . composed_transform ( )
x1 , y1 = transform . apply_to_point ( [ 0 , 0 ] )
x2 , y2 = transform . apply_to_point ( [ 1 , 1 ] )
bbox + = inkex . BoundingBox ( ( x1 , x2 ) , ( y1 , y2 ) )
if abs ( bbox . width ) == math . inf or abs ( bbox . height ) == math . inf :
inkex . utils . debug ( " bounding box could not be calculated " )
#else:
# inkex.utils.debug("bounding box is {}".format(bbox))
page_width = self . svg . unittouu ( self . document . getroot ( ) . attrib [ ' width ' ] )
width_height = self . svg . unittouu ( self . document . getroot ( ) . attrib [ ' height ' ] )
fmm = self . svg . unittouu ( str ( so . bbox_offset ) + " mm " )
2021-10-23 23:10:32 +02:00
if bbox . left > = fmm :
2021-10-23 22:42:22 +02:00
inkex . utils . debug ( " left border... ok " )
else :
2021-10-23 23:10:32 +02:00
inkex . utils . debug ( " left border... fail: {:0.3f} mm " . format ( self . svg . uutounit ( bbox . left , " mm " ) ) )
2021-10-23 22:42:22 +02:00
2021-10-23 23:10:32 +02:00
if bbox . top > = fmm :
2021-10-23 22:42:22 +02:00
inkex . utils . debug ( " top border... ok " )
else :
2021-10-23 23:10:32 +02:00
inkex . utils . debug ( " top border... fail: {:0.3f} mm " . format ( self . svg . uutounit ( bbox . top , " mm " ) ) )
2021-10-23 22:42:22 +02:00
if bbox . right + fmm < = page_width :
inkex . utils . debug ( " right border... ok " )
else :
2021-10-23 23:10:32 +02:00
inkex . utils . debug ( " right border... fail: {:0.3f} mm " . format ( self . svg . uutounit ( bbox . right , " mm " ) ) )
2021-10-23 02:32:35 +02:00
2021-10-23 22:42:22 +02:00
if bbox . bottom + fmm < = width_height :
inkex . utils . debug ( " bottom border... ok " )
else :
2021-10-23 23:10:32 +02:00
inkex . utils . debug ( " bottom border... fail: {:0.3f} mm " . format ( self . svg . uutounit ( bbox . bottom , " mm " ) ) )
2021-10-23 22:42:22 +02:00
2021-10-26 10:39:15 +02:00
'''
Different stroke widths might behave the same like different stroke colors . Reduce to a minimum set .
Ideally all stroke widths are set to 1 pixel .
'''
2021-10-23 22:42:22 +02:00
if so . checks == " check_all " or so . stroke_widths is True :
inkex . utils . debug ( " \n ---------- Stroke widths " )
strokeWidths = [ ]
for element in shapes :
2021-10-23 02:32:35 +02:00
style = element . get ( ' style ' )
2021-10-23 22:42:22 +02:00
if style is not None :
2021-10-23 02:32:35 +02:00
stroke_width = re . search ( ' stroke-width:(.*?)(;|$) ' , style )
2021-10-23 22:42:22 +02:00
if stroke_width is not None :
strokeWidth = self . svg . unittouu ( stroke_width [ 0 ] . split ( " stroke-width: " ) [ 1 ] . split ( " ; " ) [ 0 ] )
if strokeWidth not in strokeWidths :
strokeWidths . append ( strokeWidth )
inkex . utils . debug ( " {} different stroke widths in total " . format ( len ( strokeWidths ) ) )
for strokeWidth in strokeWidths :
inkex . utils . debug ( " stroke width {} " . format ( strokeWidth ) )
2021-10-26 10:39:15 +02:00
'''
Cosmetic dashes cause simulation issues and are no real cut paths . It ' s similar to the thing
with clip paths . Please convert lines to real dash segments if you want to laser them .
'''
2021-10-23 22:42:22 +02:00
if so . checks == " check_all " or so . cosmestic_dashes is True :
inkex . utils . debug ( " \n ---------- Cosmetic dashes - should be converted to paths " )
strokeDasharrays = [ ]
for element in shapes :
style = element . get ( ' style ' )
if style is not None :
stroke_dasharray = re . search ( ' stroke-dasharray:(.*?)(;|$) ' , style )
if stroke_dasharray is not None :
strokeDasharray = stroke_dasharray [ 0 ] . split ( " stroke-dasharray: " ) [ 1 ] . split ( " ; " ) [ 0 ]
if strokeDasharray not in strokeDasharrays :
strokeDasharrays . append ( strokeDasharray )
inkex . utils . debug ( " {} different stroke dash arrays in total " . format ( len ( strokeDasharrays ) ) )
for strokeDasharray in strokeDasharrays :
inkex . utils . debug ( " stroke dash array {} " . format ( strokeDasharray ) )
2021-10-23 02:32:35 +02:00
2021-10-26 10:39:15 +02:00
'''
Shapes / paths with the same color like the background , 0 % opacity , etc . lead to strange
laser cutting results , like duplicated edges , enlarged laser times and more . Please double
check for such occurences .
'''
2021-10-23 22:42:22 +02:00
if so . checks == " check_all " or so . invisible_shapes is True :
inkex . utils . debug ( " \n ---------- Invisible shapes " )
invisibles = [ ]
for element in shapes :
if element . tag not in ( inkex . addNS ( ' tspan ' , ' svg ' ) ) :
style = element . get ( ' style ' )
if style is not None :
stroke = re . search ( ' stroke:(.*?)(;|$) ' , style ) #filter white on white (we guess the background color of the document is white too but we do not check)
if stroke is None :
strokeVis = 0
elif stroke [ 0 ] . split ( " stroke: " ) [ 1 ] . split ( " ; " ) [ 0 ] == ' none ' :
strokeVis = 0
elif stroke [ 0 ] . split ( " stroke: " ) [ 1 ] . split ( " ; " ) [ 0 ] in ( ' #ffffff ' , ' white ' , ' rgb(255,255,255) ' ) :
strokeVis = 0
else :
strokeVis = 1
stroke_width = re . search ( ' stroke-width:(.*?)(;|$) ' , style )
if stroke_width is None :
widthVis = 0
elif self . svg . unittouu ( stroke_width [ 0 ] . split ( " stroke-width: " ) [ 1 ] . split ( " ; " ) [ 0 ] ) < 0.005 : #really thin (0,005pc = 0,080px)
widthVis = 0
else :
widthVis = 1
stroke_opacity = re . search ( ' stroke-opacity:(.*?)(;|$) ' , style )
if stroke_opacity is None :
strokeOpacityVis = 0
elif float ( stroke_opacity [ 0 ] . split ( " stroke-opacity: " ) [ 1 ] . split ( " ; " ) [ 0 ] ) < 0.05 : #nearly invisible (<5% opacity)
strokeOpacityVis = 0
else :
strokeOpacityVis = 1
if pagecolor == ' #ffffff ' :
invisColors = [ pagecolor , ' white ' , ' rgb(255,255,255) ' ]
else :
invisColors = [ pagecolor ] #we could add some parser to convert pagecolor to rgb/hsl/cmyk
fill = re . search ( ' fill:(.*?)(;|$) ' , style )
if fill is None :
fillVis = 0
elif fill [ 0 ] . split ( " fill: " ) [ 1 ] . split ( " ; " ) [ 0 ] == ' none ' :
fillVis = 0
elif fill [ 0 ] . split ( " fill: " ) [ 1 ] . split ( " ; " ) [ 0 ] in invisColors :
fillVis = 0
else :
fillVis = 1
fill_opacity = re . search ( ' fill-opacity:(.*?)(;|$) ' , style )
if fill_opacity is None :
fillOpacityVis = 0
elif float ( fill_opacity [ 0 ] . split ( " fill-opacity: " ) [ 1 ] . split ( " ; " ) [ 0 ] ) < 0.05 : #nearly invisible (<5% opacity)
fillOpacityVis = 0
else :
fillOpacityVis = 1
#inkex.utils.debug("strokeVis={}, widthVis={}, strokeOpacityVis={}, fillVis={}, fillOpacityVis={}".format(strokeVis, widthVis, strokeOpacityVis, fillVis, fillOpacityVis))
if ( strokeVis == 0 or widthVis == 0 or strokeOpacityVis == 0 ) and ( fillVis == 0 or fillOpacityVis == 0 ) :
if element not in invisibles :
invisibles . append ( element )
inkex . utils . debug ( " {} invisible shapes in total " . format ( len ( invisibles ) ) )
for invisible in invisibles :
inkex . utils . debug ( " id= {} " . format ( invisible . get ( ' id ' ) ) )
2021-10-26 10:39:15 +02:00
'''
Additionally , stroke opacities less than 1.0 cause problems in most laser softwares . Please
adjust all strokes to use full opacity .
'''
2021-10-23 22:42:22 +02:00
if so . checks == " check_all " or so . stroke_opacities is True :
inkex . utils . debug ( " \n ---------- Objects with stroke transparencies < 1.0 - should be set to 1.0 " )
transparencies = [ ]
for element in shapes :
style = element . get ( ' style ' )
if style is not None :
stroke_opacity = re . search ( ' stroke-opacity:(.*?)(;|$) ' , style )
if stroke_opacity is not None :
if float ( stroke_opacity [ 0 ] . split ( " stroke-opacity: " ) [ 1 ] . split ( " ; " ) [ 0 ] ) < 1.0 :
if element not in transparencies :
transparencies . append ( element )
inkex . utils . debug ( " {} objects with stroke transparencies < 1.0 in total " . format ( len ( transparencies ) ) )
for transparency in transparencies :
inkex . utils . debug ( " id= {} " . format ( transparency . get ( ' id ' ) ) )
2021-10-26 10:39:15 +02:00
'''
We look for paths which are just points . Those are useless in case of lasercutting .
Note : this scan only works for paths , not for subpaths . If so , you need to break apart before
'''
2021-10-23 22:42:22 +02:00
if so . checks == " check_all " or so . pointy_paths is True :
inkex . utils . debug ( " \n ---------- Pointy paths - should be deleted " )
pointyPaths = [ ]
for element in shapes :
if isinstance ( element , inkex . PathElement ) :
p = element . path
commandsCoords = p . to_arrays ( )
if len ( commandsCoords ) == 1 or \
( len ( commandsCoords ) == 2 and commandsCoords [ 0 ] [ 1 ] == commandsCoords [ 1 ] [ 1 ] ) or \
( len ( commandsCoords ) == 2 and commandsCoords [ - 1 ] [ 0 ] == ' Z ' ) or \
( len ( commandsCoords ) == 3 and commandsCoords [ 0 ] [ 1 ] == commandsCoords [ 1 ] [ 1 ] and commandsCoords [ 2 ] [ 1 ] == ' Z ' ) :
pointyPaths . append ( element )
inkex . utils . debug ( " {} pointy paths in total " . format ( len ( pointyPaths ) ) )
for pointyPath in pointyPaths :
inkex . utils . debug ( " id= {} " . format ( pointyPath . get ( ' id ' ) ) )
2021-10-23 02:32:35 +02:00
2021-10-23 22:42:22 +02:00
2021-10-26 10:39:15 +02:00
'''
Transformations often lead to wrong stroke widths or mis - rendering in end software . The best we
can do with a final SVG is to remove all relative translations , rotations and scalings . We should
apply absolute coordinates only .
'''
2021-10-23 22:42:22 +02:00
if so . checks == " check_all " or so . transformations is True :
inkex . utils . debug ( " \n ---------- Transformations - should be applied to absolute " )
transformations = [ ]
for element in shapes :
if element . get ( ' transform ' ) is not None :
transformations . append ( element )
inkex . utils . debug ( " {} transformation in total " . format ( len ( transformations ) ) )
for transformation in transformations :
inkex . utils . debug ( " transformation in id= {} " . format ( transformation . get ( ' id ' ) ) )
2021-10-26 10:39:15 +02:00
'''
Really short paths can cause issues with laser cutter mechanics and should be avoided to
have healthier stepper motor belts , etc .
'''
2021-10-23 22:42:22 +02:00
if so . checks == " check_all " or so . short_paths is True :
inkex . utils . debug ( " \n ---------- Short paths (< {} mm) " . format ( so . short_paths_min ) )
shortPaths = [ ]
totalLength = 0
totalDropLength = 0
for element in shapes :
if isinstance ( element , inkex . PathElement ) :
slengths , stotal = csplength ( element . path . transform ( element . composed_transform ( ) ) . to_superpath ( ) )
totalLength + = stotal
if stotal < self . svg . unittouu ( str ( so . short_paths_min ) + " mm " ) :
shortPaths . append ( element )
totalDropLength + = stotal
inkex . utils . debug ( " {} short paths in total " . format ( len ( shortPaths ) ) )
if totalLength > 0 :
inkex . utils . debug ( " {:0.2f} % o f total ( {:0.2f} mm / {:0.2f} mm) " . format ( totalDropLength / totalLength , totalDropLength , totalLength ) )
for shortPath in shortPaths :
inkex . utils . debug ( " id= {} " . format ( shortPath . get ( ' id ' ) ) )
2021-10-26 10:39:15 +02:00
'''
Shapes like rectangles , ellipses , arcs , spirals should be converted to svg : path to have more
convenience in the file
'''
2021-10-23 22:42:22 +02:00
if so . checks == " check_all " or so . non_path_shapes is True :
inkex . utils . debug ( " \n ---------- Non-path shapes - should be converted to paths " )
nonPathShapes = [ ]
for element in shapes :
if not isinstance ( element , inkex . PathElement ) and not isinstance ( element , inkex . Group ) :
nonPathShapes . append ( element )
inkex . utils . debug ( " {} non-path shapes in total " . format ( len ( nonPathShapes ) ) )
for nonPathShape in nonPathShapes :
inkex . utils . debug ( " id= {} " . format ( nonPathShape . get ( ' id ' ) ) )
2021-10-23 23:10:32 +02:00
exit ( 0 )
2021-10-23 02:32:35 +02:00
if __name__ == ' __main__ ' :
LaserCheck ( ) . run ( )