refactor: excel parse
This commit is contained in:
@@ -0,0 +1,779 @@
|
||||
# Purpose: read and write AutoCAD CTB files
|
||||
# Copyright (c) 2010-2023, Manfred Moitzi
|
||||
# License: MIT License
|
||||
# IMPORTANT: use only standard 7-Bit ascii code
|
||||
from __future__ import annotations
|
||||
from typing import (
|
||||
Union,
|
||||
Optional,
|
||||
BinaryIO,
|
||||
TextIO,
|
||||
Iterable,
|
||||
Iterator,
|
||||
Any,
|
||||
)
|
||||
import os
|
||||
from abc import abstractmethod
|
||||
from io import StringIO
|
||||
from array import array
|
||||
from struct import pack
|
||||
import zlib
|
||||
|
||||
END_STYLE_BUTT = 0
|
||||
END_STYLE_SQUARE = 1
|
||||
END_STYLE_ROUND = 2
|
||||
END_STYLE_DIAMOND = 3
|
||||
END_STYLE_OBJECT = 4
|
||||
|
||||
JOIN_STYLE_MITER = 0
|
||||
JOIN_STYLE_BEVEL = 1
|
||||
JOIN_STYLE_ROUND = 2
|
||||
JOIN_STYLE_DIAMOND = 3
|
||||
JOIN_STYLE_OBJECT = 5
|
||||
|
||||
FILL_STYLE_SOLID = 64
|
||||
FILL_STYLE_CHECKERBOARD = 65
|
||||
FILL_STYLE_CROSSHATCH = 66
|
||||
FILL_STYLE_DIAMONDS = 67
|
||||
FILL_STYLE_HORIZONTAL_BARS = 68
|
||||
FILL_STYLE_SLANT_LEFT = 69
|
||||
FILL_STYLE_SLANT_RIGHT = 70
|
||||
FILL_STYLE_SQUARE_DOTS = 71
|
||||
FILL_STYLE_VERICAL_BARS = 72
|
||||
FILL_STYLE_OBJECT = 73
|
||||
|
||||
DITHERING_ON = 1 # bit coded color_policy
|
||||
GRAYSCALE_ON = 2 # bit coded color_policy
|
||||
NAMED_COLOR = 4 # bit coded color_policy
|
||||
|
||||
AUTOMATIC = 0
|
||||
OBJECT_LINEWEIGHT = 0
|
||||
OBJECT_LINETYPE = 31
|
||||
OBJECT_COLOR = -1
|
||||
OBJECT_COLOR2 = -1006632961
|
||||
|
||||
STYLE_COUNT = 255
|
||||
|
||||
DEFAULT_LINE_WEIGHTS = [
|
||||
0.00, # 0
|
||||
0.05, # 1
|
||||
0.09, # 2
|
||||
0.10, # 3
|
||||
0.13, # 4
|
||||
0.15, # 5
|
||||
0.18, # 6
|
||||
0.20, # 7
|
||||
0.25, # 8
|
||||
0.30, # 9
|
||||
0.35, # 10
|
||||
0.40, # 11
|
||||
0.45, # 12
|
||||
0.50, # 13
|
||||
0.53, # 14
|
||||
0.60, # 15
|
||||
0.65, # 16
|
||||
0.70, # 17
|
||||
0.80, # 18
|
||||
0.90, # 19
|
||||
1.00, # 20
|
||||
1.06, # 21
|
||||
1.20, # 22
|
||||
1.40, # 23
|
||||
1.58, # 24
|
||||
2.00, # 25
|
||||
2.11, # 26
|
||||
]
|
||||
|
||||
# color_type: (thx to Rammi)
|
||||
|
||||
# Take color from layer, ignore other bytes.
|
||||
COLOR_BY_LAYER = 0xC0
|
||||
|
||||
# Take color from insertion, ignore other bytes
|
||||
COLOR_BY_BLOCK = 0xC1
|
||||
|
||||
# RGB value, other bytes are R,G,B.
|
||||
COLOR_RGB = 0xC2
|
||||
|
||||
# ACI, AutoCAD color index, other bytes are 0,0,index ???
|
||||
COLOR_ACI = 0xC3
|
||||
|
||||
|
||||
def color_name(index: int) -> str:
|
||||
return "Color_%d" % (index + 1)
|
||||
|
||||
|
||||
def get_bool(value: Union[str, bool]) -> bool:
|
||||
if isinstance(value, str):
|
||||
upperstr = value.upper()
|
||||
if upperstr == "TRUE":
|
||||
value = True
|
||||
elif upperstr == "FALSE":
|
||||
value = False
|
||||
else:
|
||||
raise ValueError("Unknown bool value '%s'." % str(value))
|
||||
return value
|
||||
|
||||
|
||||
class PlotStyle:
|
||||
def __init__(
|
||||
self,
|
||||
index: int,
|
||||
data: Optional[dict] = None,
|
||||
parent: Optional[PlotStyleTable] = None,
|
||||
):
|
||||
data = data or {}
|
||||
self.parent = parent
|
||||
self.index = int(index)
|
||||
self.name = str(data.get("name", color_name(index)))
|
||||
self.localized_name = str(data.get("localized_name", color_name(index)))
|
||||
self.description = str(data.get("description", ""))
|
||||
# do not set _color, _mode_color or _color_policy directly
|
||||
# use set_color() method, and the properties dithering and grayscale
|
||||
self._color = int(data.get("color", OBJECT_COLOR))
|
||||
self._color_type = COLOR_RGB
|
||||
if self._color != OBJECT_COLOR:
|
||||
self._mode_color = int(data.get("mode_color", self._color))
|
||||
self._color_policy = int(data.get("color_policy", DITHERING_ON))
|
||||
self.physical_pen_number = int(data.get("physical_pen_number", AUTOMATIC))
|
||||
self.virtual_pen_number = int(data.get("virtual_pen_number", AUTOMATIC))
|
||||
self.screen = int(data.get("screen", 100))
|
||||
self.linepattern_size = float(data.get("linepattern_size", 0.5))
|
||||
self.linetype = int(data.get("linetype", OBJECT_LINETYPE)) # 0 .. 30
|
||||
self.adaptive_linetype = get_bool(data.get("adaptive_linetype", True))
|
||||
|
||||
# lineweight index
|
||||
self.lineweight = int(data.get("lineweight", OBJECT_LINEWEIGHT))
|
||||
self.end_style = int(data.get("end_style", END_STYLE_OBJECT))
|
||||
self.join_style = int(data.get("join_style", JOIN_STYLE_OBJECT))
|
||||
self.fill_style = int(data.get("fill_style", FILL_STYLE_OBJECT))
|
||||
|
||||
@property
|
||||
def color(self) -> Optional[tuple[int, int, int]]:
|
||||
"""Get style color as ``(r, g, b)`` tuple or ``None``, if style has
|
||||
object color.
|
||||
"""
|
||||
if self.has_object_color():
|
||||
return None # object color
|
||||
else:
|
||||
return int2color(self._mode_color)[:3]
|
||||
|
||||
@color.setter
|
||||
def color(self, rgb: tuple[int, int, int]) -> None:
|
||||
"""Set color as RGB values."""
|
||||
r, g, b = rgb
|
||||
# when defining a user-color, `mode_color` represents the real
|
||||
# true_color as (r, g, b) tuple and color_type = COLOR_RGB (0xC2) as
|
||||
# highest byte, the `color` value calculated for a user-color is not a
|
||||
# (r, g, b) tuple and has color_type = COLOR_ACI (0xC3) (sometimes), set
|
||||
# for `color` the same value as for `mode_color`, because AutoCAD
|
||||
# corrects the `color` value by itself.
|
||||
self._mode_color = mode_color2int(r, g, b, color_type=self._color_type)
|
||||
self._color = self._mode_color
|
||||
|
||||
@property
|
||||
def color_type(self):
|
||||
if self.has_object_color():
|
||||
return None # object color
|
||||
else:
|
||||
return self._color_type
|
||||
|
||||
@color_type.setter
|
||||
def color_type(self, value: int):
|
||||
self._color_type = value
|
||||
|
||||
def set_object_color(self) -> None:
|
||||
"""Set color to object color."""
|
||||
self._color = OBJECT_COLOR
|
||||
self._mode_color = OBJECT_COLOR
|
||||
|
||||
def set_lineweight(self, lineweight: float) -> None:
|
||||
"""Set `lineweight` in millimeters. Use ``0.0`` to set lineweight by
|
||||
object.
|
||||
"""
|
||||
assert self.parent is not None
|
||||
self.lineweight = self.parent.get_lineweight_index(lineweight)
|
||||
|
||||
def get_lineweight(self) -> float:
|
||||
"""Returns the lineweight in millimeters or `0.0` for use entity
|
||||
lineweight.
|
||||
"""
|
||||
assert self.parent is not None
|
||||
return self.parent.lineweights[self.lineweight]
|
||||
|
||||
def has_object_color(self) -> bool:
|
||||
"""``True`` if style has object color."""
|
||||
return self._color in (OBJECT_COLOR, OBJECT_COLOR2)
|
||||
|
||||
@property
|
||||
def aci(self) -> int:
|
||||
""":ref:`ACI` in range from ``1`` to ``255``. Has no meaning for named
|
||||
plot styles. (int)
|
||||
"""
|
||||
return self.index + 1
|
||||
|
||||
@property
|
||||
def dithering(self) -> bool:
|
||||
"""Depending on the capabilities of your plotter, dithering approximates
|
||||
the colors with dot patterns. When this option is ``False``, the colors
|
||||
are mapped to the nearest color, resulting in a smaller range of
|
||||
colors when plotting.
|
||||
|
||||
Dithering is available only whether you select the object’s color or
|
||||
assign a plot style color.
|
||||
|
||||
"""
|
||||
return bool(self._color_policy & DITHERING_ON)
|
||||
|
||||
@dithering.setter
|
||||
def dithering(self, status: bool) -> None:
|
||||
if status:
|
||||
self._color_policy |= DITHERING_ON
|
||||
else:
|
||||
self._color_policy &= ~DITHERING_ON
|
||||
|
||||
@property
|
||||
def grayscale(self) -> bool:
|
||||
"""Plot colors in grayscale. (bool)"""
|
||||
return bool(self._color_policy & GRAYSCALE_ON)
|
||||
|
||||
@grayscale.setter
|
||||
def grayscale(self, status: bool) -> None:
|
||||
if status:
|
||||
self._color_policy |= GRAYSCALE_ON
|
||||
else:
|
||||
self._color_policy &= ~GRAYSCALE_ON
|
||||
|
||||
@property
|
||||
def named_color(self) -> bool:
|
||||
return bool(self._color_policy & NAMED_COLOR)
|
||||
|
||||
@named_color.setter
|
||||
def named_color(self, status: bool) -> None:
|
||||
if status:
|
||||
self._color_policy |= NAMED_COLOR
|
||||
else:
|
||||
self._color_policy &= ~NAMED_COLOR
|
||||
|
||||
def write(self, stream: TextIO) -> None:
|
||||
"""Write style data to file-like object `stream`."""
|
||||
index = self.index
|
||||
stream.write(" %d{\n" % index)
|
||||
stream.write(' name="%s\n' % self.name)
|
||||
stream.write(' localized_name="%s\n' % self.localized_name)
|
||||
stream.write(' description="%s\n' % self.description)
|
||||
stream.write(" color=%d\n" % self._color)
|
||||
if self._color != OBJECT_COLOR:
|
||||
stream.write(" mode_color=%d\n" % self._mode_color)
|
||||
stream.write(" color_policy=%d\n" % self._color_policy)
|
||||
stream.write(" physical_pen_number=%d\n" % self.physical_pen_number)
|
||||
stream.write(" virtual_pen_number=%d\n" % self.virtual_pen_number)
|
||||
stream.write(" screen=%d\n" % self.screen)
|
||||
stream.write(" linepattern_size=%s\n" % str(self.linepattern_size))
|
||||
stream.write(" linetype=%d\n" % self.linetype)
|
||||
stream.write(
|
||||
" adaptive_linetype=%s\n" % str(bool(self.adaptive_linetype)).upper()
|
||||
)
|
||||
stream.write(" lineweight=%s\n" % str(self.lineweight))
|
||||
stream.write(" fill_style=%d\n" % self.fill_style)
|
||||
stream.write(" end_style=%d\n" % self.end_style)
|
||||
stream.write(" join_style=%d\n" % self.join_style)
|
||||
stream.write(" }\n")
|
||||
|
||||
|
||||
class PlotStyleTable:
|
||||
"""PlotStyle container"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
description: str = "",
|
||||
scale_factor: float = 1.0,
|
||||
apply_factor: bool = False,
|
||||
):
|
||||
self.description = description
|
||||
self.scale_factor = scale_factor
|
||||
self.apply_factor = apply_factor
|
||||
|
||||
# set custom_lineweight_display_units to 1 for showing lineweight in inch in
|
||||
# AutoCAD CTB editor window, but lineweight is always defined in mm
|
||||
self.custom_lineweight_display_units = 0
|
||||
self.lineweights = array("f", DEFAULT_LINE_WEIGHTS)
|
||||
|
||||
def get_lineweight_index(self, lineweight: float) -> int:
|
||||
"""Get index of `lineweight` in the lineweight table or append
|
||||
`lineweight` to lineweight table.
|
||||
"""
|
||||
try:
|
||||
return self.lineweights.index(lineweight)
|
||||
except ValueError:
|
||||
self.lineweights.append(lineweight)
|
||||
return len(self.lineweights) - 1
|
||||
|
||||
def set_table_lineweight(self, index: int, lineweight: float) -> int:
|
||||
"""Argument `index` is the lineweight table index, not the :ref:`ACI`.
|
||||
|
||||
Args:
|
||||
index: lineweight table index = :attr:`PlotStyle.lineweight`
|
||||
lineweight: in millimeters
|
||||
|
||||
"""
|
||||
try:
|
||||
self.lineweights[index] = lineweight
|
||||
return index
|
||||
except IndexError:
|
||||
self.lineweights.append(lineweight)
|
||||
return len(self.lineweights) - 1
|
||||
|
||||
def get_table_lineweight(self, index: int) -> float:
|
||||
"""Returns lineweight in millimeters of lineweight table entry `index`.
|
||||
|
||||
Args:
|
||||
index: lineweight table index = :attr:`PlotStyle.lineweight`
|
||||
|
||||
Returns:
|
||||
lineweight in mm or ``0.0`` for use entity lineweight
|
||||
|
||||
"""
|
||||
return self.lineweights[index]
|
||||
|
||||
def save(self, filename: str | os.PathLike) -> None:
|
||||
"""Save CTB or STB file as `filename` to the file system."""
|
||||
with open(filename, "wb") as stream:
|
||||
self.write(stream)
|
||||
|
||||
def write(self, stream: BinaryIO) -> None:
|
||||
"""Compress and write the CTB or STB file to binary `stream`."""
|
||||
memfile = StringIO()
|
||||
self.write_content(memfile)
|
||||
memfile.write(chr(0)) # end of file
|
||||
body = memfile.getvalue()
|
||||
memfile.close()
|
||||
_compress(stream, body)
|
||||
|
||||
@abstractmethod
|
||||
def write_content(self, stream: TextIO) -> None:
|
||||
pass
|
||||
|
||||
def _write_lineweights(self, stream: TextIO) -> None:
|
||||
"""Write custom lineweight table to text `stream`."""
|
||||
stream.write("custom_lineweight_table{\n")
|
||||
for index, weight in enumerate(self.lineweights):
|
||||
stream.write(" %d=%.2f\n" % (index, weight))
|
||||
stream.write("}\n")
|
||||
|
||||
def parse(self, text: str) -> None:
|
||||
"""Parse plot styles from CTB string `text`."""
|
||||
|
||||
def set_lineweights(lineweights):
|
||||
if lineweights is None:
|
||||
return
|
||||
self.lineweights = array("f", [0.0] * len(lineweights))
|
||||
for key, value in lineweights.items():
|
||||
self.lineweights[int(key)] = float(value)
|
||||
|
||||
parser = PlotStyleFileParser(text)
|
||||
self.description = parser.get("description", "")
|
||||
self.scale_factor = float(parser.get("scale_factor", 1.0))
|
||||
self.apply_factor = get_bool(parser.get("apply_factor", True))
|
||||
self.custom_lineweight_display_units = int(
|
||||
parser.get("custom_lineweight_display_units", 0)
|
||||
)
|
||||
set_lineweights(parser.get("custom_lineweight_table", None))
|
||||
self.load_styles(parser.get("plot_style", {}))
|
||||
|
||||
@abstractmethod
|
||||
def load_styles(self, styles):
|
||||
pass
|
||||
|
||||
|
||||
class ColorDependentPlotStyles(PlotStyleTable):
|
||||
def __init__(
|
||||
self,
|
||||
description: str = "",
|
||||
scale_factor: float = 1.0,
|
||||
apply_factor: bool = False,
|
||||
):
|
||||
super().__init__(description, scale_factor, apply_factor)
|
||||
self._styles: list[PlotStyle] = [
|
||||
PlotStyle(index, parent=self) for index in range(STYLE_COUNT)
|
||||
]
|
||||
self._styles.insert(
|
||||
0, PlotStyle(256)
|
||||
) # 1-based array: insert dummy value for index 0
|
||||
|
||||
def __getitem__(self, aci: int) -> PlotStyle:
|
||||
"""Returns :class:`PlotStyle` for :ref:`ACI` `aci`."""
|
||||
if 0 < aci < 256:
|
||||
return self._styles[aci]
|
||||
else:
|
||||
raise IndexError(aci)
|
||||
|
||||
def __setitem__(self, aci: int, style: PlotStyle):
|
||||
"""Set plot `style` for `aci`."""
|
||||
if 0 < aci < 256:
|
||||
style.parent = self
|
||||
self._styles[aci] = style
|
||||
else:
|
||||
raise IndexError(aci)
|
||||
|
||||
def __iter__(self):
|
||||
"""Iterable of all plot styles."""
|
||||
return iter(self._styles[1:])
|
||||
|
||||
def new_style(self, aci: int, data: Optional[dict] = None) -> PlotStyle:
|
||||
"""Set `aci` to new attributes defined by `data` dict.
|
||||
|
||||
Args:
|
||||
aci: :ref:`ACI`
|
||||
data: ``dict`` of :class:`PlotStyle` attributes: description, color,
|
||||
physical_pen_number, virtual_pen_number, screen,
|
||||
linepattern_size, linetype, adaptive_linetype,
|
||||
lineweight, end_style, join_style, fill_style
|
||||
|
||||
"""
|
||||
# ctb table index = aci - 1
|
||||
# ctb table starts with index 0, where aci == 0 means BYBLOCK
|
||||
style = PlotStyle(index=aci - 1, data=data)
|
||||
style.color_type = COLOR_RGB
|
||||
self[aci] = style
|
||||
return style
|
||||
|
||||
def get_lineweight(self, aci: int):
|
||||
"""Returns the assigned lineweight for :class:`PlotStyle` `aci` in
|
||||
millimeter.
|
||||
"""
|
||||
style = self[aci]
|
||||
lineweight = style.get_lineweight()
|
||||
if lineweight == 0.0:
|
||||
return None
|
||||
else:
|
||||
return lineweight
|
||||
|
||||
def write_content(self, stream: TextIO) -> None:
|
||||
"""Write the CTB-file to text `stream`."""
|
||||
self._write_header(stream)
|
||||
self._write_aci_table(stream)
|
||||
self._write_plot_styles(stream)
|
||||
self._write_lineweights(stream)
|
||||
|
||||
def _write_header(self, stream: TextIO) -> None:
|
||||
"""Write header values of CTB-file to text `stream`."""
|
||||
stream.write('description="%s\n' % self.description)
|
||||
stream.write("aci_table_available=TRUE\n")
|
||||
stream.write("scale_factor=%.1f\n" % self.scale_factor)
|
||||
stream.write("apply_factor=%s\n" % str(self.apply_factor).upper())
|
||||
stream.write(
|
||||
"custom_lineweight_display_units=%s\n"
|
||||
% str(self.custom_lineweight_display_units)
|
||||
)
|
||||
|
||||
def _write_aci_table(self, stream: TextIO) -> None:
|
||||
"""Write AutoCAD Color Index table to text `stream`."""
|
||||
stream.write("aci_table{\n")
|
||||
for style in self:
|
||||
index = style.index
|
||||
stream.write(' %d="%s\n' % (index, color_name(index)))
|
||||
stream.write("}\n")
|
||||
|
||||
def _write_plot_styles(self, stream: TextIO) -> None:
|
||||
"""Write user styles to text `stream`."""
|
||||
stream.write("plot_style{\n")
|
||||
for style in self:
|
||||
style.write(stream)
|
||||
stream.write("}\n")
|
||||
|
||||
def load_styles(self, styles):
|
||||
for index, style in styles.items():
|
||||
index = int(index)
|
||||
style = PlotStyle(index, style)
|
||||
style.color_type = COLOR_RGB
|
||||
aci = index + 1
|
||||
self[aci] = style
|
||||
|
||||
|
||||
class NamedPlotStyles(PlotStyleTable):
|
||||
def __init__(
|
||||
self,
|
||||
description: str = "",
|
||||
scale_factor: float = 1.0,
|
||||
apply_factor: bool = False,
|
||||
):
|
||||
super().__init__(description, scale_factor, apply_factor)
|
||||
normal = PlotStyle(
|
||||
0,
|
||||
data={
|
||||
"name": "Normal",
|
||||
"localized_name": "Normal",
|
||||
},
|
||||
)
|
||||
self._styles: dict[str, PlotStyle] = {"Normal": normal}
|
||||
|
||||
def __iter__(self) -> Iterable[str]:
|
||||
"""Iterable of all plot style names."""
|
||||
return self.keys()
|
||||
|
||||
def __getitem__(self, name: str) -> PlotStyle:
|
||||
"""Returns :class:`PlotStyle` by `name`."""
|
||||
return self._styles[name]
|
||||
|
||||
def __delitem__(self, name: str) -> None:
|
||||
"""Delete plot style `name`. Plot style ``'Normal'`` is not deletable."""
|
||||
if name != "Normal":
|
||||
del self._styles[name]
|
||||
else:
|
||||
raise ValueError("Can't delete plot style 'Normal'. ")
|
||||
|
||||
def keys(self) -> Iterable[str]:
|
||||
"""Iterable of all plot style names."""
|
||||
keys = set(self._styles.keys())
|
||||
keys.discard("Normal")
|
||||
result = ["Normal"]
|
||||
result.extend(sorted(keys))
|
||||
return iter(result)
|
||||
|
||||
def items(self) -> Iterator[tuple[str, PlotStyle]]:
|
||||
"""Iterable of all plot styles as (``name``, class:`PlotStyle`) tuples."""
|
||||
for key in self.keys():
|
||||
yield key, self._styles[key]
|
||||
|
||||
def values(self) -> Iterable[PlotStyle]:
|
||||
"""Iterable of all class:`PlotStyle` objects."""
|
||||
for key, value in self.items():
|
||||
yield value
|
||||
|
||||
def new_style(
|
||||
self,
|
||||
name: str,
|
||||
data: Optional[dict] = None,
|
||||
localized_name: Optional[str] = None,
|
||||
) -> PlotStyle:
|
||||
"""Create new class:`PlotStyle` `name` by attribute dict `data`, replaces
|
||||
existing class:`PlotStyle` objects.
|
||||
|
||||
Args:
|
||||
name: plot style name
|
||||
localized_name: name shown in plot style editor, uses `name` if ``None``
|
||||
data: ``dict`` of :class:`PlotStyle` attributes: description, color,
|
||||
physical_pen_number, virtual_pen_number, screen,
|
||||
linepattern_size, linetype, adaptive_linetype, lineweight,
|
||||
end_style, join_style, fill_style
|
||||
|
||||
"""
|
||||
if name.lower() == "Normal":
|
||||
raise ValueError("Can't replace or modify plot style 'Normal'. ")
|
||||
data = data or {}
|
||||
data["name"] = name
|
||||
data["localized_name"] = localized_name or name
|
||||
index = len(self._styles)
|
||||
style = PlotStyle(index=index, data=data, parent=self)
|
||||
style.color_type = COLOR_ACI
|
||||
style.named_color = True
|
||||
self._styles[name] = style
|
||||
return style
|
||||
|
||||
def get_lineweight(self, name: str):
|
||||
"""Returns the assigned lineweight for :class:`PlotStyle` `name` in
|
||||
millimeter.
|
||||
"""
|
||||
style = self[name]
|
||||
lineweight = style.get_lineweight()
|
||||
if lineweight == 0.0:
|
||||
return None
|
||||
else:
|
||||
return lineweight
|
||||
|
||||
def write_content(self, stream: TextIO) -> None:
|
||||
"""Write the STB-file to text `stream`."""
|
||||
self._write_header(stream)
|
||||
self._write_plot_styles(stream)
|
||||
self._write_lineweights(stream)
|
||||
|
||||
def _write_header(self, stream: TextIO) -> None:
|
||||
"""Write header values of CTB-file to text `stream`."""
|
||||
stream.write('description="%s\n' % self.description)
|
||||
stream.write("aci_table_available=FALSE\n")
|
||||
stream.write("scale_factor=%.1f\n" % self.scale_factor)
|
||||
stream.write("apply_factor=%s\n" % str(self.apply_factor).upper())
|
||||
stream.write(
|
||||
"custom_lineweight_display_units=%s\n"
|
||||
% str(self.custom_lineweight_display_units)
|
||||
)
|
||||
|
||||
def _write_plot_styles(self, stream: TextIO) -> None:
|
||||
"""Write user styles to text `stream`."""
|
||||
stream.write("plot_style{\n")
|
||||
for index, style in enumerate(self.values()):
|
||||
style.index = index
|
||||
style.write(stream)
|
||||
stream.write("}\n")
|
||||
|
||||
def load_styles(self, styles):
|
||||
for index, style in styles.items():
|
||||
index = int(index)
|
||||
style = PlotStyle(index, style)
|
||||
style.color_type = COLOR_ACI
|
||||
self._styles[style.name] = style
|
||||
|
||||
|
||||
def _read_ctb(stream: BinaryIO) -> ColorDependentPlotStyles:
|
||||
"""Read a CTB-file from binary `stream`."""
|
||||
content: bytes = _decompress(stream)
|
||||
styles = ColorDependentPlotStyles()
|
||||
styles.parse(content.decode())
|
||||
return styles
|
||||
|
||||
|
||||
def _read_stb(stream: BinaryIO) -> NamedPlotStyles:
|
||||
"""Read a STB-file from binary `stream`."""
|
||||
content: bytes = _decompress(stream)
|
||||
styles = NamedPlotStyles()
|
||||
styles.parse(content.decode())
|
||||
return styles
|
||||
|
||||
|
||||
def load(
|
||||
filename: str | os.PathLike,
|
||||
) -> Union[ColorDependentPlotStyles, NamedPlotStyles]:
|
||||
"""Load the CTB or STB file `filename` from file system."""
|
||||
filename = str(filename)
|
||||
with open(filename, "rb") as stream:
|
||||
if filename.lower().endswith(".ctb"):
|
||||
return _read_ctb(stream)
|
||||
elif filename.lower().endswith(".stb"):
|
||||
return _read_stb(stream)
|
||||
else:
|
||||
raise ValueError('Invalid file type: "{}"'.format(filename))
|
||||
|
||||
|
||||
def new_ctb() -> ColorDependentPlotStyles:
|
||||
"""Create a new CTB file."""
|
||||
return ColorDependentPlotStyles()
|
||||
|
||||
|
||||
def new_stb() -> NamedPlotStyles:
|
||||
"""Create a new STB file."""
|
||||
return NamedPlotStyles()
|
||||
|
||||
|
||||
def _decompress(stream: BinaryIO) -> bytes:
|
||||
"""Read and decompress the file content from binray `stream`."""
|
||||
content = stream.read()
|
||||
data = zlib.decompress(content[60:]) # type: bytes
|
||||
return data[:-1] # truncate trailing \nul
|
||||
|
||||
|
||||
def _compress(stream: BinaryIO, content: str):
|
||||
"""Compress `content` and write to binary `stream`."""
|
||||
comp_body = zlib.compress(content.encode())
|
||||
adler_chksum = zlib.adler32(comp_body)
|
||||
stream.write(b"PIAFILEVERSION_2.0,CTBVER1,compress\r\npmzlibcodec")
|
||||
stream.write(pack("LLL", adler_chksum, len(content), len(comp_body)))
|
||||
stream.write(comp_body)
|
||||
|
||||
|
||||
class PlotStyleFileParser:
|
||||
"""A very simple CTB/STB file parser. CTB/STB files are created by
|
||||
applications, so the file structure should be correct in the most cases.
|
||||
"""
|
||||
|
||||
def __init__(self, text: str):
|
||||
self.data = {}
|
||||
for element, value in PlotStyleFileParser.iteritems(text):
|
||||
self.data[element] = value
|
||||
|
||||
@staticmethod
|
||||
def iteritems(text: str):
|
||||
"""Iterate over all first level (start at col 0) elements."""
|
||||
line_index = 0
|
||||
|
||||
def get_name() -> str:
|
||||
"""Get element name of line <line_index>."""
|
||||
line = lines[line_index]
|
||||
if line.endswith("{"): # start of a list like 'plot_style{'
|
||||
name = line[:-1]
|
||||
else: # simple name=value line
|
||||
name = line.split("=", 1)[0]
|
||||
return name.strip()
|
||||
|
||||
def get_mapping() -> dict:
|
||||
"""Get mapping of elements enclosed by { }.
|
||||
|
||||
e. g. lineweights, plot_styles, aci_table
|
||||
|
||||
"""
|
||||
|
||||
def end_of_list():
|
||||
return lines[line_index].endswith("}")
|
||||
|
||||
nonlocal line_index
|
||||
data = dict()
|
||||
while not end_of_list():
|
||||
name = get_name()
|
||||
value = get_value() # get value or sub-list
|
||||
data[name] = value
|
||||
line_index += 1
|
||||
return data # skip '}' - end of list
|
||||
|
||||
def get_value() -> Union[str, dict]:
|
||||
"""Get value of line <line_index> or the list that starts in line
|
||||
<line_index>.
|
||||
"""
|
||||
nonlocal line_index
|
||||
line = lines[line_index]
|
||||
if line.endswith("{"): # start of a list
|
||||
line_index += 1
|
||||
return get_mapping()
|
||||
else: # it's a simple name=value line
|
||||
value: str = line.split("=", 1)[1]
|
||||
value = sanitized_value(value)
|
||||
line_index += 1
|
||||
return value
|
||||
|
||||
def skip_empty_lines():
|
||||
nonlocal line_index
|
||||
while line_index < len(lines) and len(lines[line_index]) == 0:
|
||||
line_index += 1
|
||||
|
||||
lines = text.split("\n")
|
||||
while line_index < len(lines):
|
||||
name = get_name()
|
||||
value = get_value()
|
||||
yield name, value
|
||||
skip_empty_lines()
|
||||
|
||||
def get(self, name: str, default: Any) -> Any:
|
||||
return self.data.get(name, default)
|
||||
|
||||
|
||||
def sanitized_value(value: str) -> str:
|
||||
value = value.strip()
|
||||
if value.startswith('"'): # strings: <name>="string
|
||||
return value[1:]
|
||||
|
||||
# remove unknown appendix like this: "0.0076200000000 (+7.Z+"8V?S_LC )"
|
||||
# the pattern is "<float|int> (<some data>)", see issue #1069
|
||||
if value.endswith(")"):
|
||||
return value.split(" ")[0]
|
||||
return value
|
||||
|
||||
|
||||
def int2color(color: int) -> tuple[int, int, int, int]:
|
||||
"""Convert color integer value from CTB-file to ``(r, g, b, color_type)
|
||||
tuple.
|
||||
"""
|
||||
# Take color from layer, ignore other bytes.
|
||||
color_type = (color & 0xFF000000) >> 24
|
||||
red = (color & 0xFF0000) >> 16
|
||||
green = (color & 0xFF00) >> 8
|
||||
blue = color & 0xFF
|
||||
return red, green, blue, color_type
|
||||
|
||||
|
||||
def mode_color2int(red: int, green: int, blue: int, color_type=COLOR_RGB) -> int:
|
||||
"""Convert mode_color (r, g, b, color_type) tuple to integer."""
|
||||
return -color2int(red, green, blue, color_type)
|
||||
|
||||
|
||||
def color2int(red: int, green: int, blue: int, color_type: int) -> int:
|
||||
"""Convert color (r, g, b, color_type) to integer."""
|
||||
return -((color_type << 24) + (red << 16) + (green << 8) + blue) & 0xFFFFFFFF
|
||||
Reference in New Issue
Block a user