/*
 * PDF cleaning tool: general purpose pdf syntax washer.
 *
 * Rewrite PDF with pretty printed objects.
 * Garbage collect unreachable objects.
 * Inflate compressed streams.
 * Encrypt output.
 */

#include "pdftool.h"

static pdf_crypt *outcrypt = NULL;
static FILE *out = NULL;

static char *uselist = NULL;
static int *ofslist = NULL;
static int *genlist = NULL;

static int dogarbage = 0;
static int doexpand = 0;

/*
 * Garbage collect objects not reachable from the trailer.
 */

static fz_error sweepref(pdf_xref *xref, fz_obj *ref);

static fz_error sweepobj(pdf_xref *xref, fz_obj *obj)
{
    fz_error error;
    int i;

    if (fz_isindirect(obj))
	return sweepref(xref, obj);

    if (fz_isdict(obj))
    {
	for (i = 0; i < fz_dictlen(obj); i++)
	{
	    error = sweepobj(xref, fz_dictgetval(obj, i));
	    if (error)
		return error; /* too deeply nested for rethrow */
	}
    }

    if (fz_isarray(obj))
    {
	for (i = 0; i < fz_arraylen(obj); i++)
	{
	    error = sweepobj(xref, fz_arrayget(obj, i));
	    if (error)
		return error; /* too deeply nested for rethrow */
	}
    }

    return fz_okay;
}

static fz_error sweepref(pdf_xref *xref, fz_obj *ref)
{
    fz_error error;
    fz_obj *obj;
    fz_obj *len;
    int oid, gen;

    oid = fz_tonum(ref);
    gen = fz_tonum(ref);

    if (oid < 0 || oid >= xref->len)
	return fz_throw("object out of range (%d %d R)", oid, gen);

    if (uselist[oid])
	return fz_okay;

    uselist[oid] = 1;

    obj = fz_resolveindirect(ref);

    /* Bake in /Length in stream objects */
    if (xref->table[oid].stmofs)
    {
	len = fz_dictgets(obj, "Length");
	if (fz_isindirect(len))
	{
	    len = fz_resolveindirect(len);
	    fz_dictputs(obj, "Length", len);
	}
    }

    error = sweepobj(xref, obj);
    if (error)
    {
	fz_dropobj(obj);
	return error; /* too deeply nested for rethrow */
    }

    return fz_okay;
}

static void preloadobjstms(void)
{
    fz_error error;
    fz_obj *obj;
    int oid;

    for (oid = 0; oid < xref->len; oid++)
    {
	if (xref->table[oid].type == 'o')
	{
	    error = pdf_loadobject(&obj, xref, oid, 0);
	    if (error)
		die(error);
	    fz_dropobj(obj);
	}
    }
}

static void copystream(fz_obj *obj, int oid, int gen)
{
    fz_error error;
    fz_buffer *buf;

    error = pdf_loadrawstream(&buf, xref, oid, gen);
    if (error)
	die(error);

    fprintf(out, "%d %d obj\n", oid, gen);
    fz_fprintobj(out, obj, !doexpand);
    fprintf(out, "stream\n");
    fwrite(buf->rp, 1, buf->wp - buf->rp, out);
    fprintf(out, "endstream\nendobj\n\n");

    fz_dropbuffer(buf);
}

static void expandstream(fz_obj *obj, int oid, int gen)
{
    fz_error error;
    fz_buffer *buf;
    fz_obj *newdict, *newlen;

    error = pdf_loadstream(&buf, xref, oid, gen);
    if (error)
	die(error);

    fz_copydict(&newdict, obj);
    fz_dictdels(newdict, "Filter");
    fz_dictdels(newdict, "DecodeParms");

    fz_newint(&newlen, buf->wp - buf->rp);
    fz_dictputs(newdict, "Length", newlen);
    fz_dropobj(newlen);

    fprintf(out, "%d %d obj\n", oid, gen);
    fz_fprintobj(out, newdict, !doexpand);
    fprintf(out, "stream\n");
    fwrite(buf->rp, 1, buf->wp - buf->rp, out);
    fprintf(out, "endstream\nendobj\n\n");

    fz_dropobj(newdict);

    fz_dropbuffer(buf);
}

static void saveobject(int oid, int gen)
{
    fz_error error;
    fz_obj *obj;
    fz_obj *type;

    error = pdf_loadobject(&obj, xref, oid, gen);
    if (error)
	die(error);

    /* skip ObjStm and XRef objects */
    if (fz_isdict(obj))
    {
	type = fz_dictgets(obj, "Type");
	if (fz_isname(type) && !strcmp(fz_toname(type), "ObjStm"))
	{
	    uselist[oid] = 0;
	    fz_dropobj(obj);
	    return;
	}
	if (fz_isname(type) && !strcmp(fz_toname(type), "XRef"))
	{
	    uselist[oid] = 0;
	    fz_dropobj(obj);
	    return;
	}
    }


    if (!xref->table[oid].stmofs)
    {
	fprintf(out, "%d %d obj\n", oid, gen);
	fz_fprintobj(out, obj, !doexpand);
	fprintf(out, "endobj\n\n");
    }
    else
    {
	if (doexpand)
	    expandstream(obj, oid, gen);
	else
	    copystream(obj, oid, gen);
    }


    fz_dropobj(obj);
}

static void savexref(void)
{
    fz_obj *trailer;
    fz_obj *obj;
    int startxref;
    int oid;

    startxref = ftell(out);

    fprintf(out, "xref\n0 %d\n", xref->len);
    for (oid = 0; oid < xref->len; oid++)
    {
	if (uselist[oid])
	    fprintf(out, "%010d %05d n \n", ofslist[oid], genlist[oid]);
	else
	    fprintf(out, "%010d %05d f \n", ofslist[oid], genlist[oid]);
    }
    fprintf(out, "\n");

    fz_newdict(&trailer, 5);

    fz_newint(&obj, xref->len);
    fz_dictputs(trailer, "Size", obj);
    fz_dropobj(obj);

    obj = fz_dictgets(xref->trailer, "Info");
    if (obj)
	fz_dictputs(trailer, "Info", obj);

    obj = fz_dictgets(xref->trailer, "Root");
    if (obj)
	fz_dictputs(trailer, "Root", obj);

    obj = fz_dictgets(xref->trailer, "ID");
    if (obj)
	    fz_dictputs(trailer, "ID", obj);

    fprintf(out, "trailer\n");
    fz_fprintobj(out, trailer, !doexpand);
    fprintf(out, "\n");

    fprintf(out, "startxref\n%d\n%%%%EOF\n", startxref);
}

static void cleanusage(void)
{
    fprintf(stderr,
	    "usage: pdfclean [options] input.pdf [outfile.pdf]\n"
	    "  -p -\tpassword for decryption\n"
	    "  -g  \tgarbage collect unused objects\n"
	    "  -x  \texpand compressed streams\n");
    exit(1);
}

int main(int argc, char **argv)
{
    char *infile;
    char *outfile = "out.pdf";
    char *password = "";
    fz_error error;
    int c, oid;
    int lastfree;

    while ((c = fz_getopt(argc, argv, "gxp:")) != -1)
    {
	switch (c)
	{
	    case 'p': password = fz_optarg; break;
	    case 'g': dogarbage ++; break;
	    case 'x': doexpand ++; break;
	    default: cleanusage(); break;
	}
    }

    if (argc - fz_optind < 1)
	cleanusage();

    infile = argv[fz_optind++];
    if (argc - fz_optind > 0)
	outfile = argv[fz_optind++];

    openxref(infile, password, 0);

    out = fopen(outfile, "wb");
    if (!out)
	die(fz_throw("cannot open output file '%s'", outfile));

    fprintf(out, "%%PDF-%d.%d\n", xref->version / 10, xref->version % 10);
    fprintf(out, "%%\342\343\317\323\n\n");

    uselist = malloc(sizeof (char) * (xref->len + 1));
    ofslist = malloc(sizeof (int) * (xref->len + 1));
    genlist = malloc(sizeof (int) * (xref->len + 1));

    for (oid = 0; oid < xref->len; oid++)
    {
	uselist[oid] = 0;
	ofslist[oid] = 0;
	genlist[oid] = 0;
    }

    /* Make sure any objects hidden in compressed streams have been loaded */
    preloadobjstms();

    /* Sweep & mark objects from the trailer */
    error = sweepobj(xref, xref->trailer);
    if (error)
	die(fz_rethrow(error, "cannot mark used objects"));

    for (oid = 0; oid < xref->len; oid++)
    {
	if (xref->table[oid].type == 'f')
	    uselist[oid] = 0;

	if (xref->table[oid].type == 'f')
	    genlist[oid] = xref->table[oid].gen;
	if (xref->table[oid].type == 'n')
	    genlist[oid] = xref->table[oid].gen;
	if (xref->table[oid].type == 'o')
	    genlist[oid] = 0;

	if (dogarbage && !uselist[oid])
	    continue;

	if (xref->table[oid].type == 'n' || xref->table[oid].type == 'o')
	{
	    ofslist[oid] = ftell(out);
	    saveobject(oid, genlist[oid]);
	}
    }

    /* construct linked list of free object slots */
    lastfree = 0;
    for (oid = 0; oid < xref->len; oid++)
    {
	if (!uselist[oid])
	{
	    genlist[oid]++;
	    ofslist[lastfree] = oid;
	    lastfree = oid;
	}
    }

    savexref();

    closexref();
}

