unit JPEG; // FIXME finish. // TODO the color space defaults to YCbCr and the H and V sampling factors for each component, Y, Cb, and Cr, default to 2, 1, and 1, respectively (2 for both H and V of the Y component, etc.) {$MODE OBJFPC} {$M+} {$ASSERTIONS ON} interface uses type_fixes, loaders, classes, framebuffers, colors; type { note that the pixmap info palette contains the GLOBAL palette of the file, which may not be what you expected... } TLoader = class(TInterfacedObject, ILoader, IInterface) private fStream : TStream; fBHeadersLoaded : TBoolean; //fInitialPosition : TINT64; fPixmapInfo : IPixmapInfo; fPixmapInfoR : TPixmapInfo; fACHuffmanTables : array[0..15] of THuffmanTable; // waste, since most of the time only the first two are used. fDCHuffmanTables : array[0..15] of THuffmanTable; protected function GetHeaders() : IPixmapInfo; procedure LoadHuffmanTables(aStream : TStream); published constructor Create(aStream : TStream); property Headers : IPixmapInfo read GetHeaders; function NextRun() : TRun; protected procedure LoadHeaders(); end; implementation uses sysutils; constructor TLoader.Create(aStream : TStream); begin fStream := aStream; fPixmapInfoR := TPixmapInfo.Create(); fPixmapInfo := fPixmapInfoR; // pin it down until it's not needed anymore. end; function TLoader.NextRun() : TRun; begin Result := nil; // FIXME. end; function TLoader.GetHeaders() : IPixmapInfo; begin if not fBHeadersLoaded then LoadHeaders(); Result := fPixmapInfo; end; {$PACKRECORDS C} type THeader = record Magic : TUINT16; // = $FFD8; all markers start with $FF. { Width : TUINT16; Height : TUINT16; PixelComponentCount : TByte; // 1 = grayscale, 3 = RGB. HVSamplingFactors : array[0..2] of TByte; // for component 1. } end; // JFIF (JPEG File Interchange Format) TJFIFHeader = record // marker: $FFE0. Size : TUINT16; // header size including itself. Magic : array[0..5] of Char; // = 'JFIF'#0. Version : array[0..1] of TByte; // 01, 02. MSB = major revision. Units : TByte; // 0: no units; 1: X and Y in dots per inch; 2: X and Y in dots per cm. Xdensity : TUINT16; Ydensity : TUINT16; Xthumbnail : TByte; // 0 = no thumbnail. Ythumbnail : TByte; // 0 = no thumbnail. end; // after that: array[Xthumbnail * Ythumbnail] of TPaletteItem; TStartOfFrameHeader = record // marker: $FFC0. Size : TUINT16; // header size including itself. SamplePrecision : TByte; // in bits, usually 8. Y : TUINT16; X : TUINT16; PixelComponentCount : TByte; // 1: grayscale; 3: RGB color. end; // after that: PixelComponentCount * TStartOfFrameComponent. TStartOfFrameComponent = record ID : TByte; // component ID. HVSampling : TByte; // H is first four bits, V is second four bits. // The H and V sampling factors dictate the final size of the component they are associated with. // For instance, the color space defaults to YCbCr and the H and V sampling factors for each component, Y, Cb, and Cr, default to 2, 1, and 1, respectively (2 for both H and V of the Y component, etc.) in the Jpeg-6a library by the Independent Jpeg Group. While this does mean that the Y component will be twice the size of the other two components--giving it a higher resolution, the lower resolution components are quartered in size during compression in order to achieve this difference. Thus, the Cb and Cr components must be quadrupled in size during decompression. QuantizationTableNumber : TByte; end; TStartOfFrame = record Header : TStartOfFrameHeader; Components : array of TStartOfFrameComponent; end; TJFIF = record Header : TJFIFHeader; Thumbnail : array of TPaletteEntry; end; THuffmanTableHeader = record // marker: $FFC4. Size : TUINT16; // header size including itself. { * until length is exhausted (usually four Huffman tables) o index -- one byte: if >15 (i.e. 0x10 or more) then an AC table, otherwise a DC table o bits -- 16 bytes o Huffman values -- # of bytes = the sum of the previous 16 bytes DHT } end; //THuffmanTable = record // SOS marker: $FFDA { Define Quantization table marker (FFDB) * the first two bytes, the length, after the marker indicate the number of bytes, including the two length bytes, that this header contains * until the length is exhausted (loads two quantization tables for baseline JPEG) o the precision and the quantization table index -- one byte: precision is specified by the higher four bits and index is specified by the lower four bits + precision in this case is either 0 or 1 and indicates the precision of the quantized values; 8-bit (baseline) for 0 and up to 16-bit for 1 o the quantization values -- 64 bytes + the quantization tables are stored in zigzag format Start of Scan marker (FFDA) * the first two bytes, the length, after the marker indicate the number of bytes, including the two length bytes, that this header contains * Number of components, n -- one byte: the number of components in this scan * n times: o Component ID -- one byte o DC and AC table numbers -- one byte: DC # is first four bits and AC # is last four bits * Ss -- one byte * Se -- one byte * Ah and Al -- one byte should it contain FF, it will be encoded FF 00. } procedure EnsureValidHeader(const aHeader : THeader); inline; begin if aHeader.Magic <> $FFD8 then raise EReadError.Create('invalid JPEG header.'); end; function LoadJFIFHeader(aStream : TStream) : TJFIF; inline; var vThumbnailCount : TUINT16; begin aStream.ReadBuffer(Result.Header, SizeOf(Result.Header)); if Result.Header.Size <> SizeOf(Result.Header) then raise EReadError.Create('invalid JFIF header: size is wrong.'); with Result.Header do if (Magic[0] <> chr($4A)) or (Magic[1] <> chr($46)) or (Magic[2] <> chr($49)) or (Magic[3] <> chr($46)) or (Magic[4] <> chr($0)) then // "JFIF"#0. raise EReadError.Create('invlaid JFIF header: magic is wrong.'); vThumbnailCount := Result.Header.XThumbnail * Result.Header.YThumbnail; SetLength(Result.Thumbnail, vThumbnailCount); aStream.ReadBuffer(Result.Thumbnail, SizeOf(Result.Thumbnail[0]) * vThumbnailCount); end; function LoadStartOfFrame(aStream : TStream) : TStartOfFrame; var vComponentIndex : Integer; begin aStream.ReadBuffer(Result.Header, SizeOf(Result.Header)); if Result.Header.Size <> SizeOf(Result.Header) then // FIXME: or is that including pixel components? raise EReadError.Create('invalid JFIF header: size is wrong.'); if not Result.Header.PixelComponentCount in [1, 3] then raise EReadError.Create('invalid JFIF header: pixel component count should be 1 or 3.'); SetLength(Result.Components, Result.Header.PixelComponentCount); for vComponentIndex := 0 to Result.Header.PixelComponentCount - 1 do aStream.ReadBuffer(Result.Components[vComponentIndex], SizeOf(TStartOfFrameComponent)); end; function LoadComment(aStream : TStream) : String; var vSize : TUINT16; begin ReadBigEndian(aStream, vSize); // including itself. Dec(vSize, 2); SetLength(Result, vSize); aStream.ReadBuffer(Result[1], vSize); end; procedure TLoader.LoadHuffmanTables(aStream : TStream); var fIndex : TByte; fEndPosition : Integer; fTable : THuffmanTable; begin * DHT Class=0 ID=0 - Used for DC component of Luminance (Y) * DHT Class=1 ID=0 - Used for AC component of Luminance (Y) * DHT Class=0 ID=1 - Used for DC component of Chrominance (Cb & Cr) * DHT Class=1 ID=1 - Used for AC component of Chrominance (Cb & Cr) THuffmanTableHeader = record // marker: $FFC4. Size : TUINT16; // header size including itself. { * until length is exhausted (usually four Huffman tables) o index -- one byte: if >15 (i.e. 0x10 or more) then an AC table, otherwise a DC table o bits -- 16 bytes o Huffman values -- # of bytes = the sum of the previous 16 bytes DHT } end; fEndPosition := aStream.Position + Size - 2; while aStream.Position < fEndPosition do begin fIndex := aStream.ReadByte(); // if > 15 then AC else DC. //THuffmanTable.SkipFromJFIF(Self, aStream); fTable := THuffmanTable.LoadFromJFIF(Self, aStream); if fIndex > 31 then // ERROR. TODO: abort loading or warn? FreeAndNil(fTable) else if (fIndex and (1 shl 4)) <> 0 then fACHuffmanTables[fIndex and 15] := fTable else fDCHuffmanTables[fIndex and 15] := fTable end; end; TLoader.DecodeScanline(); begin fStream.ReadByte();`??? if = $FF then if ReadByte() = 0 then FF else handle byte in parent; buffer into memorystream create bitstream using that memorystream. traverse huffman tables. end; procedure TLoader.LoadHeaders(); var vHeader : THeader; vMarkerMagic : TByte; vMarkerType : TByte; vJFIF : TJFIF; begin if fBHeadersLoaded then Exit; fStream.ReadBuffer(vHeader, Sizeof(vHeader)); EnsureValidHeader(vHeader); repeat vMarkerMagic := fStream.ReadByte(); if vMarkerMagic <> $FF then raise EReadError.Create('invalid JPEG file: marker magic is wrong.'); vMarkerType := fStream.ReadByte(); case vMarkerType of $D9: Break; $E0: vJFIF := LoadJFIFHeader(fStream); $C0: LoadStartOfFrame(fStream); $FE: LoadComment(fStream); $C4: LoadHuffmanTables(fStream); $DA: DecodeScanline(fStream); else begin raise EReadError.Create(Format('read error: JPEG marker type ''%X unknown', [vMarkerType])); // TODO skip? end; end; until vMarkerType = $D9; // end of image. //Strictly speaking, JPEG files do not have formal headers, but fg_jpeghead() and fgi_jpeghead() return relevant information from the file's start of frame segment. We call it a header for consistency with other image file formats. end; procedure EnsureThumbnailPacking(); inline; var vStartOfFrame : TStartOfFrame; vDistance : Integer; begin SetLength(vStartOfFrame.Components, 2); vDistance := PByte(@vStartOfFrame.Components[1].ID) - PByte(@vStartOfFrame.Components[0].ID); assert(vDistance = SizeOf(TPaletteEntry)); end; { Chroma Subsampling HxV Description Comments 1x1 No Subsampling Very few digital cameras (Sigma SD is a notable exception) output images without subsampling the color channel. Instead, you'll generally find these files generated from image editing applications when a high quality setting was used. 2x1 Horizontal subsampled The majority of reasonable digital cameras produce JPEG images with 2x1. 1x2 Vertical subsampled Typically from 2x1 subsampled images that have been rotated by 90 degrees. 2x2 Horizontal & Vertical subsampled Cheaper digital cameras and image editors saving photos at low quality settings. others Not very common at all for digital photos. Other sources such as miniDV camcorders will use 4x1 chroma subsampling. Coded Sequence for 1x1 (4:4:4) The following 3 components make up one MCU, and are repeated in this sequence for every MCU. Note that the MCU represents a pixel area of 8x8 pixels. [Y0(dc), Y0(ac)], [Cb(dc), Cb(ac)], [Cr(dc), Cr(ac)] Coded Sequence for 2x1 (4:2:2) The following 4 components make up one MCU. The MCU represents a pixel area of 16x8 pixels. Y0 and Y1 refer to two 8x8 pixel regions adjacent horizontally, while Cb and Cr refer to the single 16x8 pixel region that covers Y0 and Y1. To perform the encoding process, the Cb and Cr channels are first sub-sampled horizontally (this is usually done by averaging each pair of horizontal pixels into a single value). Then an MCU is made up of 8 averaged pixels horizontally by 8 original pixels veritically. The DC and AC coefficients are calculated on this new region. [Y0(dc), Y0(ac)] [Y1(dc), Y1(ac)], [Cb(dc), Cb(ac)], [Cr(dc), Cr(ac)] Coded Sequence for 2x2 (4:2:0) The following 6 components make up one MCU. The MCU represents a pixel area of 16x16 pixels. Y00 and Y10 refer to two 8x8 pixel regions adjacent horizontally, Y01 and Y11 refer to the two 8x8 pixel regions below Y00 and Y10. Cb and Cr refer to a single 16x16 pixel region that covers all four pixels (Y00, Y10, Y01 and Y11). To perform the encoding process, the Cb and Cr channels are first sub-sampled horizontally and vertically (this is usually done by averaging all four pixels into a single value). Then an MCU is made up of 8 averaged pixels horizontally by 8 averaged pixels veritically (which are based on the original region of 16x16 pixels). The DC and AC coefficients are calculated on this new region. [Y00(dc), Y00(ac)] [Y10(dc), Y10(ac)], [Y01(dc), Y01(ac)], [Y11(dc), Y11(ac)], [Cb(dc), Cb(ac)], [Cr(dc), Cr(ac)] } // max. 162 code strings. {$ASSERTIONS ON} initialization assert(SizeOf(TPaletteEntry) = 3); assert(SizeOf(THeader) = 2); assert(SizeOf(TJFIFHeader) = 17); assert(SizeOf(TStartOfFrameHeader) = 8); assert(SizeOf(Char) = 1); EnsureThumbnailPacking(); end.