3 import wx.lib.scrolledpanel as scrolled
13 def __init__(self, x,y):
17 return "pixel at (%d,%d)" % (self.x,self.y)
20 def verifies(self, model):
23 class PixelColorCheck(Check):
24 def __init__(self, x,y, color):
25 Check.__init__(self,x,y)
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
39 return "pixel at (%d,%d)" % (self.x2,self.y2)
41 class PixelBrighterThan(TwoPixelCheck):
44 class PixelDarkerThan(TwoPixelCheck):
47 class PixelEqualTo(TwoPixelCheck):
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
55 return "area at (%d,%d,%d,%d)" % (self.x,self.y,self.x2,self.y2)
57 class AreaPlain(AreaCheck):
60 class AreaNotPlain(AreaCheck):
63 class AreaText(AreaCheck):
64 def __init__(self, x,y, x2, y2, text=""):
65 AreaCheck.__init__(self,x,y,x2,y2)
68 checktypes = [PixelColorCheck,PixelBrighterThan,PixelDarkerThan,PixelEqualTo,AreaPlain,AreaNotPlain,AreaText]
72 def convert_to_ppm(pdf):
74 f = os.popen("pdfinfo "+pdf, "rb")
77 width,heigth = re.compile(r"Page size:\s*([0-9]+) x ([0-9]+) pts").findall(info)[0]
78 dpi = int(72.0 * 612 / int(width))
80 os.system("pdf2swf -s zoom="+str(dpi)+" -p1 "+pdf+" -o test.swf")
81 os.system("swfrender --legacy test.swf -o test.png")
85 os.system("pdftoppm -r "+str(dpi)+" -f 1 -l 1 "+pdf+" test")
86 return "test-000001.ppm"
90 def __init__(self, specfile, docfile, checks):
91 self.specfile = specfile
92 self.docfile = docfile
93 self.imgfilename = convert_to_ppm(self.docfile)
94 self.bitmap = wx.Bitmap(self.imgfilename)
95 self.image = wx.ImageFromBitmap(self.bitmap)
96 self.width = self.bitmap.GetWidth()
97 self.height = self.bitmap.GetHeight()
100 self.appendListeners = []
101 self.drawModeListeners = []
102 self.drawmode = PixelColorCheck
105 try: os.unlink(self.imgfilename)
108 def getPixel(self,x,y):
109 return (self.image.GetRed(x,y), self.image.GetGreen(x,y), self.image.GetBlue(x,y))
111 def setdrawmode(self, mode):
113 for f in self.drawModeListeners:
116 def find(self, x, y):
117 return self.xy2check.get((x,y),None)
119 def delete(self, check):
120 i = self.checks.index(check)
122 del self.xy2check[(check.x,check.y)]
123 for f in self.appendListeners:
126 def append(self, check):
127 self.checks += [check]
128 self.xy2check[(check.x,check.y)] = check
129 for f in self.appendListeners:
134 # convenience, allow to do "edit_spec.py file.pdf"
135 p,ext = os.path.splitext(filename)
138 if not os.path.isfile(path):
140 if not os.path.isfile(path):
141 print "No file %s found, creating new..." % path
142 return Model(path, filename, [])
146 fi = open(path, "rb")
147 r_file = re.compile(r"^convert_file \"([^\"]*)\"")
148 r_pixelcolor = re.compile(r"^pixel_at\(([0-9]+),([0-9]+)\).should_be_of_color (0x[0-9a-fA-F]+)")
149 r_pixelbrighter = re.compile(r"^pixel_at\(([0-9]+),([0-9]+)\).should_be_brighter_than pixel_at\(([0-9]+),([0-9]+)\)")
150 r_pixeldarker = re.compile(r"^pixel_at\(([0-9]+),([0-9]+)\).should_be_darker_than pixel_at\(([0-9]+),([0-9]+)\)")
151 r_pixelequalto = re.compile(r"^pixel_at\(([0-9]+),([0-9]+)\).should_be_the_same_as pixel_at\(([0-9]+),([0-9]+)\)")
152 r_areaplain = re.compile(r"^area_at\(([0-9]+),([0-9]+),([0-9]+),([0-9]+)\).should_be_plain_colored")
153 r_areanotplain = re.compile(r"^area_at\(([0-9]+),([0-9]+),([0-9]+),([0-9]+)\).should_not_be_plain_colored")
154 r_areatext = re.compile(r"^area_at\(([0-9]+),([0-9]+),([0-9]+),([0-9]+)\).should_contain_text '(.*)'")
155 r_width = re.compile(r"^width.should be ([0-9]+)")
156 r_height = re.compile(r"^height.should be ([0-9]+)")
157 r_describe = re.compile(r"^describe \"pdf conversion\"")
158 r_header = re.compile(r"^require File.dirname")
159 r_end = re.compile(r"^end$")
162 for nr,line in enumerate(fi.readlines()):
166 m = r_file.match(line)
169 raise Exception("can't load multi-file specs (in line %d)" % (nr+1))
170 filename = m.group(1);
171 model = Model(path, filename, [])
173 m = r_pixelcolor.match(line)
174 if m: model.append(PixelColorCheck(int(m.group(1)),int(m.group(2)),int(m.group(3),16)));continue
175 m = r_pixelbrighter.match(line)
176 if m: model.append(PixelBrighterThan(int(m.group(1)),int(m.group(2)),int(m.group(3)),int(m.group(4))));continue
177 m = r_pixeldarker.match(line)
178 if m: model.append(PixelDarkerThan(int(m.group(1)),int(m.group(2)),int(m.group(3)),int(m.group(4))));continue
179 m = r_pixelequalto.match(line)
180 if m: model.append(PixelEqualTo(int(m.group(1)),int(m.group(2)),int(m.group(3)),int(m.group(4))));continue
181 m = r_areaplain.match(line)
182 if m: model.append(AreaPlain(int(m.group(1)),int(m.group(2)),int(m.group(3)),int(m.group(4))));continue
183 m = r_areanotplain.match(line)
184 if m: model.append(AreaNotPlain(int(m.group(1)),int(m.group(2)),int(m.group(3)),int(m.group(4))));continue
185 m = r_areatext.match(line)
186 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
187 if r_width.match(line) or r_height.match(line):
188 continue # compatibility
189 if r_describe.match(line) or r_end.match(line) or r_header.match(line):
192 raise Exception("invalid file format: can't load this file (in line %d)" % (nr+1))
199 fi = open(path, "wb")
200 fi.write("require File.dirname(__FILE__) + '/spec_helper'\n\ndescribe \"pdf conversion\" do\n")
201 fi.write(" convert_file \"%s\" do\n" % self.docfile)
202 for check in self.checks:
204 if c == PixelColorCheck:
205 fi.write(" pixel_at(%d,%d).should_be_of_color 0x%06x\n" % (check.x,check.y,check.color))
206 elif c == PixelBrighterThan:
207 fi.write(" pixel_at(%d,%d).should_be_brighter_than pixel_at(%d,%d)\n" % (check.x,check.y,check.x2,check.y2))
208 elif c == PixelDarkerThan:
209 fi.write(" pixel_at(%d,%d).should_be_darker_than pixel_at(%d,%d)\n" % (check.x,check.y,check.x2,check.y2))
210 elif c == PixelEqualTo:
211 fi.write(" pixel_at(%d,%d).should_be_the_same_as pixel_at(%d,%d)\n" % (check.x,check.y,check.x2,check.y2))
213 fi.write(" area_at(%d,%d,%d,%d).should_be_plain_colored\n" % (check.x,check.y,check.x2,check.y2))
214 elif c == AreaNotPlain:
215 fi.write(" area_at(%d,%d,%d,%d).should_not_be_plain_colored\n" % (check.x,check.y,check.x2,check.y2))
217 fi.write(" area_at(%d,%d,%d,%d).should_contain_text '%s'\n" % (check.x,check.y,check.x2,check.y2,check.text))
222 class ZoomWindow(wx.Window):
223 def __init__(self, parent, model):
224 wx.Window.__init__(self, parent, pos=(0,0), size=(15*32,15*32))
226 self.Bind(wx.EVT_PAINT, self.OnPaint)
230 def setpos(self,x,y):
235 def OnPaint(self, event):
236 dc = wx.PaintDC(self)
239 def Draw(self,dc=None):
241 dc = wx.ClientDC(self)
242 dc.SetBackground(wx.Brush((0,0,0)))
248 if 0<=x<self.model.width and 0<=y<self.model.height:
249 color = self.model.getPixel(x,y)
252 dc.SetPen(wx.Pen(color))
253 m = self.model.find(x,y)
254 dc.SetBrush(wx.Brush(color))
255 dc.DrawRectangle(32*xx, 32*yy, 32, 32)
257 if (xx==8 and yy==8) or m:
258 dc.SetPen(wx.Pen((0, 0, 0)))
259 dc.DrawRectangleRect((32*xx, 32*yy, 32, 32))
260 dc.DrawRectangleRect((32*xx+2, 32*yy+2, 28, 28))
262 if (xx==8 and yy==8):
263 dc.SetPen(wx.Pen((255, 255, 255)))
265 dc.SetPen(wx.Pen((255, 255, 0)))
266 dc.DrawRectangleRect((32*xx+1, 32*yy+1, 30, 30))
267 #dc.SetPen(wx.Pen((0, 0, 0)))
268 #dc.SetPen(wx.Pen(color))
270 class ImageWindow(wx.Window):
271 def __init__(self, parent, model, zoom):
272 wx.Window.__init__(self, parent)
274 self.Bind(wx.EVT_PAINT, self.OnPaint)
275 self.Bind(wx.EVT_MOUSE_EVENTS, self.OnMouse)
276 self.SetSize((model.width, model.height))
282 self.firstclick = None
283 self.model.drawModeListeners += [self.reset]
286 self.firstclick = None
288 def OnMouseClick(self, event):
289 x = min(max(event.X, 0), self.model.width-1)
290 y = min(max(event.Y, 0), self.model.height-1)
291 if self.model.drawmode == PixelColorCheck:
292 check = self.model.find(x,y)
294 self.model.delete(check)
296 p = self.model.getPixel(x,y)
297 color = p[0]<<16|p[1]<<8|p[2]
298 self.model.append(PixelColorCheck(x,y,color))
300 if not self.firstclick:
301 self.firstclick = (x,y)
303 x1,y1 = self.firstclick
304 self.model.append(self.model.drawmode(x1,y1,x,y))
305 self.firstclick = None
309 def OnMouse(self, event):
310 if event.LeftIsDown():
311 return self.OnMouseClick(event)
314 self.x = min(max(event.X, 0), self.model.width-1)
315 self.y = min(max(event.Y, 0), self.model.height-1)
316 if lastx!=self.x or lasty!=self.y:
317 self.zoom.setpos(self.x,self.y)
320 def OnPaint(self, event):
321 dc = wx.PaintDC(self)
324 def Draw(self,dc=None):
326 dc = wx.ClientDC(self)
328 dc.SetBackground(wx.Brush((0,0,0)))
329 dc.DrawBitmap(self.model.bitmap, 0, 0, False)
331 red = wx.Pen((192,0,0),2)
334 x,y = self.firstclick
335 if AreaCheck in self.model.drawmode.__bases__:
336 dc.SetBrush(wx.TRANSPARENT_BRUSH)
337 dc.DrawRectangle(x,y,self.x-x,self.y-y)
338 dc.SetBrush(wx.WHITE_BRUSH)
339 elif TwoPixelCheck in self.model.drawmode.__bases__:
340 x,y = self.firstclick
341 dc.DrawLine(x,y,self.x,self.y)
343 for check in self.model.checks:
344 if TESTMODE and not check.verifies(model):
347 dc.SetPen(wx.BLACK_PEN)
348 if AreaCheck in check.__class__.__bases__:
349 dc.SetBrush(wx.TRANSPARENT_BRUSH)
350 dc.DrawRectangle(check.x,check.y,check.x2-check.x,check.y2-check.y)
351 dc.SetBrush(wx.WHITE_BRUSH)
358 dc.DrawLine(x+10*math.sin(l), y+10*math.cos(l), x+10*math.sin(r), y+10*math.cos(r))
360 dc.DrawLine(x,y,x+1,y)
361 if TwoPixelCheck in check.__class__.__bases__:
362 dc.DrawLine(x,y,check.x2,check.y2)
363 dc.SetPen(wx.BLACK_PEN)
365 class EntryPanel(scrolled.ScrolledPanel):
366 def __init__(self, parent, model):
368 scrolled.ScrolledPanel.__init__(self, parent, -1, size=(480,10*32), pos=(0,16*32))
372 def delete(self, event):
373 self.model.delete(self.id2check[event.Id])
375 def text(self, event):
376 check = self.id2check[event.GetEventObject().Id]
377 check.text = event.GetString()
379 def append(self, check):
380 self.vbox = wx.BoxSizer(wx.VERTICAL)
381 self.vbox.Add(wx.StaticLine(self, -1, size=(500,-1)), 0, wx.ALL, 5)
382 for nr,check in enumerate(model.checks):
383 hbox = wx.BoxSizer(wx.HORIZONTAL)
385 button = wx.Button(self, label="X", size=(32,32))
386 hbox.Add(button, 0, wx.ALIGN_CENTER_VERTICAL)
388 self.id2check[button.Id] = check
389 self.Bind(wx.EVT_BUTTON, self.delete, button)
391 def setdefault(lb,nr):
392 lb.Select(nr);self.Bind(wx.EVT_CHOICE, lambda lb:lb.EventObject.Select(nr), lb)
394 desc = wx.StaticText(self, -1, check.left())
396 hbox.Add(desc, 0, wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5)
397 if isinstance(check,AreaCheck):
398 choices = ["is plain","is not plain","contains text"]
399 lb = wx.Choice(self, -1, (100, 50), choices = choices)
400 hbox.Add(lb, 0, wx.ALIGN_LEFT|wx.ALL, 5)
401 if isinstance(check, AreaPlain):
403 elif isinstance(check, AreaNotPlain):
407 tb = wx.TextCtrl(self, -1, check.text, size=(100, 25))
408 self.id2check[tb.Id] = check
409 self.Bind(wx.EVT_TEXT, self.text, tb)
411 hbox.Add(tb, 0, wx.ALIGN_LEFT|wx.ALL, 5)
412 elif isinstance(check,TwoPixelCheck):
413 choices = ["is the same as","is brighter than","is darker than"]
414 lb = wx.Choice(self, -1, (100, 50), choices = choices)
415 hbox.Add(lb, 0, wx.ALIGN_LEFT|wx.ALL, 5)
416 if isinstance(check, PixelEqualTo):
418 elif isinstance(check, PixelBrighterThan):
420 elif isinstance(check, PixelDarkerThan):
422 elif isinstance(check,PixelColorCheck):
423 # TODO: color control
426 desc2 = wx.StaticText(self, -1, check.right())
427 hbox.Add(desc2, 0, wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5)
430 self.vbox.Add(wx.StaticLine(self, -1, size=(500,-1)), 0, wx.ALL, 5)
431 self.end = wx.Window(self, -1, size=(1,1))
432 self.vbox.Add(self.end)
433 self.SetSizer(self.vbox)
434 self.SetAutoLayout(1)
435 self.SetupScrolling(scrollToTop=False)
436 self.ScrollChildIntoView(self.end)
438 class ToolChoiceWindow(wx.Choice):
439 def __init__(self, parent, model):
441 self.choices = [c.__name__ for c in checktypes]
442 wx.Choice.__init__(self, parent, -1, (100,50), choices = self.choices)
443 self.Bind(wx.EVT_CHOICE, self.choice)
444 def choice(self, event):
445 self.model.setdrawmode(eval(self.choices[self.GetCurrentSelection()]))
447 class MainFrame(wx.Frame):
448 def __init__(self, application, model):
449 wx.Frame.__init__(self, None, -1, style = wx.DEFAULT_FRAME_STYLE, pos=(50,50))
450 self.application = application
452 self.toolchoice = ToolChoiceWindow(self, model)
453 self.toolchoice.Show()
454 self.zoom = ZoomWindow(self, model)
456 self.image = ImageWindow(self, model, self.zoom)
458 self.entries = EntryPanel(self, model)
461 model.appendListeners += [self.append]
463 hbox = wx.BoxSizer(wx.HORIZONTAL)
466 vbox = wx.BoxSizer(wx.VERTICAL)
467 vbox.Add(self.toolchoice)
470 #vbox.Add(self.entries)
472 self.SetAutoLayout(True)
475 def append(self, new):
480 self.entries = EntryPanel(self, model)
483 def createToolbar(self):
485 self.toolbar = self.CreateToolBar(wx.TB_HORIZONTAL | wx.NO_BORDER | wx.TB_FLAT)
486 self.toolbar.AddSimpleTool(wx.ID_CUT,
487 wx.ArtProvider.GetBitmap(wx.ART_CROSS_MARK, wx.ART_TOOLBAR, tsize),
489 self.toolbar.AddSimpleTool(wx.ID_SETUP,
490 wx.ArtProvider.GetBitmap(wx.ART_TIP, wx.ART_TOOLBAR, tsize),
492 self.toolbar.AddSimpleTool(wx.ID_SETUP,
493 wx.ArtProvider.GetBitmap(wx.ART_GO_UP, wx.ART_TOOLBAR, tsize),
495 #self.toolbar.AddSeparator()
496 self.toolbar.Realize()
499 if __name__ == "__main__":
500 from optparse import OptionParser
502 parser = OptionParser()
503 parser.add_option("-t", "--test", dest="test", help="Test checks against swf", action="store_true")
504 (options, args) = parser.parse_args()
506 TESTMODE = options.test
508 app = wx.PySimpleApp()
509 model = Model.load(args[0])
511 main = MainFrame(app, model)