PNGデコーダの調査結果:ESP8266でのデコードは厳しいかも

ESP8266でJPEGGIFがデコードできましたので、PNGも…と思い、仕様や既存のライブラリを調査していました。
その結果をここでまとめておきます。

結論としては、ESP8266でのPNG画像のデコードは不可能ではないものの、RAMの消費量や現時点で使えるデコーダライブラリの状況からは、JPEGやGIFに比べると課題が多いと言えます。

まず、PNGの圧縮方法について触れておきます。
PNGは、原理的には「ビットマップ画像をZIPで圧縮した」イメージに近いです。
圧縮アルゴリズムはDeflateと呼ばれているもので、ZIP、gzip等で使われているものと同じです。
Deflate圧縮は、出現頻度の高いビット列に短い符号を割り当てるハフマン符号化と、過去に既出のビットパターンを参照することでデータの繰り返しを圧縮するLZ77を組み合わせたものです。

Image compression
Deflate
SWFバイナリ編集のススメ番外編 (zlib 伸張) 前編 | GREE Engineers’ Blog
SWFバイナリ編集のススメ番外編 (zlib 伸張) 中編 | GREE Engineers’ Blog

次に、Deflate圧縮の特徴ですが、過去に既出のパターンがあったかどうかを調べるための辞書が必要となります。
どれくらい過去までさかのぼるか(スライディングウインドウ)で辞書のサイズが決まってきますが、PNGではこのサイズは最大32768となっています。
従って、デコード側では32768バイトのデータを保持しておく必要があります。
これだけでRAMのデータ用領域を半分近く消費してしまいますので、ESP8266のメモリ内でのデコード処理はなかなか困難です。
現実的にはファイルなどに書き出して過去のデータを参照可能にしておく必要がありそうです。

また、PNGは単に画像をDeflate圧縮するのではなく、スキャンライン単位で前処理を適用することで、圧縮率を高めています。
前処理は可逆変換で、ピクセル間の差分のみを記録する方法が4通り用意されています。

PNG画像を自力で読む

この前処理は、デコード時に逆の処理を行って元の画像を復元しなければなりませんので、最低でも1スキャンライン分は、画像をバッファしておく必要があります。

ということで、ESP8266でオンメモリでPNG画像をデコードするのは困難ではないかと思われます。
SPIFFSなどの外部記憶を使う必要があるでしょう。

次に、既存のPNGデコーダを調べてみました。

標準のデコーダはlibpngで、Deflate圧縮にはzlibを使用しています。

PNGをデコードするのに必要なファイルだけをlibpngとzlibから抽出し、ARM系のマイコンであるSTM32F4で動作させた例が以下で紹介されています。

ねむいさんのぶろぐ | STM32F4シリーズを使ってみる5 -libpngを実装する-

libpng+zlibはESP8266で使うにはやや大規模すぎます。
zlibのcontribフォルダに、puffというより小規模なDeflateの実装があります。
puffはブートローダなど、より制約の厳しいソフトウェアで使うことが想定されています。
ただし、提供されているAPIはデータを一度に全て解凍しますので、画面全体を納めるバッファが必要です。
組み込みで使うには、漸進的に解凍するような処理を自分で書く必要があるでしょう。

zlib/contrib/puff at master · madler/zlib

puffはzlibよりもスピードは遅いですが、これでPNGデコーダを作ることもできます。
そのようなデコーダの例としてlpngがあります。
ただし、puffをそのまま使っていますので、復号後の画像を保持できるだけのバッファが必要になります。

apankrat/lpng

zlib以外のDeflateアルゴリズムの実装、特に伸張部分のみの実装の一覧がWikipediaにあります。

DEFLATE – Wikipedia, the free encyclopedia

この中で、組み込み向けと思われるものにminizがあります。
画像をPNGファイルに書き出す関数は組み込まれていますが、PNG画像を展開する関数は実装されていません。

miniz – Single C source file Deflate/Inflate compression library with zlib-compatible API, ZIP archive reading/writing, PNG writing – Google Project Hosting

tinf(Tiny INFlate)も小さなDeflate(からの解凍)の実装です。
他のものと同様、圧縮されたビット列と展開したビット列がオンメモリであるという前提の実装です。
ただ、これくらい短いコードだと、漸進的に解凍するように変更することも容易かもしれません。

jibsen / tinf — Bitbucket

zlibに依存しないPNGデコーダ・エンコーダ実装としては、LodePNGがあります。
内部的に、メモリを確保するためにmallocを使用していますので、そのままでは小規模な組み込みシステムで使用するのはちょっと苦しそうです。

LodePNG

LodePNGのページの中に、さらに小規模なデコーダ実装であるpicopngも紹介されています。
こちらは、メモリを確保するのにSTLのVectorを使用しています。
STL自体は、Arduinoにも移植されていますが、picopngを使用しても、一時的に大量のRAMが必要になるという問題自体の解決にはなりません。
picopngはメモリのフットプリントが小さいわけではなく、1つのファイルに短くまとまったポータブルなデコーダと見るべきで、必ずしも組み込みに向いているわけではなさそうです。

lodev.org/lodepng/picopng.cpp
maniacbug/StandardCplusplus

LodePNGはエンコーダも含んだライブラリですが、デコーダ部分だけを抜き出したupngという派生系があります。
こちらはSTLも必要なく、picopngよりも組み込み向きかもしれません。
とはいえワークメモリ確保にはmallocを使っていますし、全体を一度にデコードするので一時的に大量のRAMが必要になる点も違いはありません。

elanthis/upng

他には、以下のページで日本の方がC++で作成されたエンコーダ/デコーダの実装が公開されています。
こちらは、入力データはバッファへ全て読み込み、デコードは1バイトずつ出力する形態になっています。

YSFLIGHT.COM – PNG Encoder / Decode in C++

という感じで、いろいろデコーダを眺めてみましたが、PNGのデコード処理が

 ハフマン符号の列(不定長の符号)→ピクセル列(復元前)→元画像の復元

というステップを辿るため、基本的にはこれらのステップを直列的につないだデコーダばかりでした。
そのため、元画像の復元前の段階で、画像全体をメモリ上に保持するような内部構造になっています。
実際には復元処理には、1つ上のスキャンラインを保持していれば十分なはずですので、上の3ステップは並列に動けるはずです。
libpngにはそのような実装の解説もされていますが、より小さなデコーダでProgressive Decodeを実装したものは見当たりませんでした。

Reading PNG Images Progressively (PNG: The Definitive Guide)

Deflate圧縮・伸張自体は、データを読みながら出力することは可能です(でなければ巨大なファイルを圧縮・伸張できません)ので、PNGの展開でも同様のことは可能だと思います。
次のリンク先のコードは、zlibを使った、読みながら出力するプログラムの例です。

https://oku.edu.mie-u.ac.jp/~okumura/compression/comptest.c

以上、結局ESP8266で使えそうなPNGデコーダは見つかりませんでしたが、もし実装するとしたら以下のような形態になるのではないかと思います。

・Deflateされたデータを解凍した結果は、一時ファイルに保存する。解凍処理時に32768個前までのデータを参照できることが必要で、ESP8266には十分なRAMが無いため。

・解凍したデータを表示する前に、差分情報から原画像を復元する。これは全データを解凍後に一時ファイルから復元しても良いし、解凍中に平行して行うこともできるはず。

オンメモリでデコードすることも不可能ではないと思いますが、実用性という点では疑問符が付きます。
PNGが登場したのは1990年代後半ですから、PCには既に数十MBのRAMが搭載されていました。
GIFやJPEGの登場時よりも、10倍くらい潤沢なメモリが使えたことになります。
さすがに、100KBに満たないRAMでデコードする場合のことはあまり考慮されていなかったのかもしれません。

ESP8266でGIF画像をデコードする

esp826611.jpg

JPEG画像に続いて、今度はGIF画像のデコードを試してみました。

GIF画像の圧縮・展開は、結構メモリが必要になります。
具体的には、最低でも25KB以上は必要です。

まず、GIFは最大256色を使いますが、色番号に対応する色はRGB各8ビットで表します。
従って256×3=768バイトを、カラーテーブルで使用します。

また、GIF画像は辞書を使う圧縮方式を使っています。
この辞書のエントリ数は最大で4096となっています。
1つの辞書エントリはコード3つ(そのエントリ自身と、次のエントリと前のエントリ)分のスペースを必要とします。
コードは最大12ビットですので、コード1つに2バイトを割り当てるとすると、辞書のためのメモリは約24KBほど必要になります。

なお、今回は使っていませんが、辞書を使わずに毎回復号コードを手続き的に求める方法を考案されている方もおられます。

ユニシス特許に抵触しないGIF画像のデコード

この方法を実装したLZ-Rというデコーダも上記ホームページで公開されています。

ESP8266はRAMを96KB積んでおり、Arduino環境で動かす場合は、そのうち80KB程度をユーザープログラムで使えますので、上記の辞書用メモリは何とか確保できそうです。

マイコンで使えそうなデコーダを、いろいろ既存のライブラリから探してみたのですが、意外と難航しました。
まずライブラリ自体が結構大きく多機能なものが多く、最低限の機能だけを実装したものがなかなか見当たりません。
また、デコードの方法も、展開後の画像全体を保持するバッファを渡すような形態が一般的です。
QVGA画像程度でも、全体を保持しようとすると320x240x3=230KBほどのRAMが必要となりますので、ESP8266ではこの方法はちょっと使えません。

ということで相当探し回った結果、Particleというマイコンボードのフォーラムで、使えそうな小さなデコーダを見つけました。
(ちなみに以前はParticleではなくSparkという名前でしたが、改名したようです。)

Parsing images from SD – General – Particle (formerly Spark)

このコードを元に、少々修正してESP8266で動くようにしました。
デコーダは別ファイルに分けており、メインのスケッチは以下のようになります。

#include <arduino.h>
#include <SPI.h>
#include <FS.h>
#include "gif.h"
#include <Adafruit_GFX.h>    // Core graphics library
#include <Adafruit_ILI9341.h> // Hardware-specific library

#define TFT_CS     15
#define TFT_RST    5
#define TFT_DC     4

Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC);

GIF gifdecoder;  


void setup() {
 File f;

 pinMode(TFT_RST, OUTPUT);
 digitalWrite(TFT_RST, LOW);
 digitalWrite(TFT_RST, HIGH);

 Serial.begin(115200);
 Serial.println("");

 tft.begin();
 // read diagnostics (optional but can help debug problems)
 uint8_t x = tft.readcommand8(ILI9341_RDMODE);
 Serial.print("Display Power Mode: 0x"); Serial.println(x, HEX);
 x = tft.readcommand8(ILI9341_RDMADCTL);
 Serial.print("MADCTL Mode: 0x"); Serial.println(x, HEX);
 x = tft.readcommand8(ILI9341_RDPIXFMT);
 Serial.print("Pixel Format: 0x"); Serial.println(x, HEX);
 x = tft.readcommand8(ILI9341_RDIMGFMT);
 Serial.print("Image Format: 0x"); Serial.println(x, HEX);
 x = tft.readcommand8(ILI9341_RDSELFDIAG);
 Serial.print("Self Diagnostic: 0x"); Serial.println(x, HEX);

 tft.fillScreen(ILI9341_BLACK);

 SPIFFS.begin();
 f = SPIFFS.open("/test.gif", "r");
 if (f) {
   Serial.println("File Open OK");
   gifDraw(f);
 }
}

void gifDraw(File f) {
 GIF_IMAGE g;
 
 gifdecoder.init_decoder(f, &g);
 Serial.print("Image Width: ");
 Serial.println(gifdecoder.imgWidth);
 Serial.print("Image Height: ");
 Serial.println(gifdecoder.imgHeight);

 tft.setAddrWindow(0,0,g.imageDesc.IMAGE_WIDTH-1, g.imageDesc.IMAGE_HEIGHT-1);
 gifdecoder.drawGIFImage(drawpixel);
}

void drawpixel(unsigned char r, unsigned char g, unsigned char b)
{
 tft.pushColor(tft.color565(r, g, b));
}


void loop() {
 // put your main code here, to run repeatedly:

}

SPIFFS上で、”/test.gif”というファイルをデコードしてILI9340ベースのTFT LCDに表示します。

デコーダに、ピクセル描画関数へのポインタを渡す構成にしていますので、デコーダ自体は表示デバイスには依存しません。(ファイルシステムのAPIには依存しています。)

QVGAサイズの画像を表示させたときの速度はこんな感じです。

ちなみにアニメーションGIFには対応していません。
アニメーションGIFについては、小さなアニメGIFをマイコンとマトリクスLEDで表示する「Aurora」というプロジェクトがありますので、そのデコーダが参考にできるかもしれません。

pixelmatix/aurora

Overview | SmartMatrix Animated GIF Player | Adafruit Learning System

ここで使われているのはTeensyというボードで、CPUはARMですがRAMは64KBと、ESP8266よりも若干貧弱なスペックです。

今回使ったデコーダライブラリ(修正後)も、ちょっと長いですがそのまま掲載しておきます。

gif.h:

#ifndef __GIF_H__
#define __GIF_H__

typedef void(*gif_draw_pixel_t)(unsigned char r, unsigned char g, unsigned char b);

typedef struct{
unsigned short SCREEN_WIDTH;
unsigned short SCREEN_HEIGHT;
unsigned char BYTE_5;
unsigned char BACKGROUND;
unsigned char ASPECT_RATIO;
} SCREEN_DESC;

typedef struct{
unsigned short LEFT;
unsigned short UP;
unsigned short IMAGE_WIDTH;
unsigned short IMAGE_HEIGHT;
unsigned char BYTE_9;
} IMAGE_DESC;

typedef struct{
unsigned char R;
unsigned char G;
unsigned char B;
} COLOR;

typedef struct{
SCREEN_DESC screenDesc;
IMAGE_DESC imageDesc;
} GIF_IMAGE;

typedef struct{
unsigned short code;
unsigned short prevCode;
unsigned short nextCode;
} DICTIONARY_ENTRY;

class GIF {
private:
unsigned char BUFFER[16];
unsigned short PrimaryCodeSize, PrimaryDictSize;
unsigned short currentCodeSize, currentDictSize;

unsigned short code, oldcode = 0;
unsigned short code1, code2;
unsigned short code_CLEAR, code_END;

COLOR g_GlobalColorTable[256];

#define MAX_DICT_SIZE 4098
DICTIONARY_ENTRY Dictionary[MAX_DICT_SIZE];
unsigned char bitsRemaining;
unsigned short getNextCode(void);
int checkSignature(char*);
File g_fileObject;
gif_draw_pixel_t gif_draw_pixel;

void read_file(File, unsigned char *, int);
unsigned char read_code;

unsigned char byte5;
unsigned char byte9;
unsigned char currentDataSectionLength;

public:
int init_decoder(File, GIF_IMAGE *);
int drawGIFImage(gif_draw_pixel_t);
unsigned char counter;
void GIFDrawPixel(unsigned char);
unsigned short imgWidth, imgHeight;
};

#endif

gif.cpp:

#include <FS.h>
#include "gif.h"

int GIF::init_decoder(File fileObject, GIF_IMAGE * gifImage) {
	unsigned int i;
	unsigned char bpp;
	unsigned short minLZWCodeLength;
	unsigned int GCT_size;

	g_fileObject = fileObject;

	read_file(g_fileObject, BUFFER, 6);
	if (checkSignature((char*)BUFFER) != 1) return -1;
 
	read_file(g_fileObject, BUFFER, 7);

	(*gifImage).screenDesc.SCREEN_WIDTH = (BUFFER[1] << 8) + BUFFER[0];
	(*gifImage).screenDesc.SCREEN_HEIGHT = (BUFFER[3] << 8) + BUFFER[2];
	(*gifImage).screenDesc.BYTE_5 = BUFFER[4];
	(*gifImage).screenDesc.BACKGROUND = BUFFER[5];
	(*gifImage).screenDesc.ASPECT_RATIO = BUFFER[6];

	byte5 = (*gifImage).screenDesc.BYTE_5;

	bpp = (byte5 & 0x07);

	GCT_size = 2 << bpp;

	if (byte5 & 0x80){

		for (i = 0; i < GCT_size; i++){
			read_file(g_fileObject, BUFFER, 3);
			g_GlobalColorTable[i].R = BUFFER[0];
			g_GlobalColorTable[i].G = BUFFER[1];
			g_GlobalColorTable[i].B = BUFFER[2];
		}
	}
	else return -1;

	do {
		read_file(g_fileObject, BUFFER, 1);
	} while (BUFFER[0] != 0x2C);

	read_file(g_fileObject, BUFFER, 11);

	(*gifImage).imageDesc.LEFT = (BUFFER[1] << 8) + BUFFER[0];
	(*gifImage).imageDesc.UP = (BUFFER[3] << 8) + BUFFER[2];
	(*gifImage).imageDesc.IMAGE_WIDTH = (BUFFER[5] << 8) + BUFFER[4];
	(*gifImage).imageDesc.IMAGE_HEIGHT = (BUFFER[7] << 8) + BUFFER[6];
	(*gifImage).imageDesc.BYTE_9 = BUFFER[8];

	imgWidth = (*gifImage).imageDesc.IMAGE_WIDTH;
	imgHeight = (*gifImage).imageDesc.IMAGE_HEIGHT;
	byte9 = (*gifImage).imageDesc.BYTE_9;

	minLZWCodeLength = BUFFER[9] + 1;
	currentDataSectionLength = BUFFER[10];

	code_CLEAR = GCT_size;
	code_END = GCT_size + 1;
	PrimaryDictSize = GCT_size + 2;
	PrimaryCodeSize = minLZWCodeLength;
	currentCodeSize = minLZWCodeLength;

	return 0;
}

void GIF::read_file(File fileObject, unsigned char *buf, int count){
 fileObject.read(buf,count);
}

int GIF::drawGIFImage(gif_draw_pixel_t draw_pixel_func){
	unsigned int i;

	gif_draw_pixel = draw_pixel_func;
	currentDictSize = PrimaryDictSize;

	counter = 0;

	for (i = 0; i < MAX_DICT_SIZE; i++)
		Dictionary[i].prevCode = Dictionary[i].nextCode = 0;

	bitsRemaining = 0;

	while ((code = getNextCode()) != code_END){


		if (code == code_CLEAR){
			currentCodeSize = PrimaryCodeSize;
			currentDictSize = PrimaryDictSize;
			oldcode = getNextCode();

			if (oldcode > currentDictSize){
				return -3;
			}
			GIFDrawPixel(oldcode);
			continue;
		}


		if (code < currentDictSize){
			code1 = code;
			code2 = 0;

			while (code1 >= PrimaryDictSize){
				Dictionary[code1 - PrimaryDictSize].nextCode = code2;
				code2 = code1;
				code1 = Dictionary[code1 - PrimaryDictSize].prevCode;
				if (code1 >= code2)
					return -3;
			}

			GIFDrawPixel(code1);
			while (code2 != 0){
				GIFDrawPixel(Dictionary[code2 - PrimaryDictSize].code);
				code2 = Dictionary[code2 - PrimaryDictSize].nextCode;
			}
			Dictionary[currentDictSize - PrimaryDictSize].code = code1;
			Dictionary[currentDictSize - PrimaryDictSize].prevCode = oldcode;
			++currentDictSize;

			if (currentDictSize == MAX_DICT_SIZE) return -2;

			if ((currentDictSize) == (0x0001 << currentCodeSize))
				++currentCodeSize;
			if (currentCodeSize > 12)
				currentCodeSize = 12;

			oldcode = code;
		}
		else {
			code1 = oldcode;
			code2 = 0;

			while (code1 >= PrimaryDictSize){
				Dictionary[code1 - PrimaryDictSize].nextCode = code2;
				code2 = code1;
				code1 = Dictionary[code1 - PrimaryDictSize].prevCode;
				if (code1 >= code2)
					return -3;
			}

			GIFDrawPixel(code1);
			while (code2 != 0){
				GIFDrawPixel(Dictionary[code2 - PrimaryDictSize].code);
				code2 = Dictionary[code2 - PrimaryDictSize].nextCode;
			}
			GIFDrawPixel(code1);

			Dictionary[currentDictSize - PrimaryDictSize].code = code1;
			Dictionary[currentDictSize - PrimaryDictSize].prevCode = oldcode;
			++currentDictSize;
			//std::cout << "dictionary size: " << std::dec << currentDictSize << std::endl;

			if (currentDictSize == MAX_DICT_SIZE) return -2;


			if ((currentDictSize) == (0x0001 << currentCodeSize))
				++currentCodeSize;

			if (currentCodeSize > 12)
				currentCodeSize = 12;

			oldcode = code;
		}
	}
	return 0;
}


int GIF::checkSignature(char * IN){
	char signature[7];
	int i;

	for (i = 0; i <= 6; i++)
		signature[i] = IN[i];

	if ((strcmp(signature, "GIF87a") == 0) || (strcmp(signature, "GIF89a") == 0))
		return 1;
	else
		return 0;
}

unsigned short GIF::getNextCode(void){
	unsigned int retval = 0, temp;

	if (bitsRemaining >= currentCodeSize){
		retval = (read_code & ((0x01 << currentCodeSize) - 1));
		read_code >>= currentCodeSize;
		bitsRemaining -= currentCodeSize;
	}
	else {
		retval = (read_code & ((0x01 << bitsRemaining) - 1));
		read_file(g_fileObject, &read_code, 1);
		++counter;
		if (counter == currentDataSectionLength){
			counter = 0;
			read_file(g_fileObject, &currentDataSectionLength, 1);
		}

		if ((currentCodeSize - bitsRemaining) <= 8){
			temp = (read_code & ((0x01 << (currentCodeSize - bitsRemaining)) - 1));
			retval += (temp << bitsRemaining);
			read_code >>= (currentCodeSize - bitsRemaining);
			bitsRemaining = 8 - (currentCodeSize - bitsRemaining);
		}
		else {
			retval += (read_code << bitsRemaining);
			read_file(g_fileObject, &read_code, 1);
			++counter;
			if (counter == currentDataSectionLength){
				counter = 0;
				read_file(g_fileObject, &currentDataSectionLength, 1);
			}
			retval += ((read_code & ((0x01 << (currentCodeSize - bitsRemaining - 8)) - 1)) << (bitsRemaining + 8));
			read_code >>= (currentCodeSize - bitsRemaining - 8);
			bitsRemaining = 8 - (currentCodeSize - bitsRemaining - 8);
		}
	}
	return retval;
}

void GIF::GIFDrawPixel(unsigned char code){
	COLOR rgbColor;

	rgbColor = g_GlobalColorTable[code];
	(*gif_draw_pixel)(rgbColor.R, rgbColor.G, rgbColor.B);
}

ILI9340液晶の表示の高速化

前回の続きです。

前回の最後のほうで

現在は1ピクセルずつdrawPixelで書いている部分をsetAddrWindowとpushColorで書き直せば、もう少し高速化できると思います。

と書きましたが、大した手間でもないのでやってみました。

描画のループ部分で使う描画命令をdrawPixelからpushColorへ変更した結果、2倍ほど高速になりました。
あまり綺麗ではありませんが、動画を載せておきます。


修正したスケッチは以下に貼っておきます。

#include <arduino.h>
#include <SPI.h>
#include <FS.h>
#include <JPEGDecoder.h>
#include <Adafruit_GFX.h>    // Core graphics library
#include <Adafruit_ILI9341.h> // Hardware-specific library

#define TFT_CS     15
#define TFT_RST    5
#define TFT_DC     4

Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC);

void setup() {
 pinMode(TFT_RST, OUTPUT);
 digitalWrite(TFT_RST, LOW);
 digitalWrite(TFT_RST, HIGH);

 Serial.begin(115200);
 Serial.println("");
 delay(10);

 tft.begin();
 // read diagnostics (optional but can help debug problems)
 uint8_t x = tft.readcommand8(ILI9341_RDMODE);
 Serial.print("Display Power Mode: 0x"); Serial.println(x, HEX);
 x = tft.readcommand8(ILI9341_RDMADCTL);
 Serial.print("MADCTL Mode: 0x"); Serial.println(x, HEX);
 x = tft.readcommand8(ILI9341_RDPIXFMT);
 Serial.print("Pixel Format: 0x"); Serial.println(x, HEX);
 x = tft.readcommand8(ILI9341_RDIMGFMT);
 Serial.print("Image Format: 0x"); Serial.println(x, HEX);
 x = tft.readcommand8(ILI9341_RDSELFDIAG);
 Serial.print("Self Diagnostic: 0x"); Serial.println(x, HEX);

 tft.fillScreen(ILI9341_BLACK);

 SPIFFS.begin();
 jpegDraw("/test1.jpg");
}

void jpegDraw(char* filename) {
 char str[100];
 uint8 *pImg;
 int x,y,bx,by;
 
 // Decoding start
 JpegDec.decode(filename,0);

 // Image Information
 Serial.print("Width     :");
 Serial.println(JpegDec.width);
 Serial.print("Height    :");
 Serial.println(JpegDec.height);
 Serial.print("Components:");
 Serial.println(JpegDec.comps);
 Serial.print("MCU / row :");
 Serial.println(JpegDec.MCUSPerRow);
 Serial.print("MCU / col :");
 Serial.println(JpegDec.MCUSPerCol);
 Serial.print("Scan type :");
 Serial.println(JpegDec.scanType);
 Serial.print("MCU width :");
 Serial.println(JpegDec.MCUWidth);
 Serial.print("MCU height:");
 Serial.println(JpegDec.MCUHeight);
 Serial.println("");
 
 sprintf(str,"#SIZE,%d,%d",JpegDec.width,JpegDec.height);
 Serial.println(str);

 // Raw Image Data
 while(JpegDec.read()){
   pImg = JpegDec.pImage ;
   x = JpegDec.MCUx * JpegDec.MCUWidth;
   y = JpegDec.MCUy * JpegDec.MCUHeight;
   tft.setAddrWindow(x, y, x+JpegDec.MCUWidth-1, y+JpegDec.MCUHeight-1);
   for(by=0; by<JpegDec.MCUHeight; by++){
     for(bx=0; bx<JpegDec.MCUWidth; bx++){
       if(JpegDec.comps == 1){ // Grayscale          
         tft.pushColor(tft.color565(pImg[0], pImg[0], pImg[0]));
       }else{ // RGB
         tft.pushColor(tft.color565(pImg[0], pImg[1], pImg[2]));
       }        
       pImg += JpegDec.comps ;
     }
   }
 }
}
   
void loop() {
}