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