I am trying to understand the imposition of text on the image.

The situation is as follows: there is a javafx application, the main window is marked up as a BorderPane with a size of 1280x800. All borderPane zones, with the exception of center, have fixed sizes. It turns out that when you maximize the window, the center area increases. When launched, this zone is approximately 975x740 px.

In the center zone itself there is a scrollPane, inside which there is a StackPane, with the following parameters (from css)

-fx-hbar-policy: never; -fx-vbar-policy: never; -fx-fit-to-width: true; -fx-fit-to-height: true; -fx-pannable: true; -fx-alignment: center; 

Inside the StackPane is an ImageView, which displays various loadable images, centering the center zone and scaling implemented like this:

 //центрирование stackPaneImageHolder.minWidthProperty().bind(Bindings.createDoubleBinding(() -> scrollPaneImageView.getViewportBounds().getWidth(), scrollPaneImageView.viewportBoundsProperty())); //масштабирование imageView.fitWidthProperty().bind(scrollPaneImageView.widthProperty()); imageView.fitHeightProperty().bind(scrollPaneImageView.heightProperty()); 

The question is: I would like to be able to write on the image displayed in the ImageView.

As I understand it - the issue is resolved via Canvas (javafx.scene.canvas). But there was a problem - I don’t understand how to connect the canvas with the loaded image:

For example - I uploaded an image with a resolution of 4000x3000px. I scaled it in ImageView to the same 975x740px. When I experiment with Canvas, it pushes me away from the size of an already scaled image, and if I try to give it the dimensions of the original image, either the test caption is not visible at all in ImageView, or I get vertical scrolling inside the imageView and not the fact that the caption is visible ( In the end, now they have completely removed from the canvas code, because there are already built up very scary constructions that do not work).

I need to figure out how to make a Canvas the size of a real image and a height of say 200 pixels (to create a caption on the photo below), and display the finished canvas with the text \ background on the scaled image so that when expanding / restoring the window size, it does not go somewhere then sideways, and was tied to a specific part of the image. With the subsequent saving of the image with this inscription in the original size.

Please tell me in which direction to dig and maybe I did not come from that side at all. Current code posted below

Thanks in advance for your reply.



mainWindowController

 package imageSigner.controller; import imageSigner.MainApp; import imageSigner.model.FileItem; import imageSigner.storage.FileItemsStorage; import imageSigner.tools.FileOperations; import javafx.beans.binding.Bindings; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; import javafx.scene.control.TextField; import javafx.scene.image.*; import javafx.scene.layout.BorderPane; import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; import java.io.File; public class mainWindowController { //даем контроллеру доступ к экземпляру mainApp private MainApp mainApp; //создаем лист для обработки файлов private ObservableList<FileItem> fileItemsList = FileItemsStorage.getInstance().getFileItemsList(); //переменная для хранения индекса выбранного файла private int currentFileIndex; //объявляем поля и элементы из FXML @FXML public BorderPane borderPaneMain; @FXML public ScrollPane scrollPaneImageView; @FXML public StackPane stackPaneImageHolder; @FXML public ImageView imageView; @FXML public Button buttonSelectFiles; @FXML public Button buttonPrevPhoto; @FXML public Button buttonNextPhoto; @FXML public Button buttonApplySignature; @FXML public Label labelQuantitySelectedFiles; @FXML public TextField textFieldCurrentFile; @FXML public TextField textFieldSignature; //initialize public void initialize() { refreshCounter(); //отслеживание изменения кол-ва выбранных файлов fileItemsList.addListener((ListChangeListener<FileItem>) c -> refreshCounter()); //отслеживание предпросмотра текущего файла (изменение текстового поля с именем файла) textFieldCurrentFile.textProperty().addListener( ((observable, oldValue, newValue) -> showPhotoPreview())); } //обновление счетчика private void refreshCounter() { FileItemsStorage.getInstance().refreshLabelCounter(labelQuantitySelectedFiles); } //передача файла с окна выбора файла в основное окно public void selectFileToTextField() { if (fileItemsList.size() != 0) { try { currentFileIndex = mainApp.getFwController().tableView.getSelectionModel().getSelectedIndex(); currentFileToTextFieldCF(); } catch (ArrayIndexOutOfBoundsException e) { currentFileIndex = 0; currentFileToTextFieldCF(); } } } //прописывает текущий фвйл в соответсвующее текстовое поле private void currentFileToTextFieldCF() { FileItem f = fileItemsList.get(currentFileIndex); getTextFieldCurrentFile().setText(f.getFilePath()); } //показывать превью фото private void showPhotoPreview() { if (fileItemsList.size() != 0) { Image img = getCurrentImage(); imageView.setImage(img); arrangeImageView(); } } //выравнивание и масштабирование изображения в превью private void arrangeImageView() { scrollPaneImageView.setContent(stackPaneImageHolder); //центрирование stackPaneImageHolder.minWidthProperty().bind(Bindings.createDoubleBinding(() -> scrollPaneImageView.getViewportBounds().getWidth(), scrollPaneImageView.viewportBoundsProperty())); //масштабирование imageView.fitWidthProperty().bind(scrollPaneImageView.widthProperty()); imageView.fitHeightProperty().bind(scrollPaneImageView.heightProperty()); } //TODO показ измененного фото ТЕСТ РИСОВАНИЯ БЕЛОЙ ПОЛОСЫ ПОД ФОТО private void showChangedPhoto(int signLineSize) { Image image = getCurrentImage(); // Создаем WritableImage WritableImage writableImage = new WritableImage( (int) image.getWidth(), (int) image.getHeight() + signLineSize); PixelReader pixelReader = image.getPixelReader(); PixelWriter pixelWriter = writableImage.getPixelWriter(); // Проходим все пиксели исходного изображения for (int readY = 0; readY < image.getHeight(); readY++ ) { for (int readX = 0; readX < image.getWidth(); readX++) { Color color = pixelReader.getColor(readX, readY); //считываем цвет пикселя с исходного изображения pixelWriter.setColor(readX, readY, color); //записываем цвет пикселя в writableImage } } // заполнение цветом пространства для подписи for (int rY = (int)image.getHeight() + 1; rY < writableImage.getHeight(); rY++) { for (int rX = 0; rX < image.getWidth(); rX++) { pixelWriter.setColor(rX, rY, Color.WHITE); } } imageView.setImage(writableImage); //отображение измененного изображения } /** buttons */ public void showSelectedFilesWindow() { mainApp.showFilesWindow(); } //переключает на предыдущее фото public void prevPhotoButton() { if (currentFileIndex > 0) { currentFileIndex --; currentFileToTextFieldCF(); //todo preview } } //переключает на следующее фото public void nextPhotoButton() { if (currentFileIndex < fileItemsList.size() - 1) { currentFileIndex++; currentFileToTextFieldCF(); } } //Применить подпись public void buttonApplySignature() { //создаем копию оригинального файла в специальной папке FileOperations.backupOriginal(currentFileIndex); //отображаем измененное фото в ImageView showChangedPhoto(200); //todo передаем пока временный параметр } /**Setters and getters */ public void setMainApp(MainApp mainApp) { this.mainApp = mainApp; } public TextField getTextFieldCurrentFile() { return textFieldCurrentFile; } public int getCurrentFileIndex() { return currentFileIndex; } public Image getCurrentImage() { Image img = new Image(new File(fileItemsList.get(currentFileIndex).getFilePath()).toURI().toString()); return img; } } 

mainWindowView.fxml

 <?xml version="1.0" encoding="UTF-8"?> <?import java.lang.*?> <?import javafx.scene.control.*?> <?import javafx.scene.image.*?> <?import javafx.scene.layout.*?> <BorderPane fx:id="borderPaneMain" styleClass="BorderPaneMain" stylesheets="@css/mainWindow.css" xmlns="http://javafx.com/javafx/8.0.40" xmlns:fx="http://javafx.com/fxml/1" fx:controller="imageSigner.controller.mainWindowController"> <top> <AnchorPane styleClass="panelTop"> <Label layoutX="8.0" layoutY="4.0" styleClass="fontHeaders1" text="Предпросмотр" /> <Label layoutX="985.0" layoutY="4.0" styleClass="fontHeaders1" text="Инструменты" /> </AnchorPane> </top> <bottom> <AnchorPane styleClass="panelBottom"> <TextField fx:id="textFieldSignature" layoutX="5.0" layoutY="56.0" styleClass="text-field-Signature" /> <Button fx:id="buttonApplySignature" layoutX="1072.0" layoutY="18.0" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" mnemonicParsing="false" onAction="#buttonApplySignature" prefHeight="100.0" prefWidth="190.0" styleClass="fontHeaders2_arial, fontHeaders2_bold" text="Применить подпись" /> </AnchorPane> </bottom> <right> <AnchorPane styleClass="panelTools"> <Label layoutX="3.0" layoutY="11.0" styleClass="fontHeaders2, fontHeaders2_bold" text="1. Выберите файл JPEG или папку" /> <Button fx:id="buttonSelectFiles" layoutX="13.0" layoutY="34.0" mnemonicParsing="false" onAction="#showSelectedFilesWindow" styleClass="button-selectFiles" text="Выбор фото" /> <Label fx:id="labelQuantitySelectedFiles" layoutX="220.0" layoutY="34.0" styleClass="fontHeaders2, fontHeaders2_arial" /> <TextField fx:id="textFieldCurrentFile" layoutX="13.0" layoutY="94.0" styleClass="text-field-currentFile" /> <Button fx:id="buttonPrevPhoto" layoutX="14.0" layoutY="136.0" mnemonicParsing="false" onAction="#prevPhotoButton" styleClass="buttons-prev-and-next" text="Предыдущее фото" /> <Label layoutX="20.0" layoutY="76.0" styleClass="fontHeaders2" text="Текущее выбранное фото" /> <Button fx:id="buttonNextPhoto" layoutX="152.0" layoutY="136.0" mnemonicParsing="false" onAction="#nextPhotoButton" styleClass="buttons-prev-and-next" text="Следующее фото" /> </AnchorPane> </right> <left> <AnchorPane styleClass="panelLeft" /> </left> <center> <ScrollPane fx:id="scrollPaneImageView" styleClass="panelImageScrollPane"> <StackPane fx:id="stackPaneImageHolder" styleClass="panelImageStackPane"> <ImageView fx:id="imageView" pickOnBounds="true" preserveRatio="true" styleClass="imageView" /> </StackPane> </ScrollPane> </center> </BorderPane> 

mainWindow.css

 .BorderPaneMain { -fx-pref-width: 1280px; -fx-pref-height: 800px; -fx-min-width: 1280px; -fx-min-height: 800px; } .panelTop, .panelTools, .panelBottom, .panelLeft { -fx-background-color: #A6ABB2; } .panelTop { -fx-pref-width: 1280px; -fx-pref-height: 25px; -fx-min-height: 25px; -fx-max-height: 25px; -fx-alignment: center; } .panelBottom { -fx-alignment: bottom-left; -fx-pref-width: 980px; -fx-pref-height: 135px; } .panelTools { -fx-alignment: top-right; -fx-pref-width: 300px; -fx-pref-height: 775px; } .panelLeft { -fx-alignment: center; -fx-pref-width: 5px; -fx-pref-height: 640px; } .panelImageScrollPane { -fx-hbar-policy: never; -fx-vbar-policy: never; -fx-fit-to-width: true; -fx-fit-to-height: true; -fx-pannable: true; -fx-alignment: center; } .panelImageStackPane { -fx-background-color: #404040; } .imageView { -fx-alignment: center; } .fontHeaders1 { -fx-font-family: Verdana; -fx-font-size: 14px; -fx-text-fill: #333333; } .fontHeaders2 { -fx-font-family: Verdana; -fx-font-size: 12px; -fx-text-fill: #171717; } .fontHeaders2_bold { -fx-font-weight: bold; } .fontHeaders2_arial { -fx-font-family: Arial; } .text-field-Signature { -fx-pref-height: 25px; -fx-pref-width: 975px; } .text-field-currentFile { -fx-pref-width: 275px; -fx-pref-height: 25px; } .button-selectFiles { -fx-pref-width: 180px; -fx-pref-height: 25px; -fx-font-family: Arial; -fx-font-size: 12px; -fx-text-fill: #171717; } .buttons-prev-and-next { -fx-pref-width: 135px; -fx-pref-height: 25px; -fx-font-family: Arial; -fx-font-size: 12px; -fx-text-fill: #171717; } 
  • Maybe you didn’t quite correctly understand the task before you, but at first glance there’s at least 2 ways: Either in the ScrollPane, put the BorderPane where you can put a photo in the center and display the signature by the need to output at the bottom (there’s a problem in offset when adding text), or stitch for example, Pane which will be a container for all content and it will correct the propertie with dependencies on the size and output of the signature ... everything is more harmonious z the index will not let the text disappear and xor color can be organized ... There are a lot of options if you need a good example ite case scenario - by Peter Slusar
  • Thanks for the answer. Rather, the second option will be closer. The task is to write the text on the image and save a copy of this image with superimposed text in jpeg. At the same time leave the saved image the resolution of the original image, but not scaled. - qop
  • For example: the photo is displayed in imageView. I in some way (for example through textField) enter the text and press the button. After clicking on a button, I use a pixelReader / pixelWriter to form a WritableImage. To it, I add to the height of the photo, for example, 200 pixels. white at the bottom of the image across the entire width (this moment is already in the code). and after that there you need to paste the text from the textField and display the WritableImage in the ImageView. Here there is a problem - how to put the text on the desired coordinates in the image, and display it normally in the ImageView. then save to jpg with this text. - qop
  • Clearly I will try to throw my vision of the code a bit later ... - Peter Slusar
  • I ask for a petition for the delay just finished the contract) I hope it is still relevant - Peter Slusar

1 answer 1

I'll start with the code:

 public class Test extends Application { public static void main(String[] args) { launch(args); } @Override public void start(Stage primaryStage) throws Exception { Cadre cadre = new Cadre( "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e1/FullMoon2010.jpg/1024px-FullMoon2010.jpg"); BorderPane root = new BorderPane(cadre); cadre.setText("Moon"); Scene s = new Scene(root, 1000, 1000); primaryStage.setScene(s); primaryStage.show(); } class Cadre extends StackPane { private Image image; private ImageView view; private WritableImage wImage; private Canvas canvas = new Canvas(0, 0); private GraphicsContext gContext = canvas.getGraphicsContext2D(); private double border, bottomBorder; public Cadre(String url) { image = new Image(url); view = new ImageView(); parentProperty().addListener(e -> { initCanvas(); view.setPreserveRatio(true); view.fitWidthProperty().bind(((BorderPane) ((ReadOnlyObjectProperty) e).getValue()).widthProperty()); }); getChildren().add(view); } public void setText(String caption) { Font font = new Font(canvas.getWidth() / 10); Text t = new Text(caption); t.setFont(font); double wLength = t.getLayoutBounds().getWidth(); gContext.setFont(font); gContext.strokeText(caption, canvas.getWidth() / 2 - wLength / 2, canvas.getHeight() - bottomBorder / 2); paint(); } private void initCanvas() { border = image.getWidth() * 0.1d; bottomBorder = image.getHeight() * 0.3d; canvas.setWidth(image.getWidth() + (border * 2)); canvas.setHeight(image.getHeight() + border + bottomBorder); gContext.drawImage(image, border, border); gContext.setFill(Color.BLACK); gContext.setLineWidth(border * 0.15); gContext.strokeRect(0, 0, canvas.getWidth(), canvas.getHeight()); paint(); } private void paint() { wImage = new WritableImage((int) canvas.getWidth(), (int) canvas.getHeight()); canvas.snapshot(new SnapshotParameters(), wImage); view.setImage(wImage); } } } 

So class Cadre is a container for an object of type Canvas. Here you should pay attention to the parent's listener in the constructor of this object - this is a convenient way not to exhaust the code by passing the reference to the parent and implement dynamic adjustment to the size bins (Just in case I’ll add if they get to extend this near-crutch - organize dynamic changes via High-Level Binding The API will not survive another closure of Java)). Further, in general, an object like Canvas is obviously created and an image is drawn and the method responsible for drawing / generating an object is implemented using the old memory iridescent type Image ... In any case, this is a prototype of any further expansion of your fascinating quest ... For example, what is logical in setText () can be passed as an argument an object of type Text and remove its position relative to the image, etc.

  • Thank you so much. While waiting for your answer, I found a way to implement it through AWT. Through SwingFXUtils like the whole thing connected. But, all the same, I was tormented by doubts - why use third-party library components in a JavaFX application, when you can implement everything through FX. Your version (while in draft) is embedded - everything works as it should. The truth is, however, with the listener, it hasn't gone wrong (I swore at the caste - ScrollPaneSkin cannot be cast to ScrollPane) - I registered a link to the scrollPane - it works. Later I will analyze in more detail. Thank! - qop