首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> J2ME

Java Image 轉 PNG 位元組陣列:PngEncoder 完整原始碼解析與 J2ME 移植指南

     編輯:J2ME

核心功能

PngEncoder 將 java.awt.Image (DirectColorModel) 編碼為標準 PNG 位元組流,支援:

  • 真彩色 (RGB) / 真彩色+Alpha (RGBA)
  • 無濾波 / Sub / Up 濾波器 (PNG 標準)
  • Deflate 壓縮等級 0~9
  • CRC-32 校驗碼自動計算

輸出 byte[] 可直接 FileOutputStream.write() 存檔,或在 J2ME 中 Image.createImage(bytes, 0, len) 解碼顯示。

關鍵類結構

public class PngEncoder {
    // 常數
    public static final boolean ENCODE_ALPHA = true;
    public static final boolean NO_ALPHA = false;
    public static final int FILTER_NONE = 0, FILTER_SUB = 1, FILTER_UP = 2;

    // 狀態
    protected byte[] pngBytes;      // 輸出緩衝
    protected byte[] priorRow;      // Up 濾波用上一行
    protected byte[] leftBytes;     // Sub 濾波用左側像素
    protected Image image;          // 來源 Image
    protected int width, height;
    protected int bytePos, maxPos;  // 緩衝寫入指標
    protected CRC32 crc;            // CRC-32 計算器
    protected boolean encodeAlpha;
    protected int filter;
    protected int bytesPerPixel;    // 3 (RGB) 或 4 (RGBA)
    protected int compressionLevel; // 0~9

    // 建構子多載...
    // 核心方法:
    public byte[] pngEncode(boolean encodeAlpha)  // 返回完整 PNG byte[]
    public byte[] pngEncode()                     // 使用建構子設定
    // 內部:writeHeader, writeImageData, writeEnd, filterSub, filterUp, writeBytes, writeInt2/4, writeString
}

編碼流程圖解

pngEncode()
 ├─ writeBytes(PNG_SIGNATURE)           // 8 bytes: 89 50 4E 47 0D 0A 1A 0A
 ├─ writeHeader()                       // IHDR chunk: 寬、高、位深 8、色彩類型 2/6、壓縮/濾波/隔行 0
 ├─ writeImageData()                    // 一或多個 IDAT chunk
 │    ├─ 分塊讀取像素 (PixelGrabber, 32KB 上限)
 │    ├─ 掃描列前加濾波器類型 byte (0/1/2)
 │    ├─ Sub/Up 濾波 (可選)
 │    ├─ DeflaterOutputStream 壓縮
 │    └─ 寫入 IDAT chunk (長度 + "IDAT" + 壓縮資料 + CRC)
 └─ writeEnd()                          // IEND chunk: 長度 0 + "IEND" + CRC

完整原始碼 (精簡版,保留核心邏輯)

import java.awt.*;
import java.awt.image.*;
import java.io.*;
import java.util.zip.*;

public class PngEncoder {
    private static final byte[] PNG_SIG = {(byte)0x89,'P','N','G',0x0D,0x0A,0x1A,0x0A};

    public byte[] pngEncode(Image img, boolean alpha, int filter, int level) {
        this.image = img; this.encodeAlpha = alpha; this.filter = filter; this.compressionLevel = level;
        width = img.getWidth(null); height = img.getHeight(null);
        bytesPerPixel = alpha ? 4 : 3;

        pngBytes = new byte[(width+1)*height*bytesPerPixel + 500]; // 預估
        bytePos = 0; maxPos = 0; crc = new CRC32();

        writeBytes(PNG_SIG);
        writeHeader();
        if (writeImageData()) writeEnd();
        else return null;

        byte[] result = new byte[maxPos];
        System.arraycopy(pngBytes, 0, result, 0, maxPos);
        return result;
    }

    // --- IHDR ---
    private void writeHeader() {
        int start = bytePos;
        writeInt4(13); writeString("IHDR");
        writeInt4(width); writeInt4(height);
        writeByte(8);                          // bit depth
        writeByte(encodeAlpha ? 6 : 2);        // color type: 6=RGBA, 2=RGB
        writeByte(0); writeByte(0); writeByte(0); // compression, filter, interlace
        writeCrc(start);
    }

    // --- IDAT ---
    private boolean writeImageData() {
        Deflater def = new Deflater(compressionLevel);
        ByteArrayOutputStream baos = new ByteArrayOutputStream(1024);
        DeflaterOutputStream dos = new DeflaterOutputStream(baos, def);
        PixelGrabber pg;
        byte[] scanline = new byte[width * bytesPerPixel + 1]; // +1 filter byte

        try {
            for (int y = 0; y < height; y++) {
                int[] pixels = new int[width];
                pg = new PixelGrabber(image, 0, y, width, 1, pixels, 0, width);
                pg.grabPixels();
                if ((pg.getStatus() & ImageObserver.ABORT) != 0) return false;

                scanline[0] = (byte) filter;
                for (int x = 0; x < width; x++) {
                    int p = pixels[x];
                    int base = 1 + x * bytesPerPixel;
                    scanline[base]   = (byte)(p >> 16);        // R
                    scanline[base+1] = (byte)(p >> 8);         // G
                    scanline[base+2] = (byte)p;                // B
                    if (encodeAlpha) scanline[base+3] = (byte)(p >> 24); // A
                }
                if (filter == FILTER_SUB) filterSub(scanline, 1, width);
                else if (filter == FILTER_UP) filterUp(scanline, 1, width);
                dos.write(scanline);
            }
            dos.finish();
            byte[] compressed = baos.toByteArray();
            writeChunk("IDAT", compressed);
            return true;
        } catch (Exception e) { return false; }
    }

    private void filterSub(byte[] line, int start, int w) {
        for (int i = start + bytesPerPixel; i < start + w * bytesPerPixel; i++) {
            line[i] = (byte)((line[i] - line[i - bytesPerPixel]) & 0xFF);
        }
    }
    private void filterUp(byte[] line, int start, int w) {
        if (priorRow == null) priorRow = new byte[w * bytesPerPixel];
        for (int i = 0; i < w * bytesPerPixel; i++) {
            byte cur = line[start + i];
            line[start + i] = (byte)((cur - priorRow[i]) & 0xFF);
            priorRow[i] = cur;
        }
    }

    // --- IEND ---
    private void writeEnd() { writeChunk("IEND", new byte[0]); }

    // --- 通用 Chunk 寫入 ---
    private void writeChunk(String type, byte[] data) {
        int start = bytePos;
        writeInt4(data.length);
        writeString(type);
        crc.reset(); crc.update(type.getBytes()); crc.update(data);
        writeInt4((int)crc.getValue());
    }

    // --- 低層寫入輔助 ---
    private void writeBytes(byte[] b) { ensure(b.length); System.arraycopy(b,0,pngBytes,bytePos,b.length); bytePos+=b.length; maxPos=Math.max(maxPos,bytePos); }
    private void ensure(int need) { if (bytePos+need > pngBytes.length) pngBytes = resize(pngBytes, pngBytes.length+Math.max(1000,need)); }
    private byte[] resize(byte[] a, int n) { byte[] na=new byte[n]; System.arraycopy(a,0,na,0,Math.min(a.length,n)); return na; }
    private void writeInt4(int v) { writeBytes(new byte[]{(byte)(v>>24),(byte)(v>>16),(byte)(v>>8),(byte)v}); }
    private void writeInt2(int v) { writeBytes(new byte[]{(byte)(v>>8),(byte)v}); }
    private void writeByte(int v) { writeBytes(new byte[]{(byte)v}); }
    private void writeString(String s) { writeBytes(s.getBytes()); }
    private void writeCrc(int start) { crc.reset(); crc.update(pngBytes,start,bytePos-start); writeInt4((int)crc.getValue()); }
}

移植到 J2ME (MIDP) 的關鍵差異

Java SEJ2ME / MIDP 替代方案
java.awt.Image, PixelGrabber無對應。改用 javax.microedition.lcdui.Image + getRGB(int[], int, int, int, int, int, int) 逐行讀取像素
DeflaterOutputStream無內建。選項:
1. 不壓縮 (存儲模式,手寫 DEFLATE 區塊頭)
2. 移植輕量 JZlib / tiny-inflater (約 15KB)
3. 只做 PNG 非壓縮模式 (IDAT 直接存原始掃描列,檔案較大但零依賴)
CRC32無內建。自行實作 CRC-32 表驅動版 (約 30 行、1KB 表)
ByteArrayOutputStream有 java.io.ByteArrayOutputStream (CLDC 1.1+)

J2ME 版極簡 PNG 編碼器 (無壓縮、無濾波、純 RGB)

// 僅供參考:產生最小可用 PNG (適合極小圖示、動態生成)
public class MicroPngEncoder {
    public static byte[] encodeRGB(Image src) { // src: javax.microedition.lcdui.Image
        int w = src.getWidth(), h = src.getHeight();
        int[] rgb = new int[w];
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(baos);

        // Signature
        dos.write(new byte[]{(byte)0x89,'P','N','G',0x0D,0x0A,0x1A,0x0A});
        // IHDR
        writeChunk(dos, "IHDR", new byte[]{
            (byte)(w>>24),(byte)(w>>16),(byte)(w>>8),(byte)w,
            (byte)(h>>24),(byte)(h>>16),(byte)(h>>8),(byte)h,
            8, 2, 0, 0, 0
        });
        // IDAT (無壓縮: 每行 1 byte filter 0 + 3w bytes RGB, 以 DEFLATE stored block 封裝)
        // 這裡為演示直接寫入 raw IDAT (非標準),實際需依 RFC 1950/1951 封裝 zlib + DEFLATE stored blocks
        // 建議:引入 JZlib 完成正規壓縮
        // ...
        // IEND
        writeChunk(dos, "IEND", new byte[0]);
        return baos.toByteArray();
    }
    private static void writeChunk(DataOutputStream dos, String type, byte[] data) throws IOException {
        dos.writeInt(data.length);
        dos.write(type.getBytes());
        dos.write(data);
        // CRC 略 (需自行實作)
    }
}

結語

PngEncoder 是 Java SE 端生成 PNG 的經典參考實作,展示了 PNG 區塊結構、濾波器、Deflate 壓縮、CRC 校驗的完整流程。移植到 J2ME 時,核心挑戰在於 無 PixelGrabber、無 Deflater、無 CRC32。實務上建議:離線預生成 PNG 打包進 JAR;若必須運行期生成,引入 JZlib (CLDC 移植版) 並自行實作 CRC32,或採用「不壓縮 PNG」妥協方案。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved