1--
2-- Convert any image or animation file to BMP file(s).
3--
4-- Middle-size test/demo for the GID (Generic Image Decoder) package.
5--
6-- Supports:
7-- - Transparency (blends transparent or partially opaque areas with a
8--     background image, gid.gif, or a fixed, predefined colour)
9-- - Display orientation (JPEG EXIF informations from digital cameras)
10--
11-- For a smaller and simpler example, look for mini.adb .
12--
13
14with GID;
15
16with Ada.Calendar;
17with Ada.Characters.Handling;           use Ada.Characters.Handling;
18with Ada.Command_Line;                  use Ada.Command_Line;
19with Ada.Streams.Stream_IO;             use Ada.Streams.Stream_IO;
20with Ada.Strings.Unbounded;             use Ada.Strings.Unbounded;
21with Ada.Text_IO;                       use Ada.Text_IO;
22with Ada.Unchecked_Deallocation;
23
24with Interfaces;
25
26procedure To_BMP is
27
28  default_bkg_name: constant String:= "gid.gif";
29
30  procedure Blurb is
31  begin
32    Put_Line(Standard_Error, "To_BMP * Converts any image file to a BMP file");
33    Put_Line(Standard_Error, "Simple test for the GID (Generic Image Decoder) package");
34    Put_Line(Standard_Error, "Package version " & GID.version & " dated " & GID.reference);
35    Put_Line(Standard_Error, "URL: " & GID.web);
36    New_Line(Standard_Error);
37    Put_Line(Standard_Error, "Syntax:");
38    Put_Line(Standard_Error, "to_bmp [-] [-<background_image_name>] <image_1> [<image_2>...]");
39    New_Line(Standard_Error);
40    Put_Line(Standard_Error, "Options:");
41    Put_Line(Standard_Error, "  '-': don't output image (testing only)");
42    Put_Line(Standard_Error, "  '-<background_image_name>':");
43    Put_Line(Standard_Error, "      use specifed background to mix with transparent images");
44    Put_Line(Standard_Error, "      (otherwise, trying with '"& default_bkg_name &"' or single color)");
45    New_Line(Standard_Error);
46    Put_Line(Standard_Error, "Output: "".dib"" is added the full input name(s)");
47    Put_Line(Standard_Error, "  Reason of "".dib"": unknown synonym of "".bmp"";");
48    Put_Line(Standard_Error, "  just do ""del *.dib"" for cleanup");
49    New_Line(Standard_Error);
50  end Blurb;
51
52  -- Image used as background for displaying images having transparency
53  background_image_name: Unbounded_String:= Null_Unbounded_String;
54
55  use Interfaces;
56
57  type Byte_Array is array(Integer range <>) of Unsigned_8;
58  type p_Byte_Array is access Byte_Array;
59  procedure Dispose is new Ada.Unchecked_Deallocation(Byte_Array, p_Byte_Array);
60
61  forgive_errors: constant Boolean:= False;
62  error: Boolean;
63
64  img_buf, bkg_buf: p_Byte_Array:= null;
65  bkg: GID.Image_descriptor;
66
67  generic
68    correct_orientation: GID.Orientation;
69  -- Load image into a 24-bit truecolor BGR raw bitmap (for a BMP output)
70  procedure Load_raw_image(
71    image : in out GID.Image_descriptor;
72    buffer: in out p_Byte_Array;
73    next_frame: out Ada.Calendar.Day_Duration
74  );
75  --
76  procedure Load_raw_image(
77    image : in out GID.Image_descriptor;
78    buffer: in out p_Byte_Array;
79    next_frame: out Ada.Calendar.Day_Duration
80  )
81  is
82    subtype Primary_color_range is Unsigned_8;
83    subtype U16 is Unsigned_16;
84    image_width: constant Positive:= GID.Pixel_width(image);
85    image_height: constant Positive:= GID.Pixel_height(image);
86    padded_line_size_x: constant Positive:=
87      4 * Integer(Float'Ceiling(Float(image_width) * 3.0 / 4.0));
88    padded_line_size_y: constant Positive:=
89      4 * Integer(Float'Ceiling(Float(image_height) * 3.0 / 4.0));
90    -- (in bytes)
91    idx: Integer;
92    mem_x, mem_y: Natural;
93    bkg_padded_line_size: Positive;
94    bkg_width, bkg_height: Natural;
95    --
96    procedure Set_X_Y (x, y: Natural) is
97    pragma Inline(Set_X_Y);
98      use GID;
99      rev_x: constant Natural:= image_width - (x+1);
100      rev_y: constant Natural:= image_height - (y+1);
101    begin
102      case correct_orientation is
103        when Unchanged =>
104          idx:= 3 * x + padded_line_size_x * y;
105        when Rotation_90 =>
106          idx:= 3 * rev_y + padded_line_size_y * x;
107        when Rotation_180 =>
108          idx:= 3 * rev_x + padded_line_size_x * rev_y;
109        when Rotation_270 =>
110          idx:= 3 * y + padded_line_size_y * rev_x;
111      end case;
112      mem_x:= x;
113      mem_y:= y;
114    end Set_X_Y;
115    --
116    -- No background version of Put_Pixel
117    --
118    procedure Put_Pixel_without_bkg (
119      red, green, blue : Primary_color_range;
120      alpha            : Primary_color_range
121    )
122    is
123    pragma Inline(Put_Pixel_without_bkg);
124    pragma Warnings(off, alpha); -- alpha is just ignored
125      use GID;
126    begin
127      buffer(idx..idx+2):= (blue, green, red);
128      -- GID requires us to look to next pixel for next time:
129      case correct_orientation is
130        when Unchanged =>
131          idx:= idx + 3;
132        when Rotation_90 =>
133          idx:= idx + padded_line_size_y;
134        when Rotation_180 =>
135          idx:= idx - 3;
136        when Rotation_270 =>
137          idx:= idx - padded_line_size_y;
138      end case;
139    end Put_Pixel_without_bkg;
140    --
141    -- Unicolor background version of Put_Pixel
142    --
143    procedure Put_Pixel_with_unicolor_bkg (
144      red, green, blue : Primary_color_range;
145      alpha            : Primary_color_range
146    )
147    is
148    pragma Inline(Put_Pixel_with_unicolor_bkg);
149      u_red  : constant:= 200;
150      u_green: constant:= 133;
151      u_blue : constant:= 32;
152    begin
153      if alpha = 255 then
154        buffer(idx..idx+2):= (blue, green, red);
155      else -- blend with bckground color
156        buffer(idx)  := Primary_color_range((U16(alpha) * U16(blue)  + U16(255-alpha) * u_blue )/255);
157        buffer(idx+1):= Primary_color_range((U16(alpha) * U16(green) + U16(255-alpha) * u_green)/255);
158        buffer(idx+2):= Primary_color_range((U16(alpha) * U16(red)   + U16(255-alpha) * u_red  )/255);
159      end if;
160      idx:= idx + 3;
161      -- ^ GID requires us to look to next pixel on the right for next time.
162    end Put_Pixel_with_unicolor_bkg;
163    --
164    -- Background image version of Put_Pixel
165    --
166    procedure Put_Pixel_with_image_bkg (
167      red, green, blue : Primary_color_range;
168      alpha            : Primary_color_range
169    )
170    is
171    pragma Inline(Put_Pixel_with_image_bkg);
172      b_red,
173      b_green,
174      b_blue : Primary_color_range;
175      bkg_idx: Natural;
176    begin
177      if alpha = 255 then
178        buffer(idx..idx+2):= (blue, green, red);
179      else -- blend with background image
180        bkg_idx:= 3 * (mem_x mod bkg_width) + bkg_padded_line_size * (mem_y mod bkg_height);
181        b_blue := bkg_buf(bkg_idx);
182        b_green:= bkg_buf(bkg_idx+1);
183        b_red  := bkg_buf(bkg_idx+2);
184        buffer(idx)  := Primary_color_range((U16(alpha) * U16(blue)  + U16(255-alpha) * U16(b_blue) )/255);
185        buffer(idx+1):= Primary_color_range((U16(alpha) * U16(green) + U16(255-alpha) * U16(b_green))/255);
186        buffer(idx+2):= Primary_color_range((U16(alpha) * U16(red)   + U16(255-alpha) * U16(b_red)  )/255);
187      end if;
188      idx:= idx + 3;
189      -- ^ GID requires us to look to next pixel on the right for next time.
190      mem_x:= mem_x + 1;
191    end Put_Pixel_with_image_bkg;
192
193    stars: Natural:= 0;
194    procedure Feedback(percents: Natural) is
195      so_far: constant Natural:= percents / 5;
196    begin
197      for i in stars+1..so_far loop
198        Put( Standard_Error, '*');
199      end loop;
200      stars:= so_far;
201    end Feedback;
202
203    -- Here, the exciting thing: the instanciation of
204    -- GID.Load_image_contents. In our case, we load the image
205    -- into a 24-bit bitmap (because we provide a Put_Pixel
206    -- that does that with the pixels), but we could do plenty
207    -- of other things instead, like display the image live on a GUI.
208
209    -- More exciting: for tuning performance, we have 3 different
210    -- instances of GID.Load_image_contents (each of them with the full
211    -- decoders for all formats, own specialized generic instances, inlines,
212    -- etc.) depending on the transparency features.
213
214    procedure BMP24_Load_without_bkg is
215      new GID.Load_image_contents(
216        Primary_color_range,
217        Set_X_Y,
218        Put_Pixel_without_bkg,
219        Feedback,
220        GID.fast
221      );
222
223    procedure BMP24_Load_with_unicolor_bkg is
224      new GID.Load_image_contents(
225        Primary_color_range,
226        Set_X_Y,
227        Put_Pixel_with_unicolor_bkg,
228        Feedback,
229        GID.fast
230      );
231
232    procedure BMP24_Load_with_image_bkg is
233      new GID.Load_image_contents(
234        Primary_color_range,
235        Set_X_Y,
236        Put_Pixel_with_image_bkg,
237        Feedback,
238        GID.fast
239      );
240
241  begin
242    error:= False;
243    Dispose(buffer);
244    case correct_orientation is
245      when GID.Unchanged | GID.Rotation_180 =>
246        buffer:= new Byte_Array(0..padded_line_size_x * GID.Pixel_height(image) - 1);
247      when GID.Rotation_90 | GID.Rotation_270 =>
248        buffer:= new Byte_Array(0..padded_line_size_y * GID.Pixel_width(image) - 1);
249    end case;
250    if GID.Expect_transparency(image) then
251      if background_image_name = Null_Unbounded_String then
252        BMP24_Load_with_unicolor_bkg(image, next_frame);
253      else
254        bkg_width:= GID.Pixel_width(bkg);
255        bkg_height:= GID.Pixel_height(bkg);
256        bkg_padded_line_size:=
257          4 * Integer(Float'Ceiling(Float(bkg_width) * 3.0 / 4.0));
258        BMP24_Load_with_image_bkg(image, next_frame);
259      end if;
260    else
261      BMP24_Load_without_bkg(image, next_frame);
262    end if;
263    --  -- For testing: white rectangle with a red half-frame.
264    --  buffer.all:= (others => 255);
265    --  for x in 0..GID.Pixel_width(image)-1 loop
266    --    Put_Pixel_with_unicolor_bkg(x,0,255,0,0,255);
267    --  end loop;
268    --  for y in 0..GID.Pixel_height(image)-1 loop
269    --    Put_Pixel_with_unicolor_bkg(0,y,255,0,0,255);
270    --  end loop;
271  exception
272    when others =>
273      if forgive_errors then
274        error:= True;
275        next_frame:= 0.0;
276      else
277        raise;
278      end if;
279  end Load_raw_image;
280
281  procedure Load_raw_image_0 is new Load_raw_image(GID.Unchanged);
282  procedure Load_raw_image_90 is new Load_raw_image(GID.Rotation_90);
283  procedure Load_raw_image_180 is new Load_raw_image(GID.Rotation_180);
284  procedure Load_raw_image_270 is new Load_raw_image(GID.Rotation_270);
285
286  procedure Dump_BMP_24(name: String; i: GID.Image_descriptor) is
287    f: Ada.Streams.Stream_IO.File_Type;
288    type BITMAPFILEHEADER is record
289      bfType     : Unsigned_16;
290      bfSize     : Unsigned_32;
291      bfReserved1: Unsigned_16:= 0;
292      bfReserved2: Unsigned_16:= 0;
293      bfOffBits  : Unsigned_32;
294    end record;
295    -- ^ No packing needed
296    BITMAPFILEHEADER_Bytes: constant:= 14;
297
298    type BITMAPINFOHEADER is record
299      biSize         : Unsigned_32;
300      biWidth        : Unsigned_32;
301      biHeight       : Unsigned_32;
302      biPlanes       : Unsigned_16:= 1;
303      biBitCount     : Unsigned_16;
304      biCompression  : Unsigned_32:= 0;
305      biSizeImage    : Unsigned_32;
306      biXPelsPerMeter: Unsigned_32:= 0;
307      biYPelsPerMeter: Unsigned_32:= 0;
308      biClrUsed      : Unsigned_32:= 0;
309      biClrImportant : Unsigned_32:= 0;
310    end record;
311    -- ^ No packing needed
312    BITMAPINFOHEADER_Bytes: constant:= 40;
313
314    FileInfo  : BITMAPINFOHEADER;
315    FileHeader: BITMAPFILEHEADER;
316    --
317    generic
318      type Number is mod <>;
319    procedure Write_Intel_x86_number(n: in Number);
320
321    procedure Write_Intel_x86_number(n: in Number) is
322      m: Number:= n;
323      bytes: constant Integer:= Number'Size/8;
324    begin
325      for i in 1..bytes loop
326        Unsigned_8'Write(Stream(f), Unsigned_8(m and 255));
327        m:= m / 256;
328      end loop;
329    end Write_Intel_x86_number;
330    procedure Write_Intel is new Write_Intel_x86_number( Unsigned_16 );
331    procedure Write_Intel is new Write_Intel_x86_number( Unsigned_32 );
332  begin
333    FileHeader.bfType := 16#4D42#; -- 'BM'
334    FileHeader.bfOffBits := BITMAPINFOHEADER_Bytes + BITMAPFILEHEADER_Bytes;
335    FileInfo.biSize       := BITMAPINFOHEADER_Bytes;
336    case GID.Display_orientation(i) is
337      when GID.Unchanged | GID.Rotation_180 =>
338        FileInfo.biWidth  := Unsigned_32(GID.Pixel_width(i));
339        FileInfo.biHeight := Unsigned_32(GID.Pixel_height(i));
340      when GID.Rotation_90 | GID.Rotation_270 =>
341        FileInfo.biWidth  := Unsigned_32(GID.Pixel_height(i));
342        FileInfo.biHeight := Unsigned_32(GID.Pixel_width(i));
343    end case;
344    FileInfo.biBitCount   := 24;
345    FileInfo.biSizeImage  := Unsigned_32(img_buf.all'Length);
346
347    FileHeader.bfSize := FileHeader.bfOffBits + FileInfo.biSizeImage;
348
349    Create(f, Out_File, name & ".dib");
350    -- BMP Header, endian-safe:
351    Write_Intel(FileHeader.bfType);
352    Write_Intel(FileHeader.bfSize);
353    Write_Intel(FileHeader.bfReserved1);
354    Write_Intel(FileHeader.bfReserved2);
355    Write_Intel(FileHeader.bfOffBits);
356    --
357    Write_Intel(FileInfo.biSize);
358    Write_Intel(FileInfo.biWidth);
359    Write_Intel(FileInfo.biHeight);
360    Write_Intel(FileInfo.biPlanes);
361    Write_Intel(FileInfo.biBitCount);
362    Write_Intel(FileInfo.biCompression);
363    Write_Intel(FileInfo.biSizeImage);
364    Write_Intel(FileInfo.biXPelsPerMeter);
365    Write_Intel(FileInfo.biYPelsPerMeter);
366    Write_Intel(FileInfo.biClrUsed);
367    Write_Intel(FileInfo.biClrImportant);
368    -- BMP raw BGR image:
369    declare
370      -- Workaround for the severe xxx'Read xxx'Write performance
371      -- problems in the GNAT and ObjectAda compilers (as in 2009)
372      -- This is possible if and only if Byte = Stream_Element and
373      -- arrays types are both packed the same way.
374      --
375      subtype Size_test_a is Byte_Array(1..19);
376      subtype Size_test_b is Ada.Streams.Stream_Element_Array(1..19);
377      workaround_possible: constant Boolean:=
378        Size_test_a'Size = Size_test_b'Size and then
379        Size_test_a'Alignment = Size_test_b'Alignment;
380      --
381    begin
382      if workaround_possible then
383        declare
384          use Ada.Streams;
385          SE_Buffer   : Stream_Element_Array (0..Stream_Element_Offset(img_buf'Length-1));
386          for SE_Buffer'Address use img_buf.all'Address;
387          pragma Import (Ada, SE_Buffer);
388        begin
389          Ada.Streams.Write(Stream(f).all, SE_Buffer(0..Stream_Element_Offset(img_buf'Length-1)));
390        end;
391      else
392        Byte_Array'Write(Stream(f), img_buf.all); -- the workaround is about this line...
393      end if;
394    end;
395    Close(f);
396  end Dump_BMP_24;
397
398  procedure Process(name: String; as_background, test_only: Boolean) is
399    f: Ada.Streams.Stream_IO.File_Type;
400    i: GID.Image_descriptor;
401    up_name: constant String:= To_Upper(name);
402    --
403    next_frame, current_frame: Ada.Calendar.Day_Duration:= 0.0;
404  begin
405    --
406    -- Load the image in its original format
407    --
408    Open(f, In_File, name);
409    Put_Line(Standard_Error, "Processing " & name & "...");
410    --
411    GID.Load_image_header(
412      i,
413      Stream(f).all,
414      try_tga =>
415        name'Length >= 4 and then
416        up_name(up_name'Last-3..up_name'Last) = ".TGA"
417    );
418    Put_Line(Standard_Error,
419      "  Image format: " & GID.Image_format_type'Image(GID.Format(i))
420    );
421    Put_Line(Standard_Error,
422      "  Image detailed format: " & GID.Detailed_format(i)
423    );
424    Put_Line(Standard_Error,
425      "  Image sub-format ID (if any): " & Integer'Image(GID.Subformat(i))
426    );
427    Put_Line(Standard_Error,
428      "  Dimensions in pixels: " &
429      Integer'Image(GID.Pixel_width(i)) & " x" &
430      Integer'Image(GID.Pixel_height(i))
431    );
432    Put_Line(Standard_Error,
433      "  Display orientation: " &
434      GID.Orientation'Image(GID.Display_orientation(i))
435    );
436    Put(Standard_Error,
437      "  Color depth: " &
438      Integer'Image(GID.Bits_per_pixel(i)) & " bits"
439    );
440    if GID.Bits_per_pixel(i) <= 24 then
441      Put_Line(Standard_Error,
442        ',' &
443        Integer'Image(2**GID.Bits_per_pixel(i)) & " colors"
444      );
445    else
446      New_Line(Standard_Error);
447    end if;
448    Put_Line(Standard_Error,
449      "  Palette: " & Boolean'Image(GID.Has_palette(i))
450    );
451    Put_Line(Standard_Error,
452      "  Greyscale: " & Boolean'Image(GID.Greyscale(i))
453    );
454    Put_Line(Standard_Error,
455      "  RLE encoding (if any): " & Boolean'Image(GID.RLE_encoded(i))
456    );
457    Put_Line(Standard_Error,
458      "  Interlaced (GIF: each frame's choice): " & Boolean'Image(GID.Interlaced(i))
459    );
460    Put_Line(Standard_Error,
461      "  Expect transparency: " & Boolean'Image(GID.Expect_transparency(i))
462    );
463    Put_Line(Standard_Error, "1........10........20");
464    Put_Line(Standard_Error, "         |         | ");
465    --
466    if as_background then
467      case GID.Display_orientation(i) is
468        when GID.Unchanged =>
469          Load_raw_image_0(i, bkg_buf, next_frame);
470        when GID.Rotation_90 =>
471          Load_raw_image_90(i, bkg_buf, next_frame);
472        when GID.Rotation_180 =>
473          Load_raw_image_180(i, bkg_buf, next_frame);
474        when GID.Rotation_270 =>
475          Load_raw_image_270(i, bkg_buf, next_frame);
476      end case;
477      bkg:= i;
478      New_Line(Standard_Error);
479      Close(f);
480      return;
481    end if;
482    loop
483      case GID.Display_orientation(i) is
484        when GID.Unchanged =>
485          Load_raw_image_0(i, img_buf, next_frame);
486        when GID.Rotation_90 =>
487          Load_raw_image_90(i, img_buf, next_frame);
488        when GID.Rotation_180 =>
489          Load_raw_image_180(i, img_buf, next_frame);
490        when GID.Rotation_270 =>
491          Load_raw_image_270(i, img_buf, next_frame);
492      end case;
493      if not test_only then
494        Dump_BMP_24(name & Duration'Image(current_frame), i);
495      end if;
496      New_Line(Standard_Error);
497      if error then
498        Put_Line(Standard_Error, "Error!");
499      end if;
500      exit when next_frame = 0.0;
501      current_frame:= next_frame;
502    end loop;
503    Close(f);
504  exception
505    when GID.unknown_image_format =>
506      Put_Line(Standard_Error, "  Image format is unknown!");
507      if Is_Open(f) then
508        Close(f);
509      end if;
510  end Process;
511
512  test_only: Boolean:= False;
513
514begin
515  if Argument_Count=0 then
516    Blurb;
517    return;
518  end if;
519  Put_Line(Standard_Error, "To_BMP, using GID version " & GID.version & " dated " & GID.reference);
520  begin
521    Process(default_bkg_name, True, False);
522    -- if success:
523    background_image_name:= To_Unbounded_String(default_bkg_name);
524  exception
525    when Ada.Text_IO.Name_Error =>
526      null; -- nothing bad, just couldn't find default background
527  end;
528  for i in 1..Argument_Count loop
529    declare
530      arg: constant String:= Argument(i);
531    begin
532      if arg /= "" and then arg(arg'First)='-' then
533        declare
534          opt: constant String:= arg(arg'First+1..arg'Last);
535        begin
536          if opt = "" then
537            test_only:= True;
538          else
539            Put_Line(Standard_Error, "Background image is " & opt);
540            Process(opt, True, False);
541            -- define this only after processing, otherwise
542            -- a transparent background will try to use
543            -- an undefined background
544            background_image_name:= To_Unbounded_String(opt);
545          end if;
546        end;
547      else
548        Process(arg, False, test_only);
549      end if;
550    end;
551  end loop;
552end To_BMP;
553