1063 lines
32 KiB
Python
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"
|