DEADSOFTWARE

added Vampyre Imaging Library; now textures can be in various formats, including...
[d2df-sdl.git] / src / lib / vampimg / ImagingPortableMaps.pas
1 {
2 $Id: ImagingPortableMaps.pas 163 2009-07-28 21:44:10Z galfar $
3 Vampyre Imaging Library
4 by Marek Mauder
5 http://imaginglib.sourceforge.net
7 The contents of this file are used with permission, subject to the Mozilla
8 Public License Version 1.1 (the "License"); you may not use this file except
9 in compliance with the License. You may obtain a copy of the License at
10 http://www.mozilla.org/MPL/MPL-1.1.html
12 Software distributed under the License is distributed on an "AS IS" basis,
13 WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
14 the specific language governing rights and limitations under the License.
16 Alternatively, the contents of this file may be used under the terms of the
17 GNU Lesser General Public License (the "LGPL License"), in which case the
18 provisions of the LGPL License are applicable instead of those above.
19 If you wish to allow use of your version of this file only under the terms
20 of the LGPL License and not to allow others to use your version of this file
21 under the MPL, indicate your decision by deleting the provisions above and
22 replace them with the notice and other provisions required by the LGPL
23 License. If you do not delete the provisions above, a recipient may use
24 your version of this file under either the MPL or the LGPL License.
26 For more information about the LGPL: http://www.gnu.org/copyleft/lesser.html
27 }
29 { This unit contains loader/saver for Portable Maps file format family (or PNM).
30 That includes PBM, PGM, PPM, PAM, and PFM formats.}
31 unit ImagingPortableMaps;
33 {$I ImagingOptions.inc}
35 interface
37 uses
38 SysUtils, ImagingTypes, Imaging, ImagingFormats, ImagingUtility;
40 type
41 { Types of pixels of PNM images.}
42 TTupleType = (ttInvalid, ttBlackAndWhite, ttGrayScale, ttRGB, ttBlackAndWhiteAlpha,
43 ttGrayScaleAlpha, ttRGBAlpha, ttGrayScaleFP, ttRGBFP);
45 { Record with info about PNM image used in both loading and saving functions.}
46 TPortableMapInfo = record
47 Width: LongInt;
48 Height: LongInt;
49 FormatId: AnsiChar;
50 MaxVal: LongInt;
51 BitCount: LongInt;
52 Depth: LongInt;
53 TupleType: TTupleType;
54 Binary: Boolean;
55 HasPAMHeader: Boolean;
56 IsBigEndian: Boolean;
57 end;
59 { Base class for Portable Map file formats (or Portable AnyMaps or PNM).
60 There are several types of PNM file formats that share common
61 (simple) structure. This class can actually load all supported PNM formats.
62 Saving is also done by this class but descendants (each for different PNM
63 format) control it.}
64 TPortableMapFileFormat = class(TImageFileFormat)
65 protected
66 FIdNumbers: TChar2;
67 FSaveBinary: LongBool;
68 function LoadData(Handle: TImagingHandle; var Images: TDynImageDataArray;
69 OnlyFirstLevel: Boolean): Boolean; override;
70 function SaveDataInternal(Handle: TImagingHandle; const Images: TDynImageDataArray;
71 Index: LongInt; var MapInfo: TPortableMapInfo): Boolean;
72 public
73 constructor Create; override;
74 function TestFormat(Handle: TImagingHandle): Boolean; override;
75 published
76 { If set to True images will be saved in binary format. If it is False
77 they will be saved in text format (which could result in 5-10x bigger file).
78 Default is value True. Note that PAM and PFM files are always saved in binary.}
79 property SaveBinary: LongBool read FSaveBinary write FSaveBinary;
80 end;
82 { Portable Bit Map is used to store monochrome 1bit images. Raster data
83 can be saved as text or binary data. Either way value of 0 represents white
84 and 1 is black. As Imaging does not have support for 1bit data formats
85 PBM images can be loaded but not saved. Loaded images are returned in
86 ifGray8 format (witch pixel values scaled from 1bit to 8bit).}
87 TPBMFileFormat = class(TPortableMapFileFormat)
88 public
89 constructor Create; override;
90 end;
92 { Portable Gray Map is used to store grayscale 8bit or 16bit images.
93 Raster data can be saved as text or binary data.}
94 TPGMFileFormat = class(TPortableMapFileFormat)
95 protected
96 function SaveData(Handle: TImagingHandle; const Images: TDynImageDataArray;
97 Index: LongInt): Boolean; override;
98 procedure ConvertToSupported(var Image: TImageData;
99 const Info: TImageFormatInfo); override;
100 public
101 constructor Create; override;
102 end;
104 { Portable Pixel Map is used to store RGB images with 8bit or 16bit channels.
105 Raster data can be saved as text or binary data.}
106 TPPMFileFormat = class(TPortableMapFileFormat)
107 protected
108 function SaveData(Handle: TImagingHandle; const Images: TDynImageDataArray;
109 Index: LongInt): Boolean; override;
110 procedure ConvertToSupported(var Image: TImageData;
111 const Info: TImageFormatInfo); override;
112 public
113 constructor Create; override;
114 end;
116 { Portable Arbitrary Map is format that can store image data formats
117 of PBM, PGM, and PPM formats with optional alpha channel. Raster data
118 can be stored only in binary format. All data formats supported
119 by this format are ifGray8, ifGray16, ifA8Gray8, ifA16Gray16,
120 ifR8G8B8, ifR16G16R16, ifA8R8G8B8, and ifA16R16G16B16.}
121 TPAMFileFormat = class(TPortableMapFileFormat)
122 protected
123 function SaveData(Handle: TImagingHandle; const Images: TDynImageDataArray;
124 Index: LongInt): Boolean; override;
125 procedure ConvertToSupported(var Image: TImageData;
126 const Info: TImageFormatInfo); override;
127 public
128 constructor Create; override;
129 end;
131 { Portable Float Map is unofficial extension of PNM format family which
132 can store images with floating point pixels. Raster data is saved in
133 binary format as array of IEEE 32 bit floating point numbers. One channel
134 or RGB images are supported by PFM format (so no alpha).}
135 TPFMFileFormat = class(TPortableMapFileFormat)
136 protected
137 function SaveData(Handle: TImagingHandle; const Images: TDynImageDataArray;
138 Index: LongInt): Boolean; override;
139 procedure ConvertToSupported(var Image: TImageData;
140 const Info: TImageFormatInfo); override;
141 public
142 constructor Create; override;
143 end;
145 implementation
147 const
148 PortableMapDefaultBinary = True;
150 SPBMFormatName = 'Portable Bit Map';
151 SPBMMasks = '*.pbm';
152 SPGMFormatName = 'Portable Gray Map';
153 SPGMMasks = '*.pgm';
154 PGMSupportedFormats = [ifGray8, ifGray16];
155 SPPMFormatName = 'Portable Pixel Map';
156 SPPMMasks = '*.ppm';
157 PPMSupportedFormats = [ifR8G8B8, ifR16G16B16];
158 SPAMFormatName = 'Portable Arbitrary Map';
159 SPAMMasks = '*.pam';
160 PAMSupportedFormats = [ifGray8, ifGray16, ifA8Gray8, ifA16Gray16,
161 ifR8G8B8, ifR16G16B16, ifA8R8G8B8, ifA16R16G16B16];
162 SPFMFormatName = 'Portable Float Map';
163 SPFMMasks = '*.pfm';
164 PFMSupportedFormats = [ifR32F, ifA32B32G32R32F];
166 const
167 { TAB, CR, LF, and Space are used as seperators in Portable map headers and data.}
168 WhiteSpaces = [#9, #10, #13, #32];
169 SPAMWidth = 'WIDTH';
170 SPAMHeight = 'HEIGHT';
171 SPAMDepth = 'DEPTH';
172 SPAMMaxVal = 'MAXVAL';
173 SPAMTupleType = 'TUPLTYPE';
174 SPAMEndHdr = 'ENDHDR';
176 { Size of buffer used to speed up text PNM loading/saving.}
177 LineBufferCapacity = 16 * 1024;
179 TupleTypeNames: array[TTupleType] of string = (
180 'INVALID', 'BLACKANDWHITE', 'GRAYSCALE', 'RGB',
181 'BLACKANDWHITE_ALPHA', 'GRAYSCALE_ALPHA', 'RGB_ALPHA', 'GRAYSCALEFP',
182 'RGBFP');
184 { TPortableMapFileFormat }
186 constructor TPortableMapFileFormat.Create;
187 begin
188 inherited Create;
189 FCanLoad := True;
190 FCanSave := True;
191 FIsMultiImageFormat := False;
192 FSaveBinary := PortableMapDefaultBinary;
193 end;
195 function TPortableMapFileFormat.LoadData(Handle: TImagingHandle;
196 var Images: TDynImageDataArray; OnlyFirstLevel: Boolean): Boolean;
197 var
198 I, ScanLineSize, MonoSize: LongInt;
199 Dest: PByte;
200 MonoData: Pointer;
201 Info: TImageFormatInfo;
202 PixelFP: TColorFPRec;
203 LineBuffer: array[0..LineBufferCapacity - 1] of AnsiChar;
204 LineEnd, LinePos: LongInt;
205 MapInfo: TPortableMapInfo;
206 LineBreak: string;
208 procedure CheckBuffer;
209 begin
210 if (LineEnd = 0) or (LinePos = LineEnd) then
211 begin
212 // Reload buffer if its is empty or its end was reached
213 LineEnd := GetIO.Read(Handle, @LineBuffer[0], LineBufferCapacity);
214 LinePos := 0;
215 end;
216 end;
218 procedure FixInputPos;
219 begin
220 // Sets input's position to its real pos as it would be without buffering
221 if LineEnd > 0 then
222 begin
223 GetIO.Seek(Handle, -LineEnd + LinePos, smFromCurrent);
224 LineEnd := 0;
225 end;
226 end;
228 function ReadString: string;
229 var
230 S: AnsiString;
231 C: AnsiChar;
232 begin
233 // First skip all whitespace chars
234 SetLength(S, 1);
235 repeat
236 CheckBuffer;
237 S[1] := LineBuffer[LinePos];
238 Inc(LinePos);
239 if S[1] = '#' then
240 repeat
241 // Comment detected, skip everything until next line is reached
242 CheckBuffer;
243 S[1] := LineBuffer[LinePos];
244 Inc(LinePos);
245 until S[1] = #10;
246 until not(S[1] in WhiteSpaces);
247 // Now we have reached some chars other than white space, read them until
248 // there is whitespace again
249 repeat
250 SetLength(S, Length(S) + 1);
251 CheckBuffer;
252 S[Length(S)] := LineBuffer[LinePos];
253 Inc(LinePos);
254 // Repeat until current char is whitespace or end of file is reached
255 // (Line buffer has 0 bytes which happens only on EOF)
256 until (S[Length(S)] in WhiteSpaces) or (LineEnd = 0);
257 // Get rid of last char - whitespace or null
258 SetLength(S, Length(S) - 1);
259 // Move position to the beginning of next string (skip white space - needed
260 // to make the loader stop at the right input position)
261 repeat
262 CheckBuffer;
263 C := LineBuffer[LinePos];
264 Inc(LinePos);
265 until not (C in WhiteSpaces) or (LineEnd = 0);
266 // Dec pos, current is the begining of the the string
267 Dec(LinePos);
269 Result := string(S);
270 end;
272 function ReadIntValue: LongInt; {$IFDEF USE_INLINE}inline;{$ENDIF}
273 begin
274 Result := StrToInt(ReadString);
275 end;
277 procedure FindLineBreak;
278 var
279 C: AnsiChar;
280 begin
281 LineBreak := #10;
282 repeat
283 CheckBuffer;
284 C := LineBuffer[LinePos];
285 Inc(LinePos);
287 if C = #13 then
288 LineBreak := #13#10;
290 until C = #10;
291 end;
293 function ParseHeader: Boolean;
294 var
295 Id: TChar2;
296 I: TTupleType;
297 TupleTypeName: string;
298 Scale: Single;
299 OldSeparator: Char;
300 begin
301 Result := False;
302 with GetIO do
303 begin
304 FillChar(MapInfo, SizeOf(MapInfo), 0);
305 Read(Handle, @Id, SizeOf(Id));
306 FindLineBreak;
308 if Id[1] in ['1'..'6'] then
309 begin
310 // Read header for PBM, PGM, and PPM files
311 MapInfo.Width := ReadIntValue;
312 MapInfo.Height := ReadIntValue;
314 if Id[1] in ['1', '4'] then
315 begin
316 MapInfo.MaxVal := 1;
317 MapInfo.BitCount := 1
318 end
319 else
320 begin
321 // Read channel max value, <=255 for 8bit images, >255 for 16bit images
322 // but some programs think its max colors so put <=256 here
323 MapInfo.MaxVal := ReadIntValue;
324 MapInfo.BitCount := Iff(MapInfo.MaxVal <= 256, 8, 16);
325 end;
327 MapInfo.Depth := 1;
328 case Id[1] of
329 '1', '4': MapInfo.TupleType := ttBlackAndWhite;
330 '2', '5': MapInfo.TupleType := ttGrayScale;
331 '3', '6':
332 begin
333 MapInfo.TupleType := ttRGB;
334 MapInfo.Depth := 3;
335 end;
336 end;
337 end
338 else if Id[1] = '7' then
339 begin
340 // Read values from PAM header
341 // WIDTH
342 if (ReadString <> SPAMWidth) then Exit;
343 MapInfo.Width := ReadIntValue;
344 // HEIGHT
345 if (ReadString <> SPAMheight) then Exit;
346 MapInfo.Height := ReadIntValue;
347 // DEPTH
348 if (ReadString <> SPAMDepth) then Exit;
349 MapInfo.Depth := ReadIntValue;
350 // MAXVAL
351 if (ReadString <> SPAMMaxVal) then Exit;
352 MapInfo.MaxVal := ReadIntValue;
353 MapInfo.BitCount := Iff(MapInfo.MaxVal <= 256, 8, 16);
354 // TUPLETYPE
355 if (ReadString <> SPAMTupleType) then Exit;
356 TupleTypeName := ReadString;
357 for I := Low(TTupleType) to High(TTupleType) do
358 if SameText(TupleTypeName, TupleTypeNames[I]) then
359 begin
360 MapInfo.TupleType := I;
361 Break;
362 end;
363 // ENDHDR
364 if (ReadString <> SPAMEndHdr) then Exit;
365 end
366 else if Id[1] in ['F', 'f'] then
367 begin
368 // Read header of PFM file
369 MapInfo.Width := ReadIntValue;
370 MapInfo.Height := ReadIntValue;
371 OldSeparator := DecimalSeparator;
372 DecimalSeparator := '.';
373 Scale := StrToFloatDef(ReadString, 0);
374 DecimalSeparator := OldSeparator;
375 MapInfo.IsBigEndian := Scale > 0.0;
376 if Id[1] = 'F' then
377 MapInfo.TupleType := ttRGBFP
378 else
379 MapInfo.TupleType := ttGrayScaleFP;
380 MapInfo.Depth := Iff(MapInfo.TupleType = ttRGBFP, 3, 1);
381 MapInfo.BitCount := Iff(MapInfo.TupleType = ttRGBFP, 96, 32);
382 end;
384 FixInputPos;
385 MapInfo.Binary := (Id[1] in ['4', '5', '6', '7', 'F', 'f']);
387 if MapInfo.Binary and not (Id[1] in ['F', 'f']) then
388 begin
389 // Mimic the behaviour of Photoshop and other editors/viewers:
390 // If linenreaks in file are DOS CR/LF 16bit binary values are
391 // little endian, Unix LF only linebreak indicates big endian.
392 MapInfo.IsBigEndian := LineBreak = #10;
393 end;
395 // Check if values found in header are valid
396 Result := (MapInfo.Width > 0) and (MapInfo.Height > 0) and
397 (MapInfo.BitCount in [1, 8, 16, 32, 96]) and (MapInfo.TupleType <> ttInvalid);
398 // Now check if image has proper number of channels (PAM)
399 if Result then
400 case MapInfo.TupleType of
401 ttBlackAndWhite, ttGrayScale: Result := MapInfo.Depth = 1;
402 ttBlackAndWhiteAlpha, ttGrayScaleAlpha: Result := MapInfo.Depth = 2;
403 ttRGB: Result := MapInfo.Depth = 3;
404 ttRGBAlpha: Result := MapInfo.Depth = 4;
405 end;
406 end;
407 end;
409 begin
410 Result := False;
411 LineEnd := 0;
412 LinePos := 0;
413 SetLength(Images, 1);
414 with GetIO, Images[0] do
415 begin
416 Format := ifUnknown;
417 // Try to parse file header
418 if not ParseHeader then Exit;
419 // Select appropriate data format based on values read from file header
420 case MapInfo.TupleType of
421 ttBlackAndWhite: Format := ifGray8;
422 ttBlackAndWhiteAlpha: Format := ifA8Gray8;
423 ttGrayScale: Format := IffFormat(MapInfo.BitCount = 8, ifGray8, ifGray16);
424 ttGrayScaleAlpha: Format := IffFormat(MapInfo.BitCount = 8, ifA8Gray8, ifA16Gray16);
425 ttRGB: Format := IffFormat(MapInfo.BitCount = 8, ifR8G8B8, ifR16G16B16);
426 ttRGBAlpha: Format := IffFormat(MapInfo.BitCount = 8, ifA8R8G8B8, ifA16R16G16B16);
427 ttGrayScaleFP: Format := ifR32F;
428 ttRGBFP: Format := ifA32B32G32R32F;
429 end;
430 // Exit if no matching data format was found
431 if Format = ifUnknown then Exit;
433 NewImage(MapInfo.Width, MapInfo.Height, Format, Images[0]);
434 Info := GetFormatInfo(Format);
436 // Now read pixels from file to dest image
437 if not MapInfo.Binary then
438 begin
439 Dest := Bits;
440 for I := 0 to Width * Height - 1 do
441 begin
442 case Format of
443 ifGray8:
444 begin
445 Dest^ := ReadIntValue;
446 if MapInfo.BitCount = 1 then
447 // If source is 1bit mono image (where 0=white, 1=black)
448 // we must scale it to 8bits
449 Dest^ := 255 - Dest^ * 255;
450 end;
451 ifGray16: PWord(Dest)^ := ReadIntValue;
452 ifR8G8B8:
453 with PColor24Rec(Dest)^ do
454 begin
455 R := ReadIntValue;
456 G := ReadIntValue;
457 B := ReadIntValue;
458 end;
459 ifR16G16B16:
460 with PColor48Rec(Dest)^ do
461 begin
462 R := ReadIntValue;
463 G := ReadIntValue;
464 B := ReadIntValue;
465 end;
466 end;
467 Inc(Dest, Info.BytesPerPixel);
468 end;
469 end
470 else
471 begin
472 if MapInfo.BitCount > 1 then
473 begin
474 if not (MapInfo.TupleType in [ttGrayScaleFP, ttRGBFP]) then
475 begin
476 // Just copy bytes from binary Portable Maps (non 1bit, non FP)
477 Read(Handle, Bits, Size);
478 end
479 else
480 begin
481 Dest := Bits;
482 // FP images are in BGR order and endian swap maybe needed.
483 // Some programs store scanlines in bottom-up order but
484 // I will stick with Photoshops behaviour here
485 for I := 0 to Width * Height - 1 do
486 begin
487 Read(Handle, @PixelFP, MapInfo.BitCount div 8);
488 if MapInfo.TupleType = ttRGBFP then
489 with PColorFPRec(Dest)^ do
490 begin
491 A := 1.0;
492 R := PixelFP.R;
493 G := PixelFP.G;
494 B := PixelFP.B;
495 if MapInfo.IsBigEndian then
496 SwapEndianLongWord(PLongWord(Dest), 3);
497 end
498 else
499 begin
500 PSingle(Dest)^ := PixelFP.B;
501 if MapInfo.IsBigEndian then
502 SwapEndianLongWord(PLongWord(Dest), 1);
503 end;
504 Inc(Dest, Info.BytesPerPixel);
505 end;
506 end;
508 if MapInfo.TupleType in [ttBlackAndWhite, ttBlackAndWhiteAlpha] then
509 begin
510 // Black and white PAM files must be scaled to 8bits. Note that
511 // in PAM files 1=white, 0=black (reverse of PBM)
512 for I := 0 to Width * Height * Iff(MapInfo.TupleType = ttBlackAndWhiteAlpha, 2, 1) - 1 do
513 PByteArray(Bits)[I] := PByteArray(Bits)[I] * 255;
514 end
515 else if MapInfo.TupleType in [ttRGB, ttRGBAlpha] then
516 begin
517 // Swap channels of RGB/ARGB images. Binary RGB image files use BGR order.
518 SwapChannels(Images[0], ChannelBlue, ChannelRed);
519 end;
521 // Swap byte order if needed
522 if (MapInfo.BitCount = 16) and MapInfo.IsBigEndian then
523 SwapEndianWord(Bits, Width * Height * Info.BytesPerPixel div SizeOf(Word));
524 end
525 else
526 begin
527 // Handle binary PBM files (ttBlackAndWhite 1bit)
528 ScanLineSize := (Width + 7) div 8;
529 // Get total binary data size, read it from file to temp
530 // buffer and convert the data to Gray8
531 MonoSize := ScanLineSize * Height;
532 GetMem(MonoData, MonoSize);
533 try
534 Read(Handle, MonoData, MonoSize);
535 Convert1To8(MonoData, Bits, Width, Height, ScanLineSize);
536 // 1bit mono images must be scaled to 8bit (where 0=white, 1=black)
537 for I := 0 to Width * Height - 1 do
538 PByteArray(Bits)[I] := 255 - PByteArray(Bits)[I] * 255;
539 finally
540 FreeMem(MonoData);
541 end;
542 end;
543 end;
545 FixInputPos;
547 if (MapInfo.MaxVal <> Pow2Int(MapInfo.BitCount) - 1) and
548 (MapInfo.TupleType in [ttGrayScale, ttGrayScaleAlpha, ttRGB, ttRGBAlpha]) then
549 begin
550 Dest := Bits;
551 // Scale color values according to MaxVal we got from header
552 // if necessary.
553 for I := 0 to Width * Height * Info.BytesPerPixel div (MapInfo.BitCount shr 3) - 1 do
554 begin
555 if MapInfo.BitCount = 8 then
556 Dest^ := Dest^ * 255 div MapInfo.MaxVal
557 else
558 PWord(Dest)^ := PWord(Dest)^ * 65535 div MapInfo.MaxVal;
559 Inc(Dest, MapInfo.BitCount shr 3);
560 end;
561 end;
563 Result := True;
564 end;
565 end;
567 function TPortableMapFileFormat.SaveDataInternal(Handle: TImagingHandle;
568 const Images: TDynImageDataArray; Index: Integer; var MapInfo: TPortableMapInfo): Boolean;
569 const
570 // Use Unix linebreak, for many viewers/editors it means that
571 // 16bit samples are stored as big endian - so we need to swap byte order
572 // before saving
573 LineDelimiter = #10;
574 PixelDelimiter = #32;
575 var
576 ImageToSave: TImageData;
577 MustBeFreed: Boolean;
578 Info: TImageFormatInfo;
579 I, LineLength: LongInt;
580 Src: PByte;
581 Pixel32: TColor32Rec;
582 Pixel64: TColor64Rec;
583 W: Word;
585 procedure WriteString(S: string; Delimiter: Char = LineDelimiter);
586 begin
587 SetLength(S, Length(S) + 1);
588 S[Length(S)] := Delimiter;
589 {$IF Defined(DCC) and Defined(UNICODE)}
590 GetIO.Write(Handle, @AnsiString(S)[1], Length(S));
591 {$ELSE}
592 GetIO.Write(Handle, @S[1], Length(S));
593 {$IFEND}
594 Inc(LineLength, Length(S));
595 end;
597 procedure WriteHeader;
598 var
599 OldSeparator: Char;
600 begin
601 WriteString('P' + MapInfo.FormatId);
602 if not MapInfo.HasPAMHeader then
603 begin
604 // Write header of PGM, PPM, and PFM files
605 WriteString(IntToStr(ImageToSave.Width));
606 WriteString(IntToStr(ImageToSave.Height));
607 case MapInfo.TupleType of
608 ttGrayScale, ttRGB: WriteString(IntToStr(Pow2Int(MapInfo.BitCount) - 1));
609 ttGrayScaleFP, ttRGBFP:
610 begin
611 OldSeparator := DecimalSeparator;
612 DecimalSeparator := '.';
613 // Negative value indicates that raster data is saved in little endian
614 WriteString(FloatToStr(-1.0));
615 DecimalSeparator := OldSeparator;
616 end;
617 end;
618 end
619 else
620 begin
621 // Write PAM file header
622 WriteString(Format('%s %d', [SPAMWidth, ImageToSave.Width]));
623 WriteString(Format('%s %d', [SPAMHeight, ImageToSave.Height]));
624 WriteString(Format('%s %d', [SPAMDepth, MapInfo.Depth]));
625 WriteString(Format('%s %d', [SPAMMaxVal, Pow2Int(MapInfo.BitCount) - 1]));
626 WriteString(Format('%s %s', [SPAMTupleType, TupleTypeNames[MapInfo.TupleType]]));
627 WriteString(SPAMEndHdr);
628 end;
629 end;
631 begin
632 Result := False;
633 if MakeCompatible(Images[Index], ImageToSave, MustBeFreed) then
634 with GetIO, ImageToSave do
635 try
636 Info := GetFormatInfo(Format);
637 // Fill values of MapInfo record that were not filled by
638 // descendants in their SaveData methods
639 MapInfo.BitCount := (Info.BytesPerPixel div Info.ChannelCount) * 8;
640 MapInfo.Depth := Info.ChannelCount;
641 if MapInfo.TupleType = ttInvalid then
642 begin
643 if Info.HasGrayChannel then
644 begin
645 if Info.HasAlphaChannel then
646 MapInfo.TupleType := ttGrayScaleAlpha
647 else
648 MapInfo.TupleType := ttGrayScale;
649 end
650 else
651 begin
652 if Info.HasAlphaChannel then
653 MapInfo.TupleType := ttRGBAlpha
654 else
655 MapInfo.TupleType := ttRGB;
656 end;
657 end;
658 // Write file header
659 WriteHeader;
661 if not MapInfo.Binary then
662 begin
663 Src := Bits;
664 LineLength := 0;
665 // For each pixel find its text representation and write it to file
666 for I := 0 to Width * Height - 1 do
667 begin
668 case Format of
669 ifGray8: WriteString(IntToStr(Src^), PixelDelimiter);
670 ifGray16: WriteString(IntToStr(PWord(Src)^), PixelDelimiter);
671 ifR8G8B8:
672 with PColor24Rec(Src)^ do
673 WriteString(SysUtils.Format('%d %d %d', [R, G, B]), PixelDelimiter);
674 ifR16G16B16:
675 with PColor48Rec(Src)^ do
676 WriteString(SysUtils.Format('%d %d %d', [R, G, B]), PixelDelimiter);
677 end;
678 // Lines in text PNM images should have length <70
679 if LineLength > 65 then
680 begin
681 LineLength := 0;
682 WriteString('', LineDelimiter);
683 end;
684 Inc(Src, Info.BytesPerPixel);
685 end;
686 end
687 else
688 begin
689 // Write binary images
690 if not (MapInfo.TupleType in [ttGrayScaleFP, ttRGBFP]) then
691 begin
692 // Save integer binary images
693 if MapInfo.BitCount = 8 then
694 begin
695 if MapInfo.TupleType in [ttGrayScale, ttGrayScaleAlpha] then
696 begin
697 // 8bit grayscale images can be written in one Write call
698 Write(Handle, Bits, Size);
699 end
700 else
701 begin
702 // 8bit RGB/ARGB images: read and blue must be swapped and
703 // 3 or 4 bytes must be written
704 Src := Bits;
705 for I := 0 to Width * Height - 1 do
706 with PColor32Rec(Src)^ do
707 begin
708 if MapInfo.TupleType = ttRGBAlpha then
709 Pixel32.A := A;
710 Pixel32.R := B;
711 Pixel32.G := G;
712 Pixel32.B := R;
713 Write(Handle, @Pixel32, Info.BytesPerPixel);
714 Inc(Src, Info.BytesPerPixel);
715 end;
716 end;
717 end
718 else
719 begin
720 // Images with 16bit channels: make sure that channel values are saved in big endian
721 Src := Bits;
722 if MapInfo.TupleType in [ttGrayScale, ttGrayScaleAlpha] then
723 begin
724 // 16bit grayscale image
725 for I := 0 to Width * Height * Info.BytesPerPixel div SizeOf(Word) - 1 do
726 begin
727 W := SwapEndianWord(PWord(Src)^);
728 Write(Handle, @W, SizeOf(Word));
729 Inc(Src, SizeOf(Word));
730 end;
731 end
732 else
733 begin
734 // RGB images with 16bit channels: swap RB and endian too
735 for I := 0 to Width * Height - 1 do
736 with PColor64Rec(Src)^ do
737 begin
738 if MapInfo.TupleType = ttRGBAlpha then
739 Pixel64.A := SwapEndianWord(A);
740 Pixel64.R := SwapEndianWord(B);
741 Pixel64.G := SwapEndianWord(G);
742 Pixel64.B := SwapEndianWord(R);
743 Write(Handle, @Pixel64, Info.BytesPerPixel);
744 Inc(Src, Info.BytesPerPixel);
745 end;
746 end;
747 end;
748 end
749 else
750 begin
751 // Floating point images (no need to swap endian here - little
752 // endian is specified in file header)
753 if MapInfo.TupleType = ttGrayScaleFP then
754 begin
755 // Grayscale images can be written in one Write call
756 Write(Handle, Bits, Size);
757 end
758 else
759 begin
760 // Expected data format of PFM RGB file is B32G32R32F which is not
761 // supported by Imaging. We must write pixels one by one and
762 // write only RGB part of A32B32G32B32 image.
763 Src := Bits;
764 for I := 0 to Width * Height - 1 do
765 begin
766 Write(Handle, Src, SizeOf(Single) * 3);
767 Inc(Src, Info.BytesPerPixel);
768 end;
769 end;
770 end;
771 end;
772 Result := True;
773 finally
774 if MustBeFreed then
775 FreeImage(ImageToSave);
776 end;
777 end;
779 function TPortableMapFileFormat.TestFormat(Handle: TImagingHandle): Boolean;
780 var
781 Id: TChar4;
782 ReadCount: LongInt;
783 begin
784 Result := False;
785 if Handle <> nil then
786 with GetIO do
787 begin
788 ReadCount := Read(Handle, @Id, SizeOf(Id));
789 Seek(Handle, -ReadCount, smFromCurrent);
790 Result := (Id[0] = 'P') and (Id[1] in [FIdNumbers[0], FIdNumbers[1]]) and
791 (Id[2] in WhiteSpaces);
792 end;
793 end;
795 { TPBMFileFormat }
797 constructor TPBMFileFormat.Create;
798 begin
799 inherited Create;
800 FName := SPBMFormatName;
801 FCanSave := False;
802 AddMasks(SPBMMasks);
803 FIdNumbers := '14';
804 end;
806 { TPGMFileFormat }
808 constructor TPGMFileFormat.Create;
809 begin
810 inherited Create;
811 FName := SPGMFormatName;
812 FSupportedFormats := PGMSupportedFormats;
813 AddMasks(SPGMMasks);
814 RegisterOption(ImagingPGMSaveBinary, @FSaveBinary);
815 FIdNumbers := '25';
816 end;
818 function TPGMFileFormat.SaveData(Handle: TImagingHandle;
819 const Images: TDynImageDataArray; Index: Integer): Boolean;
820 var
821 MapInfo: TPortableMapInfo;
822 begin
823 FillChar(MapInfo, SizeOf(MapInfo), 0);
824 if FSaveBinary then
825 MapInfo.FormatId := FIdNumbers[1]
826 else
827 MapInfo.FormatId := FIdNumbers[0];
828 MapInfo.Binary := FSaveBinary;
829 Result := SaveDataInternal(Handle, Images, Index, MapInfo);
830 end;
832 procedure TPGMFileFormat.ConvertToSupported(var Image: TImageData;
833 const Info: TImageFormatInfo);
834 var
835 ConvFormat: TImageFormat;
836 begin
837 if Info.IsFloatingPoint then
838 // All FP images go to 16bit
839 ConvFormat := ifGray16
840 else if Info.HasGrayChannel then
841 // Grayscale will be 8 or 16 bit - depends on input's bitcount
842 ConvFormat := IffFormat(Info.BytesPerPixel div Info.ChannelCount > 1,
843 ifGray16, ifGray8)
844 else if Info.BytesPerPixel > 4 then
845 // Large bitcounts -> 16bit
846 ConvFormat := ifGray16
847 else
848 // Rest of the formats -> 8bit
849 ConvFormat := ifGray8;
851 ConvertImage(Image, ConvFormat);
852 end;
854 { TPPMFileFormat }
856 constructor TPPMFileFormat.Create;
857 begin
858 inherited Create;
859 FName := SPPMFormatName;
860 FSupportedFormats := PPMSupportedFormats;
861 AddMasks(SPPMMasks);
862 RegisterOption(ImagingPPMSaveBinary, @FSaveBinary);
863 FIdNumbers := '36';
864 end;
866 function TPPMFileFormat.SaveData(Handle: TImagingHandle;
867 const Images: TDynImageDataArray; Index: Integer): Boolean;
868 var
869 MapInfo: TPortableMapInfo;
870 begin
871 FillChar(MapInfo, SizeOf(MapInfo), 0);
872 if FSaveBinary then
873 MapInfo.FormatId := FIdNumbers[1]
874 else
875 MapInfo.FormatId := FIdNumbers[0];
876 MapInfo.Binary := FSaveBinary;
877 Result := SaveDataInternal(Handle, Images, Index, MapInfo);
878 end;
880 procedure TPPMFileFormat.ConvertToSupported(var Image: TImageData;
881 const Info: TImageFormatInfo);
882 var
883 ConvFormat: TImageFormat;
884 begin
885 if Info.IsFloatingPoint then
886 // All FP images go to 48bit RGB
887 ConvFormat := ifR16G16B16
888 else if Info.HasGrayChannel then
889 // Grayscale will be 24 or 48 bit RGB - depends on input's bitcount
890 ConvFormat := IffFormat(Info.BytesPerPixel div Info.ChannelCount > 1,
891 ifR16G16B16, ifR8G8B8)
892 else if Info.BytesPerPixel > 4 then
893 // Large bitcounts -> 48bit RGB
894 ConvFormat := ifR16G16B16
895 else
896 // Rest of the formats -> 24bit RGB
897 ConvFormat := ifR8G8B8;
899 ConvertImage(Image, ConvFormat);
900 end;
902 { TPAMFileFormat }
904 constructor TPAMFileFormat.Create;
905 begin
906 inherited Create;
907 FName := SPAMFormatName;
908 FSupportedFormats := PAMSupportedFormats;
909 AddMasks(SPAMMasks);
910 FIdNumbers := '77';
911 end;
913 function TPAMFileFormat.SaveData(Handle: TImagingHandle;
914 const Images: TDynImageDataArray; Index: Integer): Boolean;
915 var
916 MapInfo: TPortableMapInfo;
917 begin
918 FillChar(MapInfo, SizeOf(MapInfo), 0);
919 MapInfo.FormatId := FIdNumbers[0];
920 MapInfo.Binary := True;
921 MapInfo.HasPAMHeader := True;
922 Result := SaveDataInternal(Handle, Images, Index, MapInfo);
923 end;
925 procedure TPAMFileFormat.ConvertToSupported(var Image: TImageData;
926 const Info: TImageFormatInfo);
927 var
928 ConvFormat: TImageFormat;
929 begin
930 if Info.IsFloatingPoint then
931 ConvFormat := IffFormat(Info.HasAlphaChannel, ifA16R16G16B16, ifR16G16B16)
932 else if Info.HasGrayChannel then
933 ConvFormat := IffFormat(Info.HasAlphaChannel, ifA16Gray16, ifGray16)
934 else
935 begin
936 if Info.BytesPerPixel <= 4 then
937 ConvFormat := IffFormat(Info.HasAlphaChannel, ifA8R8G8B8, ifR8G8B8)
938 else
939 ConvFormat := IffFormat(Info.HasAlphaChannel, ifA16R16G16B16, ifR16G16B16);
940 end;
941 ConvertImage(Image, ConvFormat);
942 end;
944 { TPFMFileFormat }
946 constructor TPFMFileFormat.Create;
947 begin
948 inherited Create;
949 FName := SPFMFormatName;
950 AddMasks(SPFMMasks);
951 FIdNumbers := 'Ff';
952 FSupportedFormats := PFMSupportedFormats;
953 end;
955 function TPFMFileFormat.SaveData(Handle: TImagingHandle;
956 const Images: TDynImageDataArray; Index: Integer): Boolean;
957 var
958 Info: TImageFormatInfo;
959 MapInfo: TPortableMapInfo;
960 begin
961 FillChar(MapInfo, SizeOf(MapInfo), 0);
962 Info := GetFormatInfo(Images[Index].Format);
964 if (Info.ChannelCount > 1) or Info.IsIndexed then
965 MapInfo.TupleType := ttRGBFP
966 else
967 MapInfo.TupleType := ttGrayScaleFP;
969 if MapInfo.TupleType = ttGrayScaleFP then
970 MapInfo.FormatId := FIdNumbers[1]
971 else
972 MapInfo.FormatId := FIdNumbers[0];
974 MapInfo.Binary := True;
975 Result := SaveDataInternal(Handle, Images, Index, MapInfo);
976 end;
978 procedure TPFMFileFormat.ConvertToSupported(var Image: TImageData;
979 const Info: TImageFormatInfo);
980 begin
981 if (Info.ChannelCount > 1) or Info.IsIndexed then
982 ConvertImage(Image, ifA32B32G32R32F)
983 else
984 ConvertImage(Image, ifR32F);
985 end;
987 initialization
988 RegisterImageFileFormat(TPBMFileFormat);
989 RegisterImageFileFormat(TPGMFileFormat);
990 RegisterImageFileFormat(TPPMFileFormat);
991 RegisterImageFileFormat(TPAMFileFormat);
992 RegisterImageFileFormat(TPFMFileFormat);
995 File Notes:
997 -- TODOS ----------------------------------------------------
998 - nothing now
1000 -- 0.26.3 Changes/Bug Fixes -----------------------------------
1001 - Fixed D2009 Unicode related bug in PNM saving.
1003 -- 0.24.3 Changes/Bug Fixes -----------------------------------
1004 - Improved compatibility of 16bit/component image loading.
1005 - Changes for better thread safety.
1007 -- 0.21 Changes/Bug Fixes -----------------------------------
1008 - Made modifications to ASCII PNM loading to be more "stream-safe".
1009 - Fixed bug: indexed images saved as grayscale in PFM.
1010 - Changed converting to supported formats little bit.
1011 - Added scaling of channel values (non-FP and non-mono images) according
1012 to MaxVal.
1013 - Added buffering to loading of PNM files. More than 10x faster now
1014 for text files.
1015 - Added saving support to PGM, PPM, PAM, and PFM format.
1016 - Added PFM file format.
1017 - Initial version created.
1020 end.