mirror of https://github.com/buggins/dlangui.git
1307 lines
33 KiB
D
1307 lines
33 KiB
D
/*
|
|
Copyright (c) 2015 Timur Gafarov
|
|
|
|
Boost Software License - Version 1.0 - August 17th, 2003
|
|
|
|
Permission is hereby granted, free of charge, to any person or organization
|
|
obtaining a copy of the software and accompanying documentation covered by
|
|
this license (the "Software") to use, reproduce, display, distribute,
|
|
execute, and transmit the Software, and to prepare derivative works of the
|
|
Software, and to permit third-parties to whom the Software is furnished to
|
|
do so, all subject to the following:
|
|
|
|
The copyright notices in the Software and this entire statement, including
|
|
the above license grant, this restriction and the following disclaimer,
|
|
must be included in all copies of the Software, in whole or in part, and
|
|
all derivative works of the Software, unless such copies or derivative
|
|
works are solely in the form of machine-executable object code generated by
|
|
a source language processor.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
|
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
|
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
|
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
DEALINGS IN THE SOFTWARE.
|
|
*/
|
|
|
|
// dimage is actually stripped out part of dlib - just to support reading PNG and JPEG
|
|
module dimage.jpeg;
|
|
|
|
version = USE_DIMAGE;
|
|
version(USE_DIMAGE):
|
|
|
|
|
|
import std.stdio;
|
|
import std.algorithm;
|
|
import std.string;
|
|
import std.traits;
|
|
|
|
import dimage.huffman;
|
|
import dimage.stream;
|
|
import dimage.compound;
|
|
import dimage.array;
|
|
//import dimage.color;
|
|
import dimage.image;
|
|
import dimage.bitio;
|
|
import dimage.memory;
|
|
import dimage.idct;
|
|
//import dlib.core.memory;
|
|
//import dlib.core.stream;
|
|
//import dlib.core.compound;
|
|
//import dlib.container.array;
|
|
//import dlib.filesystem.local;
|
|
//import dlib.image.color;
|
|
//import dlib.image.image;
|
|
//import dlib.image.io.idct;
|
|
|
|
//import dlib.core.bitio;
|
|
//import dlib.coding.huffman;
|
|
|
|
/*
|
|
* Simple JPEG decoder
|
|
*
|
|
* Limitations:
|
|
* - Doesn't support progressive JPEG
|
|
* - Doesn't perform chroma interpolation
|
|
* - Doesn't read EXIF metadata
|
|
*/
|
|
|
|
// Uncomment this to see debug messages
|
|
//version = JPEGDebug;
|
|
|
|
T readNumeric(T) (InputStream istrm, Endian endian = Endian.Little)
|
|
if (is(T == ubyte))
|
|
{
|
|
ubyte b;
|
|
istrm.readBytes(&b, 1);
|
|
return b;
|
|
}
|
|
|
|
T readNumeric(T) (InputStream istrm, Endian endian = Endian.Little)
|
|
if (is(T == ushort))
|
|
{
|
|
union U16
|
|
{
|
|
ubyte[2] asBytes;
|
|
ushort asUshort;
|
|
}
|
|
U16 u16;
|
|
istrm.readBytes(u16.asBytes.ptr, 2);
|
|
version(LittleEndian)
|
|
{
|
|
if (endian == Endian.Big)
|
|
return u16.asUshort.swapEndian16;
|
|
else
|
|
return u16.asUshort;
|
|
}
|
|
else
|
|
{
|
|
if (endian == Endian.Little)
|
|
return u16.asUshort.swapEndian16;
|
|
else
|
|
return u16.asUshort;
|
|
}
|
|
}
|
|
|
|
char[size] readChars(size_t size) (InputStream istrm)
|
|
{
|
|
char[size] chars;
|
|
istrm.readBytes(chars.ptr, size);
|
|
return chars;
|
|
}
|
|
|
|
/*
|
|
* JPEG-related Huffman coding
|
|
*/
|
|
|
|
struct HuffmanCode
|
|
{
|
|
ushort bits;
|
|
ushort length;
|
|
|
|
auto bitString()
|
|
{
|
|
return .bitString(bits, length);
|
|
}
|
|
}
|
|
|
|
struct HuffmanTableEntry
|
|
{
|
|
HuffmanCode code;
|
|
ubyte value;
|
|
}
|
|
|
|
DynamicArray!char bitString(T)(T n, uint len = 1) if (isIntegral!T)
|
|
{
|
|
DynamicArray!char arr;
|
|
|
|
const int size = T.sizeof * 8;
|
|
|
|
bool s = 0;
|
|
for (int a = 0; a < size; a++)
|
|
{
|
|
bool bit = n >> (size - 1);
|
|
if (bit)
|
|
s = 1;
|
|
if (s)
|
|
{
|
|
arr.append(bit + '0');
|
|
}
|
|
n <<= 1;
|
|
}
|
|
|
|
while (arr.length < len)
|
|
arr.appendLeft('0');
|
|
|
|
return arr;
|
|
}
|
|
|
|
HuffmanTreeNode* emptyNode()
|
|
{
|
|
return New!HuffmanTreeNode(null, null, cast(ubyte)0, 0, false);
|
|
}
|
|
|
|
HuffmanTreeNode* treeFromTable(DynamicArray!(HuffmanTableEntry) table)
|
|
{
|
|
HuffmanTreeNode* root = emptyNode();
|
|
|
|
foreach(i, v; table.data)
|
|
treeAddCode(root, v.code, v.value);
|
|
|
|
return root;
|
|
}
|
|
|
|
void treeAddCode(HuffmanTreeNode* root, HuffmanCode code, ubyte value)
|
|
{
|
|
HuffmanTreeNode* node = root;
|
|
auto bs = code.bitString;
|
|
foreach(bit; bs.data)
|
|
{
|
|
if (bit == '0')
|
|
{
|
|
if (node.left is null)
|
|
{
|
|
node.left = emptyNode();
|
|
node.left.parent = node;
|
|
}
|
|
|
|
node = node.left;
|
|
}
|
|
else if (bit == '1')
|
|
{
|
|
if (node.right is null)
|
|
{
|
|
node.right = emptyNode();
|
|
node.right.parent = node;
|
|
}
|
|
|
|
node = node.right;
|
|
}
|
|
}
|
|
assert (node !is null);
|
|
node.ch = value;
|
|
bs.free();
|
|
}
|
|
|
|
/*
|
|
* JPEG-related data types
|
|
*/
|
|
|
|
enum JPEGMarkerType
|
|
{
|
|
Unknown,
|
|
SOI,
|
|
SOF0,
|
|
SOF1,
|
|
SOF2,
|
|
DHT,
|
|
DQT,
|
|
DRI,
|
|
SOS,
|
|
RSTn,
|
|
APP0,
|
|
APPn,
|
|
COM,
|
|
EOI
|
|
}
|
|
|
|
struct JPEGImage
|
|
{
|
|
struct JFIF
|
|
{
|
|
ubyte versionMajor;
|
|
ubyte versionMinor;
|
|
ubyte units;
|
|
ushort xDensity;
|
|
ushort yDensity;
|
|
ubyte thumbnailWidth;
|
|
ubyte thumbnailHeight;
|
|
ubyte[] thumbnail;
|
|
|
|
void free()
|
|
{
|
|
if (thumbnail.length)
|
|
Delete(thumbnail);
|
|
}
|
|
}
|
|
|
|
struct DQT
|
|
{
|
|
ubyte precision;
|
|
ubyte tableId;
|
|
ubyte[] table;
|
|
|
|
void free()
|
|
{
|
|
if (table.length)
|
|
Delete(table);
|
|
}
|
|
}
|
|
|
|
struct SOF0Component
|
|
{
|
|
ubyte hSubsampling;
|
|
ubyte vSubsampling;
|
|
ubyte dqtTableId;
|
|
}
|
|
|
|
struct SOF0
|
|
{
|
|
ubyte precision;
|
|
ushort height;
|
|
ushort width;
|
|
ubyte componentsNum;
|
|
SOF0Component[] components;
|
|
|
|
void free()
|
|
{
|
|
if (components.length)
|
|
Delete(components);
|
|
}
|
|
}
|
|
|
|
struct DHT
|
|
{
|
|
ubyte clas;
|
|
ubyte tableId;
|
|
DynamicArray!HuffmanTableEntry huffmanTable;
|
|
HuffmanTreeNode* huffmanTree;
|
|
|
|
void free()
|
|
{
|
|
huffmanTree.free();
|
|
Delete(huffmanTree);
|
|
huffmanTable.free();
|
|
}
|
|
}
|
|
|
|
struct SOSComponent
|
|
{
|
|
ubyte tableIdDC;
|
|
ubyte tableIdAC;
|
|
}
|
|
|
|
struct SOS
|
|
{
|
|
ubyte componentsNum;
|
|
SOSComponent[] components;
|
|
ubyte spectralSelectionStart;
|
|
ubyte spectralSelectionEnd;
|
|
ubyte successiveApproximationBitHigh;
|
|
ubyte successiveApproximationBitLow;
|
|
|
|
void free()
|
|
{
|
|
if (components.length)
|
|
Delete(components);
|
|
}
|
|
}
|
|
|
|
JFIF jfif;
|
|
DQT[] dqt;
|
|
SOF0 sof0;
|
|
DHT[] dht;
|
|
SOS sos;
|
|
|
|
DQT* addDQT()
|
|
{
|
|
if (dqt.length > 0)
|
|
reallocateArray(dqt, dqt.length+1);
|
|
else
|
|
dqt = New!(DQT[])(1);
|
|
return &dqt[$-1];
|
|
}
|
|
|
|
DHT* addDHT()
|
|
{
|
|
if (dht.length > 0)
|
|
reallocateArray(dht, dht.length+1);
|
|
else
|
|
dht = New!(DHT[])(1);
|
|
return &dht[$-1];
|
|
}
|
|
|
|
void free()
|
|
{
|
|
jfif.free();
|
|
foreach(ref t; dqt) t.free();
|
|
Delete(dqt);
|
|
sof0.free();
|
|
foreach(ref t; dht) t.free();
|
|
Delete(dht);
|
|
sos.free();
|
|
}
|
|
|
|
DQT* getQuantizationTable(ubyte id)
|
|
{
|
|
foreach(ref t; dqt)
|
|
if (t.tableId == id)
|
|
return &t;
|
|
return null;
|
|
}
|
|
|
|
DHT* getHuffmanTable(ubyte clas, ubyte id)
|
|
{
|
|
foreach(ref t; dht)
|
|
if (t.clas == clas &&
|
|
t.tableId == id)
|
|
return &t;
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Load JPEG from file using local FileSystem.
|
|
* Causes GC allocation
|
|
*/
|
|
//SuperImage loadJPEG(string filename)
|
|
//{
|
|
// InputStream input = openForInput(filename);
|
|
// auto img = loadJPEG(input);
|
|
// input.close();
|
|
// return img;
|
|
//}
|
|
|
|
/*
|
|
* Load JPEG from stream using default image factory.
|
|
* Causes GC allocation
|
|
*/
|
|
SuperImage loadJPEG(InputStream istrm)
|
|
{
|
|
Compound!(SuperImage, string) res =
|
|
loadJPEG(istrm, defaultImageFactory);
|
|
if (res[0] is null)
|
|
throw new Exception(res[1]);
|
|
else
|
|
return res[0];
|
|
}
|
|
|
|
/*
|
|
* Load JPEG from stream using specified image factory.
|
|
* GC-free
|
|
*/
|
|
Compound!(SuperImage, string) loadJPEG(
|
|
InputStream istrm,
|
|
SuperImageFactory imgFac)
|
|
{
|
|
JPEGImage jpg;
|
|
SuperImage img = null;
|
|
|
|
while (istrm.readable)
|
|
{
|
|
JPEGMarkerType mt;
|
|
auto res = readMarker(&jpg, istrm, &mt);
|
|
if (res[0])
|
|
{
|
|
// TODO: add progressive JPEG support
|
|
if (mt == JPEGMarkerType.SOF2)
|
|
{
|
|
jpg.free();
|
|
return compound(img, "loadJPEG error: progressive JPEG is not supported");
|
|
}
|
|
else if (mt == JPEGMarkerType.SOS)
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
jpg.free();
|
|
return compound(img, res[1]);
|
|
}
|
|
}
|
|
auto res = decodeScanData(&jpg, istrm, imgFac);
|
|
jpg.free();
|
|
return res;
|
|
}
|
|
|
|
/*
|
|
* Decode marker from JPEG stream
|
|
*/
|
|
Compound!(bool, string) readMarker(
|
|
JPEGImage* jpg,
|
|
InputStream istrm,
|
|
JPEGMarkerType* mt)
|
|
{
|
|
ushort magic = istrm.readNumeric!ushort(Endian.Big);
|
|
|
|
switch (magic)
|
|
{
|
|
case 0xFFD8:
|
|
*mt = JPEGMarkerType.SOI;
|
|
version(JPEGDebug) writeln("SOI");
|
|
break;
|
|
|
|
case 0xFFE0:
|
|
*mt = JPEGMarkerType.APP0;
|
|
return readJFIF(jpg, istrm);
|
|
|
|
case 0xFFE1:
|
|
*mt = JPEGMarkerType.APPn;
|
|
return readAPPn(jpg, istrm, 1);
|
|
|
|
case 0xFFE2:
|
|
*mt = JPEGMarkerType.APPn;
|
|
return readAPPn(jpg, istrm, 2);
|
|
|
|
case 0xFFE3:
|
|
*mt = JPEGMarkerType.APPn;
|
|
return readAPPn(jpg, istrm, 3);
|
|
|
|
case 0xFFE4:
|
|
*mt = JPEGMarkerType.APPn;
|
|
return readAPPn(jpg, istrm, 4);
|
|
|
|
case 0xFFE5:
|
|
*mt = JPEGMarkerType.APPn;
|
|
return readAPPn(jpg, istrm, 5);
|
|
|
|
case 0xFFE6:
|
|
*mt = JPEGMarkerType.APPn;
|
|
return readAPPn(jpg, istrm, 6);
|
|
|
|
case 0xFFE7:
|
|
*mt = JPEGMarkerType.APPn;
|
|
return readAPPn(jpg, istrm, 7);
|
|
|
|
case 0xFFE8:
|
|
*mt = JPEGMarkerType.APPn;
|
|
return readAPPn(jpg, istrm, 8);
|
|
|
|
case 0xFFE9:
|
|
*mt = JPEGMarkerType.APPn;
|
|
return readAPPn(jpg, istrm, 9);
|
|
|
|
case 0xFFEA:
|
|
*mt = JPEGMarkerType.APPn;
|
|
return readAPPn(jpg, istrm, 10);
|
|
|
|
case 0xFFEB:
|
|
*mt = JPEGMarkerType.APPn;
|
|
return readAPPn(jpg, istrm, 11);
|
|
|
|
case 0xFFEC:
|
|
*mt = JPEGMarkerType.APPn;
|
|
return readAPPn(jpg, istrm, 12);
|
|
|
|
case 0xFFED:
|
|
*mt = JPEGMarkerType.APPn;
|
|
return readAPPn(jpg, istrm, 13);
|
|
|
|
case 0xFFEE:
|
|
*mt = JPEGMarkerType.APPn;
|
|
return readAPPn(jpg, istrm, 14);
|
|
|
|
case 0xFFEF:
|
|
*mt = JPEGMarkerType.APPn;
|
|
return readAPPn(jpg, istrm, 15);
|
|
|
|
case 0xFFDB:
|
|
*mt = JPEGMarkerType.DQT;
|
|
return readDQT(jpg, istrm);
|
|
|
|
case 0xFFC0:
|
|
*mt = JPEGMarkerType.SOF0;
|
|
return readSOF0(jpg, istrm);
|
|
|
|
case 0xFFC2:
|
|
*mt = JPEGMarkerType.SOF2;
|
|
break;
|
|
|
|
case 0xFFC4:
|
|
*mt = JPEGMarkerType.DHT;
|
|
return readDHT(jpg, istrm);
|
|
|
|
case 0xFFDA:
|
|
*mt = JPEGMarkerType.SOS;
|
|
return readSOS(jpg, istrm);
|
|
|
|
case 0xFFFE:
|
|
*mt = JPEGMarkerType.COM;
|
|
return readCOM(jpg, istrm);
|
|
|
|
default:
|
|
*mt = JPEGMarkerType.Unknown;
|
|
break;
|
|
}
|
|
|
|
return compound(true, "");
|
|
}
|
|
|
|
Compound!(bool, string) readJFIF(JPEGImage* jpg, InputStream istrm)
|
|
{
|
|
ushort jfif_length = istrm.readNumeric!ushort(Endian.Big);
|
|
|
|
char[5] jfif_id = istrm.readChars!5;
|
|
if (jfif_id != "JFIF\0")
|
|
return compound(false, "loadJPEG error: illegal JFIF header");
|
|
|
|
jpg.jfif.versionMajor = istrm.readNumeric!ubyte;
|
|
jpg.jfif.versionMinor = istrm.readNumeric!ubyte;
|
|
jpg.jfif.units = istrm.readNumeric!ubyte;
|
|
jpg.jfif.xDensity = istrm.readNumeric!ushort(Endian.Big);
|
|
jpg.jfif.yDensity = istrm.readNumeric!ushort(Endian.Big);
|
|
jpg.jfif.thumbnailWidth = istrm.readNumeric!ubyte;
|
|
jpg.jfif.thumbnailHeight = istrm.readNumeric!ubyte;
|
|
|
|
uint jfif_thumb_length = jpg.jfif.thumbnailWidth * jpg.jfif.thumbnailHeight * 3;
|
|
if (jfif_thumb_length > 0)
|
|
{
|
|
jpg.jfif.thumbnail = New!(ubyte[])(jfif_thumb_length);
|
|
istrm.readBytes(jpg.jfif.thumbnail.ptr, jfif_thumb_length);
|
|
}
|
|
|
|
version(JPEGDebug)
|
|
{
|
|
writefln("APP0/JFIF length: %s", jfif_length);
|
|
writefln("APP0/JFIF identifier: %s", jfif_id);
|
|
writefln("APP0/JFIF version major: %s", jpg.jfif.versionMajor);
|
|
writefln("APP0/JFIF version minor: %s", jpg.jfif.versionMinor);
|
|
writefln("APP0/JFIF units: %s", jpg.jfif.units);
|
|
writefln("APP0/JFIF xdensity: %s", jpg.jfif.xDensity);
|
|
writefln("APP0/JFIF ydensity: %s", jpg.jfif.yDensity);
|
|
writefln("APP0/JFIF xthumbnail: %s", jpg.jfif.thumbnailWidth);
|
|
writefln("APP0/JFIF ythumbnail: %s", jpg.jfif.thumbnailHeight);
|
|
}
|
|
|
|
return compound(true, "");
|
|
}
|
|
|
|
/*
|
|
* APP1 - EXIF, XMP, ExtendedXMP, QVCI, FLIR
|
|
* APP2 - ICC, FPXR, MPF, PreviewImage
|
|
* APP3 - Kodak Meta, Stim, PreviewImage
|
|
* APP4 - Scalado, FPXR, PreviewImage
|
|
* APP5 - RMETA, PreviewImage
|
|
* APP6 - EPPIM, NITF, HP TDHD
|
|
* APP7 - Pentax, Qualcomm
|
|
* APP8 - SPIFF
|
|
* APP9 - MediaJukebox
|
|
* APP10 - PhotoStudio comment
|
|
* APP11 - JPEG-HDR
|
|
* APP12 - PictureInfo, Ducky
|
|
* APP13 - Photoshop, Adobe CM
|
|
* APP14 - Adobe
|
|
* APP15 - GraphicConverter
|
|
*/
|
|
Compound!(bool, string) readAPPn(JPEGImage* jpg, InputStream istrm, uint n)
|
|
{
|
|
ushort app_length = istrm.readNumeric!ushort(Endian.Big);
|
|
ubyte[] app = New!(ubyte[])(app_length-2);
|
|
istrm.readBytes(app.ptr, app_length-2);
|
|
|
|
// TODO: interpret APP data (EXIF etc.) and save it somewhere.
|
|
// Maybe add a generic ImageInfo object for this?
|
|
Delete(app);
|
|
|
|
version(JPEGDebug)
|
|
{
|
|
writefln("APP%s length: %s", n, app_length);
|
|
}
|
|
|
|
return compound(true, "");
|
|
}
|
|
|
|
Compound!(bool, string) readCOM(JPEGImage* jpg, InputStream istrm)
|
|
{
|
|
ushort com_length = istrm.readNumeric!ushort(Endian.Big);
|
|
ubyte[] com = New!(ubyte[])(com_length-2);
|
|
istrm.readBytes(com.ptr, com_length-2);
|
|
|
|
version(JPEGDebug)
|
|
{
|
|
writefln("COM string: \"%s\"", cast(string)com);
|
|
writefln("COM length: %s", com_length);
|
|
}
|
|
|
|
// TODO: save COM data somewhere.
|
|
// Maybe add a generic ImageInfo object for this?
|
|
Delete(com);
|
|
|
|
return compound(true, "");
|
|
}
|
|
|
|
Compound!(bool, string) readDQT(JPEGImage* jpg, InputStream istrm)
|
|
{
|
|
ushort dqt_length = istrm.readNumeric!ushort(Endian.Big);
|
|
version(JPEGDebug)
|
|
{
|
|
writefln("DQT length: %s", dqt_length);
|
|
}
|
|
|
|
dqt_length -= 2;
|
|
|
|
while(dqt_length)
|
|
{
|
|
JPEGImage.DQT* dqt = jpg.addDQT();
|
|
|
|
ubyte bite = istrm.readNumeric!ubyte;
|
|
dqt.precision = bite.hiNibble;
|
|
dqt.tableId = bite.loNibble;
|
|
|
|
dqt_length--;
|
|
|
|
if (dqt.precision == 0)
|
|
{
|
|
dqt.table = New!(ubyte[])(64);
|
|
dqt_length -= 64;
|
|
}
|
|
else if (dqt.precision == 1)
|
|
{
|
|
dqt.table = New!(ubyte[])(128);
|
|
dqt_length -= 128;
|
|
}
|
|
|
|
istrm.readBytes(dqt.table.ptr, dqt.table.length);
|
|
|
|
version(JPEGDebug)
|
|
{
|
|
writefln("DQT precision: %s", dqt.precision);
|
|
writefln("DQT table id: %s", dqt.tableId);
|
|
writefln("DQT table: %s", dqt.table);
|
|
}
|
|
}
|
|
|
|
return compound(true, "");
|
|
}
|
|
|
|
Compound!(bool, string) readSOF0(JPEGImage* jpg, InputStream istrm)
|
|
{
|
|
ushort sof0_length = istrm.readNumeric!ushort(Endian.Big);
|
|
jpg.sof0.precision = istrm.readNumeric!ubyte;
|
|
jpg.sof0.height = istrm.readNumeric!ushort(Endian.Big);
|
|
jpg.sof0.width = istrm.readNumeric!ushort(Endian.Big);
|
|
jpg.sof0.componentsNum = istrm.readNumeric!ubyte;
|
|
|
|
version(JPEGDebug)
|
|
{
|
|
writefln("SOF0 length: %s", sof0_length);
|
|
writefln("SOF0 precision: %s", jpg.sof0.precision);
|
|
writefln("SOF0 height: %s", jpg.sof0.height);
|
|
writefln("SOF0 width: %s", jpg.sof0.width);
|
|
writefln("SOF0 components: %s", jpg.sof0.componentsNum);
|
|
}
|
|
|
|
jpg.sof0.components = New!(JPEGImage.SOF0Component[])(jpg.sof0.componentsNum);
|
|
|
|
foreach(ref c; jpg.sof0.components)
|
|
{
|
|
ubyte c_id = istrm.readNumeric!ubyte;
|
|
ubyte bite = istrm.readNumeric!ubyte;
|
|
c.hSubsampling = bite.hiNibble;
|
|
c.vSubsampling = bite.loNibble;
|
|
c.dqtTableId = istrm.readNumeric!ubyte;
|
|
version(JPEGDebug)
|
|
{
|
|
writefln("SOF0 component id: %s", c_id);
|
|
writefln("SOF0 component %s hsubsampling: %s", c_id, c.hSubsampling);
|
|
writefln("SOF0 component %s vsubsampling: %s", c_id, c.vSubsampling);
|
|
writefln("SOF0 component %s table id: %s", c_id, c.dqtTableId);
|
|
}
|
|
}
|
|
|
|
return compound(true, "");
|
|
}
|
|
|
|
Compound!(bool, string) readDHT(JPEGImage* jpg, InputStream istrm)
|
|
{
|
|
ushort dht_length = istrm.readNumeric!ushort(Endian.Big);
|
|
version(JPEGDebug)
|
|
{
|
|
writefln("DHT length: %s", dht_length);
|
|
}
|
|
|
|
dht_length -= 2;
|
|
|
|
while(dht_length > 0)
|
|
{
|
|
JPEGImage.DHT* dht = jpg.addDHT();
|
|
|
|
ubyte bite = istrm.readNumeric!ubyte;
|
|
dht_length--;
|
|
dht.clas = bite.hiNibble;
|
|
dht.tableId = bite.loNibble;
|
|
|
|
ubyte[16] dht_code_lengths;
|
|
istrm.readBytes(dht_code_lengths.ptr, 16);
|
|
dht_length -= 16;
|
|
|
|
version(JPEGDebug)
|
|
{
|
|
writefln("DHT class: %s (%s)",
|
|
dht.clas,
|
|
dht.clas? "AC":"DC");
|
|
writefln("DHT tableId: %s", dht.tableId);
|
|
writefln("DHT Huffman code lengths: %s", dht_code_lengths);
|
|
}
|
|
|
|
// Read Huffman table
|
|
int totalCodes = reduce!("a + b")(0, dht_code_lengths);
|
|
int storedCodes = 0;
|
|
ubyte treeLevel = 0;
|
|
ushort bits = 0;
|
|
|
|
while (storedCodes != totalCodes)
|
|
{
|
|
while (treeLevel < 15 &&
|
|
dht_code_lengths[treeLevel] == 0)
|
|
{
|
|
treeLevel++;
|
|
bits *= 2;
|
|
}
|
|
|
|
if (treeLevel < 16)
|
|
{
|
|
uint bitsNum = treeLevel + 1;
|
|
HuffmanCode code = HuffmanCode(bits, cast(ushort)bitsNum);
|
|
|
|
auto entry = HuffmanTableEntry(code, istrm.readNumeric!ubyte);
|
|
dht.huffmanTable.append(entry);
|
|
|
|
dht_length--;
|
|
|
|
storedCodes++;
|
|
bits++;
|
|
dht_code_lengths[treeLevel]--;
|
|
}
|
|
}
|
|
|
|
dht.huffmanTree = treeFromTable(dht.huffmanTable);
|
|
}
|
|
|
|
return compound(true, "");
|
|
}
|
|
|
|
Compound!(bool, string) readSOS(JPEGImage* jpg, InputStream istrm)
|
|
{
|
|
ushort sos_length = istrm.readNumeric!ushort(Endian.Big);
|
|
jpg.sos.componentsNum = istrm.readNumeric!ubyte;
|
|
|
|
version(JPEGDebug)
|
|
{
|
|
writefln("SOS length: %s", sos_length);
|
|
writefln("SOS components: %s", jpg.sos.componentsNum);
|
|
}
|
|
|
|
jpg.sos.components = New!(JPEGImage.SOSComponent[])(jpg.sos.componentsNum);
|
|
|
|
foreach(ref c; jpg.sos.components)
|
|
{
|
|
ubyte c_id = istrm.readNumeric!ubyte;
|
|
ubyte bite = istrm.readNumeric!ubyte;
|
|
c.tableIdDC = bite.hiNibble;
|
|
c.tableIdAC = bite.loNibble;
|
|
version(JPEGDebug)
|
|
{
|
|
writefln("SOS component id: %s", c_id);
|
|
writefln("SOS component %s DC table id: %s", c_id, c.tableIdDC);
|
|
writefln("SOS component %s AC table id: %s", c_id, c.tableIdAC);
|
|
}
|
|
}
|
|
|
|
jpg.sos.spectralSelectionStart = istrm.readNumeric!ubyte;
|
|
jpg.sos.spectralSelectionEnd = istrm.readNumeric!ubyte;
|
|
ubyte bite = istrm.readNumeric!ubyte;
|
|
jpg.sos.successiveApproximationBitHigh = bite.hiNibble;
|
|
jpg.sos.successiveApproximationBitLow = bite.loNibble;
|
|
|
|
version(JPEGDebug)
|
|
{
|
|
writefln("SOS spectral selection start: %s", jpg.sos.spectralSelectionStart);
|
|
writefln("SOS spectral selection end: %s", jpg.sos.spectralSelectionEnd);
|
|
writefln("SOS successive approximation bit: %s", jpg.sos.successiveApproximationBitHigh);
|
|
writefln("SOS successive approximation bit low: %s", jpg.sos.successiveApproximationBitLow);
|
|
}
|
|
|
|
return compound(true, "");
|
|
}
|
|
|
|
struct ScanBitStream
|
|
{
|
|
InputStream istrm;
|
|
|
|
bool endMarkerFound = false;
|
|
uint bytesRead = 0;
|
|
ubyte prevByte = 0x00;
|
|
ubyte curByte = 0x00;
|
|
|
|
ubyte readNextByte()
|
|
{
|
|
ubyte b = istrm.readNumeric!ubyte;
|
|
bytesRead++;
|
|
endMarkerFound = (prevByte == 0xFF && b == 0xD9);
|
|
assert(!endMarkerFound);
|
|
if (!endMarkerFound)
|
|
{
|
|
prevByte = b;
|
|
curByte = b;
|
|
return b;
|
|
}
|
|
else
|
|
{
|
|
curByte = 0;
|
|
}
|
|
return curByte;
|
|
}
|
|
|
|
bool readable()
|
|
{
|
|
return !istrm.readable || endMarkerFound;
|
|
}
|
|
|
|
uint bitPos = 0;
|
|
|
|
// Huffman decode a byte
|
|
Compound!(bool, string) decodeByte(HuffmanTreeNode* node, ubyte* result)
|
|
{
|
|
while(!node.isLeaf)
|
|
{
|
|
ubyte b = curByte;
|
|
|
|
bool bit = getBit(b, 7-bitPos);
|
|
bitPos++;
|
|
if (bitPos == 8)
|
|
{
|
|
bitPos = 0;
|
|
readNextByte();
|
|
|
|
if (b == 0xFF)
|
|
{
|
|
b = curByte;
|
|
if (b == 0x00)
|
|
{
|
|
readNextByte();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bit)
|
|
node = node.right;
|
|
else
|
|
node = node.left;
|
|
|
|
if (node is null)
|
|
return compound(false, "loadJPEG error: no Huffman code found");
|
|
}
|
|
|
|
*result = node.ch;
|
|
return compound(true, "");
|
|
}
|
|
|
|
// Read len bits from stream to buffer
|
|
uint readBits(ubyte len)
|
|
{
|
|
uint buffer = 0;
|
|
uint i = 0;
|
|
uint by = 0;
|
|
uint bi = 0;
|
|
|
|
while (i < len)
|
|
{
|
|
ubyte b = curByte;
|
|
|
|
bool bit = getBit(b, 7-bitPos);
|
|
buffer = setBit(buffer, (by * 8 + bi), bit);
|
|
|
|
bi++;
|
|
if (bi == 8)
|
|
{
|
|
bi = 0;
|
|
by++;
|
|
}
|
|
|
|
i++;
|
|
|
|
bitPos++;
|
|
if (bitPos == 8)
|
|
{
|
|
bitPos = 0;
|
|
readNextByte();
|
|
|
|
if (b == 0xFF)
|
|
{
|
|
b = curByte;
|
|
if (b == 0x00)
|
|
readNextByte();
|
|
}
|
|
}
|
|
}
|
|
|
|
return buffer;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Decodes compressed data and creates RGB image from it
|
|
*/
|
|
Compound!(SuperImage, string) decodeScanData(
|
|
JPEGImage* jpg,
|
|
InputStream istrm,
|
|
SuperImageFactory imgFac)
|
|
{
|
|
SuperImage img = imgFac.createImage(jpg.sof0.width, jpg.sof0.height, 3, 8);
|
|
|
|
MCU mcu;
|
|
foreach(ci, ref c; jpg.sof0.components)
|
|
{
|
|
if (ci == 0)
|
|
mcu.createYBlocks(c.hSubsampling, c.vSubsampling);
|
|
else if (ci == 1)
|
|
mcu.createCbBlocks(c.hSubsampling, c.vSubsampling);
|
|
else if (ci == 2)
|
|
mcu.createCrBlocks(c.hSubsampling, c.vSubsampling);
|
|
}
|
|
|
|
Compound!(SuperImage, string) error(string errorMsg)
|
|
{
|
|
mcu.free();
|
|
if (img)
|
|
{
|
|
img.free();
|
|
img = null;
|
|
}
|
|
return compound(img, errorMsg);
|
|
}
|
|
|
|
// Decode DCT coefficient from bit buffer
|
|
int decodeCoef(uint buffer, ubyte numBits)
|
|
{
|
|
bool positive = getBit(buffer, 0);
|
|
|
|
int value = 0;
|
|
foreach(j; 0..numBits)
|
|
{
|
|
bool bit = getBit(buffer, numBits-1-j);
|
|
value = setBit(value, j, bit);
|
|
}
|
|
|
|
if (positive)
|
|
return value;
|
|
else
|
|
return value - 2^^numBits + 1;
|
|
}
|
|
|
|
static const ubyte[64] dezigzag =
|
|
[
|
|
0, 1, 8, 16, 9, 2, 3, 10,
|
|
17, 24, 32, 25, 18, 11, 4, 5,
|
|
12, 19, 26, 33, 40, 48, 41, 34,
|
|
27, 20, 13, 6, 7, 14, 21, 28,
|
|
35, 42, 49, 56, 57, 50, 43, 36,
|
|
29, 22, 15, 23, 30, 37, 44, 51,
|
|
58, 59, 52, 45, 38, 31, 39, 46,
|
|
53, 60, 61, 54, 47, 55, 62, 63
|
|
];
|
|
|
|
if (jpg.sos.componentsNum != 3)
|
|
{
|
|
return error(format(
|
|
"loadJPEG error: unsupported number of components: %s",
|
|
jpg.sos.componentsNum));
|
|
}
|
|
|
|
// Store previous DC coefficients
|
|
int[3] dcCoefPrev;
|
|
|
|
if (jpg.dqt.length == 0)
|
|
return error("loadJPEG error: no DQTs found");
|
|
|
|
ScanBitStream sbs;
|
|
sbs.endMarkerFound = false;
|
|
sbs.bytesRead = 0;
|
|
sbs.prevByte = 0x00;
|
|
sbs.curByte = 0x00;
|
|
sbs.istrm = istrm;
|
|
sbs.readNextByte();
|
|
|
|
uint numMCUsH = jpg.sof0.width / mcu.width + ((jpg.sof0.width % mcu.width) > 0);
|
|
uint numMCUsV = jpg.sof0.height / mcu.height + ((jpg.sof0.height % mcu.height) > 0);
|
|
|
|
// Read MCUs
|
|
foreach(mcuY; 0..numMCUsV)
|
|
foreach(mcuX; 0..numMCUsH)
|
|
{
|
|
// Read MCU for each channel
|
|
foreach(ci, ref c; jpg.sos.components)
|
|
{
|
|
auto tableDC = jpg.getHuffmanTable(0, c.tableIdDC);
|
|
auto tableAC = jpg.getHuffmanTable(1, c.tableIdAC);
|
|
|
|
if (tableDC is null)
|
|
return error("loadJPEG error: illegal DC table index in MCU component");
|
|
if (tableAC is null)
|
|
return error("loadJPEG error: illegal AC table index in MCU component");
|
|
|
|
auto component = jpg.sof0.components[ci];
|
|
auto hblocks = component.hSubsampling;
|
|
auto vblocks = component.vSubsampling;
|
|
auto dqtTableId = component.dqtTableId;
|
|
if (dqtTableId >= jpg.dqt.length)
|
|
return error("loadJPEG error: illegal DQT table index in MCU component");
|
|
|
|
// Read 8x8 blocks
|
|
foreach(by; 0..vblocks)
|
|
foreach(bx; 0..hblocks)
|
|
{
|
|
int[8*8] block;
|
|
|
|
// Read DC coefficient
|
|
ubyte dcDiffLen;
|
|
auto res = sbs.decodeByte(tableDC.huffmanTree, &dcDiffLen);
|
|
if (!res[0]) return error(res[1]);
|
|
|
|
if (dcDiffLen > 0)
|
|
{
|
|
uint dcBuffer = sbs.readBits(dcDiffLen);
|
|
dcCoefPrev[ci] += decodeCoef(dcBuffer, dcDiffLen);
|
|
}
|
|
|
|
block[0] = dcCoefPrev[ci];
|
|
|
|
// Read AC coefficients
|
|
{
|
|
uint i = 1;
|
|
bool eob = false;
|
|
while (!eob && i < 64)
|
|
{
|
|
ubyte code;
|
|
res = sbs.decodeByte(tableAC.huffmanTree, &code);
|
|
if (!res[0]) return error(res[1]);
|
|
|
|
if (code == 0x00) // EOB, all next values are zero
|
|
eob = true;
|
|
else if (code == 0xF0) // ZRL, next 16 values are zero
|
|
{
|
|
foreach(j; 0..16)
|
|
if (i < 64)
|
|
{
|
|
block[i] = 0;
|
|
i++;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ubyte hi = hiNibble(code);
|
|
ubyte lo = loNibble(code);
|
|
|
|
uint zeroes = hi;
|
|
foreach(j; 0..zeroes)
|
|
if (i < 64)
|
|
{
|
|
block[i] = 0;
|
|
i++;
|
|
}
|
|
|
|
int acCoef = 0;
|
|
if (lo > 0)
|
|
{
|
|
uint acBuffer = sbs.readBits(lo);
|
|
acCoef = decodeCoef(acBuffer, lo);
|
|
}
|
|
|
|
if (i < 64)
|
|
block[i] = acCoef;
|
|
|
|
i++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Multiply block by quantization matrix
|
|
foreach(i, ref v; block)
|
|
v *= jpg.dqt[dqtTableId].table[i];
|
|
|
|
// Convert matrix from zig-zag order to normal order
|
|
int[8*8] dctMatrix;
|
|
|
|
foreach(i, v; block)
|
|
dctMatrix[dezigzag[i]] = v;
|
|
|
|
idct64(dctMatrix.ptr);
|
|
|
|
// Copy the matrix into corresponding channel
|
|
int* outMatrixPtr;
|
|
if (ci == 0)
|
|
outMatrixPtr = mcu.yBlocks[by * hblocks + bx].ptr;
|
|
else if (ci == 1)
|
|
outMatrixPtr = mcu.cbBlocks[by * hblocks + bx].ptr;
|
|
else if (ci == 2)
|
|
outMatrixPtr = mcu.crBlocks[by * hblocks + bx].ptr;
|
|
else
|
|
return error("loadJPEG error: illegal component index");
|
|
|
|
for(uint i = 0; i < 64; i++)
|
|
outMatrixPtr[i] = dctMatrix[i];
|
|
}
|
|
}
|
|
|
|
// Convert MCU from YCbCr to RGB
|
|
foreach(y; 0..mcu.height) // Pixel coordinates in MCU
|
|
foreach(x; 0..mcu.width)
|
|
{
|
|
//Color4f col = mcu.getPixel(x, y);
|
|
uint col = mcu.getPixel(x, y);
|
|
|
|
// Pixel coordinates in image
|
|
uint ix = mcuX * mcu.width + x;
|
|
uint iy = mcuY * mcu.height + y;
|
|
|
|
if (ix < img.width && iy < img.height)
|
|
img[ix, iy] = col;
|
|
}
|
|
}
|
|
|
|
version(JPEGDebug)
|
|
{
|
|
writefln("Bytes read: %s", sbs.bytesRead);
|
|
}
|
|
|
|
mcu.free();
|
|
|
|
return compound(img, "");
|
|
}
|
|
|
|
/*
|
|
* MCU struct keeps a storage for one Minimal Code Unit
|
|
* and provides a generalized interface for decoding
|
|
* images with different subsampling modes.
|
|
* Decoder should read 8x8 blocks one by one for each channel
|
|
* and fill corresponding arrays in MCU.
|
|
*/
|
|
struct MCU
|
|
{
|
|
uint width;
|
|
uint height;
|
|
|
|
alias int[8*8] Block;
|
|
Block[] yBlocks;
|
|
Block[] cbBlocks;
|
|
Block[] crBlocks;
|
|
|
|
uint ySamplesH, ySamplesV;
|
|
uint cbSamplesH, cbSamplesV;
|
|
uint crSamplesH, crSamplesV;
|
|
|
|
uint yWidth, yHeight;
|
|
uint cbWidth, cbHeight;
|
|
uint crWidth, crHeight;
|
|
|
|
void createYBlocks(uint hsubsampling, uint vsubsampling)
|
|
{
|
|
yBlocks = New!(Block[])(hsubsampling * vsubsampling);
|
|
|
|
width = hsubsampling * 8;
|
|
height = vsubsampling * 8;
|
|
|
|
ySamplesH = hsubsampling;
|
|
ySamplesV = vsubsampling;
|
|
|
|
yWidth = width / ySamplesH;
|
|
yHeight = height / ySamplesV;
|
|
}
|
|
|
|
void createCbBlocks(uint hsubsampling, uint vsubsampling)
|
|
{
|
|
cbBlocks = New!(Block[])(hsubsampling * vsubsampling);
|
|
|
|
cbSamplesH = hsubsampling;
|
|
cbSamplesV = vsubsampling;
|
|
|
|
cbWidth = width / cbSamplesH;
|
|
cbHeight = height / cbSamplesV;
|
|
}
|
|
|
|
void createCrBlocks(uint hsubsampling, uint vsubsampling)
|
|
{
|
|
crBlocks = New!(Block[])(hsubsampling * vsubsampling);
|
|
|
|
crSamplesH = hsubsampling;
|
|
crSamplesV = vsubsampling;
|
|
|
|
crWidth = width / crSamplesH;
|
|
crHeight = height / crSamplesV;
|
|
}
|
|
|
|
void free()
|
|
{
|
|
if (yBlocks.length) Delete(yBlocks);
|
|
if (cbBlocks.length) Delete(cbBlocks);
|
|
if (crBlocks.length) Delete(crBlocks);
|
|
}
|
|
|
|
uint getPixel(uint x, uint y) // coordinates relative to upper-left MCU corner
|
|
{
|
|
// Y block coordinates
|
|
uint ybx = x / yWidth;
|
|
uint yby = y / yHeight;
|
|
uint ybi = yby * ySamplesH + ybx;
|
|
|
|
// Pixel coordinates in Y block
|
|
uint ybpx = x - ybx * yWidth;
|
|
uint ybpy = y - yby * yHeight;
|
|
|
|
// Cb block coordinates
|
|
uint cbx = x / cbWidth;
|
|
uint cby = y / cbHeight;
|
|
uint cbi = cby * cbSamplesH + cbx;
|
|
|
|
// Pixel coordinates in Cb block
|
|
uint cbpx = (x - cbx * cbWidth) / ySamplesH;
|
|
uint cbpy = (y - cby * cbHeight) / ySamplesV;
|
|
|
|
// Cr block coordinates
|
|
uint crx = x / crWidth;
|
|
uint cry = y / crHeight;
|
|
uint cri = cry * crSamplesH + crx;
|
|
|
|
// Pixel coordinates in Cr block
|
|
uint crpx = (x - crx * crWidth) / ySamplesH;
|
|
uint crpy = (y - cry * crHeight) / ySamplesV;
|
|
|
|
// Get color components
|
|
float Y = cast(float)yBlocks [ybi][ybpy * 8 + ybpx] + 128.0f;
|
|
float Cb = cast(float)cbBlocks[cbi][cbpy * 8 + cbpx];
|
|
float Cr = cast(float)crBlocks[cri][crpy * 8 + crpx];
|
|
|
|
// Convert from YCbCr to RGB
|
|
//Color4f col;
|
|
uint col_r = clamp(Y + 1.402f * Cr);
|
|
uint col_g = clamp(Y - 0.34414f * Cb - 0.71414f * Cr);
|
|
uint col_b = clamp(Y + 1.772f * Cb);
|
|
//col = col / 255.0f;
|
|
//col.a = 1.0f;
|
|
|
|
return 0xFF000000 | (col_r << 16) | (col_g << 8) | (col_b) ;
|
|
}
|
|
}
|
|
|
|
uint clamp(float v) {
|
|
import std.conv;
|
|
if (v < 0)
|
|
return 0;
|
|
uint res = to!uint(v);
|
|
if (v > 255)
|
|
return 255;
|
|
return res;
|
|
}
|