Почему одно и то же изображение (редактирование: PNG) создает два немного разных массива байтов в Java?

Я пишу простое приложение, и одной из функций является возможность сериализовать объект в изображение, чтобы пользователь мог поделиться фотографией своего проекта с встроенными в него инструкциями по сборке. В настоящее время другой пользователь может перетаскивать изображение в ListView JavaFX, а слушатель будет выполнять некоторый код для декодирования и анализа встроенного JSON.

Это прекрасно работает, если изображение хранится локально, но я также хочу разрешить пользователям перетаскивать изображения из браузера, не сохраняя их сначала локально. Чтобы проверить это, я связал рабочее изображение в базовом файле HTML, чтобы я мог отобразить его в Chrome.

Первоначально я использовал путь к файлу, взятому из Dragboard, но когда изображение поступает из браузера, мне нужно (я думаю) иметь возможность принимать изображение напрямую, поэтому ниже используется перегруженный метод decode().

Проблема, с которой я сталкиваюсь, заключается в том, что я получаю два немного разных массива байтов в зависимости от того, поступает ли изображение из браузера или откуда-то из моего основного локального хранилища. Я недостаточно знаю об изображениях в Java (или вообще), чтобы понять, почему это так, и не смог найти ответы в другом месте. Перетаскивание из браузера создает достаточно другой массив байтов, поэтому я не могу правильно декодировать сообщение на 100% и, следовательно, не могу десериализовать его в объект. Однако, если я щелкну правой кнопкой мыши и сохраню изображение на основе браузера, оно загрузится правильно.

Я включил воспроизводимый фрагмент и тестовое изображение ниже. Мой JDK — это самая последняя версия Amazon Corretto 8.

Воспроизводимый фрагмент:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.scene.image.Image;
import javafx.scene.image.PixelFormat;
import javafx.scene.image.PixelReader;
import javafx.scene.image.WritablePixelFormat;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;


import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.util.List;

public class MainApp extends Application {
    private static final int OFFSET = 40;
    private ListView<String> listView;
    private GridPane gridPane;

    @Override
    public void start(Stage primaryStage) throws Exception {
        listView = new ListView<>();
        gridPane = new GridPane();
        gridPane.addRow(0, listView);
        setDragDropAction();
        Scene scene = new Scene(gridPane, 250, 250);
        primaryStage.setScene(scene);
        primaryStage.sizeToScene();
        primaryStage.show();
    }

// the same drag-drop logic I am using in my project
    private void setDragDropAction() {
        gridPane.setOnDragOver(event -> {
            if (event.getDragboard().hasFiles() || event.getDragboard().hasImage()) {
                event.acceptTransferModes(TransferMode.ANY);
            }
            event.consume();
        });

        gridPane.setOnDragDropped(event -> {
            List<File> files = event.getDragboard().getFiles();
            try {
                if (!files.isEmpty()) {
                    String decoded = decode(files.get(0));
                    System.out.println(decoded);
                } else if (event.getDragboard().hasImage()) {
                    Image image = event.getDragboard().getImage();
                    String decoded = decode(image);
                    System.out.println(decoded);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
    }

    // decode using the file path
    public String decode(File file) throws IOException {
        byte[] byteImage = imageToByteArray(file);
        return getStringFromBytes(byteImage);
    }

    // this results in a different byte array and a failed decode
    public String decode(Image image) {
        int width = (int) image.getWidth();
        int height = (int) image.getHeight();
        PixelReader reader = image.getPixelReader();
        byte[] byteImage = new byte[width * height * 4];
        WritablePixelFormat<ByteBuffer> format = PixelFormat.getByteBgraInstance();
        reader.getPixels(0, 0, width, height, format, byteImage, 0, width * 4);

        return getStringFromBytes(byteImage);
    }

    private String getStringFromBytes(byte[] byteImage) {
        int offset = OFFSET;
        byte[] byteLength = new byte[4];
        System.arraycopy(byteImage, 1, byteLength, 0, (offset / 8) - 1);
        int length = byteArrayToInt(byteLength);
        byte[] result = new byte[length];
        for (int b = 0; b < length; ++b) {
            for (int i = 0; i < 8; ++i, ++offset) {
                result[b] = (byte) ((result[b] << 1) | (byteImage[offset] & 1));
            }
        }
        return new String(result);
    }

    private int byteArrayToInt(byte[] b) {
        return b[3] & 0xFF
                | (b[2] & 0xFF) << 8
                | (b[1] & 0xFF) << 16
                | (b[0] & 0xFF) << 24;
    }

    private byte[] imageToByteArray(File file) throws IOException {
        BufferedImage image;
        URL path = file.toURI().toURL();
        image = ImageIO.read(path);
        return ((DataBufferByte) image.getRaster().getDataBuffer()).getData();
    }

}

Еще одна попытка метода декодирования с использованием SwingFXUtils.fromFXImage(), которая также не работает:


// Create a buffered image with the same imageType (6) as the type returned from ImageIO.read() with a local drag-drop
public String decode(Image image) throws IOException {
        int width = (int) image.getWidth();
        int height = (int) image.getHeight();
        BufferedImage buffImageFinal = new BufferedImage(width, height, 6); // imageType = 6
        BufferedImage buffImageTemp =  SwingFXUtils.fromFXImage(image, null);
        Graphics2D g = buffImageFinal.createGraphics();
        g.drawImage(buffImageTemp, 0, 0, width, height, null);
        g.dispose();
        return getStringFromBytes(((DataBufferByte) buffImageFinal.getRaster().getDataBuffer()).getData());
    }

Изображения, показывающие изменения в данных:

Угол изображения. Непохожие пиксели выделены красным цветом.

Первые 12 байтов, показывающие разницу

Пример изображения для тестирования:

Пример изображения для тестирования

Декодированное тестовое сообщение должно быть распечатано («Это тестовое сообщение для моего минимального воспроизводимого примера». Перетаскивание изображения из браузера не работает, сохранение изображения локально и его перетаскивание работает.


person tccw    schedule 06.04.2020    source источник
comment
Какие это виды изображений? Какие расширения файлов?   -  person Hovercraft Full Of Eels    schedule 07.04.2020
comment
Все они PNG.   -  person tccw    schedule 07.04.2020
comment
@Reti43 Reti43 спасибо за ваши предложения по улучшению вопроса. Я отредактировал первый фрагмент, чтобы его можно было скомпилировать и воспроизвести проблему, с которой я столкнулся, и тестовое изображение внизу поста.   -  person tccw    schedule 07.04.2020
comment
Я следил за вашими мыслями/предложениями @Reti43 и считаю, что это связано с разницей между ImageIO.read() и new Image(). Похоже, что new Image() загружает изображение с предварительно умноженной альфой, а ImageIO.read() дает мне BufferedImage без предварительного умножения альфы. В случаях, когда альфа > 0, я могу обратить этот эффект, но данные теряются безвозвратно, когда альфа = 0, как это происходит для нескольких областей на предоставленном мной образце изображения. Я еще не понял, как заставить new Image() загружать без предварительного умножения, но если я смогу, я думаю, что это решит проблему.   -  person tccw    schedule 10.04.2020


Ответы (1)


Основываясь на комментарии OP, event.getDragboard.getImage() загружает изображение с предварительно умноженной альфой, а ImageIO.read() — нет. Один простой способ обойти это — получить URL-адрес события (независимо от того, как изображение перетаскивается) и использовать ImageIO.read().

gridPane.setOnDragDropped(event -> {
    if (event.getDragboard().hasUrl()) {
        try {
            URL path = new URL(event.getDragboard().getUrl());
            String decoded = decode(path);
            System.out.println(decoded);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
});

Со следующими модификациями

public String decode(URL url) throws IOException {
    byte[] byteImage = imageToByteArray(url);
    return getStringFromBytes(byteImage);
}

private byte[] imageToByteArray(URL url) throws IOException {
    BufferedImage image = ImageIO.read(url);
    return ((DataBufferByte) image.getRaster().getDataBuffer()).getData();
}

Обратите внимание, что загрузка с URL-адреса поддерживает только ограниченный набор форматов, как показано здесь.

person Reti43    schedule 10.04.2020
comment
Спасибо за вашу помощь! В настоящее время приложение поддерживает только PNG, поэтому event.getDragboard().getUrl() работает отлично. - person tccw; 11.04.2020