{ $Id: ImagingJpeg.pas 168 2009-08-22 18:50:21Z galfar $ Vampyre Imaging Library by Marek Mauder http://imaginglib.sourceforge.net The contents of this file are used with permission, subject to the Mozilla Public License Version 1.1 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.mozilla.org/MPL/MPL-1.1.html Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the specific language governing rights and limitations under the License. Alternatively, the contents of this file may be used under the terms of the GNU Lesser General Public License (the "LGPL License"), in which case the provisions of the LGPL License are applicable instead of those above. If you wish to allow use of your version of this file only under the terms of the LGPL License and not to allow others to use your version of this file under the MPL, indicate your decision by deleting the provisions above and replace them with the notice and other provisions required by the LGPL License. If you do not delete the provisions above, a recipient may use your version of this file under either the MPL or the LGPL License. For more information about the LGPL: http://www.gnu.org/copyleft/lesser.html } { This unit contains image format loader/saver for Jpeg images.} unit ImagingJpeg; {$I ImagingOptions.inc} { You can choose which Pascal JpegLib implementation will be used. IMJPEGLIB is version bundled with Imaging which works with all supported compilers and platforms. PASJPEG is original JpegLib translation or version modified for FPC (and shipped with it). You can use PASJPEG if this version is already linked with another part of your program and you don't want to have two quite large almost the same libraries linked to your exe. This is the case with Lazarus applications for example.} {$DEFINE IMJPEGLIB} { $DEFINE PASJPEG} { Automatically use FPC's PasJpeg when compiling with Lazarus. But not when WINDOWS is defined. See http://galfar.vevb.net/imaging/smf/index.php/topic,90.0.html} {$IF Defined(LCL) and not Defined(WINDOWS)} {$UNDEF IMJPEGLIB} {$DEFINE PASJPEG} {$IFEND} interface uses SysUtils, ImagingTypes, Imaging, ImagingColors, {$IF Defined(IMJPEGLIB)} imjpeglib, imjmorecfg, imjcomapi, imjdapimin, imjdeferr, imjerror, imjdapistd, imjcapimin, imjcapistd, imjdmarker, imjcparam, {$ELSEIF Defined(PASJPEG)} jpeglib, jmorecfg, jcomapi, jdapimin, jdeferr, jerror, jdapistd, jcapimin, jcapistd, jdmarker, jcparam, {$IFEND} ImagingUtility; {$IF Defined(FPC) and Defined(PASJPEG)} { When using FPC's pasjpeg in FPC the channel order is BGR instead of RGB} {$DEFINE RGBSWAPPED} {$IFEND} type { Class for loading/saving Jpeg images. Supports load/save of 8 bit grayscale and 24 bit RGB images. Jpegs can be saved with optional progressive encoding. Based on IJG's JpegLib so doesn't support alpha channels and lossless coding.} TJpegFileFormat = class(TImageFileFormat) private FGrayScale: Boolean; protected FQuality: LongInt; FProgressive: LongBool; procedure SetJpegIO(const JpegIO: TIOFunctions); virtual; function LoadData(Handle: TImagingHandle; var Images: TDynImageDataArray; OnlyFirstLevel: Boolean): Boolean; override; function SaveData(Handle: TImagingHandle; const Images: TDynImageDataArray; Index: LongInt): Boolean; override; procedure ConvertToSupported(var Image: TImageData; const Info: TImageFormatInfo); override; public constructor Create; override; function TestFormat(Handle: TImagingHandle): Boolean; override; procedure CheckOptionsValidity; override; published { Controls Jpeg save compression quality. It is number in range 1..100. 1 means small/ugly file, 100 means large/nice file. Accessible trough ImagingJpegQuality option.} property Quality: LongInt read FQuality write FQuality; { If True Jpeg images are saved in progressive format. Accessible trough ImagingJpegProgressive option.} property Progressive: LongBool read FProgressive write FProgressive; end; implementation const SJpegFormatName = 'Joint Photographic Experts Group Image'; SJpegMasks = '*.jpg,*.jpeg,*.jfif,*.jpe,*.jif'; JpegSupportedFormats: TImageFormats = [ifR8G8B8, ifGray8]; JpegDefaultQuality = 90; JpegDefaultProgressive = False; const { Jpeg file identifiers.} JpegMagic: TChar2 = #$FF#$D8; BufferSize = 16384; resourcestring SJpegError = 'JPEG Error'; type TJpegContext = record case Byte of 0: (common: jpeg_common_struct); 1: (d: jpeg_decompress_struct); 2: (c: jpeg_compress_struct); end; TSourceMgr = record Pub: jpeg_source_mgr; Input: TImagingHandle; Buffer: JOCTETPTR; StartOfFile: Boolean; end; PSourceMgr = ^TSourceMgr; TDestMgr = record Pub: jpeg_destination_mgr; Output: TImagingHandle; Buffer: JOCTETPTR; end; PDestMgr = ^TDestMgr; var JIO: TIOFunctions; JpegErrorMgr: jpeg_error_mgr; { Intenal unit jpeglib support functions } procedure JpegError(CInfo: j_common_ptr); var Buffer: string; begin { Create the message and raise exception } CInfo^.err^.format_message(CInfo, buffer); raise EImagingError.CreateFmt(SJPEGError + ' %d: ' + Buffer, [CInfo.err^.msg_code]); end; procedure OutputMessage(CurInfo: j_common_ptr); begin end; procedure ReleaseContext(var jc: TJpegContext); begin if jc.common.err = nil then Exit; jpeg_destroy(@jc.common); jpeg_destroy_decompress(@jc.d); jpeg_destroy_compress(@jc.c); jc.common.err := nil; end; procedure InitSource(cinfo: j_decompress_ptr); begin PSourceMgr(cinfo.src).StartOfFile := True; end; function FillInputBuffer(cinfo: j_decompress_ptr): Boolean; var NBytes: LongInt; Src: PSourceMgr; begin Src := PSourceMgr(cinfo.src); NBytes := JIO.Read(Src.Input, Src.Buffer, BufferSize); if NBytes <= 0 then begin PChar(Src.Buffer)[0] := #$FF; PChar(Src.Buffer)[1] := Char(JPEG_EOI); NBytes := 2; end; Src.Pub.next_input_byte := Src.Buffer; Src.Pub.bytes_in_buffer := NBytes; Src.StartOfFile := False; Result := True; end; procedure SkipInputData(cinfo: j_decompress_ptr; num_bytes: LongInt); var Src: PSourceMgr; begin Src := PSourceMgr(cinfo.src); if num_bytes > 0 then begin while num_bytes > Src.Pub.bytes_in_buffer do begin Dec(num_bytes, Src.Pub.bytes_in_buffer); FillInputBuffer(cinfo); end; Src.Pub.next_input_byte := @PByteArray(Src.Pub.next_input_byte)[num_bytes]; //Inc(LongInt(Src.Pub.next_input_byte), num_bytes); Dec(Src.Pub.bytes_in_buffer, num_bytes); end; end; procedure TermSource(cinfo: j_decompress_ptr); var Src: PSourceMgr; begin Src := PSourceMgr(cinfo.src); // Move stream position back just after EOI marker so that more that one // JPEG images can be loaded from one stream JIO.Seek(Src.Input, -Src.Pub.bytes_in_buffer, smFromCurrent); end; procedure JpegStdioSrc(var cinfo: jpeg_decompress_struct; Handle: TImagingHandle); var Src: PSourceMgr; begin if cinfo.src = nil then begin cinfo.src := cinfo.mem.alloc_small(j_common_ptr(@cinfo), JPOOL_PERMANENT, SizeOf(TSourceMgr)); Src := PSourceMgr(cinfo.src); Src.Buffer := cinfo.mem.alloc_small(j_common_ptr(@cinfo), JPOOL_PERMANENT, BufferSize * SizeOf(JOCTET)); end; Src := PSourceMgr(cinfo.src); Src.Pub.init_source := InitSource; Src.Pub.fill_input_buffer := FillInputBuffer; Src.Pub.skip_input_data := SkipInputData; Src.Pub.resync_to_restart := jpeg_resync_to_restart; Src.Pub.term_source := TermSource; Src.Input := Handle; Src.Pub.bytes_in_buffer := 0; Src.Pub.next_input_byte := nil; end; procedure InitDest(cinfo: j_compress_ptr); var Dest: PDestMgr; begin Dest := PDestMgr(cinfo.dest); Dest.Pub.next_output_byte := Dest.Buffer; Dest.Pub.free_in_buffer := BufferSize; end; function EmptyOutput(cinfo: j_compress_ptr): Boolean; var Dest: PDestMgr; begin Dest := PDestMgr(cinfo.dest); JIO.Write(Dest.Output, Dest.Buffer, BufferSize); Dest.Pub.next_output_byte := Dest.Buffer; Dest.Pub.free_in_buffer := BufferSize; Result := True; end; procedure TermDest(cinfo: j_compress_ptr); var Dest: PDestMgr; DataCount: LongInt; begin Dest := PDestMgr(cinfo.dest); DataCount := BufferSize - Dest.Pub.free_in_buffer; if DataCount > 0 then JIO.Write(Dest.Output, Dest.Buffer, DataCount); end; procedure JpegStdioDest(var cinfo: jpeg_compress_struct; Handle: TImagingHandle); var Dest: PDestMgr; begin if cinfo.dest = nil then cinfo.dest := cinfo.mem.alloc_small(j_common_ptr(@cinfo), JPOOL_PERMANENT, SizeOf(TDestMgr)); Dest := PDestMgr(cinfo.dest); Dest.Buffer := cinfo.mem.alloc_small(j_common_ptr(@cinfo), JPOOL_IMAGE, BufferSize * SIZEOF(JOCTET)); Dest.Pub.init_destination := InitDest; Dest.Pub.empty_output_buffer := EmptyOutput; Dest.Pub.term_destination := TermDest; Dest.Output := Handle; end; procedure InitDecompressor(Handle: TImagingHandle; var jc: TJpegContext); begin FillChar(jc, sizeof(jc), 0); // Set standard error handlers and then override some jc.common.err := jpeg_std_error(JpegErrorMgr); jc.common.err.error_exit := JpegError; jc.common.err.output_message := OutputMessage; jpeg_CreateDecompress(@jc.d, JPEG_LIB_VERSION, sizeof(jc.d)); JpegStdioSrc(jc.d, Handle); jpeg_read_header(@jc.d, True); jc.d.scale_num := 1; jc.d.scale_denom := 1; jc.d.do_block_smoothing := True; if jc.d.out_color_space = JCS_GRAYSCALE then begin jc.d.quantize_colors := True; jc.d.desired_number_of_colors := 256; end; end; procedure InitCompressor(Handle: TImagingHandle; var jc: TJpegContext; Saver: TJpegFileFormat); begin FillChar(jc, sizeof(jc), 0); // Set standard error handlers and then override some jc.common.err := jpeg_std_error(JpegErrorMgr); jc.common.err.error_exit := JpegError; jc.common.err.output_message := OutputMessage; jpeg_CreateCompress(@jc.c, JPEG_LIB_VERSION, sizeof(jc.c)); JpegStdioDest(jc.c, Handle); if Saver.FGrayScale then jc.c.in_color_space := JCS_GRAYSCALE else jc.c.in_color_space := JCS_YCbCr; jpeg_set_defaults(@jc.c); jpeg_set_quality(@jc.c, Saver.FQuality, True); if Saver.FProgressive then jpeg_simple_progression(@jc.c); end; { TJpegFileFormat class implementation } constructor TJpegFileFormat.Create; begin inherited Create; FName := SJpegFormatName; FCanLoad := True; FCanSave := True; FIsMultiImageFormat := False; FSupportedFormats := JpegSupportedFormats; FQuality := JpegDefaultQuality; FProgressive := JpegDefaultProgressive; AddMasks(SJpegMasks); RegisterOption(ImagingJpegQuality, @FQuality); RegisterOption(ImagingJpegProgressive, @FProgressive); end; procedure TJpegFileFormat.CheckOptionsValidity; begin // Check if option values are valid if not (FQuality in [1..100]) then FQuality := JpegDefaultQuality; end; function TJpegFileFormat.LoadData(Handle: TImagingHandle; var Images: TDynImageDataArray; OnlyFirstLevel: Boolean): Boolean; var PtrInc, LinesPerCall, LinesRead, I: Integer; Dest: PByte; jc: TJpegContext; Info: TImageFormatInfo; Col32: PColor32Rec; {$IFDEF RGBSWAPPED} Pix: PColor24Rec; {$ENDIF} begin // Copy IO functions to global var used in JpegLib callbacks Result := False; SetJpegIO(GetIO); SetLength(Images, 1); with JIO, Images[0] do try InitDecompressor(Handle, jc); case jc.d.out_color_space of JCS_GRAYSCALE: Format := ifGray8; JCS_RGB: Format := ifR8G8B8; JCS_CMYK: Format := ifA8R8G8B8; else Exit; end; NewImage(jc.d.image_width, jc.d.image_height, Format, Images[0]); jpeg_start_decompress(@jc.d); GetImageFormatInfo(Format, Info); PtrInc := Width * Info.BytesPerPixel; LinesPerCall := 1; Dest := Bits; while jc.d.output_scanline < jc.d.output_height do begin LinesRead := jpeg_read_scanlines(@jc.d, @Dest, LinesPerCall); {$IFDEF RGBSWAPPED} if Format = ifR8G8B8 then begin Pix := PColor24Rec(Dest); for I := 0 to Width - 1 do begin SwapValues(Pix.R, Pix.B); Inc(Pix); end; end; {$ENDIF} Inc(Dest, PtrInc * LinesRead); end; if jc.d.out_color_space = JCS_CMYK then begin Col32 := Bits; // Translate from CMYK to RGB for I := 0 to Width * Height - 1 do begin CMYKToRGB(255 - Col32.B, 255 - Col32.G, 255 - Col32.R, 255 - Col32.A, Col32.R, Col32.G, Col32.B); Col32.A := 255; Inc(Col32); end; end; jpeg_finish_output(@jc.d); jpeg_finish_decompress(@jc.d); Result := True; finally ReleaseContext(jc); end; end; function TJpegFileFormat.SaveData(Handle: TImagingHandle; const Images: TDynImageDataArray; Index: LongInt): Boolean; var PtrInc, LinesWritten: LongInt; Src, Line: PByte; jc: TJpegContext; ImageToSave: TImageData; Info: TImageFormatInfo; MustBeFreed: Boolean; {$IFDEF RGBSWAPPED} I: LongInt; Pix: PColor24Rec; {$ENDIF} begin Result := False; // Copy IO functions to global var used in JpegLib callbacks SetJpegIO(GetIO); // Makes image to save compatible with Jpeg saving capabilities if MakeCompatible(Images[Index], ImageToSave, MustBeFreed) then with JIO, ImageToSave do try GetImageFormatInfo(Format, Info); FGrayScale := Format = ifGray8; InitCompressor(Handle, jc, Self); jc.c.image_width := Width; jc.c.image_height := Height; if FGrayScale then begin jc.c.input_components := 1; jc.c.in_color_space := JCS_GRAYSCALE; end else begin jc.c.input_components := 3; jc.c.in_color_space := JCS_RGB; end; PtrInc := Width * Info.BytesPerPixel; Src := Bits; {$IFDEF RGBSWAPPED} GetMem(Line, PtrInc); {$ENDIF} jpeg_start_compress(@jc.c, True); while (jc.c.next_scanline < jc.c.image_height) do begin {$IFDEF RGBSWAPPED} if Format = ifR8G8B8 then begin Move(Src^, Line^, PtrInc); Pix := PColor24Rec(Line); for I := 0 to Width - 1 do begin SwapValues(Pix.R, Pix.B); Inc(Pix, 1); end; end; {$ELSE} Line := Src; {$ENDIF} LinesWritten := jpeg_write_scanlines(@jc.c, @Line, 1); Inc(Src, PtrInc * LinesWritten); end; jpeg_finish_compress(@jc.c); Result := True; finally ReleaseContext(jc); if MustBeFreed then FreeImage(ImageToSave); {$IFDEF RGBSWAPPED} FreeMem(Line); {$ENDIF} end; end; procedure TJpegFileFormat.ConvertToSupported(var Image: TImageData; const Info: TImageFormatInfo); begin if Info.HasGrayChannel then ConvertImage(Image, ifGray8) else ConvertImage(Image, ifR8G8B8); end; function TJpegFileFormat.TestFormat(Handle: TImagingHandle): Boolean; var ReadCount: LongInt; ID: array[0..9] of AnsiChar; begin Result := False; if Handle <> nil then with GetIO do begin FillChar(ID, SizeOf(ID), 0); ReadCount := Read(Handle, @ID, SizeOf(ID)); Seek(Handle, -ReadCount, smFromCurrent); Result := (ReadCount = SizeOf(ID)) and CompareMem(@ID, @JpegMagic, SizeOf(JpegMagic)); end; end; procedure TJpegFileFormat.SetJpegIO(const JpegIO: TIOFunctions); begin JIO := JpegIO; end; initialization RegisterImageFileFormat(TJpegFileFormat); { File Notes: -- TODOS ---------------------------------------------------- - nothing now -- 0.26.3 Changes/Bug Fixes --------------------------------- - Changed the Jpeg error manager, messages were not properly formated. -- 0.26.1 Changes/Bug Fixes --------------------------------- - Fixed wrong color space setting in InitCompressor. - Fixed problem with progressive Jpegs in FPC (modified JpegLib, can't use FPC's PasJpeg in Windows). -- 0.25.0 Changes/Bug Fixes --------------------------------- - FPC's PasJpeg wasn't really used in last version, fixed. -- 0.24.1 Changes/Bug Fixes --------------------------------- - Fixed loading of CMYK jpeg images. Could cause heap corruption and loaded image looked wrong. -- 0.23 Changes/Bug Fixes ----------------------------------- - Removed JFIF/EXIF detection from TestFormat. Found JPEGs with different headers (Lavc) which weren't recognized. -- 0.21 Changes/Bug Fixes ----------------------------------- - MakeCompatible method moved to base class, put ConvertToSupported here. GetSupportedFormats removed, it is now set in constructor. - Made public properties for options registered to SetOption/GetOption functions. - Changed extensions to filename masks. - Changed SaveData, LoadData, and MakeCompatible methods according to changes in base class in Imaging unit. - Changes in TestFormat, now reads JFIF and EXIF signatures too. -- 0.19 Changes/Bug Fixes ----------------------------------- - input position is now set correctly to the end of the image after loading is done. Loading of sequence of JPEG files stored in single stream works now - when loading and saving images in FPC with PASJPEG read and blue channels are swapped to have the same chanel order as IMJPEGLIB - you can now choose between IMJPEGLIB and PASJPEG implementations -- 0.17 Changes/Bug Fixes ----------------------------------- - added SetJpegIO method which is used by JNG image format } end.