網頁

2010年1月7日 星期四

[Java] ImageIO.read() 的怪異行為

我個人覺得這個問題還算蠻棘手的,所以在這篇文章的開頭先提供幾個關鍵字,讓同樣遇到此問題的朋友更有機會找到這篇文章。

[關鍵字]

ImageIO
Socket
Serializable
BufferedImage
NullPointerException


[問題描述]

我寫了一支 Server/Client 程式,Server 端會利用 ImageIO.write(image, "png", out) 將圖片傳給 Client 端,Client 端則用 ImageIO.read(in) 讀進為 BufferedImage 物件。

但是這支程式只有第一張圖片能夠正確的傳送,從第二張開始 ImageIO.read() 就只會回傳 null,如果在你的 code 中還有對圖片進行操作,那就可能會出現 NullPointerException 或著其他的錯誤。

[原因]

經過仔細分析後,我發現 ImageIO.read() 在讀進 PNG 格式的資料時,會留下 16 byte 在 stream 內,因此在讀取第二張圖片時就會讀到這多餘的 16 byte 而造成錯誤。

經由 PTT Java 板板友 LPH66 說明,這多出的 16 byte 中前 4 byte 是 PNG IDAT 區的 checksum,最後 12 byte 是 IEND 區。即使沒有這些資料,依然可以正確的解析圖片。

PTT Java 板板友 sbrhsieh 並補充這是因為 ImageReader 在處理數據的順序/數量上的不匹配造成的。在針對 PNG 格式時,ImageIO.read() 有短缺消耗的問題;而在處理 JPG 格式時,則有過度消耗的行為,也就是讀取一張圖片時,可能會連下一張圖片的資料也被消耗掉,使得下一張圖片無法正確讀入。在使用 ImageIO 與 FileInputStream 配合時,通常一個檔案內只會有一張圖片的資料,在讀進 JPG 時會遇到 EOF,因此並不會造成問題。

[解決方法]

當需要利用 ImaqeIO 與 Stream 傳送多張圖片時,我建議使用下列的方式:
// 修改自 PTT Java 板板友 ogamenewbie 所提供的程式
public class SerializableImage implements Serializable {
    
    byte[] data;

    public SerializableImage(BufferedImage image, String type) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ImageIO.write(image, type, baos);
        data = baos.toByteArray();
    }

    public BufferedImage getBufferedImage() throws IOException {
        return ImageIO.read(new ByteArrayInputStream(data));
    }
}
寫入圖片時:
SerializableImage serialImage = new SerializableImage(image, "png");            
out.writeObject(serialImage);
讀取圖片時:
SerializableImage serialImage = (SerializableImage)in.readObject();                
BufferedImage image = serialImage.getBufferedImage();    

[GWT] 使用具有 Constructor 參數的 Widget

本文是根據這篇文章重新以中文說明 (Using a widget that requires constructor args)

下方是使用 uiBinder 建立 UI 的簡單例子:
<!-- UserDashboard.ui.xml -->

<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
    xmlns:g='urn:import:com.google.gwt.user.client.ui'
    xmlns:my='urn:import:com.my.app.widgets' >

    <g:HTMLPanel>
        <my:CricketScores ui:field='scores' />
    </g:HTMLPanel>
</ui:UiBinder>
執行時 GWT 會偷偷的用 GWT.create() 建立 CricketScores 的實體 (經由Deferred Binding)。

但如果 CricketScores 的 Constructor 必需要傳入參數時,例如:
public CricketScores(String... teamNames) {...} 
這時 GWT.create() 就會出現錯誤:
[ERROR] com.my.app.widgets.CricketScores has no default (zero args) constructor. To fix this, you can define a @UiFactory method on the UiBinder's owner, or annotate a constructor of CricketScores with @UiConstructor.

這時候共有三種解決方法:
  1. 在 UserDashboard.java 中加入 @UiFactory method:
    // method name is insignificant
    @UiFactory CricketScores makeCricketScores() {
        return new CricketScores(teamNames);
    }
    
    @UiFactory method 的 method name 沒有特定的命名規則,GWT 會自動根據 return type 做判斷。也正因如此,如果一個 class 當中出現兩個相同 return type 的 @UiFactory method 時,GWT 也會顯示錯誤訊息:

    [ERROR] Duplicate factory in class UserDashboard for type CricketScores
  2. 使用 @UiConstructor:

    將 CricketScores 的 Constructor 加入 @UiConstructor annotation:
    public @UiConstructor CricketScores(String teamNames) {
        // ....
    } 
    
    並在 UserDashboard.ui.xml 內,CricketScores 標籤內加入與 Constructor 參數相同的屬性即可:
    <my:CricketScores ui:field='scores' teamNames='AUS, SAF, WA, QLD, VIC'/>
    
  3. 由 UiField(provided=true) 提供已建立好的物件:
    於 UserDashboard's Constructor 將 CricketScores 物件傳入,並以 @UiField 宣告 reference 變數指向 CricketScores 物件:
    public class UserDashboard extends Composite {
        interface MyUiBinder extends UiBinder<Widget, UserDashboard>;
        private static final MyUiBinder uiBinder = GWT.create(MyUiBinder.class);
    
        @UiField(provided=true)
        final CricketScores cricketScores; // cannot be private
    
        public UserDashboard(CricketScores cricketScores) {
            this.cricketScores = cricketScores;
            initWidget(uiBinder.createAndBindUi(this));
        }
    }