grafX2/src/giformat.c
Thomas Bernard 6f21c7e753
malloc return check in Save_GIF()
also translated comments from French to English
2020-12-20 18:18:30 +01:00

1377 lines
52 KiB
C

/* vim:expandtab:ts=2 sw=2:
*/
/* Grafx2 - The Ultimate 256-color bitmap paint program
Copyright owned by various GrafX2 authors, see COPYRIGHT.txt for details.
Grafx2 is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; version 2
of the License.
Grafx2 is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Grafx2; if not, see <http://www.gnu.org/licenses/>
*/
///@file giformat.c
/// Saving and loading GIF
#include <stdlib.h>
#include <string.h>
#include "struct.h"
#include "global.h"
#include "oldies.h"
#include "io.h"
#include "loadsave.h"
#include "loadsavefuncs.h"
#include "gfx2mem.h"
#include "gfx2log.h"
#ifndef MIN
#define MIN(a,b) ((a)<(b)?(a):(b))
#endif
#ifndef MAX
#define MAX(a,b) ((a)>(b)?(a):(b))
#endif
/**
* @defgroup GIF GIF format
* @ingroup loadsaveformats
* Graphics Interchange Format
*
* The GIF format uses LZW compression and stores indexed color pictures
* up to 256 colors. It has the ability to store several pictures in the same
* file : GrafX2 takes advantage of this feature for storing layered images
* and animations.
*
* GrafX2 implements GIF89a :
* https://www.w3.org/Graphics/GIF/spec-gif89a.txt
*
* @{
*/
/// Logical Screen Descriptor Block
typedef struct
{
word Width; ///< Width of the complete image area
word Height; ///< Height of the complete image area
byte Resol; ///< Informations about the resolution (and other)
byte Backcol; ///< Proposed background color
byte Aspect; ///< Informations about aspect ratio. Ratio = (Aspect + 15) / 64
} T_GIF_LSDB;
/// Image Descriptor Block
typedef struct
{
word Pos_X; ///< X offset where the image should be pasted
word Pos_Y; ///< Y offset where the image should be pasted
word Image_width; ///< Width of image
word Image_height; ///< Height of image
byte Indicator; ///< Misc image information
byte Nb_bits_pixel; ///< Nb de bits par pixel
} T_GIF_IDB;
/// Graphic Control Extension
typedef struct
{
byte Block_identifier; ///< 0x21
byte Function; ///< 0xF9
byte Block_size; ///< 4
byte Packed_fields; ///< 11100000 : Reserved
///< 00011100 : Disposal method
///< 00000010 : User input flag
///< 00000001 : Transparent flag
word Delay_time; ///< Time for this frame to stay displayed
byte Transparent_color; ///< Which color index acts as transparent
word Block_terminator; ///< 0x00
} T_GIF_GCE;
enum DISPOSAL_METHOD
{
DISPOSAL_METHOD_UNDEFINED = 0,
DISPOSAL_METHOD_DO_NOT_DISPOSE = 1,
DISPOSAL_METHOD_RESTORE_BGCOLOR = 2,
DISPOSAL_METHOD_RESTORE_PREVIOUS = 3,
};
/// Test if a file is GIF format
void Test_GIF(T_IO_Context * context, FILE * file)
{
char signature[6];
(void)context;
File_error=1;
if (Read_bytes(file,signature,6))
{
/// checks if the signature (6 first bytes) is either GIF87a or GIF89a
if ((!memcmp(signature,"GIF87a",6))||(!memcmp(signature,"GIF89a",6)))
File_error=0;
}
}
// -- Lire un fichier au format GIF -----------------------------------------
typedef struct {
word nb_bits; ///< bits for a code
word remainder_bits; ///< available bits in @ref last_byte field
byte remainder_byte; ///< Remaining bytes in current block
word current_code; ///< current code (generally the one just read)
byte last_byte; ///< buffer byte for reading bits for codes
word pos_X; ///< Current coordinates
word pos_Y;
word interlaced; ///< interlaced flag
word pass; ///< current pass in interlaced decoding
word stop; ///< Stop flag (end of picture)
} T_GIF_context;
/// Reads the next code (GIF.nb_bits bits)
static word GIF_get_next_code(FILE * GIF_file, T_GIF_context * gif)
{
word nb_bits_to_process = gif->nb_bits;
word nb_bits_processed = 0;
word current_nb_bits;
gif->current_code = 0;
while (nb_bits_to_process)
{
if (gif->remainder_bits == 0) // Il ne reste plus de bits...
{
// Lire l'octet suivant:
// Si on a atteint la fin du bloc de Raster Data
if (gif->remainder_byte == 0)
{
// Lire l'octet nous donnant la taille du bloc de Raster Data suivant
if(Read_byte(GIF_file, &gif->remainder_byte)!=1)
{
File_error=2;
return 0;
}
if (gif->remainder_byte == 0) // still nothing ? That is the end data block
{
File_error = 2;
GFX2_Log(GFX2_WARNING, "GIF 0 sized data block\n");
return gif->current_code;
}
}
if(Read_byte(GIF_file,&gif->last_byte)!=1)
{
File_error = 2;
GFX2_Log(GFX2_ERROR, "GIF failed to load data byte\n");
return 0;
}
gif->remainder_byte--;
gif->remainder_bits=8;
}
current_nb_bits=(nb_bits_to_process<=gif->remainder_bits)?nb_bits_to_process:gif->remainder_bits;
gif->current_code |= (gif->last_byte & ((1<<current_nb_bits)-1))<<nb_bits_processed;
gif->last_byte >>= current_nb_bits;
nb_bits_processed += current_nb_bits;
nb_bits_to_process -= current_nb_bits;
gif->remainder_bits -= current_nb_bits;
}
return gif->current_code;
}
/// Put a new pixel
static void GIF_new_pixel(T_IO_Context * context, T_GIF_context * gif, T_GIF_IDB *idb, int is_transparent, byte color)
{
if (!is_transparent || color!=context->Transparent_color)
Set_pixel(context, idb->Pos_X+gif->pos_X, idb->Pos_Y+gif->pos_Y,color);
gif->pos_X++;
if (gif->pos_X >= idb->Image_width)
{
gif->pos_X=0;
if (!gif->interlaced)
{
gif->pos_Y++;
if (gif->pos_Y >= idb->Image_height)
gif->stop = 1;
}
else
{
switch (gif->pass)
{
case 0 :
case 1 : gif->pos_Y+=8;
break;
case 2 : gif->pos_Y+=4;
break;
default: gif->pos_Y+=2;
}
if (gif->pos_Y >= idb->Image_height)
{
switch(++(gif->pass))
{
case 1 : gif->pos_Y=4;
break;
case 2 : gif->pos_Y=2;
break;
case 3 : gif->pos_Y=1;
break;
case 4 : gif->stop = 1;
}
}
}
}
}
/// Load GIF file
void Load_GIF(T_IO_Context * context)
{
FILE *GIF_file;
int image_mode = -1;
char signature[6];
word * alphabet_stack; // Pile de décodage d'une chaîne
word * alphabet_prefix; // Table des préfixes des codes
word * alphabet_suffix; // Table des suffixes des codes
word alphabet_free; // Position libre dans l'alphabet
word alphabet_max; // Nombre d'entrées possibles dans l'alphabet
word alphabet_stack_pos; // Position dans la pile de décodage d'un chaîne
T_GIF_context GIF;
T_GIF_LSDB LSDB;
T_GIF_IDB IDB;
T_GIF_GCE GCE;
word nb_colors; // Nombre de couleurs dans l'image
word color_index; // index de traitement d'une couleur
byte size_to_read; // Nombre de données à lire (divers)
byte block_identifier; // Code indicateur du type de bloc en cours
byte initial_nb_bits; // Nb de bits au début du traitement LZW
word special_case=0; // Mémoire pour le cas spécial
word old_code=0; // Code précédent
word byte_read; // Sauvegarde du code en cours de lecture
word value_clr; // Valeur <=> Clear tables
word value_eof; // Valeur <=> End d'image
long file_size;
int number_LID; // Nombre d'images trouvées dans le fichier
int current_layer = 0;
int last_delay = 0;
byte is_transparent = 0;
byte is_looping=0;
enum PIXEL_RATIO ratio;
byte disposal_method = DISPOSAL_METHOD_RESTORE_BGCOLOR;
byte previous_disposal_method = DISPOSAL_METHOD_RESTORE_BGCOLOR;
word previous_width=0;
word previous_height=0;
word previous_pos_x=0;
word previous_pos_y=0;
/////////////////////////////////////////////////// FIN DES DECLARATIONS //
number_LID=0;
if ((GIF_file=Open_file_read(context)))
{
file_size=File_length_file(GIF_file);
if ( (Read_bytes(GIF_file,signature,6)) &&
( (memcmp(signature,"GIF87a",6)==0) ||
(memcmp(signature,"GIF89a",6)==0) ) )
{
// Allocation de mémoire pour les tables & piles de traitement:
alphabet_stack = (word *)GFX2_malloc(4096*sizeof(word));
alphabet_prefix = (word *)GFX2_malloc(4096*sizeof(word));
alphabet_suffix = (word *)GFX2_malloc(4096*sizeof(word));
if (Read_word_le(GIF_file,&(LSDB.Width))
&& Read_word_le(GIF_file,&(LSDB.Height))
&& Read_byte(GIF_file,&(LSDB.Resol))
&& Read_byte(GIF_file,&(LSDB.Backcol))
&& Read_byte(GIF_file,&(LSDB.Aspect))
)
{
// Lecture du Logical Screen Descriptor Block réussie:
Original_screen_X=LSDB.Width;
Original_screen_Y=LSDB.Height;
ratio=PIXEL_SIMPLE; // (49 + 15) / 64 = 1:1
if (LSDB.Aspect != 0) {
if (LSDB.Aspect < 25) // (17 + 15) / 64 = 1:2
ratio=PIXEL_TALL;
else if (LSDB.Aspect < 41) // (33 + 15) / 64 = 3:4
ratio=PIXEL_TALL3;
else if (LSDB.Aspect > 82) // (113 + 15) / 64 = 2:1
ratio=PIXEL_WIDE;
}
Pre_load(context, LSDB.Width,LSDB.Height,file_size,FORMAT_GIF,ratio,(LSDB.Resol&7)+1);
// Palette globale dispo = (LSDB.Resol and $80)
// Profondeur de couleur =((LSDB.Resol and $70) shr 4)+1
// Nombre de bits/pixel = (LSDB.Resol and $07)+1
// Ordre de Classement = (LSDB.Aspect and $80)
nb_colors=(1 << ((LSDB.Resol & 0x07)+1));
if (LSDB.Resol & 0x80)
{
// Palette globale dispo:
if (Config.Clear_palette)
memset(context->Palette,0,sizeof(T_Palette));
// Load the palette
for(color_index=0;color_index<nb_colors;color_index++)
{
Read_byte(GIF_file,&(context->Palette[color_index].R));
Read_byte(GIF_file,&(context->Palette[color_index].G));
Read_byte(GIF_file,&(context->Palette[color_index].B));
}
}
// On lit un indicateur de block
Read_byte(GIF_file,&block_identifier);
while (block_identifier!=0x3B && !File_error)
{
switch (block_identifier)
{
case 0x21: // Bloc d'extension
{
byte function_code;
// Lecture du code de fonction:
Read_byte(GIF_file,&function_code);
// Lecture de la taille du bloc:
Read_byte(GIF_file,&size_to_read);
while (size_to_read!=0 && !File_error)
{
switch(function_code)
{
case 0xFE: // Comment Block Extension
// On récupère le premier commentaire non-vide,
// on jette les autres.
if (context->Comment[0]=='\0')
{
int nb_char_to_keep = MIN(size_to_read, COMMENT_SIZE);
Read_bytes(GIF_file,context->Comment,nb_char_to_keep);
context->Comment[nb_char_to_keep+1]='\0';
// Si le commentaire etait trop long, on fait avance-rapide
// sur la suite.
if (size_to_read>nb_char_to_keep)
fseek(GIF_file,size_to_read-nb_char_to_keep,SEEK_CUR);
}
// Lecture de la taille du bloc suivant:
Read_byte(GIF_file,&size_to_read);
break;
case 0xF9: // Graphics Control Extension
// Prévu pour la transparence
if ( Read_byte(GIF_file,&(GCE.Packed_fields))
&& Read_word_le(GIF_file,&(GCE.Delay_time))
&& Read_byte(GIF_file,&(GCE.Transparent_color)))
{
previous_disposal_method = disposal_method;
disposal_method = (GCE.Packed_fields >> 2) & 7;
last_delay = GCE.Delay_time;
context->Transparent_color= GCE.Transparent_color;
is_transparent = GCE.Packed_fields & 1;
GFX2_Log(GFX2_DEBUG, "GIF Graphics Control Extension : transp=%d (color #%u) delay=%ums disposal_method=%d\n", is_transparent, GCE.Transparent_color, 10*GCE.Delay_time, disposal_method);
if (number_LID == 0)
context->Background_transparent = is_transparent;
is_transparent &= is_looping;
}
else
File_error=2;
// Lecture de la taille du bloc suivant:
Read_byte(GIF_file,&size_to_read);
break;
case 0xFF: // Application Extension
// Normally, always a 11-byte block
if (size_to_read == 0x0B)
{
char aeb[0x0B];
Read_bytes(GIF_file,aeb, 0x0B);
GFX2_Log(GFX2_DEBUG, "GIF extension \"%.11s\"\n", aeb);
if (File_error)
;
else if (!memcmp(aeb,"NETSCAPE2.0",0x0B))
{
is_looping=1;
// The well-known Netscape extension.
// Load as an animation
Set_image_mode(context, IMAGE_MODE_ANIMATION);
// Skip sub-block
do
{
if (! Read_byte(GIF_file,&size_to_read))
File_error=1;
fseek(GIF_file,size_to_read,SEEK_CUR);
} while (!File_error && size_to_read!=0);
}
else if (!memcmp(aeb,"GFX2PATH\x00\x00\x00",0x0B))
{
// Original file path
Read_byte(GIF_file,&size_to_read);
if (!File_error && size_to_read > 0)
{
free(context->Original_file_directory);
context->Original_file_directory = GFX2_malloc(size_to_read);
Read_bytes(GIF_file, context->Original_file_directory, size_to_read);
Read_byte(GIF_file, &size_to_read);
if (!File_error && size_to_read > 0)
{
free(context->Original_file_name);
context->Original_file_name = GFX2_malloc(size_to_read);
Read_bytes(GIF_file, context->Original_file_name, size_to_read);
Read_byte(GIF_file, &size_to_read); // Normally 0
}
}
}
else if (!memcmp(aeb,"CRNG\0\0\0\0" "1.0",0x0B))
{
// Color animation. Similar to a CRNG chunk in IFF file format.
word rate;
word flags;
byte col1;
byte col2;
//
Read_byte(GIF_file,&size_to_read);
for(;size_to_read>0 && !File_error;size_to_read-=6)
{
if ( (Read_word_be(GIF_file, &rate))
&& (Read_word_be(GIF_file, &flags))
&& (Read_byte(GIF_file, &col1))
&& (Read_byte(GIF_file, &col2)))
{
if (col1 != col2)
{
// Valid cycling range
context->Cycle_range[context->Color_cycles].Start = MIN(col1, col2);
context->Cycle_range[context->Color_cycles].End = MAX(col1, col2);
context->Cycle_range[context->Color_cycles].Inverse = (flags&2)?1:0;
context->Cycle_range[context->Color_cycles].Speed = (flags&1)?rate/78:0;
context->Color_cycles++;
}
}
else
{
File_error=1;
}
}
// Read end-of-block delimiter
if (!File_error)
Read_byte(GIF_file,&size_to_read);
if (size_to_read!=0)
File_error=1;
}
else if (0 == memcmp(aeb, "GFX2MODE", 8))
{
Read_byte(GIF_file,&size_to_read);
if (size_to_read > 0)
{ // read the image mode. We'll set it after having loaded all layers.
char * label = GFX2_malloc((size_t)size_to_read + 1);
Read_bytes(GIF_file, label, size_to_read);
label[size_to_read] = '\0';
image_mode = Constraint_mode_from_label(label);
GFX2_Log(GFX2_DEBUG, " mode = %s (%d)\n", label, image_mode);
free(label);
Read_byte(GIF_file,&size_to_read);
// be future proof, skip following sub-blocks :
while (size_to_read!=0 && !File_error)
{
if (fseek(GIF_file,size_to_read,SEEK_CUR) < 0)
File_error = 1;
if (!Read_byte(GIF_file,&size_to_read))
File_error = 1;
}
}
}
else
{
// Unknown extension, skip.
Read_byte(GIF_file,&size_to_read);
while (size_to_read!=0 && !File_error)
{
if (fseek(GIF_file,size_to_read,SEEK_CUR) < 0)
File_error = 1;
if (!Read_byte(GIF_file,&size_to_read))
File_error = 1;
}
}
}
else
{
fseek(GIF_file,size_to_read,SEEK_CUR);
// Lecture de la taille du bloc suivant:
Read_byte(GIF_file,&size_to_read);
}
break;
default:
// On saute le bloc:
fseek(GIF_file,size_to_read,SEEK_CUR);
// Lecture de la taille du bloc suivant:
Read_byte(GIF_file,&size_to_read);
break;
}
}
}
break;
case 0x2C: // Local Image Descriptor
{
if (number_LID!=0)
{
// This a second layer/frame, or more.
// Attempt to add a layer to current image
current_layer++;
Set_loading_layer(context, current_layer);
if (context->Type == CONTEXT_MAIN_IMAGE && Main.backups->Pages->Image_mode == IMAGE_MODE_ANIMATION)
{
// Copy the content of previous layer.
memcpy(
Main.backups->Pages->Image[Main.current_layer].Pixels,
Main.backups->Pages->Image[Main.current_layer-1].Pixels,
Main.backups->Pages->Width*Main.backups->Pages->Height);
}
else
{
Fill_canvas(context, is_transparent ? context->Transparent_color : LSDB.Backcol);
}
}
else
{
// First frame/layer, fill canvas with backcolor
Fill_canvas(context, is_transparent ? context->Transparent_color : LSDB.Backcol);
}
// Duration was set in the previously loaded GCE
Set_frame_duration(context, last_delay*10);
number_LID++;
// lecture de 10 derniers octets
if ( Read_word_le(GIF_file,&(IDB.Pos_X))
&& Read_word_le(GIF_file,&(IDB.Pos_Y))
&& Read_word_le(GIF_file,&(IDB.Image_width))
&& Read_word_le(GIF_file,&(IDB.Image_height))
&& Read_byte(GIF_file,&(IDB.Indicator))
&& IDB.Image_width && IDB.Image_height)
{
GFX2_Log(GFX2_DEBUG, "GIF Image descriptor %u Pos (%u,%u) %ux%u %s%slocal palette(%ubpp)\n",
number_LID, IDB.Pos_X, IDB.Pos_Y, IDB.Image_width, IDB.Image_height,
(IDB.Indicator & 0x40) ? "interlaced " : "", (IDB.Indicator & 0x80) ? "" : "no ",
(IDB.Indicator & 7) + 1);
// Palette locale dispo = (IDB.Indicator and $80)
// Image entrelacée = (IDB.Indicator and $40)
// Ordre de classement = (IDB.Indicator and $20)
// Nombre de bits/pixel = (IDB.Indicator and $07)+1 (si palette locale dispo)
if (IDB.Indicator & 0x80)
{
// Palette locale dispo
if (Config.Clear_palette)
memset(context->Palette,0,sizeof(T_Palette));
nb_colors=(1 << ((IDB.Indicator & 0x07)+1));
// Load the palette
for(color_index=0;color_index<nb_colors;color_index++)
{
Read_byte(GIF_file,&(context->Palette[color_index].R));
Read_byte(GIF_file,&(context->Palette[color_index].G));
Read_byte(GIF_file,&(context->Palette[color_index].B));
}
}
if (number_LID!=1)
{
// This a second layer/frame, or more.
if (context->Type == CONTEXT_MAIN_IMAGE && Main.backups->Pages->Image_mode == IMAGE_MODE_ANIMATION)
{
// Need to clear previous image to back-color.
if (previous_disposal_method==DISPOSAL_METHOD_RESTORE_BGCOLOR)
{
int y;
for (y=0; y<previous_height; y++)
memset(
Main.backups->Pages->Image[Main.current_layer].Pixels
+ (previous_pos_y+y)* Main.backups->Pages->Width+previous_pos_x,
is_transparent ? context->Transparent_color : LSDB.Backcol,
previous_width);
}
}
}
previous_height=IDB.Image_height;
previous_width=IDB.Image_width;
previous_pos_x=IDB.Pos_X;
previous_pos_y=IDB.Pos_Y;
File_error=0;
if (!Read_byte(GIF_file,&(initial_nb_bits)))
File_error=1;
value_clr =(1<<initial_nb_bits)+0;
value_eof =(1<<initial_nb_bits)+1;
alphabet_free=(1<<initial_nb_bits)+2;
GIF.nb_bits =initial_nb_bits + 1;
alphabet_max =((1 << GIF.nb_bits)-1);
GIF.interlaced =(IDB.Indicator & 0x40);
GIF.pass =0;
/*Init_lecture();*/
GIF.stop = 0;
//////////////////////////////////////////// DECOMPRESSION LZW //
GIF.pos_X=0;
GIF.pos_Y=0;
alphabet_stack_pos=0;
GIF.last_byte =0;
GIF.remainder_bits =0;
GIF.remainder_byte =0;
while ( (GIF_get_next_code(GIF_file, &GIF)!=value_eof) && (!File_error) )
{
if (GIF.current_code > alphabet_free)
{
GFX2_Log(GFX2_INFO, "Load_GIF() Invalid code %u (should be <=%u)\n", GIF.current_code, alphabet_free);
File_error=2;
break;
}
else if (GIF.current_code != value_clr)
{
byte_read = GIF.current_code;
if (alphabet_free == GIF.current_code)
{
GIF.current_code=old_code;
alphabet_stack[alphabet_stack_pos++]=special_case;
}
while (GIF.current_code > value_clr)
{
if (GIF.current_code >= 4096)
{
GFX2_Log(GFX2_ERROR, "Load_GIF() GIF.current_code = %u >= 4096\n", GIF.current_code);
File_error = 2;
break;
}
alphabet_stack[alphabet_stack_pos++] = alphabet_suffix[GIF.current_code];
GIF.current_code = alphabet_prefix[GIF.current_code];
}
special_case = alphabet_stack[alphabet_stack_pos++] = GIF.current_code;
do
GIF_new_pixel(context, &GIF, &IDB, is_transparent, alphabet_stack[--alphabet_stack_pos]);
while (alphabet_stack_pos!=0);
alphabet_prefix[alphabet_free ]=old_code;
alphabet_suffix[alphabet_free++]=GIF.current_code;
old_code=byte_read;
if (alphabet_free>alphabet_max)
{
if (GIF.nb_bits<12)
alphabet_max =((1 << (++GIF.nb_bits))-1);
}
}
else // Clear code
{
GIF.nb_bits = initial_nb_bits + 1;
alphabet_max = ((1 << GIF.nb_bits)-1);
alphabet_free = (1<<initial_nb_bits)+2;
special_case = GIF_get_next_code(GIF_file, &GIF);
if (GIF.current_code >= value_clr)
{
GFX2_Log(GFX2_INFO, "Load_GIF() Invalid code %u just after clear (=%u)!\n",
GIF.current_code, value_clr);
File_error = 2;
break;
}
old_code = GIF.current_code;
GIF_new_pixel(context, &GIF, &IDB, is_transparent, GIF.current_code);
}
}
if (File_error == 2 && GIF.pos_X == 0 && GIF.pos_Y == IDB.Image_height)
File_error=0;
if (File_error >= 0 && !GIF.stop)
File_error=2;
// No need to read more than one frame in animation preview mode
if (context->Type == CONTEXT_PREVIEW && is_looping)
{
goto early_exit;
}
// Same with brush
if (context->Type == CONTEXT_BRUSH && is_looping)
{
goto early_exit;
}
} // Le fichier contenait un IDB
else
File_error=2;
}
default:
break;
}
// Lecture du code de fonction suivant:
if (!Read_byte(GIF_file,&block_identifier))
File_error=2;
}
// set the mode that have been read previously.
if (image_mode > 0)
Set_image_mode(context, image_mode);
} // Le fichier contenait un LSDB
else
File_error=1;
early_exit:
// Libération de la mémoire utilisée par les tables & piles de traitement:
free(alphabet_suffix);
free(alphabet_prefix);
free(alphabet_stack);
alphabet_suffix = alphabet_prefix = alphabet_stack = NULL;
} // Le fichier contenait au moins la signature GIF87a ou GIF89a
else
File_error=1;
fclose(GIF_file);
} // Le fichier était ouvrable
else
File_error=1;
}
// -- Sauver un fichier au format GIF ---------------------------------------
/// Flush the buffer
static void GIF_empty_buffer(FILE * file, T_GIF_context *gif, byte * GIF_buffer)
{
if (gif->remainder_byte)
{
GIF_buffer[0] = gif->remainder_byte;
if (!Write_bytes(file, GIF_buffer, (size_t)gif->remainder_byte + 1))
File_error = 1;
gif->remainder_byte = 0;
}
}
/// Write a code (GIF_nb_bits bits)
static void GIF_set_code(FILE * GIF_file, T_GIF_context * gif, byte * GIF_buffer, word Code)
{
word nb_bits_to_process = gif->nb_bits;
word nb_bits_processed =0;
word current_nb_bits;
while (nb_bits_to_process)
{
current_nb_bits = (nb_bits_to_process <= (8-gif->remainder_bits)) ?
nb_bits_to_process: (8-gif->remainder_bits);
gif->last_byte |= (Code & ((1<<current_nb_bits)-1))<<gif->remainder_bits;
Code>>=current_nb_bits;
gif->remainder_bits +=current_nb_bits;
nb_bits_processed +=current_nb_bits;
nb_bits_to_process-=current_nb_bits;
if (gif->remainder_bits==8) // Il ne reste plus de bits à coder sur l'octet courant
{
// Ecrire l'octet à balancer:
GIF_buffer[++(gif->remainder_byte)] = gif->last_byte;
// Si on a atteint la fin du bloc de Raster Data
if (gif->remainder_byte==255)
// On doit vider le buffer qui est maintenant plein
GIF_empty_buffer(GIF_file, gif, GIF_buffer);
gif->last_byte=0;
gif->remainder_bits=0;
}
}
}
/// Read the next pixel
static byte GIF_next_pixel(T_IO_Context *context, T_GIF_context *gif, T_GIF_IDB *idb)
{
byte temp;
temp = Get_pixel(context, gif->pos_X, gif->pos_Y);
if (++gif->pos_X >= (idb->Image_width + idb->Pos_X))
{
gif->pos_X = idb->Pos_X;
if (++gif->pos_Y >= (idb->Image_height + idb->Pos_Y))
gif->stop = 1;
}
return temp;
}
struct gif_alphabet {
word prefix[4096]; // code prefix array
word suffix[4096]; // code suffix array
word daughter[4096]; // daughter strings array (greater length)
word sister[4096]; // sister strings array (same length)
word free; // first free slot in the alphabet
word max; // maximum number of entry in the alphabet
};
/// Save a GIF file
void Save_GIF(T_IO_Context * context)
{
FILE * GIF_file;
byte GIF_buffer[256]; // buffer d'écriture de bloc de données compilées
struct gif_alphabet * alphabet;
word start; // Code précédent (sert au linkage des chaînes)
int descend; // Booléen "On vient de descendre"
T_GIF_context GIF;
T_GIF_LSDB LSDB;
T_GIF_IDB IDB;
byte block_identifier; // Code indicateur du type de bloc en cours
word current_string; // Code de la chaîne en cours de traitement
byte current_char; // Caractère à coder
word index; // index de recherche de chaîne
int current_layer;
word clear; // LZW clear code
word eof; // End of image code
/////////////////////////////////////////////////// FIN DES DECLARATIONS //
File_error=0;
if ((GIF_file=Open_file_write(context)))
{
// On écrit la signature du fichier
if (Write_bytes(GIF_file,"GIF89a",6))
{
// La signature du fichier a été correctement écrite.
// Allocation de mémoire pour les tables
alphabet = (struct gif_alphabet *)GFX2_malloc(sizeof(struct gif_alphabet));
if (alphabet == NULL)
{
File_error = 1;
fclose(GIF_file);
return;
}
// On initialise le LSDB du fichier
if (Config.Screen_size_in_GIF && Screen_width >= context->Width && Screen_height >= context->Height)
{
// Canvas bigger than the image
LSDB.Width=Screen_width;
LSDB.Height=Screen_height;
}
else
{
LSDB.Width=context->Width;
LSDB.Height=context->Height;
}
LSDB.Resol = 0xF7; // Global palette of 256 entries, 256 color image
// 0xF7 = 1111 0111
// <Packed Fields> = Global Color Table Flag 1 Bit
// Color Resolution 3 Bits
// Sort Flag 1 Bit
// Size of Global Color Table 3 Bits
LSDB.Backcol=context->Transparent_color;
switch(context->Ratio)
{
case PIXEL_TALL:
case PIXEL_TALL2:
LSDB.Aspect = 17; // 1:2 = 2:4
break;
case PIXEL_TALL3:
LSDB.Aspect = 33; // 3:4
break;
case PIXEL_WIDE:
case PIXEL_WIDE2:
LSDB.Aspect = 113; // 2:1 = 4:2
break;
default:
LSDB.Aspect = 0; // undefined, which is most frequent.
// 49 would be 1:1 ratio
}
// On sauve le LSDB dans le fichier
if (Write_word_le(GIF_file,LSDB.Width) &&
Write_word_le(GIF_file,LSDB.Height) &&
Write_byte(GIF_file,LSDB.Resol) &&
Write_byte(GIF_file,LSDB.Backcol) &&
Write_byte(GIF_file,LSDB.Aspect) )
{
// Le LSDB a été correctement écrit.
int i;
// On sauve la palette
for(i=0;i<256 && !File_error;i++)
{
if (!Write_byte(GIF_file,context->Palette[i].R)
||!Write_byte(GIF_file,context->Palette[i].G)
||!Write_byte(GIF_file,context->Palette[i].B))
File_error=1;
}
if (!File_error)
{
// La palette a été correctement écrite.
/// - "Netscape" animation extension :
/// <pre>
/// 0x21 Extension Label
/// 0xFF Application Extension Label
/// 0x0B Block Size
/// "NETSCAPE" Application Identifier (8 bytes)
/// "2.0" Application Authentication Code (3 bytes)
/// 0x03 Sub-block Data Size
/// 0xLL 01 to loop
/// 0xSSSS (little endian) number of loops, 0 means infinite loop
/// 0x00 Block terminator </pre>
/// see http://www.vurdalakov.net/misc/gif/netscape-looping-application-extension
if (context->Type == CONTEXT_MAIN_IMAGE && Main.backups->Pages->Image_mode == IMAGE_MODE_ANIMATION)
{
if (context->Nb_layers>1)
Write_bytes(GIF_file,"\x21\xFF\x0BNETSCAPE2.0\x03\x01\x00\x00\x00",19);
}
else if (context->Type == CONTEXT_MAIN_IMAGE && Main.backups->Pages->Image_mode > IMAGE_MODE_ANIMATION)
{
/// - GrafX2 extension to store ::IMAGE_MODES :
/// <pre>
/// 0x21 Extension Label
/// 0xFF Application Extension Label
/// 0x0B Block Size
/// "GFX2MODE" Application Identifier (8 bytes)
/// "2.6" Application Authentication Code (3 bytes)
/// 0xll Sub-block Data Size
/// string label
/// 0x00 Block terminator </pre>
/// @see Constraint_mode_label()
const char * label = Constraint_mode_label(Main.backups->Pages->Image_mode);
if (label != NULL)
{
size_t len = strlen(label);
// Write extension for storing IMAGE_MODE
Write_byte(GIF_file,0x21); // Extension Introducer
Write_byte(GIF_file,0xff); // Extension Label
Write_byte(GIF_file, 11); // Block size
Write_bytes(GIF_file, "GFX2MODE2.6", 11); // Application Identifier + Appl. Authentication Code
Write_byte(GIF_file, (byte)len); // Block size
Write_bytes(GIF_file, label, len); // Data
Write_byte(GIF_file, 0); // Block terminator
}
}
// Ecriture du commentaire
if (context->Comment[0])
{
Write_bytes(GIF_file,"\x21\xFE",2);
Write_byte(GIF_file, (byte)strlen(context->Comment));
Write_bytes(GIF_file,context->Comment,strlen(context->Comment)+1);
}
/// - "CRNG" Color cycing extension :
/// <pre>
/// 0x21 Extension Label
/// 0xFF Application Extension Label
/// 0x0B Block Size
/// "CRNG\0\0\0\0" "CRNG" Application Identifier (8 bytes)
/// "1.0" Application Authentication Code (3 bytes)
/// 0xll Sub-block Data Size (6 bytes per color cycle)
/// For each color cycle :
/// 0xRRRR (big endian) Rate
/// 0xFFFF (big endian) Flags
/// 0xSS start (lower color index)
/// 0xEE end (higher color index)
/// 0x00 Block terminator </pre>
if (context->Color_cycles)
{
int i;
Write_bytes(GIF_file,"\x21\xff\x0B" "CRNG\0\0\0\0" "1.0",14);
Write_byte(GIF_file,context->Color_cycles*6);
for (i=0; i<context->Color_cycles; i++)
{
word flags=0;
flags|= context->Cycle_range[i].Speed?1:0; // Cycling or not
flags|= context->Cycle_range[i].Inverse?2:0; // Inverted
Write_word_be(GIF_file,context->Cycle_range[i].Speed*78); // Rate
Write_word_be(GIF_file,flags); // Flags
Write_byte(GIF_file,context->Cycle_range[i].Start); // Min color
Write_byte(GIF_file,context->Cycle_range[i].End); // Max color
}
Write_byte(GIF_file,0);
}
// Loop on all layers
for (current_layer=0;
current_layer < context->Nb_layers && !File_error;
current_layer++)
{
// Write a Graphic Control Extension
T_GIF_GCE GCE;
byte disposal_method;
Set_saving_layer(context, current_layer);
GCE.Block_identifier = 0x21;
GCE.Function = 0xF9;
GCE.Block_size=4;
if (context->Type == CONTEXT_MAIN_IMAGE && Main.backups->Pages->Image_mode == IMAGE_MODE_ANIMATION)
{
// Animation frame
int duration;
if(context->Background_transparent)
disposal_method = DISPOSAL_METHOD_RESTORE_BGCOLOR;
else
disposal_method = DISPOSAL_METHOD_DO_NOT_DISPOSE;
GCE.Packed_fields=(disposal_method<<2)|(context->Background_transparent);
duration=Get_frame_duration(context)/10;
GCE.Delay_time=duration<0xFFFF?duration:0xFFFF;
}
else
{
// Layered image or brush
disposal_method = DISPOSAL_METHOD_DO_NOT_DISPOSE;
if (current_layer==0)
GCE.Packed_fields=(disposal_method<<2)|(context->Background_transparent);
else
GCE.Packed_fields=(disposal_method<<2)|(1);
GCE.Delay_time=5; // Duration 5/100s (minimum viable value for current web browsers)
if (current_layer == context->Nb_layers -1)
GCE.Delay_time=0xFFFF; // Infinity (10 minutes)
}
GCE.Transparent_color=context->Transparent_color;
GCE.Block_terminator=0x00;
if (Write_byte(GIF_file,GCE.Block_identifier)
&& Write_byte(GIF_file,GCE.Function)
&& Write_byte(GIF_file,GCE.Block_size)
&& Write_byte(GIF_file,GCE.Packed_fields)
&& Write_word_le(GIF_file,GCE.Delay_time)
&& Write_byte(GIF_file,GCE.Transparent_color)
&& Write_byte(GIF_file,GCE.Block_terminator)
)
{
byte temp, max = 0;
IDB.Pos_X=0;
IDB.Pos_Y=0;
IDB.Image_width=context->Width;
IDB.Image_height=context->Height;
if(current_layer > 0)
{
word min_X, max_X, min_Y, max_Y;
// find bounding box of changes for Animated GIFs
min_X = min_Y = 0xffff;
max_X = max_Y = 0;
for(GIF.pos_Y = 0; GIF.pos_Y < context->Height; GIF.pos_Y++) {
for(GIF.pos_X = 0; GIF.pos_X < context->Width; GIF.pos_X++) {
if (GIF.pos_X >= min_X && GIF.pos_X <= max_X && GIF.pos_Y >= min_Y && GIF.pos_Y <= max_Y)
continue; // already in the box
if(disposal_method == DISPOSAL_METHOD_DO_NOT_DISPOSE)
{
// if that pixel has same value in previous layer, no need to save it
Set_saving_layer(context, current_layer - 1);
temp = Get_pixel(context, GIF.pos_X, GIF.pos_Y);
Set_saving_layer(context, current_layer);
if(temp == Get_pixel(context, GIF.pos_X, GIF.pos_Y))
continue;
}
if (disposal_method == DISPOSAL_METHOD_RESTORE_BGCOLOR
|| context->Background_transparent
|| Main.backups->Pages->Image_mode != IMAGE_MODE_ANIMATION)
{
// if that pixel is Backcol, no need to save it
if (LSDB.Backcol == Get_pixel(context, GIF.pos_X, GIF.pos_Y))
continue;
}
if(GIF.pos_X < min_X) min_X = GIF.pos_X;
if(GIF.pos_X > max_X) max_X = GIF.pos_X;
if(GIF.pos_Y < min_Y) min_Y = GIF.pos_Y;
if(GIF.pos_Y > max_Y) max_Y = GIF.pos_Y;
}
}
if((min_X <= max_X) && (min_Y <= max_Y))
{
IDB.Pos_X = min_X;
IDB.Pos_Y = min_Y;
IDB.Image_width = max_X + 1 - min_X;
IDB.Image_height = max_Y + 1 - min_Y;
}
else
{
// if no pixel changes, store a 1 pixel image
IDB.Image_width = 1;
IDB.Image_height = 1;
}
}
// look for the maximum pixel value
// to decide how many bit per pixel are needed.
for(GIF.pos_Y = IDB.Pos_Y; GIF.pos_Y < IDB.Image_height + IDB.Pos_Y; GIF.pos_Y++) {
for(GIF.pos_X = IDB.Pos_X; GIF.pos_X < IDB.Image_width + IDB.Pos_X; GIF.pos_X++) {
temp=Get_pixel(context, GIF.pos_X, GIF.pos_Y);
if(temp > max) max = temp;
}
}
IDB.Nb_bits_pixel=2; // Find the minimum bpp value to fit all pixels
while((int)max >= (1 << IDB.Nb_bits_pixel)) {
IDB.Nb_bits_pixel++;
}
GFX2_Log(GFX2_DEBUG, "GIF image #%d %ubits (%u,%u) %ux%u\n",
current_layer, IDB.Nb_bits_pixel, IDB.Pos_X, IDB.Pos_Y,
IDB.Image_width, IDB.Image_height);
// On va écrire un block indicateur d'IDB et l'IDB du fichier
block_identifier=0x2C;
IDB.Indicator=0x07; // Image non entrelacée, pas de palette locale.
clear = 1 << IDB.Nb_bits_pixel; // Clear Code
eof = clear + 1; // End of Picture Code
if ( Write_byte(GIF_file,block_identifier) &&
Write_word_le(GIF_file,IDB.Pos_X) &&
Write_word_le(GIF_file,IDB.Pos_Y) &&
Write_word_le(GIF_file,IDB.Image_width) &&
Write_word_le(GIF_file,IDB.Image_height) &&
Write_byte(GIF_file,IDB.Indicator) &&
Write_byte(GIF_file,IDB.Nb_bits_pixel))
{
// Le block indicateur d'IDB et l'IDB ont étés correctements
// écrits.
GIF.pos_X=IDB.Pos_X;
GIF.pos_Y=IDB.Pos_Y;
GIF.last_byte=0;
GIF.remainder_bits=0;
GIF.remainder_byte=0;
#define GIF_INVALID_CODE (65535)
index=GIF_INVALID_CODE;
File_error=0;
GIF.stop=0;
// Réintialisation de la table:
alphabet->free = clear + 2; // 258 for 8bpp
GIF.nb_bits = IDB.Nb_bits_pixel + 1; // 9 for 8 bpp
alphabet->max = clear+clear-1; // 511 for 8bpp
GIF_set_code(GIF_file, &GIF, GIF_buffer, clear); //256 for 8bpp
for (start=0; start<4096; start++)
{
alphabet->daughter[start] = GIF_INVALID_CODE;
alphabet->sister[start] = GIF_INVALID_CODE;
}
////////////////////////////////////////////// COMPRESSION LZW //
start=current_string=GIF_next_pixel(context, &GIF, &IDB);
descend=1;
while ((!GIF.stop) && (!File_error))
{
current_char=GIF_next_pixel(context, &GIF, &IDB);
// look for (current_string,current_char) in the alphabet
while ( (index != GIF_INVALID_CODE) &&
( (current_string != alphabet->prefix[index]) ||
(current_char != alphabet->suffix[index]) ) )
{
descend = 0;
start = index;
index = alphabet->sister[index];
}
if (index != GIF_INVALID_CODE)
{
// (current_string,current_char) == (alphabet_prefix,alphabet_suffix)[index]
// We have found (current_string,current_char) in the alphabet
// at the index position. So go on and prepare for then next character
descend = 1;
start = current_string = index;
index = alphabet->daughter[index];
}
else
{
// (current_string,current_char) was not found in the alphabet
// so write current_string to the Gif stream
GIF_set_code(GIF_file, &GIF, GIF_buffer, current_string);
if(alphabet->free < 4096) {
// link current_string and the new one
if (descend)
alphabet->daughter[start] = alphabet->free;
else
alphabet->sister[start] = alphabet->free;
// add (current_string,current_char) to the alphabet
alphabet->prefix[alphabet->free] = current_string;
alphabet->suffix[alphabet->free] = current_char;
alphabet->free++;
}
if (alphabet->free >= 4096)
{
// clear alphabet
GIF_set_code(GIF_file, &GIF, GIF_buffer, clear); // 256 for 8bpp
alphabet->free=clear+2; // 258 for 8bpp
GIF.nb_bits = IDB.Nb_bits_pixel + 1; // 9 for 8bpp
alphabet->max = clear+clear-1; // 511 for 8bpp
for (start=0;start<4096;start++)
{
alphabet->daughter[start] = GIF_INVALID_CODE;
alphabet->sister[start] = GIF_INVALID_CODE;
}
}
else if (alphabet->free > (alphabet->max + 1))
{
// On augmente le nb de bits
GIF.nb_bits++;
alphabet->max = (1<<GIF.nb_bits)-1;
}
// initialize current_string as the string "current_char"
index = alphabet->daughter[current_char];
start = current_string = current_char;
descend = 1;
}
}
if (!File_error)
{
// Write the last code (before EOF)
GIF_set_code(GIF_file, &GIF, GIF_buffer, current_string);
// we need to update alphabet->free / GIF.nb_bits here because
// the decoder will update them after each code,
// so in very rare cases there might be a problem if we
// don't do it.
// see http://pulkomandy.tk/projects/GrafX2/ticket/125
if(alphabet->free < 4096)
{
alphabet->free++;
if ((alphabet->free > alphabet->max+1) && (GIF.nb_bits < 12))
{
GIF.nb_bits++;
alphabet->max = (1 << GIF.nb_bits) - 1;
}
}
GIF_set_code(GIF_file, &GIF, GIF_buffer, eof); // 257 for 8bpp // Code de End d'image
if (GIF.remainder_bits!=0)
{
// Write last byte (this is an incomplete byte)
GIF_buffer[++GIF.remainder_byte]=GIF.last_byte;
GIF.last_byte=0;
GIF.remainder_bits=0;
}
GIF_empty_buffer(GIF_file, &GIF, GIF_buffer); // On envoie les dernières données du buffer GIF dans le buffer KM
// On écrit un \0
if (! Write_byte(GIF_file,'\x00'))
File_error=1;
}
} // On a pu écrire l'IDB
else
File_error=1;
}
else
File_error=1;
}
// After writing all layers
if (!File_error)
{
/// - If requested, write a specific extension for storing
/// original file path.
/// This is used by the backup system.
/// The format is :
/// <pre>
/// 0x21 Extension Label
/// 0xFF Application Extension Label
/// 0x0B Block Size
/// "GFX2PATH" "GFX2PATH" Application Identifier (8 bytes)
/// "\0\0\0" Application Authentication Code (3 bytes)
/// 0xll Sub-block Data Size : path size (including null)
/// "..path.." path (null-terminated)
/// 0xll Sub-block Data Size : filename size (including null)
/// "..file.." file name (null-terminated)
/// 0x00 Block terminator </pre>
if (context->Original_file_name != NULL
&& context->Original_file_directory != NULL)
{
long name_size = 1+strlen(context->Original_file_name);
long dir_size = 1+strlen(context->Original_file_directory);
if (name_size<256 && dir_size<256)
{
if (! Write_bytes(GIF_file,"\x21\xFF\x0BGFX2PATH\x00\x00\x00", 14)
|| ! Write_byte(GIF_file,dir_size)
|| ! Write_bytes(GIF_file, context->Original_file_directory, dir_size)
|| ! Write_byte(GIF_file,name_size)
|| ! Write_bytes(GIF_file, context->Original_file_name, name_size)
|| ! Write_byte(GIF_file,0))
File_error=1;
}
}
// On écrit un GIF TERMINATOR, exigé par SVGA et SEA.
if (! Write_byte(GIF_file,'\x3B'))
File_error=1;
}
} // On a pu écrire la palette
else
File_error=1;
} // On a pu écrire le LSDB
else
File_error=1;
// Libération de la mémoire utilisée par les tables
free(alphabet);
} // On a pu écrire la signature du fichier
else
File_error=1;
fclose(GIF_file);
if (File_error)
Remove_file(context);
} // On a pu ouvrir le fichier en écriture
else
File_error=1;
}
/** @} */