Code archives/File Utilities/Save animated GIFs

This code has been declared by its author to be Public Domain code.

Download source code

Save animated GIFs by Pineapple2012
Dependencies:
pine.heap: http://blitzbasic.com/codearcs/codearcs.php?code=2970 - required for automatic palette creation
pine.HashTable: http://blitzbasic.com/Community/posts.php?topic=97992#1141947 - required for lzw compression algorithm
log2.bmx: http://blitzbasic.com/codearcs/codearcs.php?code=2965 - required for writing various bits of the GIF file format
' 	--+-----------------------------------------------------------------------------------------+--
'	  |   This code was originally written by Sophie Kirschner (sophiek@pineapplemachine.com)   |  
' 	  | It is released as public domain. Please don't interpret that as liberty to claim credit |  
' 	  |   that isn't yours, or to sell this code when it could otherwise be obtained for free   |  
'	  |                because that would be a really shitty thing of you to do.                |
' 	--+-----------------------------------------------------------------------------------------+--


SuperStrict

Import brl.stream
Import brl.math
Import brl.pixmap
Import pine.HashTable ' http://blitzbasic.com/Community/posts.php?topic=97992#1141947
Import pine.heap ' http://blitzbasic.com/codearcs/codearcs.php?code=2970
Import "log2.bmx" ' http://blitzbasic.com/codearcs/codearcs.php?code=2965


' Example code
Rem
Local t:TPixmap[4]
For Local i%=0 To 3
	t[i]=CreatePixmap(128,128,pf_rgb888)
Next
ClearPixels t[0],$ff0000
ClearPixels t[1],$00ff00
ClearPixels t[2],$0070ff
ClearPixels t[3],$eeee00
SavePixmapsGIF t,"test.gif",100
EndRem


Rem
bbdoc: Save a Pixmap in GIF format
EndRem
Function SavePixmapGIF%(pix:TPixmap,url:Object,maxcolorcount%=256,tolerance%=-1,sub%=1,add%=2)
	If tolerance=-1 Then tolerance=768/maxcolorcount
	Local pal%[]=MakePalFromPixmap([pix],maxcolorcount,tolerance,sub,add,0)
	Local f:TStream=WriteStream(url)
	If Not f Then Return False
	WriteGif f,[pix],pal
	CloseStream f
	Return True
End Function

Rem
bbdoc: Save multiple pixmaps as an animation in GIF format
EndRem
Function SavePixmapsGIF%(pix:TPixmap[],url:Object,framedelay%,maxcolorcount%=256,tolerance%=-1,sub%=1,add%=2)
	If tolerance=-1 Then tolerance=768/maxcolorcount
	Local pal%[]=MakePalFromPixmap(pix,maxcolorcount,tolerance,sub,add,0)
	Local f:TStream=WriteStream(url)
	If Not f Then Return False
	WriteGif f,pix,pal,[framedelay]
	CloseStream f
	Return True
End Function

Rem
bbdoc: Automatically generate a palette from one or more pixmaps.
about: 
images:TPixmap[] is an array containing one or more pixmaps from which to operate
maxcolorcount% is the maximum number of colors allowed - set to 0 to return a basic palette of 8 colors.
tolerance% is a variable controlling how different two colors should be in order to be counted as separate
sub%, add% control how colors are prioritized and their precise effect can be very arbitrary. experimentation with different values for these and tolerance is highly recommended.
note: sub:add is treated as a ratio - sub=1,add=2 will produce the same result as sub=2,add=4.
transparency% specifies whether colors which are fully transparent should be ignored
EndRem
Global basicpal%[]=[$ffffff,$ff0000,$00ff00,$0000ff,$ffff00,$ff00ff,$00ffff,$000000,$808080]
Function MakePalFromPixmap%[](images:TPixmap[],maxcolorcount%=256,tolerance%=64,sub%=1,add%=2,transparency%=0)
	If maxcolorcount=0 Then Return basicpal
	Local colors:TList=CreateList(),rgb%
	For Local p:TPixmap=EachIn images
		If Not p Then Continue
		For Local x%=0 To PixmapWidth(p)-1
		For Local y%=0 To PixmapHeight(p)-1
			If (y Mod 4) Then Continue
			rgb=ReadPixel(p,x,y)
			If transparency And Not (rgb & $ff000000) Then Continue
			Local this:_cdata=New _cdata
			this.r=rgb Shr 16
			this.g=rgb Shr 8
			this.b=rgb
			Local found:TLink=Null,foundd%=0
			Local on:TLink=colors._head._succ,c:_cdata
			While on<>on._value
				c=_cdata(on._value)
				Local fd%=c.distance(this)
				If fd<=tolerance Then
					If (Not found) Or fd<=foundd
						found=on
						foundd=fd
						_cdata(found._value).pop:-sub
						If fd=0 Then Exit
					EndIf
				EndIf
				on=on._succ
			Wend
			If found
				_cdata(found._value).pop:+add
				movelinktofront found,colors
			Else
				colors.addfirst this
			EndIf
		Next
		Next
	Next
	Local cheap:THeap=CreateHeap()
	For Local c:_cdata=EachIn colors
		HeapInsert cheap,c
	Next
	Local ret%[]=New Int[Min(maxcolorcount,CountHeap(cheap))]
	For Local x%=0 To ret.length-1
		Local c:_cdata=_cdata(HeapRemove(cheap))
		ret[x]=$ff000000|(c.r Shl 16)|(c.g Shl 8)|c.b
	Next
	Return ret
End Function

Rem
bbdoc: Write an array of pixmaps to a stream as an animated GIF
about: 
f:TStream is the stream to which the GIF will be written
pixmaps:TPixmap[] is an array containing each frame as an individual pixmap
pal%[] is an array containing up to 256 colors and will be used as the palette
animdelay%[] is an array containing the duration (in 100ths of a second) of each frame. if this array is shorter than the pixmaps array then the frame delay to be used is the value in the index of the current frame modulo the number of frame delays defined.
looptimes% is how many times the animation should loop when viewed. $ffff indicates that it should loop forever.
transparentcolor% defines which index of the palette should be flagged as the transparent background color. -1 indicates that there is no transparent color.
animwidth% is the width of the animation. -1 indicates that it should be determined automatically as the maximum width of all the frames.
animheight% is the height of the animation. -1 indicates that it should be determined automatically as the maximum height of all the frames.
EndRem
Function WriteGIF(f:TStream,pixmaps:TPixmap[],pal%[],animdelay%[]=Null,looptimes%=$ffff,transparentcolor%=-1,animwidth%=-1,animheight%=-1)
	Assert f,"Encountered null stream."
	Assert pal,"Encountered null palette."
	Assert pixmaps,"Encountered null pixmap array."
	Assert pal.length<=256,"Palette too long. (GIF supports sizes only up to 256)"
	Local logical_width%=animwidth,logical_height%=animheight
	Local detw%=animwidth=-1,deth%=animheight=-1
	For Local pix:TPixmap=EachIn pixmaps
		Assert pix,"Encountered null pixmap."
		If detw logical_width=Max(pix.width,logical_width)
		If deth logical_height=Max(pix.height,logical_height)
	Next
	' header
	WriteString f,"GIF89a"
	' logical screen descriptor
	WriteShort f,logical_width
	WriteShort f,logical_height
	Local lbits%=%11110000 | ((clog2(pal.length)-1))
	WriteByte f,lbits
	WriteByte f,0
	WriteByte f,0
	' global color table
	gifwritepalette f,pal
	' application extension block
	WriteByte f,$21
	WriteByte f,$ff
	WriteByte f,11
	WriteString f,"NETSCAPE"
	WriteString f,"2.0"
	WriteByte f,3
	WriteByte f,1
	WriteShort f,looptimes
	WriteByte f,0
	' write frames
	Local di%=0
	For Local pix:TPixmap=EachIn pixmaps
		Local del%=1
		If animdelay Then del=animdelay[di Mod animdelay.length]
		WriteGIFFrame f,pix,pal,del,0,0,transparentcolor,False
		di:+1
	Next
	WriteByte f,$3b
End Function
Rem
bbdoc: Writes an individual pixmap to a stream as the frame of a GIF image.
f:TStream is the stream to which the GIF will be written
pix:TPixmap is the pixmap to write
pal%[] is an array containing up to 256 colors and will be used as the palette
framedelay% is how long in 100ths of a second the frame should last when animating
xcorner% is the x offset (left toward right) of this frame in the animation
ycorner% is the y offset (top toward bottom) of this frame in the animation
transparentcolor% defines which index of the palette should be flagged as the transparent background color. -1 indicates that there is no transparent color.
localtable% is a flag that decides whether the palette should be considered unique to this frame of the GIF animation
EndRem
Function WriteGIFFrame(f:TStream,pix:TPixmap,pal%[],framedelay%=50,xcorner%=0,ycorner%=0,transparentcolor%=-1,localtable%=True)
	' graphic control exension
	WriteByte f,$21
	WriteByte f,$f9
	WriteByte f,$04
	Local hastransparentcolor%=(transparentcolor>-1)
	WriteByte f,hastransparentcolor
	WriteShort f,framedelay
	WriteByte f,transparentcolor*hastransparentcolor
	WriteByte f,$00
	' image descriptor
	WriteByte f,$2c
	WriteShort f,xcorner ' location of x corner
	WriteShort f,ycorner ' y corner
	WriteShort f,pix.width
	WriteShort f,pix.height
	If localtable Then
		WriteByte f,%10000000 | ((clog2(pal.length)-1) Shl 4)
		gifwritepalette f,pal
	Else
		WriteByte f,0
	EndIf
	Local bpp%=Max(clog2(pal.length),2)
	WriteByte f,bpp
	' lzw-compress the data
	Local minsize%=bpp+1
	Const maxsize%=12
	Local clearcode%=1 Shl bpp
	Local endcode%=clearcode+1
	Local startoncode%=endcode+1
	Local oncode%=startoncode
	Local currentsize%=minsize
	Local firstcode:lzwc=lzwc.Create(currentsize,clearcode)
	Local thiscode:lzwc=firstcode
	Local c$="",ck$,k$
	Local bitlength%=currentsize
	Local table:HashTable=_lzwtable(minsize)
	Local yv%=0
	For Local y%=0 Until pix.height
		For Local x%=0 Until pix.width
			Local val%=gifgetclosestpalcolor(pix.ReadPixel(x,y),pal,pal.length)
			k=Chr(val);ck=c+k
			If table.find(ck)
				c=ck
			Else
				thiscode.succ=lzwc.Create(currentsize,lzwi(table.find(c)).value)
				bitlength:+currentsize
				thiscode=thiscode.succ
				table.insert ck,lzwi.Create(oncode);oncode:+1
				c=k
				If clog2(oncode)>currentsize Then 
					If currentsize=maxsize Then
						thiscode.succ=lzwc.Create(currentsize,clearcode)
						thiscode=thiscode.succ
						bitlength:+currentsize
						currentsize=minsize
						oncode=startoncode
						table=_lzwtable(minsize)
					Else
						currentsize:+1
					EndIf
				EndIf
			EndIf
		Next
		yv:+pix.width
	Next
	thiscode.succ=lzwc.Create(currentsize,lzwi(table.find(c)).value)
	thiscode.succ.succ=lzwc.Create(currentsize,endcode)
	bitlength:+currentsize+currentsize
	' turn into an array of bytes
	Local data@[Ceil(bitlength/8.0)],onbit%=0
	thiscode=firstcode;firstcode=Null
	While thiscode
		For Local i%=0 Until thiscode.bits
			Local di%=onbit Shr 3
			Local thisbit%=(((thiscode.value Shr i)&1) Shl (onbit&7))
			data[di]=data[di] | thisbit
			onbit:+1
		Next
		Local n:lzwc=thiscode.succ;thiscode.succ=Null;thiscode=n
	Wend
	' write the bytes
	For Local i%=0 Until data.length
		If (i Mod 255)=0 Then 
			Local chunksize%=Min(255,data.length-i)
			WriteByte f,chunksize
		EndIf
		WriteByte f,data[i]
	Next
	WriteByte f,$00
End Function
' Writes a palette for a GIF image
Function gifwritepalette(f:TStream,pal%[])
	For Local i%=0 Until (1 Shl clog2(pal.length))
		If i<pal.length
			WriteByte f,(pal[i] Shr 16) '& $ff
			WriteByte f,(pal[i] Shr 8) '& $ff
			WriteByte f,(pal[i]) '& $ff
		Else
			WriteByte f,0;WriteByte f,0;WriteByte f,0
		EndIf
	Next
End Function
' Finds the closest color in a palette to a given color
Global cachedcolor%=0
Function gifgetclosestpalcolor%(argb%,pal% Ptr,pallength%)
	' note: ignores alpha
	Local besti%=cachedcolor,bestdist%=gifgetcolordistance(argb,pal[cachedcolor])
	For Local i%=0 Until pallength
		Local d%=gifgetcolordistance(argb,pal[i])
		If d<bestdist Then
			besti=i;bestdist=d
		EndIf
		If bestdist=0 Exit
	Next
	Return besti
End Function
' Gets the distance from one color to another, slightly adjusted to account for luminosity
Function gifgetcolordistance%(argb1%,argb2%)
	Local r1%=(argb1 Shr 16)&$ff
	Local g1%=(argb1 Shr 8)&$ff
	Local b1%=(argb1)&$ff
	Local r2%=(argb2 Shr 16)&$ff
	Local g2%=(argb2 Shr 8)&$ff
	Local b2%=(argb2)&$ff
	Return (Abs(r1-r2) Shl 1)+(Abs(g1-g2) Shl 2)+Abs(b1-b2)
End Function

Private
' Returns a new hash table to be used as the string table in lzw compression
Function _lzwtable:HashTable(minsize%)
	Local ret:HashTable=CreateHash(1 Shl minsize,_lzwhash)
	For Local i%=0 Until (1 Shl minsize)
		ret.insert Chr(i),lzwi.Create(i)
	Next
	Return ret
End Function
' Hash function, should be faster and well-suited to the specific data going in from the lzw compression
Function _lzwhash%(str$)
	Local ret%=str.length
	For Local i%=0 Until str.length
		ret:+(str[i] Shl ((i&7) Shl 4))
	Next
	Return ret
End Function
' integer container object because I couldn't be arsed to write a hash table specifically for the lzw compression algorithm (and pine.hash can only contain objects)
Type lzwi
	Field value%
	Function Create:lzwi(value%)
		Local n:lzwi=New lzwi
		n.value=value
		Return n
	End Function
End Type
' lzw code object containing a value, the number of bits needed to represent that value, and the next code in the series
Type lzwc
	Field bits%
	Field value%
	Field succ:lzwc
	Function Create:lzwc(bits%,value%)
		Local n:lzwc=New lzwc
		n.bits=bits
		n.value=value
		Return n
	End Function
End Type
' object used for constructing a limited palette from a pixmap
Type _cdata
	Field r@,g@,b@
	Field pop%=1
	Function Create:_cdata(r%,g%,b%)
		Local c:_cdata=New _cdata
		c.r=r;c.g=g;c.b=b
		Return c
	End Function
	Method distance%(o:_cdata)
		Return (Abs(r-o.r) Shl 1)+(Abs(g-o.g) Shl 2)+Abs(b-o.b)
	End Method
	Method compare%(o1:Object)
		If pop>_cdata(o1).pop Return 1
		Return -1
	End Method
End Type
' moves a TLink to the front of a TList
Function movelinktofront(link:TLink,list:TList)
	If link=list.firstlink() Then Return
	link._succ._pred=link._pred
	link._pred._succ=link._succ
	link._pred=list._head
	link._succ=list._head._succ
	link._succ._pred=link
	list._head._succ=link
End Function

Comments

None.

Code Archives Forum