; Image Editor ; ------------ ; A small multi-window image tool built from the GUI commands and 2D image commands. ; ; File > Open loads one or more images, each into its own window (or drop image files on a window). ; Drag on an image to select an area. Drag inside the selection to move it, or drag a grip to resize it. ; Edit > Copy copies the selection (or the whole image), Edit > Paste puts it into the active image ; as a floating area you can move and resize. Enter or a click outside applies it; Escape cancels it. ; Image > Scale resizes the whole image; Image > Crop to Selection crops it. ; The arrow keys nudge the selection or pasted area by one pixel. The mouse wheel zooms; ; drag with the right mouse button to scroll a zoomed image. ; The status bar shows the image size, zoom, cursor position and colour, and the selection or paste ; rectangle. File > Save writes PNG or BMP. AppTitle "Image Editor" Const EVENT_KEY_DOWN=$101 Const EVENT_MOUSE_DOWN=$201 Const EVENT_MOUSE_UP=$202 Const EVENT_MOUSE_MOVE=$203 Const EVENT_MOUSE_WHEEL=$204 Const EVENT_MOUSE_LEAVE=$206 Const EVENT_GADGET_ACTION=$401 Const EVENT_FILE_DROP=$404 Const EVENT_WINDOW_SIZE=$802 Const EVENT_WINDOW_CLOSE=$803 Const EVENT_WINDOW_ACTIVATE=$804 Const EVENT_MENU_ACTION=$1001 Const MENU_OPEN=101 Const MENU_SAVE=102 Const MENU_SAVE_AS=103 Const MENU_CLOSE=104 Const MENU_QUIT=199 Const MENU_UNDO=201 Const MENU_COPY=202 Const MENU_PASTE=203 Const MENU_PASTE_NEW=204 Const MENU_SELECT_ALL=205 Const MENU_DESELECT=206 Const MENU_APPLY_PASTE=207 Const MENU_CANCEL_PASTE=208 Const MENU_PASTE_ORIGINAL_SIZE=209 Const MENU_SCALE=301 Const MENU_CROP=302 Const MENU_ZOOM_IN=401 Const MENU_ZOOM_OUT=402 Const MENU_ACTUAL_SIZE=403 Const MENU_FIT=404 Const MENU_SHORTCUTS=501 Const DRAG_NONE=0 Const DRAG_NEW_SELECTION=1 Const DRAG_SELECTION=2 Const DRAG_PASTE=3 Const DRAG_PAN=4 ; Hit-test results: 1-8 are the handles clockwise from the top-left corner, 9 is the inside Const HIT_INSIDE=9 Const UNDO_LIMIT=20 Const ZOOM_STEPS=17 Type Doc Field window,canvas Field image,path$,name$,modified,version Field zoom#,ox#,oy# Field hasSelection,selX,selY,selW,selH Field paste,pasteX,pasteY,pasteW,pasteH Field view,viewKey$,viewX,viewY Field pasteView,pasteViewKey$,pasteViewX,pasteViewY Field cursorIn,cursorX,cursorY Field note$,dirty End Type Type UndoStep Field doc.Doc Field image End Type Global gRunning=True Global gCurrent.Doc=Null Global gClipboard=0 Global gUntitled=0 Global gWindowCount=0 Global gLastWheel=0 Global gDragDoc.Doc=Null Global gDragMode=DRAG_NONE Global gDragHandle=0 Global gDragStartX#,gDragStartY# Global gDragX,gDragY,gDragW,gDragH Global gPanX#,gPanY# Global gScaleWindow=0 Global gScaleDoc.Doc=Null Global gScaleWidth,gScaleHeight,gScalePercent,gScaleKeep,gScaleOK,gScaleCancel Global gScaleLast=0 ; Outputs of helper functions that produce more than one value Global gRenderX,gRenderY Global gHX,gHY Global gRX,gRY,gRW,gRH Dim gZoomStep#(ZOOM_STEPS-1) Restore ZoomSteps For i=0 To ZOOM_STEPS-1 Read gZoomStep(i) Next HotKeyEvent 24,2,EVENT_MENU_ACTION,MENU_OPEN ; Ctrl+O HotKeyEvent 31,2,EVENT_MENU_ACTION,MENU_SAVE ; Ctrl+S HotKeyEvent 31,3,EVENT_MENU_ACTION,MENU_SAVE_AS ; Ctrl+Shift+S HotKeyEvent 17,2,EVENT_MENU_ACTION,MENU_CLOSE ; Ctrl+W HotKeyEvent 16,2,EVENT_MENU_ACTION,MENU_QUIT ; Ctrl+Q HotKeyEvent 44,2,EVENT_MENU_ACTION,MENU_UNDO ; Ctrl+Z HotKeyEvent 46,2,EVENT_MENU_ACTION,MENU_COPY ; Ctrl+C HotKeyEvent 47,2,EVENT_MENU_ACTION,MENU_PASTE ; Ctrl+V HotKeyEvent 47,3,EVENT_MENU_ACTION,MENU_PASTE_NEW ; Ctrl+Shift+V HotKeyEvent 30,2,EVENT_MENU_ACTION,MENU_SELECT_ALL ; Ctrl+A HotKeyEvent 32,2,EVENT_MENU_ACTION,MENU_DESELECT ; Ctrl+D HotKeyEvent 23,3,EVENT_MENU_ACTION,MENU_SCALE ; Ctrl+Shift+I HotKeyEvent 46,3,EVENT_MENU_ACTION,MENU_CROP ; Ctrl+Shift+C HotKeyEvent 13,2,EVENT_MENU_ACTION,MENU_ZOOM_IN ; Ctrl+= HotKeyEvent 12,2,EVENT_MENU_ACTION,MENU_ZOOM_OUT ; Ctrl+- HotKeyEvent 11,2,EVENT_MENU_ACTION,MENU_ACTUAL_SIZE ; Ctrl+0 HotKeyEvent 10,2,EVENT_MENU_ACTION,MENU_FIT ; Ctrl+9 NewDoc(0,"") OpenCommandLine While gRunning For d.Doc=Each Doc If d\dirty Then DrawDoc d Next id=WaitEvent() While id<>0 And gRunning HandleEvent id id=PeekEvent() Wend Wend If gClipboard Then FreeImage gClipboard End .ZoomSteps Data 0.05,0.1,0.125,0.1667,0.25,0.3333,0.5,0.6667,1,1.5,2,3,4,6,8,12,16 ;----------------------------------------------------------------------------- ; Events Function HandleEvent(id) src=EventSource() If gScaleWindow If IsScaleGadget(src) HandleScaleEvent id,src Return EndIf EndIf d.Doc=DocForGadget(src) Select id Case EVENT_MENU_ACTION RunCommand EventData() Case EVENT_WINDOW_CLOSE If d<>Null Then CloseDoc d Case EVENT_WINDOW_SIZE If d<>Null Then d\dirty=True Case EVENT_WINDOW_ACTIVATE If d<>Null Then gCurrent=d Case EVENT_FILE_DROP If d<>Null Then OpenPathList EventText(),d Case EVENT_MOUSE_DOWN If d<>Null gCurrent=d OnMouseDown d,EventData(),EventX()*Dpi(d),EventY()*Dpi(d) EndIf Case EVENT_MOUSE_MOVE If d<>Null Then OnMouseMove d,EventX()*Dpi(d),EventY()*Dpi(d) Case EVENT_MOUSE_UP If d<>Null Then OnMouseUp d Case EVENT_MOUSE_WHEEL If d<>Null Then OnWheel d,EventData() Case EVENT_MOUSE_LEAVE If d<>Null d\cursorIn=False UpdateStatus d EndIf Case EVENT_KEY_DOWN If d<>Null Then OnKey d,EventData() End Select End Function Function RunCommand(command) d.Doc=TargetDoc() If command=MENU_QUIT QuitApp Return ElseIf command=MENU_SHORTCUTS ShowShortcuts Return EndIf If d=Null Then Return d\note="" Select command Case MENU_OPEN : OpenFiles d Case MENU_SAVE : SaveDoc d,False Case MENU_SAVE_AS : SaveDoc d,True Case MENU_CLOSE : CloseDoc d Case MENU_UNDO : UndoDoc d Case MENU_COPY : CopyArea d Case MENU_PASTE : PasteArea d Case MENU_PASTE_NEW : PasteAsNewImage Case MENU_SELECT_ALL : SelectAll d Case MENU_DESELECT : Deselect d Case MENU_APPLY_PASTE If d\paste Then CommitPaste d Case MENU_CANCEL_PASTE If d\paste Then CancelPaste d Case MENU_PASTE_ORIGINAL_SIZE : PasteOriginalSize d Case MENU_SCALE : OpenScaleDialog d Case MENU_CROP : CropToSelection d Case MENU_ZOOM_IN : ZoomStep d,1,CanvasPixelWidth(d\canvas)/2.0,CanvasPixelHeight(d\canvas)/2.0 Case MENU_ZOOM_OUT : ZoomStep d,-1,CanvasPixelWidth(d\canvas)/2.0,CanvasPixelHeight(d\canvas)/2.0 Case MENU_ACTUAL_SIZE : SetZoom d,1,CanvasPixelWidth(d\canvas)/2.0,CanvasPixelHeight(d\canvas)/2.0 Case MENU_FIT : FitZoom d,True End Select If DocAlive(d) Then UpdateStatus d End Function ; The document of the window the user is working in. Menu actions and ; shortcuts carry no source gadget, so they go to the active image window, ; or the last one used while a dialog is in front. Function TargetDoc.Doc() active=ActiveWindow() For d.Doc=Each Doc If d\window=active Then Return d Next If gCurrent<>Null Then Return gCurrent Return First Doc End Function Function DocForGadget.Doc(gadget) If gadget=0 Then Return Null For d.Doc=Each Doc If d\window=gadget Or d\canvas=gadget Then Return d Next Return Null End Function Function DocAlive(target.Doc) For d.Doc=Each Doc If d=target Then Return True Next Return False End Function ;----------------------------------------------------------------------------- ; Windows Function NewDoc.Doc(image,path$) d.Doc=New Doc d\image=image d\path=path d\zoom=1 If path="" gUntitled=gUntitled+1 d\name="Untitled "+gUntitled Else d\name=FileName(path) EndIf width=640 height=440 If image width=IClamp(ImageWidth(image)+16,420,1100) height=IClamp(ImageHeight(image)+16,280,760) EndIf x=60+(gWindowCount Mod 8)*32 y=50+(gWindowCount Mod 8)*32 gWindowCount=gWindowCount+1 d\window=CreateWindow(d\name,x,y,width,height+60,0,15) SetMinWindowSize d\window,320,240 d\canvas=CreateCanvas(0,0,ClientWidth(d\window),ClientHeight(d\window),d\window) SetGadgetLayout d\canvas,1,1,1,1 EnableGadgetFileDrop d\canvas BuildMenus d\window If image Then FitZoom d,False UpdateTitle d UpdateStatus d d\dirty=True gCurrent=d Return d End Function Function BuildMenus(window) root=WindowMenu(window) m=CreateMenu("&File",0,root) CreateMenu "&Open...",MENU_OPEN,m CreateMenu "&Save",MENU_SAVE,m CreateMenu "Save &As...",MENU_SAVE_AS,m CreateMenu "",0,m CreateMenu "&Close Window",MENU_CLOSE,m CreateMenu "E&xit",MENU_QUIT,m m=CreateMenu("&Edit",0,root) CreateMenu "&Undo",MENU_UNDO,m CreateMenu "",0,m CreateMenu "&Copy",MENU_COPY,m CreateMenu "&Paste",MENU_PASTE,m CreateMenu "Paste as &New Image",MENU_PASTE_NEW,m CreateMenu "",0,m CreateMenu "Apply Paste",MENU_APPLY_PASTE,m CreateMenu "Cancel Paste",MENU_CANCEL_PASTE,m CreateMenu "Paste at Original Size",MENU_PASTE_ORIGINAL_SIZE,m CreateMenu "",0,m CreateMenu "Select &All",MENU_SELECT_ALL,m CreateMenu "&Deselect",MENU_DESELECT,m m=CreateMenu("&Image",0,root) CreateMenu "&Scale...",MENU_SCALE,m CreateMenu "&Crop to Selection",MENU_CROP,m m=CreateMenu("&View",0,root) CreateMenu "Zoom &In",MENU_ZOOM_IN,m CreateMenu "Zoom &Out",MENU_ZOOM_OUT,m CreateMenu "&Actual Size",MENU_ACTUAL_SIZE,m CreateMenu "&Fit to Window",MENU_FIT,m m=CreateMenu("&Help",0,root) CreateMenu "&Keyboard Shortcuts",MENU_SHORTCUTS,m UpdateWindowMenu window End Function Function UpdateTitle(d.Doc) t$=d\name If d\image Then t=t+" - "+ImageWidth(d\image)+" x "+ImageHeight(d\image) If d\modified Then t=t+" *" SetGadgetText d\window,t End Function Function CloseDoc(d.Doc) If d\modified And d\image<>0 ActivateWindow d\window answer=Proceed("Save changes to "+d\name+" before closing?") If answer=-1 Then Return False If answer=1 If Not SaveDoc(d,False) Then Return False EndIf EndIf If gScaleDoc=d Then CloseScaleDialog If gDragDoc=d gDragDoc=Null gDragMode=DRAG_NONE EndIf For u.UndoStep=Each UndoStep If u\doc=d FreeImage u\image Delete u EndIf Next If d\image Then FreeImage d\image If d\paste Then FreeImage d\paste If d\view Then FreeImage d\view If d\pasteView Then FreeImage d\pasteView FreeGadget d\window Delete d gCurrent=First Doc If gCurrent=Null Then gRunning=False Return True End Function Function QuitApp() Repeat d.Doc=First Doc If d=Null Then Exit If Not CloseDoc(d) Then Return Forever gRunning=False End Function Function ShowShortcuts() t$="Ctrl (Cmd on a Mac) shortcuts:"+Chr(10)+Chr(10) t=t+"O Open S Save Shift+S Save As W Close"+Chr(10) t=t+"Z Undo C Copy V Paste Shift+V Paste as new image"+Chr(10) t=t+"A Select all D Deselect"+Chr(10) t=t+"Shift+I Scale Shift+C Crop to selection"+Chr(10) t=t+"= Zoom in - Zoom out 0 Actual size 9 Fit"+Chr(10)+Chr(10) t=t+"On the image: Enter applies a paste, Escape cancels it or clears the selection,"+Chr(10) t=t+"arrow keys nudge by one pixel, the wheel zooms, right-drag scrolls." Notify t End Function ;----------------------------------------------------------------------------- ; Opening and saving Function OpenFiles(d.Doc) files$=RequestFiles("Open images","png;jpg;jpeg;bmp;gif;tga;tif;tiff,*") OpenPathList files,d End Function Function OpenPathList(items$,target.Doc) start=1 While start<=Len(items) separator=Instr(items,Chr(10),start) If separator=0 Then separator=Len(items)+1 path$=Trim(Mid(items,start,separator-start)) If path<>"" Then OpenPath path,target start=separator+1 Wend End Function Function OpenPath(path$,target.Doc) image=LoadImage(path) If image=0 Notify "Could not open "+path+Chr(10)+"It may not be an image in a supported format.",True Return EndIf If target<>Null If target\image=0 target\image=image target\path=path target\name=FileName(path) target\modified=False target\version=target\version+1 FitZoom target,False UpdateTitle target UpdateStatus target Return EndIf EndIf NewDoc image,path End Function ; Files named on the command line open at start-up; quote paths with spaces. Function OpenCommandLine() c$=Trim(CommandLine()) While c<>"" If Left(c,1)=Chr(34) e=Instr(c,Chr(34),2) If e=0 Then e=Len(c)+1 path$=Mid(c,2,e-2) c=Trim(Mid(c,e+1)) Else e=Instr(c," ") If e=0 Then e=Len(c)+1 path$=Mid(c,1,e-1) c=Trim(Mid(c,e+1)) EndIf If path<>"" Then OpenPath path,First Doc Wend End Function Function SaveDoc(d.Doc,saveAs) If d\image=0 Then Return False If d\paste Then CommitPaste d path$=d\path ext$=Lower(FileExt(path)) If saveAs Or path="" Or (ext<>"png" And ext<>"bmp") suggested$=d\name If FileExt(suggested)<>"" Then suggested=Left(suggested,Len(suggested)-Len(FileExt(suggested))-1) path=RequestFile("Save image","png,bmp",True,suggested+".png") If path="" Then Return False ext=Lower(FileExt(path)) If ext<>"png" And ext<>"bmp" path=path+".png" ext="png" EndIf EndIf ; SaveImage writes a PNG for a .png name and a BMP for a .bmp name. If Not SaveImage(d\image,path) Notify "Could not save "+path,True Return False EndIf d\path=path d\name=FileName(path) d\modified=False d\note="Saved "+d\name UpdateTitle d UpdateStatus d Return True End Function Function FileName$(path$) For i=Len(path) To 1 Step -1 c$=Mid(path,i,1) If c="/" Or c="\" Then Return Mid(path,i+1) Next Return path End Function Function FileExt$(path$) name$=FileName(path) For i=Len(name) To 1 Step -1 If Mid(name,i,1)="." Then Return Mid(name,i+1) Next Return "" End Function ;----------------------------------------------------------------------------- ; Drawing Function Dpi#(d.Doc) w=GadgetWidth(d\canvas) If w<=0 Then Return 1 r#=Float(CanvasPixelWidth(d\canvas))/w If r<=0 Then Return 1 Return r End Function ; Canvas pixels per image pixel. 100% zoom shows one image pixel per logical ; screen pixel, so images keep their size on high-DPI displays. Function ViewScale#(d.Doc) Return d\zoom*Dpi(d) End Function Function DrawDoc(d.Doc) d\dirty=False SetBuffer CanvasBuffer(d\canvas) cw=CanvasPixelWidth(d\canvas) ch=CanvasPixelHeight(d\canvas) ClsColor 46,46,52 Cls If d\image=0 Color 190,190,200 Text cw/2,ch/2-FontHeight(),"No image open",True,True Text cw/2,ch/2+FontHeight(),"Choose File > Open, or drop image files here",True,True FlipCanvas d\canvas Return EndIf ClampView d s#=ViewScale(d) iw#=ImageWidth(d\image)*s ih#=ImageHeight(d\image)*s viewKey$=d\version+":"+s+":"+d\ox+":"+d\oy+":"+cw+":"+ch If viewKey<>d\viewKey If d\view Then FreeImage d\view d\view=RenderClipped(d\image,d\ox,d\oy,iw,ih,cw,ch,s<1) d\viewX=gRenderX d\viewY=gRenderY d\viewKey=viewKey EndIf Color 18,18,20 Rect Floor(d\ox)-1,Floor(d\oy)-1,Floor(d\ox+iw)-Floor(d\ox)+2,Floor(d\oy+ih)-Floor(d\oy)+2,False If d\view Then DrawBlock d\view,d\viewX,d\viewY If d\paste px#=d\ox+d\pasteX*s py#=d\oy+d\pasteY*s pw#=d\pasteW*s ph#=d\pasteH*s viewKey=d\paste+":"+px+":"+py+":"+pw+":"+ph+":"+cw+":"+ch If viewKey<>d\pasteViewKey If d\pasteView Then FreeImage d\pasteView d\pasteView=RenderClipped(d\paste,px,py,pw,ph,cw,ch,s*d\pasteW/ImageWidth(d\paste)<1) d\pasteViewX=gRenderX d\pasteViewY=gRenderY d\pasteViewKey=viewKey EndIf If d\pasteView Then DrawBlock d\pasteView,d\pasteViewX,d\pasteViewY DrawFrame d,d\pasteX,d\pasteY,d\pasteW,d\pasteH,False ElseIf d\hasSelection DrawFrame d,d\selX,d\selY,d\selW,d\selH,True EndIf FlipCanvas d\canvas End Function ; Renders the part of an image that falls inside the canvas, scaled to the ; rectangle x,y,w,h in canvas pixels. Only the visible part is resized, so ; deep zooms stay cheap. Returns 0 when nothing is visible. Function RenderClipped(source,x#,y#,w#,h#,cw,ch,smooth) sw=ImageWidth(source) sh=ImageHeight(source) kx#=w/sw ky#=h/sh x0=IMax(0,Floor(-x/kx)) y0=IMax(0,Floor(-y/ky)) x1=IMin(sw,Ceil((cw-x)/kx)) y1=IMin(sh,Ceil((ch-y)/ky)) If x1<=x0 Or y1<=y0 Then Return 0 rw=x1-x0 rh=y1-y0 gRenderX=Floor(x+x0*kx) gRenderY=Floor(y+y0*ky) dw=IMax(1,Floor(x+x1*kx)-gRenderX) dh=IMax(1,Floor(y+y1*ky)-gRenderY) region=CreateImage(rw,rh) CopyRect x0,y0,rw,rh,0,0,ImageBuffer(source),ImageBuffer(region) If dw<>rw Or dh<>rh TFormFilter smooth ResizeImage region,dw,dh EndIf Return region End Function ; Draws a selection (dashed) or pasted area (solid) with its resize handles ; and a size label. Function DrawFrame(d.Doc,x,y,w,h,dashed) s#=ViewScale(d) x0=Floor(d\ox+x*s) y0=Floor(d\oy+y*s) x1=Floor(d\ox+(x+w)*s) y1=Floor(d\oy+(y+h)*s) fw=IMax(1,x1-x0) fh=IMax(1,y1-y0) If dashed Color 0,0,0 Rect x0,y0,fw,fh,False Color 255,255,255 For i=0 To fw-1 Step 8 Line x0+i,y0,x0+IMin(i+3,fw-1),y0 Line x0+i,y0+fh-1,x0+IMin(i+3,fw-1),y0+fh-1 Next For i=0 To fh-1 Step 8 Line x0,y0+i,x0,y0+IMin(i+3,fh-1) Line x0+fw-1,y0+i,x0+fw-1,y0+IMin(i+3,fh-1) Next Else Color 0,0,0 Rect x0-1,y0-1,fw+2,fh+2,False Color 70,160,255 Rect x0,y0,fw,fh,False EndIf hs=IMax(3,Floor(3.5*Dpi(d))) For i=1 To 8 HandlePosition i,x0,y0,x0+fw,y0+fh Color 0,0,0 Rect gHX-hs-1,gHY-hs-1,hs*2+3,hs*2+3,True Color 255,255,255 Rect gHX-hs,gHY-hs,hs*2+1,hs*2+1,True Next label$=w+" x "+h+" at "+x+", "+y lw=StringWidth(label)+8 lh=FontHeight()+4 ly=y0+fh+hs+4 If ly+lh>CanvasPixelHeight(d\canvas) Then ly=y0-lh-hs-4 lx=IClamp(x0,0,IMax(0,CanvasPixelWidth(d\canvas)-lw)) Color 0,0,0 Rect lx,ly,lw,lh,True Color 255,255,255 Text lx+4,ly+2,label End Function Function HandlePosition(i,x0,y0,x1,y1) mx=(x0+x1)/2 my=(y0+y1)/2 Select i Case 1 : gHX=x0 : gHY=y0 Case 2 : gHX=mx : gHY=y0 Case 3 : gHX=x1 : gHY=y0 Case 4 : gHX=x1 : gHY=my Case 5 : gHX=x1 : gHY=y1 Case 6 : gHX=mx : gHY=y1 Case 7 : gHX=x0 : gHY=y1 Case 8 : gHX=x0 : gHY=my End Select End Function ; Which grip (1-8) or the inside (HIT_INSIDE) of an image rectangle is under ; canvas pixel px,py; 0 for none. Function HitTest(d.Doc,x,y,w,h,px#,py#) s#=ViewScale(d) x0=Floor(d\ox+x*s) y0=Floor(d\oy+y*s) x1=x0+IMax(1,Floor(d\ox+(x+w)*s)-x0) y1=y0+IMax(1,Floor(d\oy+(y+h)*s)-y0) reach#=6*Dpi(d) For i=1 To 8 HandlePosition i,x0,y0,x1,y1 If Abs(px-gHX)<=reach And Abs(py-gHY)<=reach Then Return i Next If px>=x0 And px=y0 And py0 For i=0 To ZOOM_STEPS-1 If gZoomStep(i)>d\zoom*1.01 SetZoom d,gZoomStep(i),px,py Return EndIf Next Else For i=ZOOM_STEPS-1 To 0 Step -1 If gZoomStep(i)1 And (Not allowEnlarge) Then zoom=1 d\zoom=FClamp(zoom,gZoomStep(0),gZoomStep(ZOOM_STEPS-1)) d\ox=0 d\oy=0 ClampView d d\dirty=True End Function ;----------------------------------------------------------------------------- ; Mouse and keyboard Function OnMouseDown(d.Doc,button,px#,py#) ActivateGadget d\canvas d\note="" If d\image=0 Then Return s#=ViewScale(d) ix#=(px-d\ox)/s iy#=(py-d\oy)/s If button<>1 BeginDrag d,DRAG_PAN,0,px,py,0,0,0,0 gPanX=d\ox gPanY=d\oy Return EndIf If d\paste hit=HitTest(d,d\pasteX,d\pasteY,d\pasteW,d\pasteH,px,py) If hit BeginDrag d,DRAG_PASTE,hit,ix,iy,d\pasteX,d\pasteY,d\pasteW,d\pasteH Return EndIf ; A click outside the pasted area applies it. CommitPaste d d\hasSelection=False UpdateStatus d Return EndIf If d\hasSelection hit=HitTest(d,d\selX,d\selY,d\selW,d\selH,px,py) If hit BeginDrag d,DRAG_SELECTION,hit,ix,iy,d\selX,d\selY,d\selW,d\selH Return EndIf EndIf ax=IClamp(Floor(ix+0.5),0,ImageWidth(d\image)) ay=IClamp(Floor(iy+0.5),0,ImageHeight(d\image)) BeginDrag d,DRAG_NEW_SELECTION,0,ix,iy,ax,ay,0,0 d\hasSelection=False d\dirty=True UpdateStatus d End Function Function BeginDrag(d.Doc,mode,grip,startX#,startY#,x,y,w,h) gDragDoc=d gDragMode=mode gDragHandle=grip gDragStartX=startX gDragStartY=startY gDragX=x gDragY=y gDragW=w gDragH=h End Function Function OnMouseMove(d.Doc,px#,py#) If d\image=0 Then Return s#=ViewScale(d) ix#=(px-d\ox)/s iy#=(py-d\oy)/s d\cursorIn=True d\cursorX=Floor(ix) d\cursorY=Floor(iy) If gDragDoc=d iw=ImageWidth(d\image) ih=ImageHeight(d\image) dx=Floor(ix-gDragStartX+0.5) dy=Floor(iy-gDragStartY+0.5) Select gDragMode Case DRAG_PAN d\ox=gPanX+px-gDragStartX d\oy=gPanY+py-gDragStartY ClampView d Case DRAG_NEW_SELECTION cx=IClamp(Floor(ix+0.5),0,iw) cy=IClamp(Floor(iy+0.5),0,ih) d\selX=IMin(gDragX,cx) d\selY=IMin(gDragY,cy) d\selW=Abs(cx-gDragX) d\selH=Abs(cy-gDragY) d\hasSelection=(d\selW>0 And d\selH>0) Case DRAG_SELECTION If gDragHandle=HIT_INSIDE d\selX=IClamp(gDragX+dx,0,iw-gDragW) d\selY=IClamp(gDragY+dy,0,ih-gDragH) Else ResizeRect gDragHandle,gDragX,gDragY,gDragW,gDragH,dx,dy ClampRectToImage d d\selX=gRX d\selY=gRY d\selW=gRW d\selH=gRH EndIf Case DRAG_PASTE If gDragHandle=HIT_INSIDE d\pasteX=gDragX+dx d\pasteY=gDragY+dy Else ResizeRect gDragHandle,gDragX,gDragY,gDragW,gDragH,dx,dy d\pasteX=gRX d\pasteY=gRY d\pasteW=gRW d\pasteH=gRH EndIf End Select d\dirty=True EndIf UpdateStatus d End Function Function OnMouseUp(d.Doc) If gDragDoc=d gDragDoc=Null gDragMode=DRAG_NONE UpdateStatus d EndIf End Function Function OnWheel(d.Doc,detents) If d\image=0 Or detents=0 Then Return ; Trackpads send a burst of wheel events; take one zoom step per burst. If MilliSecs()-gLastWheel<60 Then Return gLastWheel=MilliSecs() ZoomStep d,Sgn(detents),CanvasMouseX(d\canvas),CanvasMouseY(d\canvas) UpdateStatus d End Function Function OnKey(d.Doc,key) If d\image=0 Then Return dx=0 dy=0 Select key Case 1 If d\paste CancelPaste d Else d\hasSelection=False EndIf Case 28,156 If d\paste Then CommitPaste d Case 203 : dx=-1 Case 205 : dx=1 Case 200 : dy=-1 Case 208 : dy=1 End Select If dx<>0 Or dy<>0 If d\paste d\pasteX=d\pasteX+dx d\pasteY=d\pasteY+dy ElseIf d\hasSelection d\selX=IClamp(d\selX+dx,0,ImageWidth(d\image)-d\selW) d\selY=IClamp(d\selY+dy,0,ImageHeight(d\image)-d\selH) EndIf EndIf d\dirty=True UpdateStatus d End Function ; Moves the edges of a rectangle named by a grip; the result is in gRX..gRH. Function ResizeRect(grip,x,y,w,h,dx,dy) x0=x y0=y x1=x+w y1=y+h If grip=1 Or grip=7 Or grip=8 Then x0=x0+dx If grip=3 Or grip=4 Or grip=5 Then x1=x1+dx If grip=1 Or grip=2 Or grip=3 Then y0=y0+dy If grip=5 Or grip=6 Or grip=7 Then y1=y1+dy gRX=IMin(x0,x1) gRY=IMin(y0,y1) gRW=IMax(1,Abs(x1-x0)) gRH=IMax(1,Abs(y1-y0)) End Function Function ClampRectToImage(d.Doc) iw=ImageWidth(d\image) ih=ImageHeight(d\image) x0=IClamp(gRX,0,iw-1) y0=IClamp(gRY,0,ih-1) gRW=IMax(1,IMin(gRX+gRW,iw)-x0) gRH=IMax(1,IMin(gRY+gRH,ih)-y0) gRX=x0 gRY=y0 End Function Function UpdateStatus(d.Doc) If d\image=0 SetStatusText d\window,"No image - File > Open, or drop image files on the window" Return EndIf iw=ImageWidth(d\image) ih=ImageHeight(d\image) t$=iw+" x "+ih+" px Zoom "+Int(d\zoom*100)+"%" If d\cursorIn And d\cursorX>=0 And d\cursorY>=0 And d\cursorX"" Then t=t+" "+d\note SetStatusText d\window,t End Function ;----------------------------------------------------------------------------- ; Editing Function MarkChanged(d.Doc) d\modified=True d\version=d\version+1 d\dirty=True UpdateTitle d End Function Function PushUndo(d.Doc) u.UndoStep=New UndoStep u\doc=d u\image=CopyImage(d\image) count=0 For u=Each UndoStep If u\doc=d Then count=count+1 Next If count>UNDO_LIMIT For u=Each UndoStep If u\doc=d FreeImage u\image Delete u Exit EndIf Next EndIf End Function Function UndoDoc(d.Doc) If d\paste CancelPaste d Return EndIf newest.UndoStep=Null For u.UndoStep=Each UndoStep If u\doc=d Then newest=u Next If newest=Null d\note="Nothing to undo" Return EndIf If d\image Then FreeImage d\image d\image=newest\image Delete newest d\hasSelection=False MarkChanged d ClampView d End Function Function SelectAll(d.Doc) If d\image=0 Then Return If d\paste Then CommitPaste d d\selX=0 d\selY=0 d\selW=ImageWidth(d\image) d\selH=ImageHeight(d\image) d\hasSelection=True d\dirty=True End Function Function Deselect(d.Doc) If d\paste Then CommitPaste d d\hasSelection=False d\dirty=True End Function Function ScaledCopy(source,w,h) result=CopyImage(source) If ImageWidth(result)<>w Or ImageHeight(result)<>h TFormFilter True ResizeImage result,w,h EndIf Return result End Function ; Copies the pasted area at its current size, else the selection, else the whole image. Function CopyArea(d.Doc) If d\image=0 Then Return If gClipboard Then FreeImage gClipboard If d\paste gClipboard=ScaledCopy(d\paste,d\pasteW,d\pasteH) ElseIf d\hasSelection gClipboard=CreateImage(d\selW,d\selH) CopyRect d\selX,d\selY,d\selW,d\selH,0,0,ImageBuffer(d\image),ImageBuffer(gClipboard) Else gClipboard=CopyImage(d\image) EndIf d\note="Copied "+ImageWidth(gClipboard)+" x "+ImageHeight(gClipboard) End Function ; Pastes as a floating area over the selection, or in the middle of the view. Function PasteArea(d.Doc) If gClipboard=0 d\note="Nothing to paste - copy an area first" Return EndIf If d\image=0 d\image=CopyImage(gClipboard) MarkChanged d FitZoom d,False Return EndIf If d\paste Then CommitPaste d d\paste=CopyImage(gClipboard) d\pasteW=ImageWidth(d\paste) d\pasteH=ImageHeight(d\paste) If d\hasSelection d\pasteX=d\selX d\pasteY=d\selY Else s#=ViewScale(d) cx=IClamp(Floor((CanvasPixelWidth(d\canvas)/2-d\ox)/s),0,ImageWidth(d\image)) cy=IClamp(Floor((CanvasPixelHeight(d\canvas)/2-d\oy)/s),0,ImageHeight(d\image)) d\pasteX=cx-d\pasteW/2 d\pasteY=cy-d\pasteH/2 EndIf d\hasSelection=False d\note="Drag to move, drag the handles to resize, Enter to apply" d\dirty=True End Function Function PasteAsNewImage() If gClipboard=0 Notify "Nothing to paste - copy an area first." Return EndIf d.Doc=NewDoc(CopyImage(gClipboard),"") d\modified=True UpdateTitle d End Function Function PasteOriginalSize(d.Doc) If d\paste=0 Then Return d\pasteW=ImageWidth(d\paste) d\pasteH=ImageHeight(d\paste) d\dirty=True End Function ; Stamps the floating pasted area into the image; it stays selected. Function CommitPaste(d.Doc) If d\paste=0 Then Return PushUndo d stamp=ScaledCopy(d\paste,d\pasteW,d\pasteH) iw=ImageWidth(d\image) ih=ImageHeight(d\image) sx=0 sy=0 dx=d\pasteX dy=d\pasteY w=ImageWidth(stamp) h=ImageHeight(stamp) If dx<0 sx=-dx w=w+dx dx=0 EndIf If dy<0 sy=-dy h=h+dy dy=0 EndIf w=IMin(w,iw-dx) h=IMin(h,ih-dy) d\hasSelection=False If w>0 And h>0 CopyRect sx,sy,w,h,dx,dy,ImageBuffer(stamp),ImageBuffer(d\image) d\selX=dx d\selY=dy d\selW=w d\selH=h d\hasSelection=True EndIf FreeImage stamp FreeImage d\paste d\paste=0 MarkChanged d End Function Function CancelPaste(d.Doc) If d\paste=0 Then Return FreeImage d\paste d\paste=0 d\dirty=True End Function Function CropToSelection(d.Doc) If d\image=0 Then Return If d\paste Then CommitPaste d If Not d\hasSelection Notify "Select an area to crop to first: drag on the image." Return EndIf PushUndo d cropped=CreateImage(d\selW,d\selH) CopyRect d\selX,d\selY,d\selW,d\selH,0,0,ImageBuffer(d\image),ImageBuffer(cropped) FreeImage d\image d\image=cropped d\hasSelection=False MarkChanged d ClampView d End Function ;----------------------------------------------------------------------------- ; Scale dialog Function OpenScaleDialog(d.Doc) If d\image=0 Then Return If d\paste Then CommitPaste d If gScaleWindow Then CloseScaleDialog gScaleDoc=d iw=ImageWidth(d\image) ih=ImageHeight(d\image) gScaleWindow=CreateWindow("Scale Image",GadgetX(d\window)+60,GadgetY(d\window)+60,300,250,d\window,1) CreateLabel "Current size: "+iw+" x "+ih+" pixels",16,14,268,20,gScaleWindow CreateLabel "Width",16,48,70,20,gScaleWindow gScaleWidth=CreateSpinBox(90,44,110,24,gScaleWindow) CreateLabel "pixels",208,48,70,20,gScaleWindow CreateLabel "Height",16,80,70,20,gScaleWindow gScaleHeight=CreateSpinBox(90,76,110,24,gScaleWindow) CreateLabel "pixels",208,80,70,20,gScaleWindow CreateLabel "Percent",16,112,70,20,gScaleWindow gScalePercent=CreateSpinBox(90,108,110,24,gScaleWindow) CreateLabel "%",208,112,70,20,gScaleWindow gScaleKeep=CreateButton("Keep proportions",16,142,260,22,gScaleWindow,2) SetButtonState gScaleKeep,True gScaleCancel=CreateButton("Cancel",104,178,84,28,gScaleWindow,5) gScaleOK=CreateButton("Scale",196,178,84,28,gScaleWindow,4) SetSpinBoxRange gScaleWidth,1,16384 SetSpinBoxRange gScaleHeight,1,16384 SetSpinBoxRange gScalePercent,1,1000 SetSpinBoxValue gScaleWidth,iw SetSpinBoxValue gScaleHeight,ih SetSpinBoxValue gScalePercent,100 gScaleLast=0 ActivateGadget gScaleWidth End Function Function IsScaleGadget(gadget) If gadget=0 Then Return False Select gadget Case gScaleWindow,gScaleWidth,gScaleHeight,gScalePercent,gScaleKeep,gScaleOK,gScaleCancel Return True End Select Return False End Function Function HandleScaleEvent(id,source) If id=EVENT_WINDOW_CLOSE CloseScaleDialog Return EndIf If id<>EVENT_GADGET_ACTION Then Return Select source Case gScaleWidth gScaleLast=0 SyncScaleFields Case gScaleHeight gScaleLast=1 SyncScaleFields Case gScalePercent gScaleLast=2 SyncScaleFields Case gScaleKeep SyncScaleFields Case gScaleOK SyncScaleFields ApplyScale gScaleDoc,SpinBoxValue(gScaleWidth),SpinBoxValue(gScaleHeight) CloseScaleDialog Case gScaleCancel CloseScaleDialog End Select End Function ; Fills the other fields from the one edited last. Function SyncScaleFields() iw#=ImageWidth(gScaleDoc\image) ih#=ImageHeight(gScaleDoc\image) w=SpinBoxValue(gScaleWidth) h=SpinBoxValue(gScaleHeight) keep=ButtonState(gScaleKeep) If gScaleLast=2 p#=SpinBoxValue(gScalePercent)/100.0 w=IMax(1,Int(iw*p)) h=IMax(1,Int(ih*p)) ElseIf keep And gScaleLast=1 w=IMax(1,Int(h*iw/ih)) ElseIf keep h=IMax(1,Int(w*ih/iw)) EndIf SetSpinBoxValue gScaleWidth,w SetSpinBoxValue gScaleHeight,h If gScaleLast<>2 Then SetSpinBoxValue gScalePercent,Int(100.0*w/iw) End Function Function ApplyScale(d.Doc,w,h) If w=ImageWidth(d\image) And h=ImageHeight(d\image) Then Return PushUndo d TFormFilter True ResizeImage d\image,w,h d\hasSelection=False d\note="Scaled to "+ImageWidth(d\image)+" x "+ImageHeight(d\image) MarkChanged d ClampView d UpdateStatus d End Function Function CloseScaleDialog() If gScaleWindow=0 Then Return FreeGadget gScaleWindow gScaleWindow=0 If gScaleDoc<>Null Then ActivateWindow gScaleDoc\window gScaleDoc=Null End Function ;----------------------------------------------------------------------------- ; Maths Function IMin(a,b) If ab Then Return a Return b End Function Function IClamp(v,low,high) If vhigh Then Return high Return v End Function Function FMin#(a#,b#) If ahigh Then Return high Return v End Function