acadctb.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612
  1. #!/usr/bin/env python
  2. # coding:utf-8
  3. # Purpose: read, create and write acad ctb files
  4. # Created: 23.03.2010 for dxfwrite, added to ezdxf package on 2016-03-06
  5. # Copyright (C) 2010, Manfred Moitzi
  6. # License: MIT License
  7. # IMPORTANT: use only standard 7-Bit ascii code
  8. from typing import Union, Tuple, Optional, BinaryIO, TextIO, Iterable, List, Any
  9. from io import StringIO
  10. from array import array
  11. from struct import pack
  12. import zlib
  13. ENDSTYLE_BUTT = 0
  14. ENDSTYLE_SQUARE = 1
  15. ENDSTYLE_ROUND = 2
  16. ENDSTYLE_DIAMOND = 3
  17. ENDSTYLE_OBJECT = 4
  18. JOINSTYLE_MITER = 0
  19. JOINSTYLE_BEVEL = 1
  20. JOINSTYLE_ROUND = 2
  21. JOINSTYLE_DIAMOND = 3
  22. JOINSTYLE_OBJECT = 5
  23. FILL_STYLE_SOLID = 64
  24. FILL_STYLE_CHECKERBOARD = 65
  25. FILL_STYLE_CROSSHATCH = 66
  26. FILL_STYLE_DIAMONDS = 67
  27. FILL_STYLE_HORIZONTAL_BARS = 68
  28. FILL_STYLE_SLANT_LEFT = 69
  29. FILL_STYLE_SLANT_RIGHT = 70
  30. FILL_STYLE_SQUARE_DOTS = 71
  31. FILL_STYLE_VERICAL_BARS = 72
  32. FILL_STYLE_OBJECT = 73
  33. DITHERING_ON = 1 # bit coded color_policy
  34. GRAYSCALE_ON = 2 # bit coded color_policy
  35. AUTOMATIC = 0
  36. OBJECT_LINEWEIGHT = 0
  37. OBJECT_LINETYPE = 31
  38. OBJECT_COLOR = -1
  39. OBJECT_COLOR2 = -1006632961
  40. STYLE_COUNT = 255
  41. DEFAULT_LINE_WEIGHTS = [
  42. 0.00, # 0
  43. 0.05, # 1
  44. 0.09, # 2
  45. 0.10, # 3
  46. 0.13, # 4
  47. 0.15, # 5
  48. 0.18, # 6
  49. 0.20, # 7
  50. 0.25, # 8
  51. 0.30, # 9
  52. 0.35, # 10
  53. 0.40, # 11
  54. 0.45, # 12
  55. 0.50, # 13
  56. 0.53, # 14
  57. 0.60, # 15
  58. 0.65, # 16
  59. 0.70, # 17
  60. 0.80, # 18
  61. 0.90, # 19
  62. 1.00, # 20
  63. 1.06, # 21
  64. 1.20, # 22
  65. 1.40, # 23
  66. 1.58, # 24
  67. 2.00, # 25
  68. 2.11, # 26
  69. ]
  70. def color_name(index: int) -> str:
  71. return 'Color_%d' % (index + 1)
  72. def get_bool(value: Union[str, bool]) -> bool:
  73. if isinstance(value, str):
  74. upperstr = value.upper()
  75. if upperstr == 'TRUE':
  76. value = True
  77. elif upperstr == 'FALSE':
  78. value = False
  79. else:
  80. raise ValueError("Unknown bool value '%s'." % str(value))
  81. return value
  82. class UserStyle:
  83. def __init__(self, index: int, data: dict = None, parent: 'UserStyles' = None):
  84. data = data or {}
  85. self.parent = parent
  86. self.index = int(index)
  87. self.description = str(data.get('description', ""))
  88. # do not set _color, _mode_color or _color_policy directly
  89. # use set_color() method, and the properties dithering and grayscale
  90. self._color = int(data.get('color', OBJECT_COLOR))
  91. if self._color != OBJECT_COLOR:
  92. self._mode_color = int(data.get('mode_color', self._color))
  93. self._color_policy = int(data.get('color_policy', DITHERING_ON))
  94. self.physical_pen_number = int(data.get('physical_pen_number', AUTOMATIC))
  95. self.virtual_pen_number = int(data.get('virtual_pen_number', AUTOMATIC))
  96. self.screen = int(data.get('screen', 100))
  97. self.linepattern_size = float(data.get('linepattern_size', 0.5))
  98. self.linetype = int(data.get('linetype', OBJECT_LINETYPE)) # 0 .. 30
  99. self.adaptive_linetype = get_bool(data.get('adaptive_linetype', True))
  100. self.lineweight = int(data.get('lineweight', OBJECT_LINEWEIGHT))
  101. self.end_style = int(data.get('end_style', ENDSTYLE_OBJECT))
  102. self.join_style = int(data.get('join_style', JOINSTYLE_OBJECT))
  103. self.fill_style = int(data.get('fill_style', FILL_STYLE_OBJECT))
  104. def set_color(self, red: int, green: int, blue: int) -> None:
  105. """
  106. Set color as rgb-tuple.
  107. """
  108. self._mode_color = mode_color2int(red, green, blue)
  109. # when defining a user-color, <mode_color> represents the real truecolor
  110. # as rgb-tuple with the magic number 0xC2 as highest byte, the <color>
  111. # value calculated for a user-color is not a rgb-tuple and has the magic
  112. # number 0xC3 (sometimes), I set for <color> the same value a for
  113. # <mode_color>, because Autocad corrects the <color> value by itself.
  114. self._color = self._mode_color
  115. def set_object_color(self) -> None:
  116. """
  117. Set color to object color.
  118. """
  119. self._color = OBJECT_COLOR
  120. self._mode_color = OBJECT_COLOR
  121. def set_lineweight(self, lineweight: float) -> None:
  122. """Set lineweight. Use 0.0 to set lineweight by object.
  123. lineweight in mm! not the lineweight index
  124. """
  125. self.lineweight = self.parent.get_lineweight_index(lineweight)
  126. def get_lineweight(self) -> float:
  127. """
  128. Returns the lineweight in millimeters.
  129. :returns: lineweight in mm or 0.0 for use entity lineweight
  130. """
  131. return self.parent.lineweights[self.lineweight]
  132. def has_object_color(self) -> bool:
  133. """
  134. True if style has object color.
  135. """
  136. return self._color == OBJECT_COLOR or \
  137. self._color == OBJECT_COLOR2
  138. def get_color(self) -> Optional[Tuple[int, int, int, int]]:
  139. """
  140. Get style color as rgb-tuple or None if style has object color.
  141. """
  142. if self.has_object_color():
  143. return None # object color
  144. else:
  145. return int2color(self._mode_color)[:3]
  146. def get_dxf_color_index(self) -> int:
  147. return self.index + 1
  148. def get_dithering(self) -> bool:
  149. return bool(self._color_policy & DITHERING_ON)
  150. def set_dithering(self, status: bool) -> None:
  151. if status:
  152. self._color_policy |= DITHERING_ON
  153. else:
  154. self._color_policy &= ~DITHERING_ON
  155. dithering = property(get_dithering, set_dithering)
  156. def get_grayscale(self) -> bool:
  157. return bool(self._color_policy & GRAYSCALE_ON)
  158. def set_grayscale(self, status: bool) -> None:
  159. if status:
  160. self._color_policy |= GRAYSCALE_ON
  161. else:
  162. self._color_policy &= ~GRAYSCALE_ON
  163. grayscale = property(get_grayscale, set_grayscale)
  164. def write(self, stream: TextIO) -> None:
  165. """
  166. Write style data to file-like object <stream>.
  167. """
  168. index = self.index
  169. stream.write(' %d{\n' % index)
  170. stream.write(' name="%s\n' % color_name(index))
  171. stream.write(' localized_name="%s\n' % color_name(index))
  172. stream.write(' description="%s\n' % self.description)
  173. stream.write(' color=%d\n' % self._color)
  174. if self._color != OBJECT_COLOR:
  175. stream.write(' mode_color=%d\n' % self._mode_color)
  176. stream.write(' color_policy=%d\n' % self._color_policy)
  177. stream.write(' physical_pen_number=%d\n' % self.physical_pen_number)
  178. stream.write(' virtual_pen_number=%d\n' % self.virtual_pen_number)
  179. stream.write(' screen=%d\n' % self.screen)
  180. stream.write(' linepattern_size=%s\n' % str(self.linepattern_size))
  181. stream.write(' linetype=%d\n' % self.linetype)
  182. stream.write(' adaptive_linetype=%s\n' % str(bool(self.adaptive_linetype)).upper())
  183. stream.write(' lineweight=%s\n' % str(self.lineweight))
  184. stream.write(' fill_style=%d\n' % self.fill_style)
  185. stream.write(' end_style=%d\n' % self.end_style)
  186. stream.write(' join_style=%d\n' % self.join_style)
  187. stream.write(' }\n')
  188. class UserStyles:
  189. """
  190. UserStyle container
  191. """
  192. def __init__(self, description: str = "", scale_factor: float = 1.0, apply_factor: bool = False):
  193. self.description = description
  194. self.scale_factor = scale_factor
  195. self.apply_factor = apply_factor
  196. # set custom_line... to 1 for showing lineweights in inch in the Autocad
  197. # ctb editor window, but lineweights are always defined in mm
  198. self.custom_lineweight_display_units = 0
  199. self.styles = [None] * (STYLE_COUNT + 1) # type: List[UserStyle]
  200. self.lineweights = array('f', DEFAULT_LINE_WEIGHTS)
  201. self.set_default_styles()
  202. def set_default_styles(self) -> None:
  203. for index in range(STYLE_COUNT):
  204. self._set_style(UserStyle(index))
  205. @staticmethod
  206. def check_color_index(dxf_color_index: int) -> int:
  207. if 0 < dxf_color_index < 256:
  208. return dxf_color_index
  209. raise IndexError('color index has to be in the range [1 .. 255].')
  210. def iter_styles(self) -> Iterable[UserStyle]:
  211. return (style for style in self.styles[1:])
  212. def _set_style(self, style: UserStyle) -> None:
  213. style.parent = self
  214. self.styles[style.get_dxf_color_index()] = style
  215. def set_style(self, dxf_color_index: int, data: dict = None) -> UserStyle:
  216. """
  217. Set <dxf_color_index> to new attributes defined in init_dict.
  218. """
  219. dxf_color_index = self.check_color_index(dxf_color_index)
  220. # ctb table index is dxf_color_index - 1
  221. # ctb table starts with index 0, where dxf_color_index=0 means BYBLOCK
  222. style = UserStyle(dxf_color_index - 1, data)
  223. self._set_style(style)
  224. return style
  225. def get_style(self, dxf_color_index: int) -> UserStyle:
  226. """
  227. Get style for <dxf_color_index>.
  228. """
  229. dxf_color_index = self.check_color_index(dxf_color_index)
  230. return self.styles[dxf_color_index]
  231. def get_color(self, dxf_color_index: int) -> Optional[Tuple[int, int, int, int]]:
  232. """
  233. Get rgb-color-tuple for <dxf_color_index> or None if not specified.
  234. """
  235. style = self.get_style(dxf_color_index)
  236. return style.get_color()
  237. def get_lineweight(self, dxf_color_index: int):
  238. """
  239. Returns the assigned lineweight for <dxf_color_index> in mm.
  240. """
  241. style = self.get_style(dxf_color_index)
  242. lineweight = style.get_lineweight()
  243. if lineweight == 0.0:
  244. return None
  245. else:
  246. return lineweight
  247. def get_lineweight_index(self, lineweight: float) -> int:
  248. """
  249. Get index of lineweight in the lineweight table or append lineweight to lineweight table.
  250. """
  251. try:
  252. return self.lineweights.index(lineweight)
  253. except ValueError:
  254. self.lineweights.append(lineweight)
  255. return len(self.lineweights) - 1
  256. def set_table_lineweight(self, index: int, weight: float) -> int:
  257. """
  258. Index is the lineweight table index, not the dxf color index.
  259. :param int index: lineweight table index = UserStyle.lineweight
  260. :param float weight: in millimeters
  261. """
  262. try:
  263. self.lineweights[index] = weight
  264. return index
  265. except IndexError:
  266. self.lineweights.append(weight)
  267. return len(self.lineweights) - 1
  268. def get_table_lineweight(self, index: int) -> float:
  269. """
  270. Returns lineweight in millimeters.
  271. :param int index: lineweight table index = UserStyle.lineweight
  272. :returns: lineweight in mm or 0.0 for use entity lineweight
  273. """
  274. return self.lineweights[index]
  275. def save(self, filename: str) -> None:
  276. """
  277. Save ctb-file to <filename>.
  278. """
  279. with open(filename, 'wb') as stream:
  280. self.write(stream)
  281. def write(self, stream: BinaryIO):
  282. """
  283. Create and compress the ctb-file to <stream>.
  284. """
  285. memfile = StringIO()
  286. self.write_content(memfile)
  287. memfile.write(chr(0)) # end of file
  288. body = memfile.getvalue()
  289. memfile.close()
  290. self._compress(stream, body)
  291. def write_content(self, stream: TextIO) -> None:
  292. """
  293. Write the ctb-file to <fileobj>.
  294. """
  295. self._write_header(stream)
  296. self._write_aci_table(stream)
  297. self._write_ctb_plot_styles(stream)
  298. self._write_lineweights(stream)
  299. def _write_header(self, stream: TextIO) -> None:
  300. """
  301. Write header values of ctb-file to <stream>.
  302. """
  303. stream.write('description="%s\n' % self.description)
  304. stream.write('aci_table_available=TRUE\n')
  305. stream.write('scale_factor=%.1f\n' % self.scale_factor)
  306. stream.write('apply_factor=%s\n' % str(self.apply_factor).upper())
  307. stream.write('custom_lineweight_display_units=%s\n' % str(
  308. self.custom_lineweight_display_units))
  309. def _write_aci_table(self, stream: TextIO) -> None:
  310. """
  311. Write autocad color index table to ctb-file <stream>.
  312. """
  313. stream.write('aci_table{\n')
  314. for style in self.iter_styles():
  315. index = style.index
  316. stream.write(' %d="%s\n' % (index, color_name(index)))
  317. stream.write('}\n')
  318. def _write_ctb_plot_styles(self, stream: TextIO) -> None:
  319. """
  320. Write user styles to ctb-file <stream>.
  321. """
  322. stream.write('plot_style{\n')
  323. for style in self.iter_styles():
  324. style.write(stream)
  325. stream.write('}\n')
  326. def _write_lineweights(self, stream: TextIO) -> None:
  327. """
  328. Write custom lineweights table to ctb-file <stream>.
  329. """
  330. stream.write('custom_lineweight_table{\n')
  331. for index, weight in enumerate(self.lineweights):
  332. stream.write(' %d=%.2f\n' % (index, weight))
  333. stream.write('}\n')
  334. def parse(self, text: str) -> None:
  335. """
  336. Parse and get values of plot styles from <text>.
  337. """
  338. def set_lineweights(lineweights):
  339. if lineweights is None:
  340. return
  341. self.lineweights = array('f', [0.0] * len(lineweights))
  342. for key, value in lineweights.items():
  343. self.lineweights[int(key)] = float(value)
  344. def set_styles(styles):
  345. for index, style in styles.items():
  346. style = UserStyle(index, style)
  347. self._set_style(style)
  348. parser = CtbParser(text)
  349. self.description = parser.get('description', "")
  350. self.scale_factor = float(parser.get('scale_factor', 1.0))
  351. self.apply_factor = get_bool(parser.get('apply_factor', True))
  352. self.custom_lineweight_display_units = int(
  353. parser.get('custom_lineweight_display_units', 0))
  354. set_lineweights(parser.get('custom_lineweight_table', None))
  355. set_styles(parser.get('plot_style', {}))
  356. def _compress(self, stream: BinaryIO, body: str):
  357. """
  358. Compress ctb-file-body and write it to <stream>.
  359. """
  360. def writestr(s):
  361. stream.write(s.encode())
  362. body = body.encode()
  363. comp_body = zlib.compress(body)
  364. adler_chksum = zlib.adler32(comp_body)
  365. writestr('PIAFILEVERSION_2.0,CTBVER1,compress\r\npmzlibcodec')
  366. stream.write(pack('LLL', adler_chksum, len(body), len(comp_body)))
  367. stream.write(comp_body)
  368. def read(stream: BinaryIO) -> UserStyles:
  369. """
  370. Read a ctb-file from the file-like object <stream>.
  371. """
  372. content = _decompress(stream)
  373. content = content.decode()
  374. styles = UserStyles()
  375. styles.parse(content)
  376. return styles
  377. def load(filename: str) -> UserStyles:
  378. """
  379. Load the ctb-file <filename>.
  380. """
  381. with open(filename, 'rb') as stream:
  382. ctbfile = read(stream)
  383. return ctbfile
  384. def _decompress(stream: BinaryIO) -> bytes:
  385. """
  386. Read and decompress the file content of the file-like object <stream>.
  387. """
  388. content = stream.read()
  389. data = zlib.decompress(content[60:]) # type: bytes
  390. return data[:-1] # truncate trailing \nul
  391. class CtbParser:
  392. """
  393. A very simple ctb-file parser. Ctb-files are created by programs, so the file structure should be correct
  394. in the most cases.
  395. """
  396. def __init__(self, text: str):
  397. """
  398. :param str text: ctb content as string
  399. """
  400. self.data = {}
  401. for element, value in CtbParser.iteritems(text):
  402. self.data[element] = value
  403. @staticmethod
  404. def iteritems(text: str):
  405. """
  406. iterate over all first level (start at col 0) elements
  407. """
  408. def get_name() -> str:
  409. """
  410. Get element name of line <line_index>.
  411. """
  412. line = lines[line_index]
  413. if line.endswith('{'): # start of a list like 'plot_style{'
  414. name = line[:-1]
  415. else: # simple name=value line
  416. name = line.split('=', 1)[0]
  417. return name.strip()
  418. def get_mapping() -> dict:
  419. """
  420. Get mapping of elements enclosed by { }.
  421. e. g. lineweigths, plot_styles, aci_table
  422. """
  423. nonlocal line_index
  424. def end_of_list():
  425. return lines[line_index].endswith('}')
  426. data = dict()
  427. while not end_of_list():
  428. name = get_name()
  429. value = get_value() # get value or sub-list
  430. data[name] = value
  431. line_index += 1
  432. return data # skip '}' - end of list
  433. def get_value() -> Union[str, dict]:
  434. """
  435. Get value of line <line_index> or the list that starts in line <line_index>.
  436. """
  437. nonlocal line_index
  438. line = lines[line_index]
  439. if line.endswith('{'): # start of a list
  440. line_index += 1
  441. value = get_mapping()
  442. else: # it's a simple name=value line
  443. value = line.split('=', 1)[1] # type: str
  444. value = value.lstrip('"') # strings look like this: name="value
  445. line_index += 1
  446. return value
  447. def skip_empty_lines():
  448. nonlocal line_index
  449. while line_index < len(lines) and len(lines[line_index]) == 0:
  450. line_index += 1
  451. lines = text.split('\n')
  452. line_index = 0
  453. while line_index < len(lines):
  454. name = get_name()
  455. value = get_value()
  456. yield (name, value)
  457. skip_empty_lines()
  458. def get(self, name: str, default: Any) -> Any:
  459. return self.data.get(name, default)
  460. # color_type: (thx to Rammi)
  461. # Take color from layer, ignore other bytes.
  462. COLOR_BY_LAYER = 0xc0
  463. # Take color from insertion, ignore other bytes
  464. COLOR_BY_BLOCK = 0xc1
  465. # RGB value, other bytes are R,G,B.
  466. COLOR_RGB = 0xc2
  467. # ACI, AutoCAD color index, other bytes are 0,0,index
  468. COLOR_ACI = 0xc3
  469. def int2color(color: int) -> Tuple[int, int, int, int]:
  470. """
  471. Convert color integer value from ctb-file to rgb-tuple plus a magic number.
  472. """
  473. # Take color from layer, ignore other bytes.
  474. color_type = (color & 0xff000000) >> 24
  475. red = (color & 0xff0000) >> 16
  476. green = (color & 0xff00) >> 8
  477. blue = color & 0xff
  478. return red, green, blue, color_type
  479. def mode_color2int(red: int, green: int, blue: int, color_type=COLOR_RGB) -> int:
  480. """
  481. Convert rgb-tuple to an int value.
  482. """
  483. return -color2int(red, green, blue, color_type)
  484. def color2int(red: int, green: int, blue: int, color_type: int) -> int:
  485. """
  486. Convert rgb-tuple to an int value.
  487. """
  488. return -((color_type << 24) + (red << 16) + (green << 8) + blue) & 0xffffffff