为什么相同的图片(编辑:PNG)在Java中生成两个略有不同的字节数组?

huangapple go评论82阅读模式
英文:

Why does the same image (edit: PNG) produce two slightly different byte arrays in Java?

问题

以下是翻译好的内容:

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 {
    // 省略其它部分...

    // 使用文件路径解码
    public String decode(File file) throws IOException {
        byte[] byteImage = imageToByteArray(file);
        return getStringFromBytes(byteImage);
    }

    // 使用图像解码,这导致了不同的字节数组和解码失败
    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);
    }

    // 省略其它部分...

    // 创建具有与ImageIO.read()返回类型(6)相同的imageType(6)的缓冲图像
    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());
    }

    // 省略其它部分...
}

希望这能满足您的要求。如果您有任何进一步的问题或需要更多帮助,请随时问我。

英文:

I am writing a simple application and one of the features is the ability to serialize an object to an image so that the user can share a photo of their project with the build instructions embedded within it. Currently the other user can drag-and-drop the image into a JavaFX ListView and a listener will execute some code to decode and parse the embedded JSON.

This works fine if the image is stored locally, but I also want to allow users to drag and drop images from a browser without having to save them locally first. To test this I linked a working image in a basic HTML file so I could render it in Chrome.

Originally I was using the path to the file taken from the Dragboard, but when the image is coming from a browser I need (I think) to be able to accept the image directly, hence the overloaded decode() method below.

The problem I am having is that I am ending up with two slightly different byte arrays depending on whether the image comes from a browser or from somewhere in my main local storage. I don't know enough about images within Java (or in general) to understand why this is and haven't been able to find answers elsewhere. The browser sourced drag-drop produces a different enough byte array that I can't decode the message 100% properly, and therefore cannot deserialize it to an object. However, if I right click and save the browser based image, it loads correctly.

I have included a reproducible snippet and a test image below. My JDK is the most recent version of Amazon Corretto 8.

Reproducible snippet:

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&lt;String&gt; listView;
private GridPane gridPane;
@Override
public void start(Stage primaryStage) throws Exception {
listView = new ListView&lt;&gt;();
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 -&gt; {
if (event.getDragboard().hasFiles() || event.getDragboard().hasImage()) {
event.acceptTransferModes(TransferMode.ANY);
}
event.consume();
});
gridPane.setOnDragDropped(event -&gt; {
List&lt;File&gt; 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&lt;ByteBuffer&gt; 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 &lt; length; ++b) {
for (int i = 0; i &lt; 8; ++i, ++offset) {
result[b] = (byte) ((result[b] &lt;&lt; 1) | (byteImage[offset] &amp; 1));
}
}
return new String(result);
}
private int byteArrayToInt(byte[] b) {
return b[3] &amp; 0xFF
| (b[2] &amp; 0xFF) &lt;&lt; 8
| (b[1] &amp; 0xFF) &lt;&lt; 16
| (b[0] &amp; 0xFF) &lt;&lt; 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();
}
}

Another attempt at the decode method using SwingFXUtils.fromFXImage() which is also not working:


// 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());
}

Images showing the changes in the data:

Corner of image. Dissimilar pixels are highlighted in red.

为什么相同的图片(编辑:PNG)在Java中生成两个略有不同的字节数组?

Example image for testing:

为什么相同的图片(编辑:PNG)在Java中生成两个略有不同的字节数组?

The decoded testing message should print out ("This is a test message for my minimal reproducible example". Dragging the image from the browser does not work, saving the image locally and drag-dropping it does.

答案1

得分: 0

根据原帖的评论,event.getDragboard.getImage()加载带有预乘alpha的图像,而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加载仅支持一组有限的格式,可以在这里看到。

英文:

Based on a comment from the OP, event.getDragboard.getImage() loads an image with premultiplied alpha, while ImageIO.read() does not. One simple way to circumvent that is to get the url from the event (regardless of how the image is drag and dropped) and use ImageIO.read().

gridPane.setOnDragDropped(event -&gt; {
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();
}
}
});

With the following modifications

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();
}

Note that loading from an url supports only a limited set of formats, as seen here.

huangapple
  • 本文由 发表于 2020年4月7日 05:05:43
  • 转载请务必保留本文链接:https://go.coder-hub.com/61068970.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定