shithub: pdffs

ref: 1e529dae29361055228348a4864c403b7914da0f
dir: /op.c/

View raw version
#include <u.h>
#include <libc.h>
#include <draw.h>
#include <memdraw.h>
#include "pdf.h"

enum {
	Evenodd = 1<<0,
	Nonstroking = 1<<1,
	Leading = 1<<2,
	Nextline = 1<<3,
	TwTc = 1<<4,
};

typedef struct Op Op;

static void
matidentity(double *arr)
{
	double src[6] = {
					1, 0,
					0, 1,
					0, 0
	};
	memcpy(arr, src, sizeof(double) * 6);
}

static void
matmult(double *m1, double *m2, double *out)
{
	double result[6];
	result[0] = m1[0] * m2[0] + m1[1] * m2[2];
	result[1] = m1[0] * m2[1] + m1[1] * m2[3];
	result[2] = m1[2] * m2[0] + m1[3] * m2[2];
	result[3] = m1[2] * m2[1] + m1[3] * m2[3];
	result[4] = m1[4] * m2[0] + m1[5] * m2[2] + m2[4];
	result[5] = m1[4] * m2[1] + m1[5] * m2[3] + m2[5];
	memcpy(out, result, sizeof(double) * 6);
}

struct Op {
	char *s;
	int (*f)(Op *op, Page *p);
	int argc;
	int flags;
};

static int
flagless(Op *op)
{
	if(op->flags != 0){
		fprint(2, "Op '%s' expected no flags\n", op->s);
		return 0;
	}
	return 1;
}

static int
cobegin(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
coend(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
gspush(Op *op, Page *p)
{
	USED(op);
	GS *r = realloc(p->GS, sizeof(GS) * (p->nGS + 1));
	if(r == nil)
		return 0;
	p->GS = r;
	p->nGS += 1;
	p->GSactive = &p->GS[p->nGS - 1];
	*(p->GSactive) = p->GS[p->nGS - 2];
	return 1;
}

static int
gspop(Op *op, Page *p)
{
	USED(op);
	GS *r = realloc(p->GS, sizeof(GS) * (p->nGS - 1));
	if(r == nil)
		return 0;
	p->GS = r;
	p->nGS -= 1;
	p->GSactive = &p->GS[p->nGS - 1];
	return 1;
}

/* six parameters give the inputs a,b,c,d,e,f for the matrix
	[a b 0]
	[c d 0]
	[e f 1]
 That matrix should be premultiplied with the current matrix
 newCTM = input x oldCTM
 (8.3.4)
 */
static int
gsctm(Op *op, Page *p)
{
	double input[6];
	int i;
	for(i = 0; i < 6; i += 1)
		input[i] = arrayget(p->stack, i)->num.d;
	matmult(input, p->GSactive->CTM, p->GSactive->CTM);
	return flagless(op);
}

static int
gswidth(Op *op, Page *p)
{
	p->GSactive->LW = arrayget(p->stack, 0)->num.i;
	return flagless(op);
}

static int
gscap(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
gsjoin(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
gsmiterlim(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
gsdash(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
gsintent(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
gsflatness(Op *op, Page *p)
{
	p->GSactive->FL = arrayget(p->stack, 0)->num.d;
	return flagless(op);
}

static int
gsstate(Op *op, Page *p)
{
	char *name = arrayget(p->stack, 0)->name;
	Object *extgstate;
	if((extgstate = dictget(dictget(dictget(p->obj, "Resources"), "ExtGState"), name)) == nil){
		werrstr("extgstate dictionary not found: %s", name);
		return 0;
	}
	fprint(2, "TODO use GS dictionary\n");
	USED(op, extgstate);
	return 1;
}

static int
pcmove(Op *op, Page *p)
{
	USED(op, p);
	return 1;
}

static int
pcline(Op *op, Page *p)
{
	USED(op, p);
	return 1;
}

static int
pccurve(Op *op, Page *p)
{
	USED(op, p);
	return 1;
}

static int
pcsubpath(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
pcrect(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
ppstroke(Op *op, Page *p)
{
	USED(op, p);
	return 1;
}

static int
ppstrokec(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
ppfill(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
ppfills(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
ppfillcfs(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
ppc(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
cpclip(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
cspace(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
ccolour(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
ccolour2(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
cgray(Op *op, Page *p)
{
	int value = 255 * arrayget(p->stack, 0)->num.d;
	int i;
	u32int *color;
	if(op->flags & Nonstroking){
		color = &p->GSactive->NSC;
		p->GSactive->NSCS = DeviceGray;
	} else{
		color = &p->GSactive->SC;
		p->GSactive->SCS = DeviceGray;
	}
	*color = 0;
	for(i = 0; i < 3; i += 1)
		*color = (*color | value) << 8;
	*color |= 255;
	return 1;
}

static int
crgb(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
ccmyk(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
sshade(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
eoobject(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
iibegin(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
iidata(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
iiend(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
tsspace(Op *op, Page *p)
{
	p->TS.Tc = arrayget(p->stack, 0)->num.d;
	return 1;
}

static int
tswspace(Op *op, Page *p)
{
	p->TS.Tw = arrayget(p->stack, 0)->num.d;
	return 1;
}

static int
tshscale(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
tslead(Op *op, Page *p)
{
	p->TS.TL = arrayget(p->stack, 0)->num.d;
	return flagless(op);
}

static int
fontwidths(Page *p)
{
	Object *o;
	int i;
	if(p->GSactive->Font.widths != nil)
		free(p->GSactive->Font.widths);
	o = dictget(p->GSactive->Font.font, "FirstChar");
	if(o == nil)
		return 1;
	p->GSactive->Font.first = o->num.i;
	p->GSactive->Font.last = dictget(p->GSactive->Font.font, "LastChar")->num.i;
	p->GSactive->Font.widths = malloc(sizeof(int) * (p->GSactive->Font.last - p->GSactive->Font.first + 1));
	if(p->GSactive->Font.widths == nil){
		print("Failed to allocate for (%d, %d): %d\n", p->GSactive->Font.first, p->GSactive->Font.last, p->GSactive->Font.last - p->GSactive->Font.first + 1);
		return 1;
	}
	o = dictget(p->GSactive->Font.font, "Widths");
	if(o == nil)
		return 0;
	for(i = 0; i < arraylen(o); i += 1)
		p->GSactive->Font.widths[i] = arrayget(o, i)->num.i;
	o = dictget(p->GSactive->Font.font, "FontDescriptor");
	p->GSactive->Font.defwidth = dictget(o, "MissingWidth")->num.i;
	return 1;
}

static int
tsfontsz(Op *op, Page *p)
{
	char *name = arrayget(p->stack, 0)->name;
	p->GSactive->Font.font = dictget(dictget(dictget(p->obj, "Resources"), "Font"), name);
	if(p->GSactive->Font.font == nil){
		werrstr("Font not found: '%s'", name);
		return 0;
	}
	p->GSactive->Font.enc = dictget(p->GSactive->Font.font, "Encoding");
	if(p->GSactive->Font.enc)
		p->GSactive->Font.enc = dictget(p->GSactive->Font.enc, "Differences");
	p->GSactive->Font.size = arrayget(p->stack, 1)->num.d;
	return fontwidths(p) && flagless(op);
}

static int
tsrendmode(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
tsrise(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
tobegin(Op *op, Page *p)
{
	if(p->TS.inobj){
		werrstr("Text objects must not be nested");
		return 0;
	}
	matidentity(p->TS.Tm);
	matidentity(p->TS.Tlm);
	p->TS.inobj = 1;
	p->TS.TL = 0;
	p->TS.Tc = 0;
	p->TS.Tw = 0;
	p->GSactive->Font.font = nil;
	return flagless(op);
}

static int
toend(Op *op, Page *p)
{
	if(!p->TS.inobj){
		werrstr("ET found without BT");
		return 0;
	}
	p->TS.inobj = 0;
	return flagless(op);
}

static int
tmove(Page *p, double x, double y, int tlm)
{
	double shift[6] = {1, 0, 0, 1, x, y};
	if(tlm){
		matmult(shift, p->TS.Tlm, p->TS.Tlm);
		memcpy(p->TS.Tm, p->TS.Tlm, sizeof(double) * 6);
	} else{
		matmult(shift, p->TS.Tm, p->TS.Tm);
	}
	return 1;
}

static int
tpmove(Op *op, Page *p)
{
	Object *x, *y;
	x = arrayget(p->stack, 0);
	y = arrayget(p->stack, 1);
	if(op->flags & Leading)
		p->TS.TL = -y->num.d;
	return tmove(p, x->num.d, y->num.d, 1);
}

static int
tpmatrix(Op *op, Page *p)
{
	int i;
	for(i = 0; i < 6; i += 1){
		p->TS.Tm[i] = arrayget(p->stack, i)->num.d;
		p->TS.Tlm[i] = p->TS.Tm[i];
	}
	return flagless(op);
}

static int
tpmove0(Op *op, Page *p)
{
	return tmove(p, 0, 0 - p->TS.TL, 1) && flagless(op);
}

//TODO: replace this with a precomputed map when the font is loaded
static int
writepatched(Page *p, uchar c)
{
	int i, len, d = 0;
	char buf[2];
	Object *o;
	if(p->GSactive->Font.enc != nil){
		len = arraylen(p->GSactive->Font.enc);
		for(i = 0; i < len; i += 1){
			o = arrayget(p->GSactive->Font.enc, i);
			if(o->type == Onum)
				d = o->num.i;
			else if(d == c){
				if(strcmp(o->name, "Omega") == 0)
					return bufput(&p->buf, (uchar*)"Ω", strlen("Ω")) == strlen("Ω");
				if(strcmp(o->name, "Pi") == 0)
					return bufput(&p->buf, (uchar*)"Π", strlen("Π")) == strlen("Π");
				if(strcmp(o->name, "mu") == 0)
					return bufput(&p->buf, (uchar*)"μ", strlen("μ")) == strlen("μ");
				if(strcmp(o->name, "pi") == 0)
					return bufput(&p->buf, (uchar*)"π", strlen("π")) == strlen("π");
				if(strcmp(o->name, "heart") == 0)
					return bufput(&p->buf, (uchar*)"♥", strlen("♥")) == strlen("♥");
				if(strcmp(o->name, "minus") == 0)
					return bufput(&p->buf, (uchar*)"1", 1) == 1;
				if(strcmp(o->name, "fl") == 0)
					return bufput(&p->buf, (uchar*)"fl", 2) == 2;
				if(strcmp(o->name, "dieresis") == 0)
					return bufput(&p->buf, (uchar*)"¨", strlen("¨")) == strlen("¨");
				if(strcmp(o->name, "endash") == 0)
					return bufput(&p->buf, (uchar*)"-", 1) == 1;
				if(strcmp(o->name, "fi") == 0)
					return bufput(&p->buf, (uchar*)"fi", 2) == 2;
				if(strcmp(o->name, "ff") == 0)
					return bufput(&p->buf, (uchar*)"ff", 2) == 2;
				if(strcmp(o->name, "ffi") == 0)
					return bufput(&p->buf, (uchar*)"ffi", 3) == 3;
				if(strcmp(o->name, "bullet") == 0)
					return bufput(&p->buf, (uchar*)"•", strlen("•")) == strlen("•");
				if(strcmp(o->name, "quotedblleft") == 0)
					return bufput(&p->buf, (uchar*)"\"", 1) == 1;
				if(strcmp(o->name, "quotedblright") == 0)
					return bufput(&p->buf, (uchar*)"\"", 1) == 1;
				if(strcmp(o->name, "quoteleft") == 0)
					return bufput(&p->buf, (uchar*)"'", 1) == 1;
				if(strcmp(o->name, "zero") == 0)
					return bufput(&p->buf, (uchar*)"0", 1) == 1;
				if(strcmp(o->name, "one") == 0)
					return bufput(&p->buf, (uchar*)"1", 1) == 1;
				if(strcmp(o->name, "two") == 0)
					return bufput(&p->buf, (uchar*)"2", 1) == 1;
				if(strcmp(o->name, "three") == 0)
					return bufput(&p->buf, (uchar*)"3", 1) == 1;
				if(strcmp(o->name, "four") == 0)
					return bufput(&p->buf, (uchar*)"4", 1) == 1;
				if(strcmp(o->name, "five") == 0)
					return bufput(&p->buf, (uchar*)"5", 1) == 1;
				if(strcmp(o->name, "six") == 0)
					return bufput(&p->buf, (uchar*)"6", 1) == 1;
				if(strcmp(o->name, "seven") == 0)
					return bufput(&p->buf, (uchar*)"7", 1) == 1;
				if(strcmp(o->name, "eight") == 0)
					return bufput(&p->buf, (uchar*)"8", 1) == 1;
				if(strcmp(o->name, "nine") == 0)
					return bufput(&p->buf, (uchar*)"9", 1) == 1;
				if(strcmp(o->name, "space") == 0)
					return bufput(&p->buf, (uchar*)" ", 1) == 1;
				if(strcmp(o->name, "quoteright") == 0)
					return bufput(&p->buf, (uchar*)"'", 1) == 1;
				if(strcmp(o->name, "backslash") == 0)
					return bufput(&p->buf, (uchar*)"\\", 1) == 1;
				if(strcmp(o->name, "braceright") == 0)
					return bufput(&p->buf, (uchar*)"}", 1) == 1;
				if(strcmp(o->name, "period") == 0)
					return bufput(&p->buf, (uchar*)".", 1) == 1;
				if(strcmp(o->name, "comma") == 0)
					return bufput(&p->buf, (uchar*)",", 1) == 1;
				if(strcmp(o->name, "braceleft") == 0)
					return bufput(&p->buf, (uchar*)"{", 1) == 1;
				if(strcmp(o->name, "arrowright") == 0)
					return bufput(&p->buf, (uchar*)"→", strlen("→")) == strlen("→");
				buf[1] = 0;
				for(buf[0] = 'A'; buf[0] <= 'Z'; buf[0] += 1)
					if(strcmp(buf, o->name) == 0)
						return bufput(&p->buf, (uchar*)buf, 1) == 1;
				for(buf[0] = 'a'; buf[0] <= 'z'; buf[0] += 1)
					if(strcmp(buf, o->name) == 0)
						return bufput(&p->buf, (uchar*)buf, 1) == 1;
				fprint(2, "TODO: recognize glyph name '%s'\n", o->name);
				return 1;
			} else
				d += 1;
		}
	}
	return bufput(&p->buf, (uchar*)&c, 1) == 1;
}

/* Renders one character / glyph and updates the text state */
static int
tchar(Page *p, ulong c)
{
	double Trm[6] = {p->GSactive->Font.size, 0, 0, p->GSactive->Font.size, 0, 0};
	double tx, w0;
	int i;
	matmult(Trm, p->TS.Tm, Trm);
	matmult(Trm, p->GSactive->CTM, Trm);
	if(c >= p->GSactive->Font.first && c <= p->GSactive->Font.last)
		w0 = (double)p->GSactive->Font.widths[c - p->GSactive->Font.first] / 1000;
	else
		w0 = (double)p->GSactive->Font.defwidth / 1000;
	tx = w0 * p->GSactive->Font.size + p->TS.Tc;
	if(c == ' ')
		tx += p->TS.Tw;
	// Check if whitespace is needed
	if(p->buf.sz > 1){
		if(p->TS.y != Trm[5]){
			for(i = 0; i < (int)((p->TS.y - Trm[5]) / p->GSactive->Font.size) && i < 1; i += 1)
				if(bufput(&p->buf, (uchar*)"\n", 1) != 1)
					return 0;
		}
		if(Trm[4] - p->TS.x > 14){
			if(bufput(&p->buf, (uchar*)" ", 1) != 1)
				return 0;
		}
	}
	usize oend = p->buf.sz;
	if(!writepatched(p, c)){
		werrstr("tchar: failed to write character: %r");
 		return 0;
	}
	if(p->buf.b != nil)
		memimagestring(p->image, Pt(Trm[4], 1100 - Trm[5]), memblack, Pt(0,0), getmemdefont(), (char*)p->buf.b + oend);
	tmove(p, tx, 0, 0);
	p->TS.x = Trm[4] + tx;
	p->TS.y = Trm[5];
	return 1;
}

static int
tstr(Page *p, char *str, ulong len)
{
	ulong i;
	for(i = 0; i < len; i += 1)
		if(!tchar(p, str[i]))
			return 0;
	return 1;
}

static int
thshow(Op *op, Page *p)
{
	if(op->flags != 0){
		fprint(2, "TODO: thshow != Tj\n");
		return 0;
	}
	Object *o = arrayget(p->stack, 0);
	if(!tstr(p, o->str, o->len))
		return 0;
	return 1;
}

static int
thshowarr(Op *op, Page *p)
{
	Object *o, *arr = arrayget(p->stack, 0);
	int i;
	for(i = 0; i < arraylen(arr); i += 1){
		o = arrayget(arr, i);
		if(o->type == Ostr){
			if(!tstr(p, o->str, o->len))
				return 0;
		}
		else{
			if(!tmove(p, 0 - (o->num.d / 1000), 0, 0))
				return 0;
		}
	}
	return flagless(op);
}

static int
t3width(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
t3widthbb(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
t4add(Op *op, Page *p)
{
	USED(op, p);
/*
	double x;
	x = objat(s+1, Onum)->num.d + objat(s+0, Onum)->num.d;
	s = pop(s);
	s->num.d = x;
	s->num.i = x;
*/
	return 0;
}

static int
t4sub(Op *op, Page *p)
{
	USED(op, p);
/*
	double x;
	x = objat(s+1, Onum)->num.d - objat(s+0, Onum)->num.d;
	s = pop(s);
	s->num.d = x;
	s->num.i = x;
*/
	return 0;
}

static int
t4mul(Op *op, Page *p)
{
	USED(op, p);
/*
	double x;

	x = objat(s+1, Onum)->num.d * objat(s+0, Onum)->num.d;
	s = pop(s);
	s->num.d = x;
	s->num.i = x;
*/
	return 0;
}

static int
t4div(Op *op, Page *p)
{
	USED(op, p);
/*
	double x;

	x = objat(s+1, Onum)->num.d / objat(s+0, Onum)->num.d;
	s = pop(s);
	s->num.d = x;
	s->num.i = x;
*/
	return 0;
}

static int
t4idiv(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
t4mod(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
t4neg(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
t4abs(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
t4ceiling(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
t4floor(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
t4round(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
t4truncate(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
t4sqrt(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
t4sin(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
t4cos(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
t4atan(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
t4exp(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
t4ln(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
t4log(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
t4cvi(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
t4cvr(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
t4eq(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
t4ne(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
t4gt(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
t4ge(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
t4lt(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
t4le(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
t4and(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
t4or(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
t4xor(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
t4not(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
t4bitshift(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
t4true(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
t4false(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
t4if(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
t4ifelse(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
t4pop(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
t4exch(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
t4dup(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
t4copy(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
t4index(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
t4roll(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static int
opignore(Op *op, Page *p)
{
	USED(op, p);
	return 0;
}

static Op ops[] = {
	/* 7.8.2 Compatibility operators */
	{"BX", cobegin, 0,}, /* begin a compatilibity section */
	{"EX", coend, 0,},   /* end a compatilibity section */

	/* 8.4.4 Graphics state operators */
	{"q", gspush, 0,},   /* push the current graphics state (gs) on gs stack */
	{"Q", gspop, 0,},    /* pop ^ */
	{"cm", gsctm, 6,},     /* current transformation matrix (ctm) */
	{"w", gswidth, 1,},    /* line width */
	{"J", gscap, 1,},      /* line cap style */
	{"j", gsjoin, 1,},     /* line join style */
	{"M", gsmiterlim, 1,}, /* miter limit */
	{"d", gsdash, 2,},     /* line dash pattern */
	{"ri", gsintent, 1,},  /* colour rendering intent */
	{"i", gsflatness, 1,}, /* flatness tolerance */
	{"gs", gsstate, 1,},   /* graphics state parameters */

	/* 8.5.2 Path construction operators */
	{"m", pcmove, 2,},   /* move to coords */
	{"l", pcline, 2,},   /* straight line to coords */
	{"c", pccurve, 6,},  /* Bézier curve */
	{"v", pccurve, 4,},  /* Bézier curve */
	{"y", pccurve, 4,},  /* Bézier curve */
	{"h", pcsubpath, 0}, /* close subpath */
	{"re", pcrect, 4,},  /* rectangle */

	/* 8.5.3 Path painting operators */
	{"S", ppstroke, 0,},            /* stroke the path */
	{"s", ppstrokec, 0,},           /* close and stroke */
	{"f", ppfill, 0,},              /* fill */
	{"F", ppfill, 0,},              /* same damn thing, but DEPRECATED */
	{"f*", ppfill, 0, Evenodd,},    /* fill, even/odd rule */
	{"B", ppfills, 0,},             /* fill and stroke */
	{"B*", ppfills, 0, Evenodd,},   /* fill and stroke, even/odd rule */
	{"b", ppfillcfs, 0,},           /* close, fill and stroke */
	{"b*", ppfillcfs, 0, Evenodd,}, /* close, fill and stroke, even/odd rule */
	{"n", ppc, 0, 0},               /* end the path */

	/* 8.5.4 Clipping path operators */
	{"W", cpclip, 0,},           /* clip */
	{"W*", cpclip, 0, Evenodd,}, /* clip, even/odd rule */

	/* 8.6.8 Colour operators */
	{"CS", cspace, 1,},               /* colour space */
	{"cs", cspace, 1, Nonstroking,},  /* colour space, nonstroking */
	{"SC", ccolour, -1,},              /* colour */
	{"sc", ccolour, -1, Nonstroking,}, /* colour, nonstroking */
	{"SCN", ccolour2, -1,},            /* color (more spaces) */
	{"scn", ccolour2, -1,},            /* color (more spaces), nonstroking */
	{"G", cgray, 1,},                 /* gray */
	{"g", cgray, 1, Nonstroking,},    /* gray, nonstroking */
	{"RG", crgb, 3,},                 /* RGB */
	{"rg", crgb, 3, Nonstroking,},    /* RGB, nonstroking */
	{"K", ccmyk, 4,},                 /* CMYK */
	{"k", ccmyk, 4, Nonstroking,},    /* CMYK, nonstroking */

	/* 8.7.4.2 Shading operator */
	{"sh", sshade, 1,}, /* shading */

	/* 8.8 External objects */
	{"Do", eoobject, 1,}, /* paint XObject */

	/* 8.9.7 Inline images */
	{"BI", iibegin, 0,}, /* begin */
	{"ID", iidata, 0,},  /* data */
	{"EI", iiend, 0,},   /* end */

	/* 9.3.1 Text state parameters */
	{"Tc", tsspace, 1,},    /* spacing */
	{"Tw", tswspace, 1,},   /* word spacing */
	{"Tz", tshscale, 1,},   /* horizontal spacing */
	{"TL", tslead, 1,},     /* leading */
	{"Tf", tsfontsz, 1,},   /* font size */
	{"Tr", tsrendmode, 1,}, /* rendeing mode */
	{"Ts", tsrise, 1,},     /* rise */

	/* 9.4.1 Text objects */
	{"BT", tobegin, 0,}, /* begin */
	{"ET", toend, 0,},   /* end */

	/* 9.4.2 Text position operators */
	{"Td", tpmove, 2,},           /* move, next line */
	{"TD", tpmove, 2, Leading,},  /* move, next line, leading */
	{"Tm", tpmatrix, 6,},         /* set Tm and Tlm */
	{"T*", tpmove0, 0,}, /* move, next line, leading */

	/* 9.4.3 Text showing operators */
	{"Tj", thshow, 1,},                /* show string */
	{"'", thshow, 1, Nextline,},       /* next line & show */
	{"\"", thshow, 3, Nextline|TwTc,}, /* next line, Tw, Tc & show */
	{"TJ", thshowarr, 1,},             /* show array */

	/* 9.6.4 Type 3 font operators */
	{"d0", t3width, 2,},   /* width info */
	{"d1", t3widthbb, 6,}, /* width & bounding box */

	/* 14.6 Marked content */
	{"BDC", opignore, 2,},
	{"EMC", opignore, 0,},

	/* 7.10.5.2 Operators and operands */
	/* B.2 Arithmetic operators */
	{"add", t4add, 2,},
	{"sub", t4sub, 2,},
	{"mul", t4mul, 2,},
	{"div", t4div, 2,},
	{"idiv", t4idiv, 2,},
	{"mod", t4mod, 2,},
	{"neg", t4neg, 1,},
	{"abs", t4abs, 1,},
	{"ceiling", t4ceiling, 1,},
	{"floor", t4floor, 1,},
	{"round", t4round, 1,},
	{"truncate", t4truncate, 1,},
	{"sqrt", t4sqrt, 1,},
	{"sin", t4sin, 1,},
	{"cos", t4cos, 1,},
	{"atan", t4atan, 2,},
	{"exp", t4exp, 2,},
	{"ln", t4ln, 1,},
	{"log", t4log, 1,},
	{"cvi", t4cvi, 1,},
	{"cvr", t4cvr, 1,},
	/* B.3 Relational, boolean, and bitwise operators */
	{"eq", t4eq, 2,},
	{"ne", t4ne, 2,},
	{"gt", t4gt, 2,},
	{"ge", t4ge, 2,},
	{"lt", t4lt, 2,},
	{"le", t4le, 2,},
	{"and", t4and, 2,},
	{"or", t4or, 2,},
	{"xor", t4xor, 2,},
	{"not", t4not, 1,},
	{"bitshift", t4bitshift, 2,},
	{"true", t4true, 0,},
	{"false", t4false, 0,},
	/* B.4 Conditional operators */
	{"if", t4if, 2,},
	{"ifelse", t4ifelse, 3,},
	/* B.5 Stack operators */
	{"pop", t4pop, 1,},
	{"exch", t4exch, 2,},
	{"dup", t4dup, 1,},
	{"copy", t4copy, -1,},
	{"index", t4index, -1,},
	{"roll", t4roll, -2,},

	/* terminator */
	{nil, nil, 0,},
};

Op *
opfind(char *name)
{
	int i = 0;
	Op *op;
	while(ops[i].s != nil){
		op = &ops[i];
		if(strcmp(op->s, name) == 0)
			return op;
		i += 1;
	}
	return nil;
}

void
pageinit(Page *page, Object *o)
{
	bufinit(&page->buf, 0, 0);
	// Stack is per-content-stream, so we don't create it here
	page->stack = nil;
	page->obj = o;
	page->TS.inobj = 0;
	page->TS.x = 0;
	page->TS.y = 0;
}

void
ctminit(Page *p, double *ctm)
{
	Object *cropbox;
	double mx, my;
	matidentity(ctm);
	if((cropbox = dictget(p->obj, "CropBox")) == nil)
		return;
	mx = arrayget(cropbox, 2)->num.d;
	my = arrayget(cropbox, 3)->num.d;
	ctm[0] = 850 / mx;
	ctm[3] = 1100 / my;
}

void
gsinit(Page *p, GS *gs)
{
	USED(p);
	/* todo: actually initialize the full state */
	ctminit(p, gs->CTM);
	gs->LW = 1;
	gs->LC = 0;
	gs->LJ = 0;
	gs->ML = 10;
	gs->SCS = gs->NSCS = DeviceGray;
	// Alpha is lowest byte; this is (0, 0, 0, 255) == black
	gs->SC = gs->NSC = 255;
	gs->Font.font = nil;
	gs->Font.enc = nil;
	gs->Font.widths = nil;
}

void
gsfree(GS gs)
{
	free(gs.Font.widths);
	pdfobjfree(gs.Font.font);
	gs.Font.font = nil;
	gs.Font.enc = nil;
	gs.Font.widths = nil;
}

void
pagegsclean(Page *p)
{
	int i;
	if(p->GSactive != nil){
		p->GSactive = nil;
		for(i = 0; i < p->nGS; i += 1)
			gsfree(p->GS[i]);
		free(p->GS);
		p->GS = nil;
		p->nGS = 0;
	}
}

static int
stackreset(Page *p)
{
	pdfobjfree(p->stack);
	p->stack = arraynew(p->obj->pdf);
	return p->stack != nil;
}

void
pagefree(Page *p)
{
	buffree(&p->buf);
	pdfobjfree(p->stack);
	pagegsclean(p);
}

static int
pagerendercontent(Page *p, Object *content)
{
	Stream *s;
	Object *o;
	Op *op;
	s = Sopen(content);
	if(s == nil){
		fprint(2, "%O\n", content);
		sysfatal("%r");
	}
	p->stack = arraynew(content->pdf);
	if(p->stack == nil)
		return 0;
	while(s->buf.off != s->buf.sz){
		while(isws(s->buf.b[s->buf.off]) && s->buf.off != s->buf.sz)
			s->buf.off += 1;
		if(s->buf.off == s->buf.sz)
			break;
		o = pdfobj(content->pdf, s);
		if(o == nil){
			werrstr("pagerendercontent: failed to parse op: %r");
			return 0;
		}
		if(o->type == Oop){
			op = opfind(o->str);
			if(op == nil){
				fprint(2, "Unknown op: %s\n", o->str);
				pdfobjfree(o);
				return 0;
			}
			pdfobjfree(o);
			werrstr("unimplemented or error not reported properly");
			if(!op->f(op, p)){
				werrstr("renderer: %s failed: %r", op->s);
				return 0;
			}
			if(!stackreset(p))
				return 0;
		} else{
			if(!arrayadd(p->stack, o)){
				fprint(2, "Failed to push operand to stack: %r\n");
				return 0;
			}
		}
	}
	if(bufput(&p->buf, (uchar*)"\n\0", 2) != 2)
		return 0;
	Sclose(s);
	return 1;
}

int
pagerender(Page *p)
{
	Object *content;
	int i;
	p->nGS = 1;
	p->GS = malloc(sizeof(GS));
	if(p->GS == nil){
		werrstr("Out of memory");
		return 0;
	}
	p->GSactive = p->GS;
	gsinit(p, p->GS);
	if((p->image = allocmemimage(Rect(0,0,850,1100), strtochan("r8g8b8"))) == nil){
		werrstr("unable to allocate image: %r");
		pagegsclean(p);
		return 0;
	}
	memfillcolor(p->image, 0xFFFFFFFF);
	content = dictget(p->obj, "Contents");
	if(content->type == Oarray){
		for(i = 0; i < arraylen(content); i += 1)
			if(!pagerendercontent(p, arrayget(content, i)))
				return 0;
	}
	else if(content->type != Onull)
		if(!pagerendercontent(p, content))
			return 0;
	pagegsclean(p);
	return 1;
}