code: purgatorio

ref: 82a3f55c5fb7b9f7a82449e4eb943c535ec3e491
dir: /appl/lib/writegif.b/

View raw version
implement WImagefile;

include "sys.m";
	sys: Sys;

include "draw.m";
	draw: Draw;
	Chans, Display, Image, Rect: import draw;

include "bufio.m";
	bufio: Bufio;
	Iobuf: import bufio;

include "imagefile.m";

Nhash: con 4001;

Entry: adt
{
	index: int;
	prefix: int;
	exten: int;
	next:	cyclic ref Entry;
};

IO: adt
{
	fd:	ref Iobuf;
	buf:	array of byte;
	i:	int;
	nbits:	int;	 # bits in right side of shift register
	sreg:	int;	# shift register
};

tbl: array of ref Entry;

colormap: array of array of byte;
log2 := array[] of {1 => 0, 2 => 1, 4 => 2, 8 => 3, * => -1};

init(iomod: Bufio)
{
	if(sys == nil){
		sys = load Sys Sys->PATH;
		draw = load Draw Draw->PATH;
	}
	bufio = iomod;
}

writeimage(fd: ref Iobuf, image: ref Image): string
{
	case image.chans.desc {
	(Draw->GREY1).desc or (Draw->GREY2).desc or
	(Draw->GREY4).desc or (Draw->GREY8).desc or
	(Draw->CMAP8).desc =>
		if(image.depth > 8 || (image.depth&(image.depth-1)) != 0)
			return "inconsistent depth";
	* =>
		return "unsupported channel type";
	}

	inittbl();

	writeheader(fd, image);
	writedescriptor(fd, image);

	err := writedata(fd, image);
	if(err != nil)
		return err;

	writetrailer(fd);
	fd.flush();
	return err;
}

inittbl()
{
	tbl = array[4096] of ref Entry;
	for(i:=0; i<len tbl; i++)
		tbl[i] = ref Entry(i, -1, i, nil);
}

# Write header, logical screen descriptor, and color map
writeheader(fd: ref Iobuf, image: ref Image): string
{
	# Header
	fd.puts("GIF89a");

	# Logical Screen Descriptor
	put2(fd, image.r.dx());
	put2(fd, image.r.dy());
	# color table present, 4 bits per color (for RGBV best case), size of color map
	fd.putb(byte ((1<<7)|(3<<4)|(image.depth-1)));
	fd.putb(byte 0);	# white background (doesn't matter anyway)
	fd.putb(byte 0);	# pixel aspect ratio - unused

	# Global Color Table
	getcolormap(image);
	ldepth := log2[image.depth];
	if(image.chans.eq(Draw->GREY8))
		ldepth = 4;
	fd.write(colormap[ldepth], len colormap[ldepth]);
	return nil;
}

# Write image descriptor
writedescriptor(fd: ref Iobuf, image: ref Image)
{
	# Image Separator
	fd.putb(byte 16r2C);

	# Left, top, width, height
	put2(fd, 0);
	put2(fd, 0);
	put2(fd, image.r.dx());
	put2(fd, image.r.dy());
	# no special processing
	fd.putb(byte 0);
}

# Write data
writedata(fd: ref Iobuf, image: ref Image): string
{
	# LZW Minimum code size
	if(image.depth == 1)
		fd.putb(byte 2);

	else
		fd.putb(byte image.depth);

	# Encode and emit the data
	err := encode(fd, image);
	if(err != nil)
		return err;

	# Block Terminator
	fd.putb(byte 0);
	return nil;
}

# Write data
writetrailer(fd: ref Iobuf)
{
	fd.putb(byte 16r3B);
}

# Write little-endian 16-bit integer
put2(fd: ref Iobuf, i: int)
{
	fd.putb(byte i);
	fd.putb(byte (i>>8));
}

# Get color map for all ldepths, in format suitable for writing out
getcolormap(image: ref Draw->Image)
{
	if(colormap != nil)
		return;
	colormap = array[5] of array of byte;
	display := image.display;
	colormap[4] = array[3*256] of byte;
	colormap[3] = array[3*256] of byte;
	colormap[2] = array[3*16] of byte;
	colormap[1] = array[3*4] of byte;
	colormap[0] = array[3*2] of byte;
	c := colormap[4];
	for(i:=0; i<256; i++){
		c[3*i+0] = byte i;
		c[3*i+1] = byte i;
		c[3*i+2] = byte i;
	}
	c = colormap[3];
	for(i=0; i<256; i++){
		(r, g, b) := display.cmap2rgb(i);
		c[3*i+0] = byte r;
		c[3*i+1] = byte g;
		c[3*i+2] = byte b;
	}
	c = colormap[2];
	for(i=0; i<16; i++){
		col := (i<<4)|i;
		(r, g, b) := display.cmap2rgb(col);
		c[3*i+0] = byte r;
		c[3*i+1] = byte g;
		c[3*i+2] = byte b;
	}
	c = colormap[1];
	for(i=0; i<4; i++){
		col := (i<<6)|(i<<4)|(i<<2)|i;
		(r, g, b) := display.cmap2rgb(col);
		c[3*i+0] = byte r;
		c[3*i+1] = byte g;
		c[3*i+2] = byte b;
	}
	c = colormap[0];
	for(i=0; i<2; i++){
		if(i == 0)
			col := 0;
		else
			col = 16rFF;
		(r, g, b) := display.cmap2rgb(col);
		c[3*i+0] = byte r;
		c[3*i+1] = byte g;
		c[3*i+2] = byte b;
	}
}

# Put n bits of c into output at io.buf[i];
output(io: ref IO, c, n: int)
{
	if(c < 0){
		if(io.nbits != 0)
			io.buf[io.i++] = byte io.sreg;
		io.fd.putb(byte io.i);
		io.fd.write(io.buf, io.i);
		io.nbits = 0;
		return;
	}

	if(io.nbits+n >= 31){
		sys->print("panic: WriteGIF sr overflow\n");
		exit;
	}
	io.sreg |= c<<io.nbits;
	io.nbits += n;

	while(io.nbits >= 8){
		io.buf[io.i++] = byte io.sreg;
		io.sreg >>= 8;
		io.nbits -= 8;
	}

	if(io.i >= 255){
		io.fd.putb(byte 255);
		io.fd.write(io.buf, 255);
		io.buf[0:] = io.buf[255:io.i];
		io.i -= 255;
	}
}

# LZW encoder
encode(fd: ref Iobuf, image: ref Image): string
{
	c, h, csize, prefix: int;
	e, oe: ref Entry;

	first := 1;
	ld := log2[image.depth];
	# ldepth 0 must generate codesize 2 with values 0 and 1 (see the spec.)
	ld0 := ld;
	if(ld0 == 0)
		ld0 = 1;
	codesize := (1<<ld0);
	CTM := 1<<codesize;
	EOD := CTM+1;

	io := ref IO (fd, array[300] of byte, 0, 0, 0);
	sreg := 0;
	nbits := 0;
	bitsperpixel := 1<<ld;
	pm := (1<<bitsperpixel)-1;

	# Read image data into memory
	# potentially one extra byte on each end of each scan line
	data := array[image.r.dy()*(2+(image.r.dx()>>(3-log2[image.depth])))] of byte;
	ndata := image.readpixels(image.r, data);
	if(ndata < 0)
		return sys->sprint("WriteGIF: readpixels: %r");
	datai := 0;
	x := image.r.min.x;

Init:
	for(;;){
		csize = codesize+1;
		nentry := EOD+1;
		maxentry := (1<<csize);
		hash := array[Nhash] of ref Entry;
		for(i := 0; i<nentry; i++){
			e = tbl[i];
			h = (e.prefix<<24) | (e.exten<<8);
			h %= Nhash;
			if(h < 0)
				h += Nhash;
			e.next = hash[h];
			hash[h] = e;
		}
		prefix = -1;
		if(first)
			output(io, CTM, csize);
		first = 0;

		# Scan over pixels.  Because of partially filled bytes on ends of scan lines,
		# which must be ignored in the data stream passed to GIF, this is more
		# complex than we'd like
	Next:
		for(;;){
			if(ld != 3){
				# beginning of scan line is difficult; prime the shift register
				if(x == image.r.min.x){
					if(datai == ndata)
						break;
					sreg = int data[datai++];
					nbits = 8-((x&(7>>ld))<<ld);
				}
				x++;
				if(x == image.r.max.x)
					x = image.r.min.x;
			}
			if(nbits == 0){
				if(datai == ndata)
					break;
				sreg = int data[datai++];
				nbits = 8;
			}
			nbits -= bitsperpixel;
			c = sreg>>nbits & pm;
			h = prefix<<24 | c<<8;
			h %= Nhash;
			if(h < 0)
				h += Nhash;
			oe = nil;
			for(e = hash[h]; e!=nil; e=e.next){
				if(e.prefix == prefix && e.exten == c){
					if(oe != nil){
						oe.next = e.next;
						e.next = hash[h];
						hash[h] = e;
					}
					prefix = e.index;
					continue Next;
				}
				oe = e;
			}

			output(io, prefix, csize);
			early:=0; # peculiar tiff feature here for reference
			if(nentry == maxentry-early){
				if(csize == 12){
					nbits += codesize;	# unget pixel
					x--;
					output(io, CTM, csize);
					continue Init;
				}
				csize++;
				maxentry = (1<<csize);
			}

			e = tbl[nentry];
			e.prefix = prefix;
			e.exten = c;
			e.next = hash[h];
			hash[h] = e;

			prefix = c;
			nentry++;
		}
		break Init;
	}
	output(io, prefix, csize);
	output(io, EOD, csize);
	output(io, -1, csize);
	return nil;
}