sclass ImageSurface extends Surface { BufferedImage image; double zoomX = 1, zoomY = 1, zoomFactor = 1.5; private Rectangle selection; new L tools; // use overlays now O overlay; // voidfunc(Graphics2D) L overlays = syncL(); Runnable onSelectionChange; settable bool verbose; bool noMinimumSize = true; S titleForUpload; O onZoom; bool specialPurposed; // true = don't show image changing commands in popup menu settable bool allowPaste; settable bool zoomable = true; bool noAlpha; // set to true to speed up drawing if you don't use alpha O interpolationMode = RenderingHints.VALUE_INTERPOLATION_BILINEAR; O onNewImage; // use imageChanged event instead BufferedImage imageToDraw; // if you want to draw a different image File file; // where image was loaded from bool autoZoomToDisplay; // only works 100% when not in scrollpane settable bool repaintInThread; // after setImage, repaint in same thread BoolVar showingVar; Pt mousePosition; event mousePositionChanged; event imageChanged; event userModifiedImage(BufferedImage image); public ImageSurface() { this(dummyImage()); } static BufferedImage dummyImage() { ret whiteImage(1); } *(File file) { setImage(file); } *(MakesBufferedImage image) { this(image != null ? image.getBufferedImage() : dummyImage()); } *(BufferedImage image) { setImage(image); clearSurface = false; // perform auto-zoom when shown, resized or when parent resized bindToComponent(this, l0 performAutoZoom, null); onResize(this, l0 performAutoZoom); onEnclosingScrollPaneResize(this, l0 performAutoZoom); componentPopupMenu2(this, ImageSurface_popupMenuMaker()); new ImageSurfaceSelector(this); jHandleFileDrop(this, voidfunc(File f) { setImage(loadBufferedImage(f)) }); imageSurfaceOnHover(this, p -> { mousePosition = p; mousePositionChanged(); }); } public ImageSurface(RGBImage image, double zoom) { this(image); setZoom(zoom); } // point is already in image coordinates protected void fillPopupMenu(JPopupMenu menu, final Point point) { if (zoomable) { JMenuItem miZoomReset = new JMenuItem("Zoom 100%"); miZoomReset.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { setZoom(1.0); centerPoint(point); } }); menu.add(miZoomReset); JMenuItem miZoomIn = new JMenuItem("Zoom in"); miZoomIn.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { zoomIn(zoomFactor); centerPoint(point); } }); menu.add(miZoomIn); JMenuItem miZoomOut = new JMenuItem("Zoom out"); miZoomOut.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { zoomOut(zoomFactor); centerPoint(point); } }); menu.add(miZoomOut); menu.add(jMenuItemStayCheckedOnClick("Zoom to window", -> autoZoomToDisplay, -> setAutoZoomToDisplay(true))); addMenuItem(menu, "Show full screen", r { showFullScreen() }); addMenuItem(menu, "Point: " + point.x + "," + point.y + " (image: " + w() + "*" + h() + ")", null); menu.addSeparator(); } if (!specialPurposed) addMenuItem(menu, "Load image...", r { selectFile("Load image", voidfunc(File f) { setImage(loadImage2(f)) }) }); addMenuItem(menu, "Save image...", r { saveImage() }); ifdef ImageSurface_AllowUpload addMenuItem(menu, "Upload image...", r { uploadTheImage() }); endifdef addMenuItem(menu, "Copy image to clipboard", r { copyImageToClipboard(getImage()) }); if (!specialPurposed || allowPaste) addMenuItem(menu, "Paste image from clipboard", r { loadFromClipboard() }); if (!specialPurposed) addMenuItem(menu, "Load image snippet...", r { selectImageSnippet(voidfunc(S imageID) { setImage(loadImage2(imageID)) }); }); if (selection != null) addMenuItem(menu, "Crop", r { crop() }); if (!specialPurposed) addMenuItem(menu, "No image", r { noImage() }); } void noImage() { setImage((BufferedImage) null); } void crop() { if (selection == null) ret; BufferedImage img = cloneClipBufferedImage(getImage(), selection); selection = null; setUserModifiedImage(img); } void setUserModifiedImage(BufferedImage img) { setImage(img); userModifiedImage(img); } void loadFromClipboard() { BufferedImage img = getImageFromClipboard(); if (img != null) setUserModifiedImage(img); } swappable File defaultImageDir() { ret getProgramDir(); } void saveImage() { var image = getImage(); JFileChooser fileChooser = new JFileChooser(defaultImageDir()); if (fileChooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) { try { main saveImage(image, file = fileChooser.getSelectedFile()); } catch e { popup(e); } } } void drawImageItself(int w, int h, Graphics2D g) { int iw = getZoomedWidth(), ih = getZoomedHeight(); if (interpolationMode == RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR || zoomX >= 1 || zoomY >= 1) { // faster g.drawImage(image, 0, 0, iw, ih, null); } else g.drawImage(resizeImage(image, iw, ih), 0, 0, null); // smoother } swappable void drawBackground(int w, int h, Graphics2D g) { g.setColor(or(getBackground(), Color.white)); g.fillRect(0, 0, w, h); } public void render(int w, int h, Graphics2D g) { if (verbose) _print("render"); g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, interpolationMode); drawBackground(w, h, g); BufferedImage image = or(imageToDraw, this.image); if (!hasImage()) drawBackground(w, h, g); else { bool alpha = !noAlpha && hasTransparency(image); if (alpha) drawBackground(w, h, g); drawImageItself(w, h, g); int iw = getZoomedWidth(), ih = getZoomedHeight(); if (!alpha) { g.fillRect(iw, 0, w-iw, h); g.fillRect(0, ih, iw, h-ih); } } if (overlay != null) { if (verbose) _print("render overlay"); pcallF(overlay, g); } for (var overlay : cloneList(overlays)) pcall { overlay.drawOn(cloneGraphics(g)); } if (selection != null) { if (verbose) _print("render selection"); // drawRect is inclusive, selection is exclusive, so... whatever, tests show it's cool. drawSelectionRect(g, selection, Color.green, Color.white); } } public void drawSelectionRect(Graphics2D g, Rectangle selection, Color green, Color white) { drawSelectionRect(g, selection, green, white, zoomX, zoomY); } public void drawSelectionRect(Graphics2D g, Rectangle selection, Color green, Color white, double zoomX, double zoomY) { g.setColor(green); int top = (int) (selection.y * zoomY); int bottom = (int) ((selection.y+selection.height) * zoomY); int left = (int) (selection.x * zoomX); int right = (int) ((selection.x+selection.width) * zoomX); g.drawRect(left-1, top-1, right-left+1, bottom-top+1); g.setColor(white); g.drawRect(left - 2, top - 2, right - left + 3, bottom - top + 3); } public ImageSurface setZoom(double zoom) { setZoom(zoom, zoom); this; } public void setZoom(double zoomX, double zoomY) { autoZoomToDisplay = false; setZoom_dontChangeAutoZoom(zoomX, zoomY); } public void setZoom_dontChangeAutoZoom(double zoomX, double zoomY default zoomX) { if (this.zoomX == zoomX && this.zoomY == zoomY) ret; if (verbose) _print("Setting zoom"); this.zoomX = zoomX; this.zoomY = zoomY; revalidateMe(); repaint(); centerPoint(new Point(getImage().getWidth()/2, getImage().getHeight()/2)); pcallF(onZoom); } public scaffolded Dimension getMinimumSize() { if (noMinimumSize) ret new Dimension(1, 1); int w = getZoomedWidth(); int h = getZoomedHeight(); Dimension min = super.getMinimumSize(); ret printIfScaffoldingEnabled(this, new Dimension(Math.max(w, min.width), Math.max(h, min.height))); } int getZoomedHeight() { return (int) (h() * zoomY); } int getZoomedWidth() { return (int) (w() * zoomX); } bool isShowing_quick() { if (showingVar == null) swing { showingVar if null = componentShowingVar(ImageSurface.this); } ret showingVar!; } public void setImageIfShowing_thisThread(BufferedImage etc image) { if (isShowing_quick()) setImage_thisThread(image); } void setImage(File file) { setFile(file); setImage(loadImage2(file)); } public void setImage(MakesBufferedImage image) swing { setImage_thisThread(image); } public void setImage(BufferedImage img) swing { setImage_thisThread(img); } public void setImage_thisThread(BufferedImage etc img) { BufferedImage newImage = img != null ? img : dummyImage(); BufferedImage oldImage = image; image = newImage; if (verbose) print("Old image size:" + imageSize(oldImage) + ", new image size: " + imageSize(newImage)); bool sameSize = imagesHaveSameSize(oldImage, newImage); if (!sameSize) { if (verbose) _print("New image size"); revalidateMe(); } quickRepaint(); pcallF(onNewImage); if (!sameSize && autoZoomToDisplay) zoomToDisplaySize(); imageChanged(); } void setImageAndZoomToDisplay(BufferedImage img) { setImage(img); zoomToDisplaySize(); } public BufferedImage getImage() { return image; } public double getZoomX() { return zoomX; } public double getZoomY() { return zoomY; } public scaffolded Dimension getPreferredSize() { ret printIfScaffoldingEnabled(this, new Dimension(getZoomedWidth(), getZoomedHeight())); } /** returns a scrollpane with the scroll-mode prevent-garbage-drawing fix applied */ public JScrollPane makeScrollPane() { JScrollPane scrollPane = new JScrollPane(this); scrollPane.getViewport().setScrollMode(JViewport.BACKINGSTORE_SCROLL_MODE); return scrollPane; } public void zoomToWindow() { zoomToDisplaySize(); } public void zoomToDisplaySize() swing { if (!hasImage()) return; Dimension display = getDisplaySize(); if (display.width == 0 || display.height == 0) ret; int w = w(), h = h(); double xRatio = (display.width-5)/(double) w; double yRatio = (display.height-5)/(double) h; if (scaffoldingEnabled(this)) printVars zoomToDisplaySize(+display, +w, +h, +xRatio, +yRatio); setZoom_dontChangeAutoZoom(min(xRatio, yRatio)); revalidateMe(); } /** tricky magic to get parent scroll pane */ private scaffolded Dimension getDisplaySize() { Container c = getParent(); while (c != null) { if (c instanceof JScrollPane) return c.getSize(); c = c.getParent(); } return getSize(); } public void setSelection(Rect r) { setSelection(toRectangle(r)); } public void setSelection(Rectangle r) { if (neq(selection, r)) { selection = r; pcallF(onSelectionChange); quickRepaint(); } } public Rectangle getSelection() { return selection; } public RGBImage getRGBImage() { return new RGBImage(getImage()); } // p is in image coordinates void centerPoint(Point p) { JScrollPane sp = enclosingScrollPane(this); if (sp == null) ret; p = new Point((int) (p.x*getZoomX()), (int) (p.y*getZoomY())); final JViewport viewport = sp.getViewport(); Dimension viewSize = viewport.getExtentSize(); //_print("centerPoint " + p); int x = max(0, p.x-viewSize.width/2); int y = max(0, p.y-viewSize.height/2); //_print("centerPoint " + p + " => " + x + "/" + y); p = new Point(x,y); //_print("centerPoint " + p); final Point _p = p; awtLater(r { viewport.setViewPosition(_p); }); } Pt pointFromEvent(MouseEvent e) { ret pointFromComponentCoordinates(new Pt(e.getX(), e.getY())); } Pt pointFromComponentCoordinates(Pt p) { ret new Pt((int) (p.x/zoomX), (int) (p.y/zoomY)); } Pt pointToComponentCoordinates(double x, double y) { ret new Pt((int) (x*zoomX), (int) (y*zoomY)); } void uploadTheImage { //call(hotwire(/*#1007313*/#1016427), "go", getImage(), titleForUpload); ifdef ImageSurface_AllowUpload var img = getImage(); JTextField tf = jTextField(titleForUpload); showFormTitled("Upload Image (PNG)", "Image title (optional)", tf, func { disableSubmitButton(getFrame(tf)); thread "Upload Image" { try { messageBox("Image uploaded as " + uploadPNGToImageServer(img, titleForUpload = getTextTrim(tf))); disposeFrame(tf); } catch e { enableSubmitButton(getFrame(tf)); messageBox(e); } } false; }); endifdef } void showFullScreen() { showFullScreenImageSurface(getImage()); } void zoomIn(double f) { setZoom(getZoomX()*f, getZoomY()*f); } void zoomOut(double f) { setZoom(getZoomX()/f, getZoomY()/f); } selfType setFile(File f) { file = f; this; } void setOverlay(IVF1 overlay) { this.overlay = overlay; } bool hasImage() { ret image != null; } public int w() { ret image.getWidth(); } public int h() { ret image.getHeight(); } void setPixelated aka pixelate(bool b) { assertTrue(b); imageSurface_pixelated(this); } selfType setAutoZoomToDisplay aka autoZoomToDisplay(bool b) { if (autoZoomToDisplay = b) zoomToDisplaySize(); this; } void quickRepaint() { if (repaintInThread) paintImmediately(0, 0, getWidth(), getHeight()); else repaint(); } void setTool(ImageSurfaceMouseHandler tool) swing { removeAllTools(); addTool(tool); } bool hasTool(AutoCloseable tool) { ret swing(-> tools.contains(tool)); } void addTool(ImageSurfaceMouseHandler tool) swing { if (!tools.contains(tool)) tool.register(this); } void removeTool(AutoCloseable tool) swing { if (tools.contains(tool)) { close(tool); tools.remove(tool); } } void removeAllTools aka clearTools() { closeAllAndClear(tools); } void performAutoZoom { if (autoZoomToDisplay) zoomToDisplaySize(); } void revalidateMe() { revalidateIncludingFullCenterContainer(this); } void addOverlay(G2Drawable overlay) { overlays.add(overlay); repaint(); } void clearOverlays() { if (nempty(overlays)) { overlays.clear(); repaint(); } } void setOverlay(G2Drawable overlay) { clearOverlays(); if (overlay != null) addOverlay(overlay); } void loadImage(File f) { setImage(loadImage2(f)); } JComponent visualize() { ret jscroll_center_borderless(this); } void standardZoom() { setZoom(1.0); } !include #1034645 // print } // end of ImageSurface // static function allows garbage collection static VF2 ImageSurface_popupMenuMaker() { ret voidfunc(ImageSurface is, JPopupMenu menu) { Point p = is.pointFromEvent(componentPopupMenu_mouseEvent.get()).getPoint(); is.fillPopupMenu(menu, p); }; }