finished first prototype of spec editor
[swftools.git] / spec / edit_spec.py
1 import wx
2 import wx.lib.scrolledpanel as scrolled
3 import os
4 import re
5 import sys
6 import time
7 import thread
8 import traceback
9 import math
10
11 class Check:
12     def __init__(self, x,y):
13         self.x = x
14         self.y = y
15     def left(self):
16         return "pixel at (%d,%d)" % (self.x,self.y)
17     def right(self):
18         return ""
19
20 class PixelColorCheck(Check):
21     def __init__(self, x,y, color):
22         Check.__init__(self,x,y)
23         self.color = color
24     def right(self):
25         return "is of color 0x%06x" % self.color
26
27 class TwoPixelCheck(Check): 
28     def __init__(self, x,y, x2,y2):
29         Check.__init__(self,x,y)
30         self.x2,self.y2 = x2,y2
31     def right(self):
32         return "pixel at (%d,%d)" % (self.x2,self.y2)
33
34 class PixelBrighterThan(TwoPixelCheck):
35     pass
36
37 class PixelDarkerThan(TwoPixelCheck):
38     pass
39
40 class PixelEqualTo(TwoPixelCheck):
41     pass
42
43 class AreaCheck(Check):
44     def __init__(self, x,y, x2,y2):
45         Check.__init__(self,x,y)
46         self.x2,self.y2 = x2,y2
47     def left(self):
48         return "area at (%d,%d,%d,%d)" % (self.x,self.y,self.x2,self.y2)
49
50 class AreaPlain(AreaCheck):
51     pass
52
53 class AreaNotPlain(AreaCheck):
54     pass
55
56 checktypes = [PixelColorCheck,PixelBrighterThan,PixelDarkerThan,PixelEqualTo,AreaPlain,AreaNotPlain]
57
58 def convert_to_ppm(pdf):
59     print pdf
60     f = os.popen("pdfinfo "+pdf, "rb")
61     info = f.read()
62     f.close()
63     width,heigth = re.compile(r"Page size:\s*([0-9]+) x ([0-9]+) pts").findall(info)[0]
64     dpi = int(72.0 * 612 / int(width))
65     os.system("pdftoppm -r "+str(dpi)+" -f 1 -l 1 "+pdf+" test")
66     return "test-000001.ppm"
67
68 class Model:
69     def __init__(self, filename, checks):
70         self.filename = filename
71         self.imgfilename = convert_to_ppm(filename)
72         self.bitmap = wx.Bitmap(self.imgfilename)
73         self.image = wx.ImageFromBitmap(self.bitmap)
74         self.width = self.bitmap.GetWidth()
75         self.height = self.bitmap.GetHeight()
76         self.checks = checks
77         self.xy2check = {}
78         self.appendListeners = []
79         self.drawModeListeners = []
80         self.drawmode = PixelColorCheck
81         
82     def setdrawmode(self, mode):
83         self.drawmode = mode
84         for f in self.drawModeListeners:
85             f()
86
87     def find(self, x, y):
88         return self.xy2check.get((x,y),None)
89
90     def delete(self, check):
91         i = self.checks.index(check)
92         del self.checks[i]
93         del self.xy2check[(check.x,check.y)]
94         for f in self.appendListeners:
95             f(check)
96
97     def append(self, check):
98         self.checks += [check]
99         self.xy2check[(check.x,check.y)] = check
100         for f in self.appendListeners:
101             f(check)
102
103     @staticmethod
104     def load(filename):
105         path = os.path.splitext(filename)[0]+".rb"
106         fi = open(path, "rb")
107         r_file = re.compile(r"^convert_file \"([^\"]*)\"")
108         r_pixelcolor = re.compile(r"^pixel_at\(([0-9]+),([0-9]+)\).should_be_of_color (0x[0-9a-fA-F]+)")
109         r_pixelbrighter = re.compile(r"^pixel_at\(([0-9]+),([0-9]+)\).should_be_brighter_than pixel_at\(([0-9]+),([0-9]+)\)")
110         r_pixeldarker = re.compile(r"^pixel_at\(([0-9]+),([0-9]+)\).should_be_darker_than pixel_at\(([0-9]+),([0-9]+)\)")
111         r_pixelequalto = re.compile(r"^pixel_at\(([0-9]+),([0-9]+)\).should_be_the_same_as pixel_at\(([0-9]+),([0-9]+)\)")
112         r_areaplain = re.compile(r"^area_at\(([0-9]+),([0-9]+),([0-9]+),([0-9]+)\).should_be_plain_colored")
113         r_areanotplain = re.compile(r"^area_at\(([0-9]+),([0-9]+),([0-9]+),([0-9]+)\).should_not_be_plain_colored")
114         r_width = re.compile(r"^width.should be ([0-9]+)")
115         r_height = re.compile(r"^height.should be ([0-9]+)")
116         r_describe = re.compile(r"^describe \"pdf conversion\"")
117         r_header = re.compile(r"^require File.dirname")
118         r_end = re.compile(r"^end$")
119         filename = None
120         checks = []
121         for nr,line in enumerate(fi.readlines()):
122             line = line.strip()
123             if not line:
124                 continue
125             m = r_file.match(line)
126             if m: 
127                 if filename:
128                     raise Exception("can't load multi-file specs (in line %d)" % (nr+1))
129                 filename = m.group(1);
130                 model = Model(filename, [])
131                 continue
132             m = r_pixelcolor.match(line)
133             if m: model.append(PixelColorCheck(int(m.group(1)),int(m.group(2)),int(m.group(3),16)));continue
134             m = r_pixelbrighter.match(line)
135             if m: model.append(PixelBrighterThan(int(m.group(1)),int(m.group(2)),int(m.group(3)),int(m.group(4))));continue
136             m = r_pixeldarker.match(line)
137             if m: model.append(PixelDarkerThan(int(m.group(1)),int(m.group(2)),int(m.group(3)),int(m.group(4))));continue
138             m = r_pixelequalto.match(line)
139             if m: model.append(PixelEqualTo(int(m.group(1)),int(m.group(2)),int(m.group(3)),int(m.group(4))));continue
140             m = r_areaplain.match(line)
141             if m: model.append(AreaPlain(int(m.group(1)),int(m.group(2)),int(m.group(3)),int(m.group(4))));continue
142             m = r_areanotplain.match(line)
143             if m: model.append(AreaNotPlain(int(m.group(1)),int(m.group(2)),int(m.group(3)),int(m.group(4))));continue
144             if r_width.match(line) or r_height.match(line):
145                 continue # compatibility
146             if r_describe.match(line) or r_end.match(line) or r_header.match(line):
147                 continue
148             print line
149             raise Exception("invalid file format: can't load this file (in line %d)" % (nr+1))
150
151         fi.close()
152         return model
153
154     def save(self):
155         path = os.path.splitext(self.filename)[0]+".rb"
156         fi = open(path, "wb")
157         fi.write("require File.dirname(__FILE__) + '/spec_helper'\n\ndescribe \"pdf conversion\" do\n")
158         fi.write("    convert_file \"%s\" do\n" % self.filename)
159         for check in self.checks:
160             c = check.__class__
161             if c == PixelColorCheck:
162                 fi.write("        pixel_at(%d,%d).should_be_of_color 0x%06x\n" % (check.x,check.y,check.color))
163             elif c == PixelBrighterThan:
164                 fi.write("        pixel_at(%d,%d).should_be_brighter_than pixel_at(%d,%d)\n" % (check.x,check.y,check.x2,check.y2))
165             elif c == PixelDarkerThan:
166                 fi.write("        pixel_at(%d,%d).should_be_darker_than pixel_at(%d,%d)\n" % (check.x,check.y,check.x2,check.y2))
167             elif c == PixelEqualTo:
168                 fi.write("        pixel_at(%d,%d).should_be_the_same_as pixel_at(%d,%d)\n" % (check.x,check.y,check.x2,check.y2))
169             elif c == AreaPlain:
170                 fi.write("        area_at(%d,%d,%d,%d).should_be_plain_colored\n" % (check.x,check.y,check.x2,check.y2))
171             elif c == AreaNotPlain:
172                 fi.write("        area_at(%d,%d,%d,%d).should_not_be_plain_colored\n" % (check.x,check.y,check.x2,check.y2))
173         fi.write("    end\n")
174         fi.write("end\n")
175         fi.close()
176
177 class ZoomWindow(wx.Window):
178     def __init__(self, parent, model):
179         wx.Window.__init__(self, parent, pos=(0,0), size=(15*32,15*32))
180         self.model = model
181         self.Bind(wx.EVT_PAINT, self.OnPaint)
182         self.x = 0
183         self.y = 0
184
185     def setpos(self,x,y):
186         self.x = x
187         self.y = y
188         self.Refresh()
189     
190     def OnPaint(self, event):
191         dc = wx.PaintDC(self)
192         self.Draw(dc)
193     
194     def Draw(self,dc=None):
195         if not dc:
196             dc = wx.ClientDC(self)
197         dc.SetBackground(wx.Brush((0,0,0)))
198         color = (0,255,0)
199         for yy in range(15):
200             y = self.y+yy-8
201             for xx in range(15):
202                 x = self.x+xx-8
203                 if 0<=x<self.model.width and 0<=y<self.model.height:
204                     color = (self.model.image.GetRed(x,y), self.model.image.GetGreen(x,y), self.model.image.GetBlue(x,y))
205                 else:
206                     color = (0,0,0)
207                 dc.SetPen(wx.Pen(color))
208                 m = self.model.find(x,y)
209                 dc.SetBrush(wx.Brush(color))
210                 dc.DrawRectangle(32*xx, 32*yy, 32, 32)
211
212                 if (xx==8 and yy==8) or m:
213                     dc.SetPen(wx.Pen((0, 0, 0)))
214                     dc.DrawRectangleRect((32*xx, 32*yy, 32, 32))
215                     dc.DrawRectangleRect((32*xx+2, 32*yy+2, 28, 28))
216
217                     if (xx==8 and yy==8):
218                         dc.SetPen(wx.Pen((255, 255, 255)))
219                     else:
220                         dc.SetPen(wx.Pen((255, 255, 0)))
221                     dc.DrawRectangleRect((32*xx+1, 32*yy+1, 30, 30))
222                     #dc.SetPen(wx.Pen((0, 0, 0)))
223                     #dc.SetPen(wx.Pen(color))
224
225 class ImageWindow(wx.Window):
226     def __init__(self, parent, model, zoom):
227         wx.Window.__init__(self, parent)
228         self.model = model
229         self.Bind(wx.EVT_PAINT, self.OnPaint)
230         self.Bind(wx.EVT_MOUSE_EVENTS, self.OnMouse)
231         self.SetSize((model.width, model.height))
232         self.zoom = zoom
233         self.x = 0
234         self.y = 0
235         self.lastx = 0
236         self.lasty = 0
237         self.firstclick = None
238         self.model.drawModeListeners += [self.reset]
239
240     def reset(self):
241         self.firstclick = None
242
243     def OnMouseClick(self, event):
244         x = min(max(event.X, 0), self.model.width-1)
245         y = min(max(event.Y, 0), self.model.height-1)
246         if self.model.drawmode == PixelColorCheck:
247             check = self.model.find(x,y)
248             if check:
249                 self.model.delete(check)
250             else:
251                 color = self.model.image.GetRed(x,y)<<16 | self.model.image.GetGreen(x,y)<<8 | self.model.image.GetBlue(x,y)
252                 self.model.append(PixelColorCheck(x,y,color))
253         else:
254             if not self.firstclick:
255                 self.firstclick = (x,y)
256             else:
257                 x1,y1 = self.firstclick
258                 self.model.append(self.model.drawmode(x1,y1,x,y))
259                 self.firstclick = None
260
261         self.Refresh()
262
263     def OnMouse(self, event):
264         if event.LeftIsDown():
265             return self.OnMouseClick(event)
266         lastx = self.x
267         lasty = self.y
268         self.x = min(max(event.X, 0), self.model.width-1)
269         self.y = min(max(event.Y, 0), self.model.height-1)
270         if lastx!=self.x or lasty!=self.y:
271             self.zoom.setpos(self.x,self.y)
272             self.Refresh()
273
274     def OnPaint(self, event):
275         dc = wx.PaintDC(self)
276         self.Draw(dc)
277
278     def Draw(self,dc=None):
279         if not dc:
280             dc = wx.ClientDC(self)
281       
282         dc.SetBackground(wx.Brush((0,0,0)))
283         dc.DrawBitmap(self.model.bitmap, 0, 0, False)
284
285         if self.firstclick:
286             x,y = self.firstclick
287             if AreaCheck in self.model.drawmode.__bases__:
288                 dc.SetBrush(wx.TRANSPARENT_BRUSH)
289                 dc.DrawRectangle(x,y,self.x-x,self.y-y)
290                 dc.SetBrush(wx.WHITE_BRUSH)
291             elif TwoPixelCheck in self.model.drawmode.__bases__:
292                 x,y = self.firstclick
293                 dc.DrawLine(x,y,self.x,self.y)
294
295         for check in self.model.checks:
296             if AreaCheck in check.__class__.__bases__:
297                 dc.SetBrush(wx.TRANSPARENT_BRUSH)
298                 dc.DrawRectangle(check.x,check.y,check.x2-check.x,check.y2-check.y)
299                 dc.SetBrush(wx.WHITE_BRUSH)
300             else:
301                 x = check.x
302                 y = check.y
303                 l = 0
304                 for r in range(10):
305                     r = (r+1)*3.141526/5
306                     dc.DrawLine(x+10*math.sin(l), y+10*math.cos(l), x+10*math.sin(r), y+10*math.cos(r))
307                     l = r
308                 dc.DrawLine(x,y,x+1,y)
309                 if TwoPixelCheck in check.__class__.__bases__:
310                     dc.DrawLine(x,y,check.x2,check.y2)
311
312 class EntryPanel(scrolled.ScrolledPanel):
313     def __init__(self, parent, model):
314         self.model = model
315         scrolled.ScrolledPanel.__init__(self, parent, -1, size=(480,10*32), pos=(0,16*32))
316         self.id2check = {}
317         self.append(None)
318
319     def delete(self, event):
320         self.model.delete(self.id2check[event.Id])
321
322     def append(self, check):
323         self.vbox = wx.BoxSizer(wx.VERTICAL)
324         self.vbox.Add(wx.StaticLine(self, -1, size=(500,-1)), 0, wx.ALL, 5)
325         for nr,check in enumerate(model.checks):
326             hbox = wx.BoxSizer(wx.HORIZONTAL) 
327             
328             button = wx.Button(self, label="X", size=(32,32))
329             hbox.Add(button, 0, wx.ALIGN_CENTER_VERTICAL)
330             hbox.Add((16,16))
331             self.id2check[button.Id] = check
332             self.Bind(wx.EVT_BUTTON, self.delete, button)
333
334             desc = wx.StaticText(self, -1, check.left())
335             hbox.Add(desc, 0, wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5)
336             if isinstance(check,AreaCheck):
337                 choices = ["is plain","is not plain"]
338                 lb = wx.Choice(self, -1, (100, 50), choices = choices)
339                 hbox.Add(lb, 0, wx.ALIGN_LEFT|wx.ALL, 5)
340             elif isinstance(check,TwoPixelCheck):
341                 choices = ["is the same as","is brighter than","is darker than"]
342                 lb = wx.Choice(self, -1, (100, 50), choices = choices)
343                 hbox.Add(lb, 0, wx.ALIGN_LEFT|wx.ALL, 5)
344             elif isinstance(check,PixelColorCheck):
345                 # TODO: color control
346                 pass
347             
348             desc = wx.StaticText(self, -1, check.right())
349             hbox.Add(desc, 0, wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5)
350
351             self.vbox.Add(hbox)
352             self.vbox.Add(wx.StaticLine(self, -1, size=(500,-1)), 0, wx.ALL, 5)
353         self.end = wx.Window(self, -1, size=(1,1))
354         self.vbox.Add(self.end)
355         self.SetSizer(self.vbox)
356         self.SetAutoLayout(1)
357         self.SetupScrolling(scrollToTop=False)
358         self.ScrollChildIntoView(self.end)
359
360 class ToolChoiceWindow(wx.Choice):
361     def __init__(self, parent, model):
362         self.model = model
363         self.choices = [c.__name__ for c in checktypes]
364         wx.Choice.__init__(self, parent, -1, (100,50), choices = self.choices)
365         self.Bind(wx.EVT_CHOICE, self.choice)
366     def choice(self, event):
367         self.model.setdrawmode(eval(self.choices[self.GetCurrentSelection()]))
368
369 class MainFrame(wx.Frame):
370     def __init__(self, application, model):
371         wx.Frame.__init__(self, None, -1, style = wx.DEFAULT_FRAME_STYLE, pos=(50,50))
372         self.application = application
373       
374         self.toolchoice = ToolChoiceWindow(self, model)
375         self.toolchoice.Show()
376         self.zoom = ZoomWindow(self, model)
377         self.zoom.Show()
378         self.image = ImageWindow(self, model, self.zoom)
379         self.image.Show()
380         self.entries = EntryPanel(self, model)
381         self.entries.Show()
382         self.createToolbar()
383         model.appendListeners += [self.append]
384         
385         hbox = wx.BoxSizer(wx.HORIZONTAL)
386         hbox.Add(self.zoom)
387         hbox.Add((16,16))
388         vbox = wx.BoxSizer(wx.VERTICAL)
389         vbox.Add(self.toolchoice)
390         vbox.Add(self.image)
391         hbox.Add(vbox)
392         #vbox.Add(self.entries)
393         self.SetSizer(hbox)
394         self.SetAutoLayout(True)
395         hbox.Fit(self)
396
397     def append(self, new):
398         self.entries.Hide()
399         e = self.entries
400         del self.entries
401         e.Destroy()
402         self.entries = EntryPanel(self, model)
403         self.entries.Show()
404
405     def createToolbar(self):
406         tsize = (16,16)
407         self.toolbar = self.CreateToolBar(wx.TB_HORIZONTAL | wx.NO_BORDER | wx.TB_FLAT)
408         self.toolbar.AddSimpleTool(wx.ID_CUT,
409                                    wx.ArtProvider.GetBitmap(wx.ART_CROSS_MARK, wx.ART_TOOLBAR, tsize),
410                                    "Remove")
411         self.toolbar.AddSimpleTool(wx.ID_SETUP,
412                                    wx.ArtProvider.GetBitmap(wx.ART_TIP, wx.ART_TOOLBAR, tsize),
413                                    "Add")
414         self.toolbar.AddSimpleTool(wx.ID_SETUP,
415                                    wx.ArtProvider.GetBitmap(wx.ART_GO_UP, wx.ART_TOOLBAR, tsize),
416                                    "Add")
417         #self.toolbar.AddSeparator()
418         self.toolbar.Realize()
419
420
421 #class ScrollFrame(wx.Frame):
422
423 def getpixels(filename, p):
424     model = Model(filename, p)
425     app = wx.PySimpleApp()
426     main = MainFrame(app, model)
427     main.Show()
428     app.MainLoop()
429     return model.pixels
430
431 if __name__ == "__main__":
432     app = wx.PySimpleApp()
433     model = Model.load(sys.argv[1])
434     main = MainFrame(app, model)
435     main.Show()
436     app.MainLoop()
437     model.save()