Libraryless. Click here for Pure Java version (19144L/119K).
1 | sclass ImageSurface extends Surface { |
2 | BufferedImage image; |
3 | double zoomX = 1, zoomY = 1, zoomFactor = 1.5; |
4 | private Rectangle selection; |
5 | new L<AutoCloseable> tools; |
6 | |
7 | // use overlays now |
8 | O overlay; // voidfunc(Graphics2D) |
9 | L<G2Drawable> overlays = syncL(); |
10 | |
11 | Runnable onSelectionChange; |
12 | settable bool verbose; |
13 | bool noMinimumSize = true; |
14 | S titleForUpload; |
15 | O onZoom; |
16 | bool specialPurposed; // true = don't show image changing commands in popup menu |
17 | settable bool allowPaste; |
18 | settable bool zoomable = true; |
19 | bool noAlpha; // set to true to speed up drawing if you don't use alpha |
20 | O interpolationMode = RenderingHints.VALUE_INTERPOLATION_BILINEAR; |
21 | O onNewImage; // use imageChanged event instead |
22 | BufferedImage imageToDraw; // if you want to draw a different image |
23 | File file; // where image was loaded from |
24 | bool autoZoomToDisplay; // only works 100% when not in scrollpane |
25 | settable bool repaintInThread; // after setImage, repaint in same thread |
26 | BoolVar showingVar; |
27 | |
28 | Pt mousePosition; |
29 | event mousePositionChanged; |
30 | event imageChanged; |
31 | event userModifiedImage(BufferedImage image); |
32 | |
33 | public ImageSurface() { |
34 | this(dummyImage()); |
35 | } |
36 | |
37 | static BufferedImage dummyImage() { |
38 | ret whiteImage(1); |
39 | } |
40 | |
41 | *(File file) { |
42 | setImage(file); |
43 | } |
44 | |
45 | *(MakesBufferedImage image) { |
46 | this(image != null ? image.getBufferedImage() : dummyImage()); |
47 | } |
48 | |
49 | *(BufferedImage image) { |
50 | setImage(image); |
51 | clearSurface = false; |
52 | |
53 | // perform auto-zoom when shown, resized or when parent resized |
54 | bindToComponent(this, l0 performAutoZoom, null); |
55 | onResize(this, l0 performAutoZoom); |
56 | onEnclosingScrollPaneResize(this, l0 performAutoZoom); |
57 | |
58 | componentPopupMenu2(this, ImageSurface_popupMenuMaker()); |
59 | new ImageSurfaceSelector(this); |
60 | |
61 | jHandleFileDrop(this, voidfunc(File f) { setImage(loadBufferedImage(f)) }); |
62 | |
63 | imageSurfaceOnHover(this, p -> { |
64 | mousePosition = p; |
65 | mousePositionChanged(); |
66 | }); |
67 | } |
68 | |
69 | public ImageSurface(RGBImage image, double zoom) { |
70 | this(image); |
71 | setZoom(zoom); |
72 | } |
73 | |
74 | // point is already in image coordinates |
75 | protected void fillPopupMenu(JPopupMenu menu, final Point point) { |
76 | if (zoomable) { |
77 | JMenuItem miZoomReset = new JMenuItem("Zoom 100%"); |
78 | miZoomReset.addActionListener(new ActionListener() { |
79 | public void actionPerformed(ActionEvent evt) { |
80 | setZoom(1.0); |
81 | centerPoint(point); |
82 | } |
83 | }); |
84 | menu.add(miZoomReset); |
85 | |
86 | JMenuItem miZoomIn = new JMenuItem("Zoom in"); |
87 | miZoomIn.addActionListener(new ActionListener() { |
88 | public void actionPerformed(ActionEvent evt) { |
89 | zoomIn(zoomFactor); |
90 | centerPoint(point); |
91 | } |
92 | }); |
93 | menu.add(miZoomIn); |
94 | |
95 | JMenuItem miZoomOut = new JMenuItem("Zoom out"); |
96 | miZoomOut.addActionListener(new ActionListener() { |
97 | public void actionPerformed(ActionEvent evt) { |
98 | zoomOut(zoomFactor); |
99 | centerPoint(point); |
100 | } |
101 | }); |
102 | menu.add(miZoomOut); |
103 | |
104 | menu.add(jMenuItemStayCheckedOnClick("Zoom to window", |
105 | -> autoZoomToDisplay, |
106 | -> setAutoZoomToDisplay(true))); |
107 | |
108 | addMenuItem(menu, "Show full screen", r { showFullScreen() }); |
109 | |
110 | addMenuItem(menu, "Point: " + point.x + "," + point.y + " (image: " + w() + "*" + h() + ")", null); |
111 | |
112 | menu.addSeparator(); |
113 | } |
114 | |
115 | if (!specialPurposed) |
116 | addMenuItem(menu, "Load image...", r { selectFile("Load image", |
117 | voidfunc(File f) { setImage(loadImage2(f)) }) }); |
118 | addMenuItem(menu, "Save image...", r { saveImage() }); |
119 | ifdef ImageSurface_AllowUpload |
120 | addMenuItem(menu, "Upload image...", r { uploadTheImage() }); |
121 | endifdef |
122 | addMenuItem(menu, "Copy image to clipboard", r { copyImageToClipboard(getImage()) }); |
123 | if (!specialPurposed || allowPaste) |
124 | addMenuItem(menu, "Paste image from clipboard", r { loadFromClipboard() }); |
125 | |
126 | if (!specialPurposed) |
127 | addMenuItem(menu, "Load image snippet...", r { |
128 | selectImageSnippet(voidfunc(S imageID) { |
129 | setImage(loadImage2(imageID)) |
130 | }); |
131 | }); |
132 | if (selection != null) |
133 | addMenuItem(menu, "Crop", r { crop() }); |
134 | if (!specialPurposed) |
135 | addMenuItem(menu, "No image", r { noImage() }); |
136 | } |
137 | |
138 | void noImage() { setImage((BufferedImage) null); } |
139 | |
140 | void crop() { |
141 | if (selection == null) ret; |
142 | BufferedImage img = cloneClipBufferedImage(getImage(), selection); |
143 | selection = null; |
144 | setUserModifiedImage(img); |
145 | } |
146 | |
147 | void setUserModifiedImage(BufferedImage img) { |
148 | setImage(img); |
149 | userModifiedImage(img); |
150 | } |
151 | |
152 | void loadFromClipboard() { |
153 | BufferedImage img = getImageFromClipboard(); |
154 | if (img != null) |
155 | setUserModifiedImage(img); |
156 | } |
157 | |
158 | swappable File defaultImageDir() { ret getProgramDir(); } |
159 | |
160 | void saveImage() { |
161 | var image = getImage(); |
162 | JFileChooser fileChooser = new JFileChooser(defaultImageDir()); |
163 | if (fileChooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) { |
164 | try { |
165 | main saveImage(image, file = fileChooser.getSelectedFile()); |
166 | } catch e { |
167 | popup(e); |
168 | } |
169 | } |
170 | } |
171 | |
172 | void drawImageItself(int w, int h, Graphics2D g) { |
173 | int iw = getZoomedWidth(), ih = getZoomedHeight(); |
174 | if (interpolationMode == RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR || zoomX >= 1 || zoomY >= 1) { |
175 | // faster |
176 | g.drawImage(image, 0, 0, iw, ih, null); |
177 | } else |
178 | g.drawImage(resizeImage(image, iw, ih), 0, 0, null); // smoother |
179 | } |
180 | |
181 | swappable void drawBackground(int w, int h, Graphics2D g) { |
182 | g.setColor(or(getBackground(), Color.white)); |
183 | g.fillRect(0, 0, w, h); |
184 | } |
185 | |
186 | public void render(int w, int h, Graphics2D g) { |
187 | if (verbose) _print("render"); |
188 | g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, interpolationMode); |
189 | drawBackground(w, h, g); |
190 | BufferedImage image = or(imageToDraw, this.image); |
191 | if (!hasImage()) |
192 | drawBackground(w, h, g); |
193 | else { |
194 | bool alpha = !noAlpha && hasTransparency(image); |
195 | if (alpha) |
196 | drawBackground(w, h, g); |
197 | |
198 | drawImageItself(w, h, g); |
199 | |
200 | int iw = getZoomedWidth(), ih = getZoomedHeight(); |
201 | if (!alpha) { |
202 | g.fillRect(iw, 0, w-iw, h); |
203 | g.fillRect(0, ih, iw, h-ih); |
204 | } |
205 | } |
206 | |
207 | if (overlay != null) { |
208 | if (verbose) _print("render overlay"); |
209 | pcallF(overlay, g); |
210 | } |
211 | |
212 | for (var overlay : cloneList(overlays)) pcall { |
213 | overlay.drawOn(cloneGraphics(g)); |
214 | } |
215 | |
216 | if (selection != null) { |
217 | if (verbose) _print("render selection"); |
218 | // drawRect is inclusive, selection is exclusive, so... whatever, tests show it's cool. |
219 | drawSelectionRect(g, selection, Color.green, Color.white); |
220 | } |
221 | } |
222 | |
223 | public void drawSelectionRect(Graphics2D g, Rectangle selection, Color green, Color white) { |
224 | drawSelectionRect(g, selection, green, white, zoomX, zoomY); |
225 | } |
226 | |
227 | public void drawSelectionRect(Graphics2D g, Rectangle selection, Color green, Color white, double zoomX, double zoomY) { |
228 | g.setColor(green); |
229 | int top = (int) (selection.y * zoomY); |
230 | int bottom = (int) ((selection.y+selection.height) * zoomY); |
231 | int left = (int) (selection.x * zoomX); |
232 | int right = (int) ((selection.x+selection.width) * zoomX); |
233 | g.drawRect(left-1, top-1, right-left+1, bottom-top+1); |
234 | g.setColor(white); |
235 | g.drawRect(left - 2, top - 2, right - left + 3, bottom - top + 3); |
236 | } |
237 | |
238 | public ImageSurface setZoom(double zoom) { |
239 | setZoom(zoom, zoom); |
240 | this; |
241 | } |
242 | |
243 | public void setZoom(double zoomX, double zoomY) { |
244 | autoZoomToDisplay = false; |
245 | setZoom_dontChangeAutoZoom(zoomX, zoomY); |
246 | } |
247 | |
248 | public void setZoom_dontChangeAutoZoom(double zoomX, double zoomY default zoomX) { |
249 | if (this.zoomX == zoomX && this.zoomY == zoomY) ret; |
250 | if (verbose) _print("Setting zoom"); |
251 | this.zoomX = zoomX; |
252 | this.zoomY = zoomY; |
253 | revalidateMe(); |
254 | repaint(); |
255 | centerPoint(new Point(getImage().getWidth()/2, getImage().getHeight()/2)); |
256 | |
257 | pcallF(onZoom); |
258 | } |
259 | |
260 | public scaffolded Dimension getMinimumSize() { |
261 | if (noMinimumSize) ret new Dimension(1, 1); |
262 | int w = getZoomedWidth(); |
263 | int h = getZoomedHeight(); |
264 | Dimension min = super.getMinimumSize(); |
265 | ret printIfScaffoldingEnabled(this, new Dimension(Math.max(w, min.width), Math.max(h, min.height))); |
266 | } |
267 | |
268 | int getZoomedHeight() { |
269 | return (int) (h() * zoomY); |
270 | } |
271 | |
272 | int getZoomedWidth() { |
273 | return (int) (w() * zoomX); |
274 | } |
275 | |
276 | bool isShowing_quick() { |
277 | if (showingVar == null) swing { |
278 | showingVar if null = componentShowingVar(ImageSurface.this); |
279 | } |
280 | |
281 | ret showingVar!; |
282 | } |
283 | |
284 | public void setImageIfShowing_thisThread(BufferedImage etc image) { |
285 | if (isShowing_quick()) |
286 | setImage_thisThread(image); |
287 | } |
288 | |
289 | void setImage(File file) { |
290 | setFile(file); |
291 | setImage(loadImage2(file)); |
292 | } |
293 | |
294 | public void setImage(MakesBufferedImage image) swing { |
295 | setImage_thisThread(image); |
296 | } |
297 | |
298 | public void setImage(BufferedImage img) swing { |
299 | setImage_thisThread(img); |
300 | } |
301 | |
302 | public void setImage_thisThread(BufferedImage etc img) { |
303 | BufferedImage newImage = img != null ? img : dummyImage(); |
304 | BufferedImage oldImage = image; |
305 | image = newImage; |
306 | if (verbose) print("Old image size:" + imageSize(oldImage) + ", new image size: " + imageSize(newImage)); |
307 | bool sameSize = imagesHaveSameSize(oldImage, newImage); |
308 | if (!sameSize) { |
309 | if (verbose) _print("New image size"); |
310 | revalidateMe(); |
311 | } |
312 | quickRepaint(); |
313 | pcallF(onNewImage); |
314 | if (!sameSize && autoZoomToDisplay) zoomToDisplaySize(); |
315 | imageChanged(); |
316 | } |
317 | |
318 | void setImageAndZoomToDisplay(BufferedImage img) { |
319 | setImage(img); |
320 | zoomToDisplaySize(); |
321 | } |
322 | |
323 | public BufferedImage getImage() { |
324 | return image; |
325 | } |
326 | |
327 | public double getZoomX() { |
328 | return zoomX; |
329 | } |
330 | |
331 | public double getZoomY() { |
332 | return zoomY; |
333 | } |
334 | |
335 | public scaffolded Dimension getPreferredSize() { |
336 | ret printIfScaffoldingEnabled(this, new Dimension(getZoomedWidth(), getZoomedHeight())); |
337 | } |
338 | |
339 | /** returns a scrollpane with the scroll-mode prevent-garbage-drawing fix applied */ |
340 | public JScrollPane makeScrollPane() { |
341 | JScrollPane scrollPane = new JScrollPane(this); |
342 | scrollPane.getViewport().setScrollMode(JViewport.BACKINGSTORE_SCROLL_MODE); |
343 | return scrollPane; |
344 | } |
345 | |
346 | public void zoomToWindow() { zoomToDisplaySize(); } |
347 | public void zoomToDisplaySize() swing { |
348 | if (!hasImage()) return; |
349 | Dimension display = getDisplaySize(); |
350 | if (display.width == 0 || display.height == 0) ret; |
351 | int w = w(), h = h(); |
352 | double xRatio = (display.width-5)/(double) w; |
353 | double yRatio = (display.height-5)/(double) h; |
354 | if (scaffoldingEnabled(this)) |
355 | printVars zoomToDisplaySize(+display, +w, +h, +xRatio, +yRatio); |
356 | setZoom_dontChangeAutoZoom(min(xRatio, yRatio)); |
357 | revalidateMe(); |
358 | } |
359 | |
360 | /** tricky magic to get parent scroll pane */ |
361 | private scaffolded Dimension getDisplaySize() { |
362 | Container c = getParent(); |
363 | while (c != null) { |
364 | if (c instanceof JScrollPane) |
365 | return c.getSize(); |
366 | c = c.getParent(); |
367 | } |
368 | return getSize(); |
369 | } |
370 | |
371 | public void setSelection(Rect r) { |
372 | setSelection(toRectangle(r)); |
373 | } |
374 | |
375 | public void setSelection(Rectangle r) { |
376 | if (neq(selection, r)) { |
377 | selection = r; |
378 | pcallF(onSelectionChange); |
379 | quickRepaint(); |
380 | } |
381 | } |
382 | |
383 | public Rectangle getSelection() { |
384 | return selection; |
385 | } |
386 | |
387 | public RGBImage getRGBImage() { |
388 | return new RGBImage(getImage()); |
389 | } |
390 | |
391 | // p is in image coordinates |
392 | void centerPoint(Point p) { |
393 | JScrollPane sp = enclosingScrollPane(this); |
394 | if (sp == null) ret; |
395 | |
396 | p = new Point((int) (p.x*getZoomX()), (int) (p.y*getZoomY())); |
397 | final JViewport viewport = sp.getViewport(); |
398 | Dimension viewSize = viewport.getExtentSize(); |
399 | |
400 | //_print("centerPoint " + p); |
401 | int x = max(0, p.x-viewSize.width/2); |
402 | int y = max(0, p.y-viewSize.height/2); |
403 | |
404 | //_print("centerPoint " + p + " => " + x + "/" + y); |
405 | p = new Point(x,y); |
406 | //_print("centerPoint " + p); |
407 | final Point _p = p; |
408 | awtLater(r { |
409 | viewport.setViewPosition(_p); |
410 | }); |
411 | } |
412 | |
413 | Pt pointFromEvent(MouseEvent e) { |
414 | ret pointFromComponentCoordinates(new Pt(e.getX(), e.getY())); |
415 | } |
416 | |
417 | Pt pointFromComponentCoordinates(Pt p) { |
418 | ret new Pt((int) (p.x/zoomX), (int) (p.y/zoomY)); |
419 | } |
420 | |
421 | Pt pointToComponentCoordinates(double x, double y) { |
422 | ret new Pt((int) (x*zoomX), (int) (y*zoomY)); |
423 | } |
424 | |
425 | void uploadTheImage { |
426 | //call(hotwire(/*#1007313*/#1016427), "go", getImage(), titleForUpload); |
427 | ifdef ImageSurface_AllowUpload |
428 | var img = getImage(); |
429 | JTextField tf = jTextField(titleForUpload); |
430 | showFormTitled("Upload Image (PNG)", |
431 | "Image title (optional)", tf, func { |
432 | disableSubmitButton(getFrame(tf)); |
433 | thread "Upload Image" { |
434 | try { |
435 | messageBox("Image uploaded as " + uploadPNGToImageServer(img, titleForUpload = getTextTrim(tf))); |
436 | disposeFrame(tf); |
437 | } catch e { |
438 | enableSubmitButton(getFrame(tf)); |
439 | messageBox(e); |
440 | } |
441 | } |
442 | false; |
443 | }); |
444 | endifdef |
445 | } |
446 | |
447 | void showFullScreen() { |
448 | showFullScreenImageSurface(getImage()); |
449 | } |
450 | |
451 | void zoomIn(double f) { setZoom(getZoomX()*f, getZoomY()*f); } |
452 | void zoomOut(double f) { setZoom(getZoomX()/f, getZoomY()/f); } |
453 | |
454 | selfType setFile(File f) { file = f; this; } |
455 | |
456 | void setOverlay(IVF1<Graphics2D> overlay) { |
457 | this.overlay = overlay; |
458 | } |
459 | |
460 | bool hasImage() { ret image != null; } |
461 | public int w() { ret image.getWidth(); } |
462 | public int h() { ret image.getHeight(); } |
463 | |
464 | void setPixelated aka pixelate(bool b) { |
465 | assertTrue(b); |
466 | imageSurface_pixelated(this); |
467 | } |
468 | |
469 | selfType setAutoZoomToDisplay aka autoZoomToDisplay(bool b) { |
470 | if (autoZoomToDisplay = b) |
471 | zoomToDisplaySize(); |
472 | this; |
473 | } |
474 | |
475 | void quickRepaint() { |
476 | if (repaintInThread) |
477 | paintImmediately(0, 0, getWidth(), getHeight()); |
478 | else |
479 | repaint(); |
480 | } |
481 | |
482 | void setTool(ImageSurfaceMouseHandler tool) swing { |
483 | removeAllTools(); |
484 | addTool(tool); |
485 | } |
486 | |
487 | bool hasTool(AutoCloseable tool) { |
488 | ret swing(-> tools.contains(tool)); |
489 | } |
490 | |
491 | void addTool(ImageSurfaceMouseHandler tool) swing { |
492 | if (!tools.contains(tool)) |
493 | tool.register(this); |
494 | } |
495 | |
496 | void removeTool(AutoCloseable tool) swing { |
497 | if (tools.contains(tool)) { |
498 | close(tool); |
499 | tools.remove(tool); |
500 | } |
501 | } |
502 | |
503 | void removeAllTools aka clearTools() { |
504 | closeAllAndClear(tools); |
505 | } |
506 | |
507 | void performAutoZoom { |
508 | if (autoZoomToDisplay) zoomToDisplaySize(); |
509 | } |
510 | |
511 | void revalidateMe() { |
512 | revalidateIncludingFullCenterContainer(this); |
513 | } |
514 | |
515 | void addOverlay(G2Drawable overlay) { |
516 | overlays.add(overlay); |
517 | repaint(); |
518 | } |
519 | |
520 | void clearOverlays() { |
521 | if (nempty(overlays)) { |
522 | overlays.clear(); |
523 | repaint(); |
524 | } |
525 | } |
526 | |
527 | void setOverlay(G2Drawable overlay) { |
528 | clearOverlays(); |
529 | if (overlay != null) addOverlay(overlay); |
530 | } |
531 | |
532 | void loadImage(File f) { |
533 | setImage(loadImage2(f)); |
534 | } |
535 | |
536 | JComponent visualize() { |
537 | ret jscroll_center_borderless(this); |
538 | } |
539 | |
540 | void standardZoom() { |
541 | setZoom(1.0); |
542 | } |
543 | |
544 | !include #1034645 // print |
545 | } // end of ImageSurface |
546 | |
547 | // static function allows garbage collection |
548 | static VF2<ImageSurface, JPopupMenu> ImageSurface_popupMenuMaker() { |
549 | ret voidfunc(ImageSurface is, JPopupMenu menu) { |
550 | Point p = is.pointFromEvent(componentPopupMenu_mouseEvent.get()).getPoint(); |
551 | is.fillPopupMenu(menu, p); |
552 | }; |
553 | } |
download show line numbers debug dex old transpilations
Travelled to 20 computer(s): aoiabmzegqzx, bhatertpkbcr, cbybwowwnfue, cfunsshuasjs, ddnzoavkxhuk, gwrvuhgaqvyk, irmadwmeruwu, ishqpsrjomds, jtubtzbbkimh, lpdgvwnxivlt, mqqgnosmbjvj, onxytkatvevr, ppjhyzlbdabe, pyentgdyhuwx, pzhvpgtvlbxg, sawdedvomwva, tslmcundralx, tvejysmllsmz, vouqrxazstgt, xrpafgyirdlv
No comments. add comment
Snippet ID: | #1004553 |
Snippet name: | ImageSurface |
Eternal ID of this version: | #1004553/163 |
Text MD5: | d1e8e48ee0a64fb49de058409c417d27 |
Transpilation MD5: | 68579e37079dfbc7cb009eb4284f2520 |
Author: | stefan |
Category: | javax / gui |
Type: | JavaX fragment (include) |
Public (visible to everyone): | Yes |
Archived (hidden from active list): | No |
Created/modified: | 2022-08-23 18:20:57 |
Source code size: | 16129 bytes / 553 lines |
Pitched / IR pitched: | No / No |
Views / Downloads: | 1342 / 7446 |
Version history: | 162 change(s) |
Referenced in: | [show references] |