Files
AI-Writie-Assistant/server/venv/lib/python3.9/site-packages/ezdxf/commands.py
T
2026-04-16 10:01:11 +08:00

1063 lines
32 KiB
Python

# Copyright (c) 2021-2024, Manfred Moitzi
# License: MIT License
from __future__ import annotations
import pathlib
import tempfile
from typing import Callable, Optional, TYPE_CHECKING, Type, Sequence
import abc
import sys
import os
import glob
import signal
import time
import logging
from pathlib import Path
# --------------------------------------------------------------------------------------
# Only imports from the core package here - no add-ons!
#
# The command `ezdxf -V` must always work!
#
# Imports depending on additional packages like Pillow, Matplotlib, PySide6, ...
# have to be local imports, see 'draw' command as an example.
# --------------------------------------------------------------------------------------
import ezdxf
from ezdxf import recover
from ezdxf.lldxf import const
from ezdxf.lldxf.validator import is_dxf_file, is_binary_dxf_file, dxf_info
from ezdxf.dwginfo import dwg_file_info
if TYPE_CHECKING:
from ezdxf.entities import DXFGraphic
from ezdxf.addons.drawing.properties import Properties, LayerProperties
__all__ = ["get", "add_parsers"]
logger = logging.getLogger("ezdxf")
def get(cmd: str) -> Optional[Callable]:
cls = _commands.get(cmd)
if cls:
return cls.run
return None
def add_parsers(subparsers) -> None:
for cmd in _commands.values(): # in order of registration
try:
cmd.add_parser(subparsers)
except ImportError:
logger.info(f"ImportError - '{cmd.NAME}' command not available")
def is_dxf_r12_file(filename: str) -> bool:
try:
with open(filename, "rt", errors="ignore") as fp:
info = dxf_info(fp)
except IOError:
return False
return info.version <= const.DXF12
class Command:
"""abstract base class for launcher commands"""
NAME = "command"
@staticmethod
@abc.abstractmethod
def add_parser(subparsers) -> None:
pass
@staticmethod
@abc.abstractmethod
def run(args) -> None:
pass
_commands: dict[str, Type[Command]] = dict()
def register(cls: Type[Command]):
"""Register a launcher sub-command."""
_commands[cls.NAME] = cls
return cls
@register
class Audit(Command):
"""Launcher sub-command: audit"""
NAME = "audit"
@staticmethod
def add_parser(subparsers):
parser = subparsers.add_parser(Audit.NAME, help="audit and repair DXF files")
parser.add_argument(
"files",
metavar="FILE",
nargs="+",
help="audit DXF files",
)
parser.add_argument(
"-s",
"--save",
action="store_true",
help='save recovered files with extension ".rec.dxf" ',
)
parser.add_argument(
"-x",
"--explore",
action="store_true",
help="filters invalid DXF tags, this may load corrupted files but "
"data loss is very likely",
)
@staticmethod
def run(args):
def build_outname(name: str) -> str:
p = Path(name)
return str(p.parent / (p.stem + ".rec.dxf"))
def log_fixes(auditor):
for error in auditor.fixes:
logger.info("fixed:" + error.message)
def log_errors(auditor):
for error in auditor.errors:
logger.error(error.message)
def _audit(filename: str) -> None:
msg = f"auditing file: {filename}"
print(msg)
logger.info(msg)
if args.explore:
logger.info("explore mode - skipping invalid tags")
loader = recover.explore if args.explore else recover.readfile
try:
doc, auditor = loader(filename)
except IOError:
msg = "Not a DXF file or a generic I/O error."
print(msg)
logger.error(msg)
return # keep on processing additional files
except const.DXFStructureError as e:
msg = f"Invalid or corrupted DXF file: {str(e)}"
print(msg)
logger.error(msg)
return # keep on processing additional files
if auditor.has_errors:
auditor.print_error_report()
log_errors(auditor)
if auditor.has_fixes:
auditor.print_fixed_errors()
log_fixes(auditor)
if auditor.has_errors is False and auditor.has_fixes is False:
print("No errors found.")
else:
print(
f"Found {len(auditor.errors)} errors, "
f"applied {len(auditor.fixes)} fixes"
)
if args.save:
outname = build_outname(filename)
try:
doc.saveas(outname)
except IOError as e:
print(f"Can not save recovered file '{outname}':\n{str(e)}")
else:
print(f"Saved recovered file as: '{outname}'")
for pattern in args.files:
names = list(glob.glob(pattern))
if len(names) == 0:
msg = f"File(s) '{pattern}' not found."
print(msg)
logger.error(msg)
continue
for filename in names:
if not os.path.exists(filename):
msg = f"File '{filename}' not found."
print(msg)
logger.error(msg)
continue
if not is_dxf_file(filename):
msg = f"File '{filename}' is not a DXF file."
print(msg)
logger.error(msg)
continue
_audit(filename)
def load_document(filename: str):
try:
doc, auditor = recover.readfile(filename)
except IOError:
msg = f'Not a DXF file or a generic I/O error: "{filename}"'
print(msg, file=sys.stderr)
sys.exit(2)
except const.DXFStructureError:
msg = f'Invalid or corrupted DXF file: "{filename}"'
print(msg, file=sys.stderr)
sys.exit(3)
if auditor.has_errors:
# But is most likely good enough for rendering.
msg = f"Audit process found {len(auditor.errors)} unrecoverable error(s)."
print(msg)
logger.error(msg)
if auditor.has_fixes:
msg = f"Audit process fixed {len(auditor.fixes)} error(s)."
print(msg)
logger.info(msg)
return doc, auditor
HELP_LTYPE = (
"select the line type rendering method, default is approximate. "
"Approximate uses the closest approximation available to the "
"backend, the accurate method renders as accurately as possible "
"but this approach is slower."
)
HELP_LWSCALE = (
"set custom line weight scaling, default is 0 to disable line " "weights at all"
)
@register
class Draw(Command):
"""Launcher sub-command: draw"""
NAME = "draw"
@staticmethod
def add_parser(subparsers):
parser = subparsers.add_parser(
Draw.NAME, help="draw and save DXF as a bitmap or vector image"
)
parser.add_argument(
"file",
metavar="FILE",
nargs="?",
help="DXF file to view or convert",
)
parser.add_argument(
"--backend",
default="matplotlib",
choices=["matplotlib", "qt", "mupdf", "custom_svg"],
help="choose the backend to use for rendering",
)
parser.add_argument(
"--formats",
action="store_true",
help="show all supported export formats and exit",
)
parser.add_argument(
"-l",
"--layout",
default="Model",
help='select the layout to draw, default is "Model"',
)
parser.add_argument(
"--background",
default="DEFAULT",
choices=[
"DEFAULT",
"WHITE",
"BLACK",
"PAPERSPACE",
"MODELSPACE",
"OFF",
"CUSTOM",
],
help="choose the background color to use",
)
parser.add_argument(
"--all-layers-visible",
action="store_true",
help="draw all layers including the ones marked as invisible",
)
parser.add_argument(
"--all-entities-visible",
action="store_true",
help="draw all entities including the ones marked as invisible "
"(some entities are individually marked as invisible even "
"if the layer is visible)",
)
parser.add_argument(
"-o",
"--out",
required=False,
type=pathlib.Path,
help="output filename for export",
)
parser.add_argument(
"--dpi",
type=int,
default=300,
help="target render resolution, default is 300",
)
parser.add_argument(
"-f",
"--force",
action="store_true",
help="overwrite the destination if it already exists",
)
parser.add_argument(
"-v",
"--verbose",
action="store_true",
help="give more output",
)
@staticmethod
def run(args):
try:
from ezdxf.addons.drawing import RenderContext, Frontend
from ezdxf.addons.drawing.config import Configuration, BackgroundPolicy
from ezdxf.addons.drawing.file_output import (
open_file,
MatplotlibFileOutput,
PyQtFileOutput,
SvgFileOutput,
MuPDFFileOutput,
)
except ImportError as e:
print(str(e))
sys.exit(1)
if args.backend == "matplotlib":
try:
file_output = MatplotlibFileOutput(args.dpi)
except ImportError as e:
print(str(e))
sys.exit(1)
elif args.backend == "qt":
try:
file_output = PyQtFileOutput(args.dpi)
except ImportError as e:
print(str(e))
sys.exit(1)
elif args.backend == "mupdf":
try:
file_output = MuPDFFileOutput(args.dpi)
except ImportError as e:
print(str(e))
sys.exit(1)
elif args.backend == "custom_svg":
# has no additional dependencies
file_output = SvgFileOutput(args.dpi)
else:
raise ValueError(args.backend)
verbose = args.verbose
if verbose:
logging.basicConfig(level=logging.INFO)
if args.formats:
print(f"formats supported by {args.backend}:")
for extension, description in file_output.supported_formats():
print(f" {extension}: {description}")
sys.exit(0)
if args.file:
filename = args.file
else:
print("argument FILE is required")
sys.exit(1)
print(f'loading file "{filename}"...')
doc, _ = load_document(filename)
try:
layout = doc.layouts.get(args.layout)
except KeyError:
print(
f'Could not find layout "{args.layout}". '
f"Valid layouts: {[l.name for l in doc.layouts]}"
)
sys.exit(1)
ctx = RenderContext(doc)
config = Configuration().with_changes(
background_policy=BackgroundPolicy[args.background]
)
out = file_output.backend()
if args.all_layers_visible:
def override_layer_properties(layer_properties: Sequence[LayerProperties]) -> None:
for properties in layer_properties:
properties.is_visible = True
ctx.set_layer_properties_override(override_layer_properties)
frontend = Frontend(ctx, out, config=config)
if args.all_entities_visible:
def override_entity_properties(entity: DXFGraphic, properties: Properties) -> None:
properties.is_visible = True
frontend.push_property_override_function(override_entity_properties)
t0 = time.perf_counter()
if verbose:
print(f"drawing layout '{layout.name}' ...")
frontend.draw_layout(layout, finalize=True)
t1 = time.perf_counter()
if verbose:
print(f"took {t1-t0:.4f} seconds")
if args.out is not None:
if pathlib.Path(args.out).suffix not in {
f".{ext}" for ext, _ in file_output.supported_formats()
}:
print(
f'the format of the output path "{args.out}" '
f"is not supported by the backend {args.backend}"
)
sys.exit(1)
if args.out.exists() and not args.force:
print(f'the destination "{args.out}" already exists. Not writing')
sys.exit(1)
else:
print(f'exporting to "{args.out}"...')
t0 = time.perf_counter()
file_output.save(args.out)
t1 = time.perf_counter()
if verbose:
print(f"took {t1 - t0:.4f} seconds")
else:
print(f"exporting to temporary file...")
output_dir = pathlib.Path(tempfile.mkdtemp(prefix="ezdxf_draw"))
output_path = output_dir / f"output.{file_output.default_format()}"
file_output.save(output_path)
print(f'saved to "{output_path}"')
if verbose:
print("opening viewer...")
open_file(output_path)
@register
class View(Command):
"""Launcher sub-command: view"""
NAME = "view"
@staticmethod
def add_parser(subparsers):
parser = subparsers.add_parser(
View.NAME, help="view DXF files by the PyQt viewer"
)
parser.add_argument(
"file",
metavar="FILE",
nargs="?",
help="DXF file to view",
)
parser.add_argument(
"-l",
"--layout",
default="Model",
help='select the layout to draw, default is "Model"',
)
# disable lineweight at all by default:
parser.add_argument(
"--lwscale",
type=float,
default=0,
help=HELP_LWSCALE,
)
@staticmethod
def run(args):
# Import on demand for a quicker startup:
try:
from ezdxf.addons.xqt import QtWidgets
except ImportError as e:
print(str(e))
sys.exit(1)
from ezdxf.addons.drawing.qtviewer import CADViewer
from ezdxf.addons.drawing.config import Configuration
config = Configuration(
lineweight_scaling=args.lwscale,
)
signal.signal(signal.SIGINT, signal.SIG_DFL) # handle Ctrl+C properly
app = QtWidgets.QApplication(sys.argv)
app.setStyle("Fusion")
set_app_icon(app)
viewer = CADViewer.from_config(config)
filename = args.file
if filename:
doc, auditor = load_document(filename)
viewer.set_document(
doc,
auditor,
layout=args.layout,
)
sys.exit(app.exec())
@register
class Browse(Command):
"""Launcher sub-command: browse"""
NAME = "browse"
@staticmethod
def add_parser(subparsers):
parser = subparsers.add_parser(Browse.NAME, help="browse DXF file structure")
parser.add_argument(
"file",
metavar="FILE",
nargs="?",
help="DXF file to browse",
)
parser.add_argument(
"-l", "--line", type=int, required=False, help="go to line number"
)
parser.add_argument(
"-g",
"--handle",
required=False,
help="go to entity by HANDLE, HANDLE has to be a hex value without "
"any prefix like 'fefe'",
)
@staticmethod
def run(args):
try:
from ezdxf.addons.xqt import QtWidgets
except ImportError as e:
print(str(e))
sys.exit(1)
from ezdxf.addons import browser
signal.signal(signal.SIGINT, signal.SIG_DFL) # handle Ctrl+C properly
app = QtWidgets.QApplication(sys.argv)
app.setStyle("Fusion")
set_app_icon(app)
main_window = browser.DXFStructureBrowser(
args.file,
line=args.line,
handle=args.handle,
resource_path=resources_path(),
)
main_window.show()
sys.exit(app.exec())
@register
class BrowseAcisData(Command):
"""Launcher sub-command: browse-acis"""
NAME = "browse-acis"
@staticmethod
def add_parser(subparsers):
parser = subparsers.add_parser(
BrowseAcisData.NAME, help="browse ACIS structures in DXF files"
)
parser.add_argument(
"file",
metavar="FILE",
nargs="?",
help="DXF file to browse",
)
parser.add_argument(
"-g",
"--handle",
required=False,
help="go to entity by HANDLE, HANDLE has to be a hex value without "
"any prefix like 'fefe'",
)
@staticmethod
def run(args):
try:
from ezdxf.addons.xqt import QtWidgets
except ImportError as e:
print(str(e))
sys.exit(1)
from ezdxf.addons.acisbrowser.browser import AcisStructureBrowser
signal.signal(signal.SIGINT, signal.SIG_DFL) # handle Ctrl+C properly
app = QtWidgets.QApplication(sys.argv)
app.setStyle("Fusion")
set_app_icon(app)
main_window = AcisStructureBrowser(
args.file,
handle=args.handle,
)
main_window.show()
sys.exit(app.exec())
@register
class Strip(Command):
"""Launcher sub-command: strip"""
NAME = "strip"
@staticmethod
def add_parser(subparsers):
parser = subparsers.add_parser(Strip.NAME, help="strip comments from DXF files")
parser.add_argument(
"file",
metavar="FILE",
nargs="+",
help='DXF file to process, wildcards "*" and "?" are supported',
)
parser.add_argument(
"-b",
"--backup",
action="store_true",
required=False,
help='make a backup copy with extension ".bak" from the '
"DXF file, overwrites existing backup files",
)
parser.add_argument(
"-t",
"--thumbnail",
action="store_true",
required=False,
help="strip THUMBNAILIMAGE section",
)
parser.add_argument(
"--handles",
action="store_true",
required=False,
help="remove handles from DXF R12 or older files",
)
parser.add_argument(
"-v",
"--verbose",
action="store_true",
required=False,
help="give more output",
)
@staticmethod
def run(args):
from ezdxf.tools.strip import strip
for pattern in args.file:
for filename in glob.glob(pattern):
codes = [999]
if args.handles:
if is_dxf_r12_file(filename):
codes.extend([5, 105])
else:
print(
f"Cannot remove handles from DXF R13 or later: {filename}"
)
strip(
filename,
backup=args.backup,
thumbnail=args.thumbnail,
verbose=args.verbose,
codes=codes,
)
@register
class Config(Command):
"""Launcher sub-command: config"""
NAME = "config"
@staticmethod
def add_parser(subparsers):
parser = subparsers.add_parser(Config.NAME, help="manage config files")
parser.add_argument(
"-p",
"--print",
action="store_true",
help="print configuration",
)
parser.add_argument(
"-w",
"--write",
metavar="FILE",
help="write configuration",
)
parser.add_argument(
"--home",
action="store_true",
help="create config file 'ezdxf.ini' in the user home directory "
"'~/.config/ezdxf', $XDG_CONFIG_HOME is supported if set",
)
parser.add_argument(
"--reset",
action="store_true",
help="factory reset, delete default config files 'ezdxf.ini'",
)
@staticmethod
def run(args):
from ezdxf import options
action = False
if args.reset:
options.reset()
options.delete_default_config_files()
action = True
if args.home:
options.write_home_config()
action = True
if args.write:
action = True
filepath = Path(args.write).expanduser()
try:
options.write_file(str(filepath))
print(f"configuration written to: {filepath}")
except IOError as e:
print(str(e))
if args.print or action is False:
options.print()
def load_every_document(filename: str):
def io_error() -> str:
msg = f'Not a DXF file or a generic I/O error: "{filename}"'
print(msg, file=sys.stderr)
return msg
def structure_error() -> str:
msg = f'Invalid or corrupted DXF file: "{filename}"'
print(msg, file=sys.stderr)
return msg
binary_fmt = False
if is_binary_dxf_file(filename):
try:
doc = ezdxf.readfile(filename)
except IOError:
raise const.DXFLoadError(io_error())
except const.DXFStructureError:
raise const.DXFLoadError(structure_error())
auditor = doc.audit()
binary_fmt = True
else:
try:
doc, auditor = recover.readfile(filename)
except IOError:
raise const.DXFLoadError(io_error())
except const.DXFStructureError:
dwginfo = dwg_file_info(filename)
if dwginfo.version != "invalid":
print(
f"This is a DWG file!!!\n"
f'Filename: "{filename}"\n'
f"Format: DWG\n"
f"Release: {dwginfo.release}\n"
f"DWG Version: {dwginfo.version}\n"
)
raise const.DXFLoadError()
raise const.DXFLoadError(structure_error())
return doc, auditor, binary_fmt
@register
class Info(Command):
"""Launcher sub-command: info"""
NAME = "info"
@staticmethod
def add_parser(subparsers):
parser = subparsers.add_parser(
Info.NAME,
help="show information and optional stats of DXF files as "
"loaded by ezdxf, this may not represent the original "
"content of the file, use the browse command to "
"see the original content",
)
parser.add_argument(
"file",
metavar="FILE",
nargs="+",
help='DXF file to process, wildcards "*" and "?" are supported',
)
parser.add_argument(
"-v",
"--verbose",
action="store_true",
required=False,
help="give more output",
)
parser.add_argument(
"-s",
"--stats",
action="store_true",
required=False,
help="show content stats",
)
@staticmethod
def run(args):
from ezdxf.document import info
def process(fn: str):
try:
doc, auditor, binary_fmt = load_every_document(fn)
except const.DXFLoadError:
pass
else:
fmt = "Binary" if binary_fmt else "ASCII"
print(
"\n".join(
info(
doc,
verbose=args.verbose,
content=args.stats,
fmt=fmt,
)
)
)
if auditor.has_fixes:
print(f"Audit process fixed {len(auditor.fixes)} error(s).")
if auditor.has_errors:
print(
f"Audit process found {len(auditor.errors)} unrecoverable error(s)."
)
print()
for pattern in args.file:
file_count = 0
for filename in glob.glob(pattern):
if os.path.isdir(filename):
dir_pattern = os.path.join(filename, "*.dxf")
for filename2 in glob.glob(dir_pattern):
process(filename2)
file_count += 1
else:
process(filename)
file_count += 1
if file_count == 0:
sys.stderr.write(f'No matching files for pattern: "{pattern}"\n')
@register
class HPGL(Command):
"""Launcher sub-command: hpgl"""
NAME = "hpgl"
@staticmethod
def add_parser(subparsers):
parser = subparsers.add_parser(
HPGL.NAME, help=f"view and/or convert HPGL/2 plot files to various formats"
)
parser.add_argument(
"file",
metavar="FILE",
nargs="?",
default="",
help=f"view and/or convert HPGL/2 plot files, wildcards (*, ?) supported in command line mode",
)
parser.add_argument(
"-e",
"--export",
metavar="FORMAT",
required=False,
help=f"convert HPGL/2 plot file to SVG, PDF or DXF from the command line (no gui)",
)
parser.add_argument(
"-r",
"--rotate",
type=int,
choices=(0, 90, 180, 270),
default=0,
required=False,
help="rotate page about 90, 180 or 270 degrees (no gui)",
)
parser.add_argument(
"-x",
"--mirror_x",
action="store_true",
required=False,
help="mirror page in x-axis direction, (no gui)",
)
parser.add_argument(
"-y",
"--mirror_y",
action="store_true",
required=False,
help="mirror page in y-axis direction, (no gui)",
)
parser.add_argument(
"-m",
"--merge_control",
type=int,
required=False,
default=2,
choices=(0, 1, 2),
help="provides control over the order of filled polygons, 0=off (print order), "
"1=luminance (order by luminance), 2=auto (default)",
)
parser.add_argument(
"-f",
"--force",
action="store_true",
required=False,
help="inserts the mandatory 'enter HPGL/2 mode' escape sequence into the data "
"stream; use this flag when no HPGL/2 data was found and you are sure the "
"file is a HPGL/2 plot file",
)
parser.add_argument(
"--aci",
action="store_true",
required=False,
help="use pen numbers as ACI colors and assign colors by layer (DXF only)",
)
parser.epilog = (
"Note that plot files are intended to be plotted on white paper."
)
parser.add_argument(
"--dpi",
type=int,
required=False,
default=96,
help="pixel density in dots per inch (PNG only)",
)
parser.epilog = (
"Note that plot files are intended to be plotted on white paper."
)
@staticmethod
def run(args):
if args.export:
if os.path.exists(args.file):
filenames = [args.file]
else:
filenames = glob.glob(args.file)
for filename in filenames:
export_hpgl2(Path(filename), args)
else:
launch_hpgl2_viewer(args.file, args.force)
def export_hpgl2(filepath: Path, args) -> None:
from ezdxf.addons.hpgl2 import api as hpgl2
from ezdxf.addons.drawing.dxf import ColorMode
fmt = args.export.upper()
start_msg = f"converting HPGL/2 plot file '{filepath.name}' to {fmt}"
try:
data = filepath.read_bytes()
except IOError as e:
print(str(e), file=sys.stderr)
return
if args.force:
data = b"%1B" + data
export_path = filepath.with_suffix(f".{fmt.lower()}")
if fmt == "DXF":
print(start_msg)
color_mode = ColorMode.ACI if args.aci else ColorMode.RGB
doc = hpgl2.to_dxf(
data,
rotation=args.rotate,
mirror_x=args.mirror_x,
mirror_y=args.mirror_y,
color_mode=color_mode,
merge_control=args.merge_control,
)
try:
doc.saveas(export_path)
except IOError as e:
print(str(e), file=sys.stderr)
elif fmt == "SVG":
print(start_msg)
svg_string = hpgl2.to_svg(
data,
rotation=args.rotate,
mirror_x=args.mirror_x,
mirror_y=args.mirror_y,
merge_control=args.merge_control,
)
try:
export_path.write_text(svg_string)
except IOError as e:
print(str(e), file=sys.stderr)
elif fmt == "PDF":
print(start_msg)
pdf_bytes = hpgl2.to_pdf(
data,
rotation=args.rotate,
mirror_x=args.mirror_x,
mirror_y=args.mirror_y,
merge_control=args.merge_control,
)
try:
export_path.write_bytes(pdf_bytes)
except IOError as e:
print(str(e), file=sys.stderr)
elif fmt == "PNG":
print(start_msg)
png_bytes = hpgl2.to_pixmap(
data,
rotation=args.rotate,
mirror_x=args.mirror_x,
mirror_y=args.mirror_y,
merge_control=args.merge_control,
fmt="png",
dpi=args.dpi,
)
try:
export_path.write_bytes(png_bytes)
except IOError as e:
print(str(e), file=sys.stderr)
else:
print(f"invalid export format: {fmt}")
exit(1)
print(f"file '{export_path.name}' successfully written")
def launch_hpgl2_viewer(filename: str, force: bool) -> None:
try:
from ezdxf.addons.xqt import QtWidgets
except ImportError as e:
print(str(e))
exit(1)
from ezdxf.addons.hpgl2.viewer import HPGL2Viewer
signal.signal(signal.SIGINT, signal.SIG_DFL) # handle Ctrl+C properly
app = QtWidgets.QApplication(sys.argv)
app.setStyle("Fusion")
set_app_icon(app)
viewer = HPGL2Viewer()
viewer.show()
if filename and os.path.exists(filename):
viewer.load_plot_file(filename, force=force)
sys.exit(app.exec())
def set_app_icon(app):
from ezdxf.addons.xqt import QtGui, QtCore
app_icon = QtGui.QIcon()
p = resources_path()
app_icon.addFile(str(p / "16x16.png"), QtCore.QSize(16, 16))
app_icon.addFile(str(p / "24x24.png"), QtCore.QSize(24, 24))
app_icon.addFile(str(p / "32x32.png"), QtCore.QSize(32, 32))
app_icon.addFile(str(p / "48x48.png"), QtCore.QSize(48, 48))
app_icon.addFile(str(p / "64x64.png"), QtCore.QSize(64, 64))
app_icon.addFile(str(p / "256x256.png"), QtCore.QSize(256, 256))
app.setWindowIcon(app_icon)
def resources_path():
from pathlib import Path
return Path(__file__).parent / "resources"