#!/usr/bin/env ruby require 'tk' begin # try to use Img extension require 'tkextlib/tkimg' rescue Exception # cannot use Img extention --> ignore end ############################ # scrolled_canvas class TkScrolledCanvas < TkCanvas include TkComposite def initialize_composite(keys={}) @h_scr = TkScrollbar.new(@frame) @v_scr = TkScrollbar.new(@frame) @canvas = TkCanvas.new(@frame) @path = @canvas.path @canvas.xscrollbar(@h_scr) @canvas.yscrollbar(@v_scr) TkGrid.rowconfigure(@frame, 0, :weight=>1, :minsize=>0) TkGrid.columnconfigure(@frame, 0, :weight=>1, :minsize=>0) @canvas.grid(:row=>0, :column=>0, :sticky=>'news') @h_scr.grid(:row=>1, :column=>0, :sticky=>'ew') @v_scr.grid(:row=>0, :column=>1, :sticky=>'ns') delegate('DEFAULT', @canvas) delegate('background', @canvas, @h_scr, @v_scr) delegate('activebackground', @h_scr, @v_scr) delegate('troughcolor', @h_scr, @v_scr) delegate('repeatdelay', @h_scr, @v_scr) delegate('repeatinterval', @h_scr, @v_scr) delegate('borderwidth', @frame) delegate('relief', @frame) delegate_alias('canvasborderwidth', 'borderwidth', @canvas) delegate_alias('canvasrelief', 'relief', @canvas) delegate_alias('scrollbarborderwidth', 'borderwidth', @h_scr, @v_scr) delegate_alias('scrollbarrelief', 'relief', @h_scr, @v_scr) configure(keys) unless keys.empty? end end ############################ class PhotoCanvas < TkScrolledCanvas USAGE = <<EOT --- WHAT IS --- You can write comments on the loaded image, and save it as a Postscipt file (original image file is not modified). Each comment is drawn as a set of an indicator circle, an arrow, and a memo text. See the following how to write comments. This can save the list of memo texts to another file. It may useful to search the saved Postscript file by the comments on them. This may not support multibyte characters (multibyte texts are broken on a Postscript file). It depends on features of canvas widgets of Tcl/Tk libraries linked your Ruby/Tk. If you use Tcl/Tk8.0-jp (Japanized Tcl/Tk), you can (possibly) get a Japanese Postscript file. --- BINDINGS --- * Button-1 : draw comments by following steps 1st - Set center of a indicator circle. 2nd - Set head position of an arrow. 3rd - Set tail position of an arrow, and show an entry box. Input a memo text and hit 'Enter' key to entry the comment. * Button-2-drag : scroll the canvas * Button-3 : when drawing, cancel current drawing * Double-Button-3 : delete the clicked comment (text, arrow, and circle) EOT def initialize(*args) super(*args) self.highlightthickness = 0 self.selectborderwidth = 0 @photo = TkPhotoImage.new @img = TkcImage.new(self, 0, 0, :image=>@photo) width = self.width height = self.height @scr_region = [-width, -height, width, height] self.scrollregion(@scr_region) self.xview_moveto(0.25) self.yview_moveto(0.25) @col = 'red' @font = 'Helvetica -12' @memo_id_num = -1 @memo_id_head = 'memo_' @memo_id_tag = nil @overlap_d = 2 @state = TkVariable.new @border = 2 @selectborder = 1 @delta = @border + @selectborder @entry = TkEntry.new(self, :relief=>:ridge, :borderwidth=>@border, :selectborderwidth=>@selectborder, :highlightthickness=>0) @entry.bind('Return'){@state.value = 0} @mode = old_mode = 0 _state0() bind('2', :x, :y){|x,y| scan_mark(x,y)} bind('B2-Motion', :x, :y){|x,y| scan_dragto(x,y)} bind('3'){ next if (old_mode = @mode) == 0 @items.each{|item| item.delete } _state0() } bind('Double-3', :widget, :x, :y){|w, x, y| next if old_mode != 0 x = w.canvasx(x) y = w.canvasy(y) tag = nil w.find_overlapping(x - @overlap_d, y - @overlap_d, x + @overlap_d, y + @overlap_d).find{|item| ! (item.tags.find{|name| if name =~ /^(#{@memo_id_head}\d+)$/ tag = $1 end }.empty?) } w.delete(tag) if tag } end #----------------------------------- private def _state0() # init @mode = 0 @memo_id_num += 1 @memo_id_tag = @memo_id_head + @memo_id_num.to_s @target = nil @items = [] @mark = [0, 0] bind_remove('Motion') bind('ButtonRelease-1', proc{|x,y| _state1(x,y)}, '%x', '%y') end def _state1(x,y) # set center @mode = 1 @target = TkcOval.new(self, [canvasx(x), canvasy(y)], [canvasx(x), canvasy(y)], :outline=>@col, :width=>3, :tags=>[@memo_id_tag]) @items << @target @mark = [x,y] bind('Motion', proc{|x,y| _state2(x,y)}, '%x', '%y') bind('ButtonRelease-1', proc{|x,y| _state3(x,y)}, '%x', '%y') end def _state2(x,y) # create circle @mode = 2 r = Integer(Math.sqrt((x-@mark[0])**2 + (y-@mark[1])**2)) @target.coords([canvasx(@mark[0] - r), canvasy(@mark[1] - r)], [canvasx(@mark[0] + r), canvasy(@mark[1] + r)]) end def _state3(x,y) # set line start @mode = 3 @target = TkcLine.new(self, [canvasx(x), canvasy(y)], [canvasx(x), canvasy(y)], :arrow=>:first, :arrowshape=>[10, 14, 5], :fill=>@col, :tags=>[@memo_id_tag]) @items << @target @mark = [x, y] bind('Motion', proc{|x,y| _state4(x,y)}, '%x', '%y') bind('ButtonRelease-1', proc{|x,y| _state5(x,y)}, '%x', '%y') end def _state4(x,y) # create line @mode = 4 @target.coords([canvasx(@mark[0]), canvasy(@mark[1])], [canvasx(x), canvasy(y)]) end def _state5(x,y) # set text @mode = 5 if x - @mark[0] >= 0 justify = 'left' dx = - @delta if y - @mark[1] >= 0 anchor = 'nw' dy = - @delta else anchor = 'sw' dy = @delta end else justify = 'right' dx = @delta if y - @mark[1] >= 0 anchor = 'ne' dy = - @delta else anchor = 'se' dy = @delta end end bind_remove('Motion') @entry.value = '' @entry.configure(:justify=>justify, :font=>@font, :foreground=>@col) ewin = TkcWindow.new(self, [canvasx(x)+dx, canvasy(y)+dy], :window=>@entry, :state=>:normal, :anchor=>anchor, :tags=>[@memo_id_tag]) @entry.focus @entry.grab @state.wait @entry.grab_release ewin.delete @target = TkcText.new(self, [canvasx(x), canvasy(y)], :anchor=>anchor, :justify=>justify, :fill=>@col, :font=>@font, :text=>@entry.value, :tags=>[@memo_id_tag]) _state0() end #----------------------------------- public def load_photo(filename) @photo.configure(:file=>filename) end def modified? ! ((find_withtag('all') - [@img]).empty?) end def fig_erase (find_withtag('all') - [@img]).each{|item| item.delete} end def reset_region width = @photo.width height = @photo.height if width > @scr_region[2] @scr_region[0] = -width @scr_region[2] = width end if height > @scr_region[3] @scr_region[1] = -height @scr_region[3] = height end self.scrollregion(@scr_region) self.xview_moveto(0.25) self.yview_moveto(0.25) end def get_texts ret = [] find_withtag('all').each{|item| if item.kind_of?(TkcText) ret << item[:text] end } ret end end ############################ # define methods for menu def open_file(canvas, fname) if canvas.modified? ret = Tk.messageBox(:icon=>'warning',:type=>'okcancel',:default=>'cancel', :message=>'Canvas may be modified. Realy erase? ') return if ret == 'cancel' end filetypes = [ ['GIF Files', '.gif'], ['GIF Files', [], 'GIFF'], ['PPM Files', '.ppm'], ['PGM Files', '.pgm'] ] begin if Tk::Img::package_version != '' filetypes << ['JPEG Files', ['.jpg', '.jpeg']] filetypes << ['PNG Files', '.png'] filetypes << ['PostScript Files', '.ps'] filetypes << ['PDF Files', '.pdf'] filetypes << ['Windows Bitmap Files', '.bmp'] filetypes << ['Windows Icon Files', '.ico'] filetypes << ['PCX Files', '.pcx'] filetypes << ['Pixmap Files', '.pixmap'] filetypes << ['SGI Files', '.sgi'] filetypes << ['Sun Raster Files', '.sun'] filetypes << ['TGA Files', '.tga'] filetypes << ['TIFF Files', '.tiff'] filetypes << ['XBM Files', '.xbm'] filetypes << ['XPM Files', '.xpm'] end rescue end filetypes << ['ALL Files', '*'] fpath = Tk.getOpenFile(:filetypes=>filetypes) return if fpath.empty? begin canvas.load_photo(fpath) rescue => e Tk.messageBox(:icon=>'error', :type=>'ok', :message=>"Fail to read '#{fpath}'.\n#{e.message}") end canvas.fig_erase canvas.reset_region fname.value = fpath end # -------------------------------- def save_memo(canvas, fname) initname = fname.value if initname != '-' initname = File.basename(initname, File.extname(initname)) fpath = Tk.getSaveFile(:filetypes=>[ ['Text Files', '.txt'], ['ALL Files', '*'] ], :initialfile=>initname) else fpath = Tk.getSaveFile(:filetypes=>[ ['Text Files', '.txt'], ['ALL Files', '*'] ]) end return if fpath.empty? begin fid = open(fpath, 'w') rescue => e Tk.messageBox(:icon=>'error', :type=>'ok', :message=>"Fail to open '#{fname.value}'.\n#{e.message}") end begin canvas.get_texts.each{|txt| fid.print(txt, "\n") } ensure fid.close end end # -------------------------------- def ps_print(canvas, fname) initname = fname.value if initname != '-' initname = File.basename(initname, File.extname(initname)) fpath = Tk.getSaveFile(:filetypes=>[ ['Postscript Files', '.ps'], ['ALL Files', '*'] ], :initialfile=>initname) else fpath = Tk.getSaveFile(:filetypes=>[ ['Postscript Files', '.ps'], ['ALL Files', '*'] ]) end return if fpath.empty? bbox = canvas.bbox('all') canvas.postscript(:file=>fpath, :x=>bbox[0], :y=>bbox[1], :width=>bbox[2] - bbox[0], :height=>bbox[3] - bbox[1]) end # -------------------------------- def quit(canvas) ret = Tk.messageBox(:icon=>'warning', :type=>'okcancel', :default=>'cancel', :message=>'Realy quit? ') exit if ret == 'ok' end # -------------------------------- # setup root root = TkRoot.new(:title=>'Fig Memo') # create canvas frame canvas = PhotoCanvas.new(root).pack(:fill=>:both, :expand=>true) usage_frame = TkFrame.new(root, :relief=>:ridge, :borderwidth=>2) hide_btn = TkButton.new(usage_frame, :text=>'hide usage', :font=>{:size=>8}, :pady=>1, :command=>proc{usage_frame.unpack}) hide_btn.pack(:anchor=>'e', :padx=>5) usage = TkLabel.new(usage_frame, :text=>PhotoCanvas::USAGE, :font=>'Helvetica 8', :justify=>:left).pack show_usage = proc{ usage_frame.pack(:before=>canvas, :fill=>:x, :expand=>true) } fname = TkVariable.new('-') f = TkFrame.new(root, :relief=>:sunken, :borderwidth=>1).pack(:fill=>:x) label = TkLabel.new(f, :textvariable=>fname, :font=>{:size=>-12, :weight=>:bold}, :anchor=>'w').pack(:side=>:left, :fill=>:x, :padx=>10) # create menu mspec = [ [ ['File', 0], ['Show Usage', proc{show_usage.call}, 5], '---', ['Open Image File', proc{open_file(canvas, fname)}, 0], ['Save Memo Texts', proc{save_memo(canvas, fname)}, 0], '---', ['Save Postscript', proc{ps_print(canvas, fname)}, 5], '---', ['Quit', proc{quit(canvas)}, 0] ] ] root.add_menubar(mspec) # manage wm_protocol root.protocol(:WM_DELETE_WINDOW){quit(canvas)} # show usage show_usage.call # -------------------------------- # start eventloop Tk.mainloop