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