shithub: pdffs

ref: a9feb43f707673f7139317fac35e843d3bab7983
dir: pdffs/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);
}

static void
mattrans(double *m1, double x, double y, double *out)
{
	double mult[6] = {
		1, 0,
		0, 1,
		x, y,
	};
	matmult(mult, m1, out);
}

static void
matscale(double *m1, double x, double y, double *out)
{
	double mult[6] = {
		x, 0,
		0, y,
		0, 0,
	};
	matmult(mult, m1, out);
}

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

static int
flagless(Op *op)
{
	if(op->flags != 0){
		werrstr("op %q: expected no flags", op->s);
		return -1;
	}
	return 0;
}

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

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

static int
gspush(Op *op, Page *p)
{
	GS *r;

	USED(op);
	if((r = realloc(p->GS, sizeof(GS) * (p->nGS + 1))) == nil)
		return -1;
	p->GS = r;
	p->nGS++;
	p->GSactive = &p->GS[p->nGS - 1];
	*(p->GSactive) = p->GS[p->nGS - 2];
	if(p->GSactive->Font.widths != nil){
		p->GSactive->Font.widths = malloc(sizeof(int) * (p->GSactive->Font.last - p->GSactive->Font.first + 1));
		if(p->GSactive->Font.widths == nil){
			werrstr("gspush: out of memory allocating space for glyph widths");
			return -1;
		}
		memcpy(p->GSactive->Font.widths, p->GS[p->nGS - 2].Font.widths, sizeof(int) * (p->GSactive->Font.last - p->GSactive->Font.first + 1));
	}
	return 0;
}

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

/* 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++)
		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.d;
	if(p->GSactive->LW < 0){
		werrstr("gswidth: invalid line width: %f", p->GSactive->LW);
		return -1;
	}
	return flagless(op);
}

static int
gscap(Op *op, Page *p)
{
	p->GSactive->LC = arrayget(p->stack, 0)->num.d;
	if(p->GSactive->LC > CapEND){
		werrstr("gscap: invalid line cap: %d", p->GSactive->LC);
		return -1;
	}
	return flagless(op);
}

static int
gsjoin(Op *op, Page *p)
{
	p->GSactive->LJ = arrayget(p->stack, 0)->num.d;
	if(p->GSactive->LJ > JoinEND){
		werrstr("gsjoin: invalid line join: %d", p->GSactive->LJ);
		return -1;
	}
	return flagless(op);
}

static int
gsmiterlim(Op *op, Page *p)
{
	p->GSactive->ML = arrayget(p->stack, 0)->num.d;
	if(p->GSactive->ML < 0){
		werrstr("gsmiterlim: invalidmiter limit: %f", p->GSactive->ML);
		return -1;
	}
	return flagless(op);
}

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

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

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 *gs, *o;
	if((gs = dictget(dictget(dictget(p->obj, "Resources"), "ExtGState"), name)) == &null){
		werrstr("extgstate dictionary not found: %s", name);
		goto err;
	}
	if((o = dictget(gs, "Type")) != &null && strcmp(o->name, "ExtGState") != 0){
		werrstr("invalid type on ExtGState object: %s", o->name);
		goto err;
	}
	if((o = dictget(gs, "LW")) != &null){
		if(o->type != Onum){
			werrstr("line width is not a number");
			goto err;
		}
		p->GSactive->LW = o->num.d;
	}
	if((o = dictget(gs, "LC")) != &null){
		if(o->type != Onum){
			werrstr("line cap is not a number");
			goto err;
		}
		p->GSactive->LC = o->num.i;
	}
	if((o = dictget(gs, "LJ")) != &null){
		if(o->type != Onum){
			werrstr("line join is not a number");
			goto err;
		}
		p->GSactive->LJ = o->num.i;
	}
	if((o = dictget(gs, "ML")) != &null){
		if(o->type != Onum){
			werrstr("miter limit is not a number");
			goto err;
		}
		p->GSactive->ML = o->num.d;
	}
	return flagless(op);
err:
	werrstr("gsstate: %r");
	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 -1;
}

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

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

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

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

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

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

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

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

static char *colorspacedevices[] = {"DeviceGray", "DeviceRGB", "DeviceCMYK"};
static ColorSpace colorspacechans[] = {
	[1] = DeviceGray,
	[3] = DeviceRGB,
	[4] = DeviceCMYK,
};

static int
iccbasedcspace(Object *icc, ColorSpace *c)
{
	Object *o;

	*c = colorspacechans[dictint(icc, "N")];
	if((o = dictget(icc, "Alternate")) != &null){
		if(*c > 2 || strcmp(colorspacedevices[*c], o->name) != 0){
			werrstr("iccbasedcspace: TODO: handle non-device alternate %q", o->name);
			return -1;
		}
	}
	return 0;
}

static int
cspace(Op *op, Page *p)
{
	ColorSpace *c = (op->flags & Nonstroking) ? &p->GSactive->NSCS : &p->GSactive->SCS;
	u32int *color = (op->flags & Nonstroking) ? &p->GSactive->NSC : &p->GSactive->SC;
	char *s = arrayget(p->stack, 0)->name;
	Object *o;
	int i;

	for(i = 0; i < nelem(colorspacedevices); i++){
		if(strcmp(s, colorspacedevices[i]) == 0){
			*c = i;
			*color = i == 2 ? 0xFF : 0;
			return 0;
		}
	}
	if((o = dictget(dictget(p->obj, "Resources"), "ColorSpace")) == &null
		|| (o = dictget(o, s)) == &null){
		werrstr("colorspace not found: %q", s);
		return -1;
	}
	if(strcmp(arrayget(o, 0)->name, "ICCBased") == 0){
		/* Don't support ICC; fall back to Alternate or default */
		o = arrayget(o, 1);
		if(!iccbasedcspace(o, c))
			return -1;
		*color = *c == 2 ? 0xFF : 0;
		return 0;
	}
	werrstr("cspace: TODO: color space %q", arrayget(o, 0)->name);
	return -1;
}

static int
ccolour(Op *op, Page *p)
{
	ColorSpace c = op->flags & Nonstroking ? p->GSactive->NSCS : p->GSactive->SCS;
	u32int *color = op->flags & Nonstroking ? &p->GSactive->NSC : &p->GSactive->SC;

	if(c == DeviceRGB){
		*color = ((u8int)(255 * arrayget(p->stack, 0)->num.d)) << 24 | ((u8int)(255 * arrayget(p->stack, 1)->num.d)) << 16 | ((u8int)(255 * arrayget(p->stack, 2)->num.d)) << 8 | 0xff;
		return 0;
	}
	return -1;
}

static int
ccolour2(Op *op, Page *p)
{
	ColorSpace c = op->flags & Nonstroking ? p->GSactive->NSCS : p->GSactive->SCS;
	if(c < 3)
		return ccolour(op, p);
	return -1;
}

static int
cgray(Op *op, Page *p)
{
	int value = 255 * arrayget(p->stack, 0)->num.d;
	u32int *color;
	int i;

	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++)
		*color = (*color | value) << 8;
	*color |= 255;
	return 0;
}

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

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

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

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

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

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

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

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

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

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

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);
	p->GSactive->Font.widths = nil;
	p->GSactive->Font.first = -1;
	p->GSactive->Font.last = -1;
	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){
		werrstr("memory");
		return -1;
	}
	o = dictget(p->GSactive->Font.font, "Widths");
	if(o == nil)
		return -1;
	for(i = 0; i < arraylen(o); i++)
		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 0;
}

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: %q", name);
		return -1;
	}
	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) == 0 && flagless(op) == 0) ? 0 : -1;
}

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

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

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

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

static void
tmove(Page *p, double x, double y, int tlm)
{
	double *mat = tlm ? p->TS.Tlm : p->TS.Tm;

	mattrans(mat, x, y, mat);
	if(tlm)
		memcpy(p->TS.Tm, p->TS.Tlm, sizeof(double) * 6);
}

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;
	tmove(p, x->num.d, y->num.d, 1);
	return 0;
}

static int
tpmatrix(Op *op, Page *p)
{
	int i;

	for(i = 0; i < 6; i++){
		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)
{
	tmove(p, 0, 0 - p->TS.TL, 1);
	return 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;
	Object *o;

	if(p->GSactive->Font.enc != nil){
		len = arraylen(p->GSactive->Font.enc);
		for(i = 0; i < len; i++){
			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("→");
				break;
			}else
				d++;
		}
	}
	return bufput(&p->buf, (uchar*)&c, 1);
}

// Returns glyph width in TEXT SPACE units.
static double
glyphwidth(Page *p, ulong c)
{
	// TODO: type 3 fonts have explicit metrics? See 9.2.4 / 9.6.5 of the spec.
	// For all other fonts, units are 1/1000th of text space
	double gwidth = 435;
//	if(c >= p->GSactive->Font.first && c <= p->GSactive->Font.last)
//		gwidth = p->GSactive->Font.widths[c - p->GSactive->Font.first];
	return gwidth / 1000;
}

static double
glyphspacing(Page *p, ulong c)
{
	double Tfs = p->GSactive->Font.size;
	double w0 = glyphwidth(p, c);
	double tx = w0 * Tfs + p->TS.Tc;
	if(c == ' ')
		tx += p->TS.Tw;
	return tx;
}

/* Renders one character / glyph and updates the text state */
static int
tchar(Page *p, ulong c)
{
	double Tfs = p->GSactive->Font.size;
	double Trm[6];
	Memimage *ink;
	double tx;
	int i;

	/* Text rendering matrix converts coordinates from font space to device space */
	matscale(p->TS.Tm, Tfs, Tfs, Trm);
	matmult(Trm, p->GSactive->CTM, Trm);
	// Check if whitespace is needed
	if(p->buf.sz > 1){
		if(p->TS.y != Trm[5]){
			for(i = 0; i < (int)((Trm[5] - p->TS.y) / p->GSactive->Font.size / 1.5) && i < 2; i++)
				if(bufput(&p->buf, (uchar*)"\n", 1) != 1)
					return -1;
		}else if(Trm[4] - p->TS.x > 5){
			if(bufput(&p->buf, (uchar*)" ", 1) != 1)
				return -1;
		}
	}
	usize oend = p->buf.sz;
	if(!writepatched(p, c)){
		werrstr("tchar: failed to write character: %r");
 		return -1;
	}
	if(p->buf.b != nil){
		ink = allocmemimage(Rect(0,0,1,1),RGB24);
		if(ink == nil){
			werrstr("failed to allocate ink");
			return -1;
		}
		ink->flags |= Frepl;
		ink->clipr = Rect(-0x3FFFFFF, -0x3FFFFFF, 0x3FFFFFF, 0x3FFFFFF);
		memfillcolor(ink, p->GSactive->NSC);
		memimagestring(p->image, Pt(Trm[4], Trm[5]), ink, Pt(0,0), getmemdefont(), (char*)p->buf.b + oend);
		freememimage(ink);
	}
	tx = glyphspacing(p, c);
	mattrans(p->TS.Tm, tx, 0, p->TS.Tm);
	p->TS.x = Trm[4] + tx;
	p->TS.y = Trm[5];
	return 0;
}

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

static int
thshow(Op *op, Page *p)
{
	if(op->flags & TwTc){
		werrstr("TODO thshow TwTc");
		return -1;
	}
	if(op->flags & Nextline)
		tmove(p, 0, 0 - p->TS.TL, 1);
	Object *o = arrayget(p->stack, 0);
	if(!tstr(p, o->str, o->len))
		return -1;
	return 0;
}

static int
thshowarr(Op *op, Page *p)
{
	Object *o, *arr = arrayget(p->stack, 0);
	int i;

	for(i = 0; i < arraylen(arr); i++){
		o = arrayget(arr, i);
		if(o->type == Ostr){
			if(!tstr(p, o->str, o->len))
				return 0;
		}else{
			mattrans(p->TS.Tm, 0 - (p->GSactive->Font.size * o->num.d / 1000), 0, p->TS.Tm);
		}
	}
	return flagless(op);
}

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

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

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 -1;
}

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 -1;
}

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 -1;
}

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 -1;
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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,Nonstroking},            /* 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++;
	}
	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;
}

Object*
pagecropbox(Page *p)
{
	Object *page = p->obj;
	Object *o;
	while(page != &null){
		o = dictget(page, "CropBox");
		if(o != &null)
			return o;
		o = dictget(page, "MediaBox");
		if(o != &null)
			return o;
		page = dictget(page, "Parent");
	}
	return &null;
}

void
ctminit(Page *p, double *ctm, double w, double h)
{
	Object *cropbox;
	double mx, my;
	matidentity(ctm);
	cropbox = pagecropbox(p);
	mx = arrayget(cropbox, 2)->num.d;
	my = arrayget(cropbox, 3)->num.d;
	ctm[0] = w / mx;
	ctm[3] = 0-h / my;
	ctm[5] = h;
}

static void
tsinit(Page *p)
{
	p->TS.TL = 0;
	p->TS.Tc = 0;
	p->TS.Tw = 0;
}

void
gsinit(Page *p, GS *gs)
{
	USED(p);
	/* todo: actually initialize the full state */
	ctminit(p, gs->CTM, 1100, 1300);
	tsinit(p);
	gs->LW = 1.0;
	gs->LC = CapButt;
	gs->LJ = JoinMiter;
	gs->ML = 10.0;
	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);
}

void
pagegsclean(Page *p)
{
	int i;

	if(p->GSactive != nil){
		p->GSactive = nil;
		for(i = 0; i < p->nGS; i++)
			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 ? 0 : -1;
}

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;

	o = nil;
	if((s = Sopen(content)) == nil){
		werrstr("content: %O", content);
		goto err;
	}
	p->stack = arraynew(content->pdf);
	if(p->stack == nil)
		goto err;
	while(s->buf.off != s->buf.sz){
		while(isws(s->buf.b[s->buf.off]) && s->buf.off != s->buf.sz)
			s->buf.off++;
		if(s->buf.off == s->buf.sz)
			break;
		o = pdfobj(content->pdf, s, 0);
		if(o == nil){
			werrstr("failed to parse op: %r");
			goto err;
		}
		if(o->type == Oop){
			op = opfind(o->str);
			pdfobjfree(o);
			o = nil;
			if(op == nil){
				werrstr("unknown op: %s\n", o->str);
				goto err;
			}
			if(op->f(op, p) != 0){
				werrstr("%s failed: %r", op->s);
				goto err;
			}
			if(stackreset(p) != 0)
				goto err;
		}else{
			if(arrayadd(p->stack, o) != 0)
				goto err;
		}
	}
	if(bufput(&p->buf, (uchar*)"\n", 2) != 2)
		goto err;
	Sclose(s);

	return 0;
err:
	werrstr("pagerendercontent: %r");
	pdfobjfree(o);
	Sclose(s);
	return -1;
}

int
pagerender(Page *p)
{
	Object *content;
	int i;

	p->nGS = 1;
	p->GS = malloc(sizeof(*p->GS));
	if(p->GS == nil){
		werrstr("memory");
		goto err;
	}
	p->GSactive = p->GS;
	gsinit(p, p->GS);
	if((p->image = allocmemimage(Rect(0,0,1100,1300), strtochan("r8g8b8"))) == nil){
		werrstr("allocmemimage: %r");
		goto err;
	}
	memfillcolor(p->image, DWhite);
	content = dictget(p->obj, "Contents");
	if(content->type == Oarray){
		for(i = 0; i < arraylen(content); i++){
			if(pagerendercontent(p, arrayget(content, i)) != 0)
				goto err;
		}
	}else if(content->type != Onull && pagerendercontent(p, content) != 0)
		goto err;
	pagegsclean(p);

	return 0;
err:
	pagegsclean(p);
	return -1;
}