dxftoimg.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. import StringIO, base64, logging, operator
  2. import dxfgrabber
  3. import Image, ImageDraw, ImageFont #PIL
  4. from homcoord import *
  5. def rint(x):return int(round(x))
  6. class BBox:
  7. """bounding box"""
  8. def __init__(self,pt1=None,pt2=None):
  9. self._corner1=None
  10. self._corner2=None
  11. if pt1: self+=pt1
  12. if pt2: self+=pt2
  13. def __iadd__(self,pt):
  14. if isinstance(pt,BBox):
  15. self+=pt._corner1
  16. self+=pt._corner2
  17. else:
  18. if not self._corner1:
  19. self._corner1=pt
  20. else:
  21. self._corner1=Pt(map(min,zip(self._corner1.xy,pt.xy)))
  22. if not self._corner2:
  23. self._corner2=pt
  24. else:
  25. self._corner2=Pt(map(max,zip(self._corner2.xy,pt.xy)))
  26. return self
  27. def __repr__(self):
  28. return "BBox(%s,%s)"%(self._corner1,self._corner2)
  29. def __call__(self):
  30. """:return: list of flatten corners"""
  31. l=list(self._corner1.xy)
  32. l.extend(list(self._corner2.xy))
  33. return l
  34. def size(self):
  35. """:return: Pt with xy sizes"""
  36. return self._corner2-self._corner1
  37. def center(self):
  38. """:return: Pt center"""
  39. res=self._corner2+self._corner1
  40. return res/2
  41. def trans(self,trans):
  42. """
  43. :param trans: Xform
  44. :return: BBox = self transformed by trans
  45. """
  46. return BBox(trans(self._corner1),trans(self._corner2))
  47. def cbox(c,r):
  48. """ bounding box of a circle
  49. :param c: Pt center
  50. :param r: float radius
  51. :return: BBox
  52. """
  53. rr=Pt(r,r)
  54. return BBox(c+rr,c-rr)
  55. def Trans(scale=1, offset=[0,0], rotation=0):
  56. res=Xform([[scale,0,offset[0]],[0,scale,offset[1]],[0,0,1]])
  57. if rotation:
  58. res=Xrotate(rotation*pi/180.)*res
  59. return res
  60. class DXF:
  61. def __init__(self, file, layers=None, ignore=[]):
  62. """reads a .dxf file
  63. :param file: string path to .dxf file to read
  64. :param layers: list or dictionary of layers to handle. Empty = all layers
  65. :param ignore: list of strings of entity types to ignore
  66. """
  67. self.dxf=dxfgrabber.readfile(file)
  68. self.layers=layers
  69. self.ignore=ignore
  70. def entities(self,ent=None):
  71. """iterator over dxf or block entities"""
  72. if not ent:
  73. ent=self.dxf.entities
  74. for e in ent:
  75. if self.layers and e.layer not in self.layers:
  76. continue
  77. elif e.dxftype in self.ignore:
  78. continue
  79. else:
  80. yield e
  81. def bbox(self):
  82. """:return: :class:BBox dwg enclosing bounding box"""
  83. box=BBox()
  84. for e in self.entities():
  85. if e.dxftype=='LINE':
  86. box+=Pt(e.start[:2])
  87. box+=Pt(e.end[:2])
  88. elif e.dxftype == 'CIRCLE':
  89. box+=cbox(Pt(e.center[:2]),e.radius)
  90. elif e.dxftype == 'ARC':
  91. c=Pt(e.center[:2])
  92. a=e.endangle-e.startangle
  93. if a>0:
  94. start=e.startangle
  95. else: #arc goes clockwise (step will be negative)
  96. start=e.endangle
  97. n=rint(abs(a)/10.) # number of points each 10° approximately
  98. n=max(n,1)
  99. step=a/n #angle between 2 points, might be negative
  100. for i in range(n+1):
  101. box+=c.radial(e.radius,radians(start+i*step))
  102. elif e.dxftype=='POLYLINE':
  103. for v in e.vertices:
  104. box+=Pt(v.location[:2])
  105. elif e.dxftype=='BLOCK':
  106. pass #TODO ...
  107. elif e.dxftype in ['TEXT','INSERT']:
  108. box+=Pt(e.insert[:2])
  109. else:
  110. logging.warning('Unknown entity %s'%e)
  111. return box
  112. def _draw(self,draw,entities,trans,pen="black"):
  113. for e in entities:
  114. if e.dxftype=='LINE':
  115. b=list(trans(Pt(e.start[:2])).xy)
  116. b.extend(list(trans(Pt(e.end[:2])).xy))
  117. draw.line(b,fill=pen)
  118. elif e.dxftype=='CIRCLE':
  119. b=cbox(Pt(e.center[:2]),e.radius)
  120. b=b.trans(trans)
  121. draw.ellipse(b(),outline=pen)
  122. elif e.dxftype=='ARC':
  123. c=Pt(e.center[:2])
  124. b=cbox(c,e.radius)
  125. b=b.trans(trans)
  126. b=map(rint,b())
  127. startangle=degrees(trans.angle(radians(e.startangle)))
  128. endangle=degrees(trans.angle(radians(e.endangle)))
  129. startangle,endangle=endangle,startangle #swap start/end because of Y symmetry
  130. draw.arc(b,int(startangle),int(endangle),fill=pen)
  131. elif e.dxftype=='POLYLINE':
  132. b=[]
  133. for v in e.vertices:
  134. b.extend(list(trans(Pt(v.location[:2])).xy))
  135. draw.line(b,fill=pen)
  136. elif e.dxftype=='TEXT':
  137. h=e.height*trans.mag()
  138. pt=Pt(e.insert[:2])+Pt(0,e.height) #ACAD places texts by top left point...
  139. font=None
  140. try:
  141. font = ImageFont.truetype("c:/windows/fonts/Courier New.ttf", rint(h))
  142. except:
  143. pass
  144. if not font:
  145. h=h*1.4 #magic factor ...
  146. fh=[8,10,12,14,16,18,20,22,24,26,28,30,36,40,48,60]
  147. i,h=min(enumerate(fh), key=lambda x: abs(x[1]-h)) #http://stackoverflow.com/questions/9706041/finding-index-of-an-item-closest-to-the-value-in-a-list-thats-not-entirely-sort
  148. import os
  149. path=os.path.realpath(__file__)
  150. path=os.path.dirname(path)
  151. font = ImageFont.load(path+'\\base_pil\\72\\Courier New_%s_72.pil'%h)
  152. draw.text(trans(pt).xy,e.text,font=font,fill=pen)
  153. elif e.dxftype=='INSERT':
  154. t2=Trans(1,e.insert,e.rotation).compose(trans)
  155. self._draw(draw,self.entities(self.dxf.blocks[e.name]._entities),t2,pen)
  156. elif e.dxftype=='BLOCK':
  157. pass # block definition is automatically stored in dxf.blocks dictionary
  158. else:
  159. logging.warning('Unknown entity %s'%e)
  160. def img(self,size=[256,256],back="white",pen="black",border=5,antialias=1):
  161. """:result: :class:`PIL:Image` rasterized image"""
  162. box=self.bbox()
  163. from Goulib.math2 import product
  164. if not product(box.size().xy): # either x or y ==0
  165. return None
  166. s=map(operator.div,[float(x-border)*antialias if x else 1E9 for x in size ],box.size().xy)
  167. trans=Trans(scale=min(s))
  168. size=trans(box.size())+Pt(2*antialias*border,2*antialias*border) #add borders as an offset
  169. offset=size/2-trans(box.center()) #offset in pixel coordinates
  170. trans=trans*Trans(offset=offset.xy)
  171. trans=trans*Xscale(1,-1) #invert y axis
  172. trans=trans*Xlate(0,size.y) #origin is lower left corner
  173. img = Image.new("RGB", map(rint,size.xy), back)
  174. self._draw(ImageDraw.Draw(img), self.entities(), trans, pen)
  175. if antialias>1:
  176. size=size/antialias
  177. img=img.resize(map(rint,size.xy), Image.ANTIALIAS)
  178. return img
  179. def img2base64(img,fmt='PNG'):
  180. """
  181. :param img: :class:`PIL:Image`
  182. :result: string base64 encoded image content in specified format
  183. :see: http://stackoverflow.com/questions/14348442/django-how-do-i-display-a-pil-image-object-in-a-template
  184. """
  185. output = StringIO.StringIO()
  186. img.save(output, fmt)
  187. output.seek(0)
  188. output_s = output.read()
  189. return base64.b64encode(output_s)
  190. if __name__ == '__main__':
  191. dxf=DXF("GV_12.DXF")
  192. img=dxf.img(size=[1280,None],border=50)
  193. print(img2base64(img))
  194. img.save('..\\tests\\out.png')