PngEncoder 將 java.awt.Image (DirectColorModel) 編碼為標準 PNG 位元組流,支援:
輸出 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()); }
}
| Java SE | J2ME / 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+) |
// 僅供參考:產生最小可用 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」妥協方案。