int cli_pdf(const char *dir, cli_ctx *ctx, off_t offset)
{
    struct pdf_struct pdf;
    fmap_t *map = *ctx->fmap;
    size_t size = map->len - offset;
    off_t versize = size > 1032 ? 1032 : size;
    off_t map_off, bytesleft;
    long xref;
    const char *pdfver, *start, *eofmap, *q, *eof;
    int rc;
    unsigned i;

    cli_dbgmsg("in cli_pdf(%s)\n", dir);
    memset(&pdf, 0, sizeof(pdf));
    pdf.ctx = ctx;
    pdf.dir = dir;
    pdf.enc_objid = ~0u;

    pdfver = start = fmap_need_off_once(map, offset, versize);

    /* Check PDF version */
    if (!pdfver) {
	cli_errmsg("cli_pdf: mmap() failed (1)\n");
	return CL_EMAP;
    }
    /* offset is 0 when coming from filetype2 */
    pdfver = cli_memstr(pdfver, versize, "%PDF-", 5);
    if (!pdfver) {
	cli_dbgmsg("cli_pdf: no PDF- header found\n");
	return CL_SUCCESS;
    }
    /* Check for PDF-1.[0-9]. Although 1.7 is highest now, allow for future
     * versions */
    if (pdfver[5] != '1' || pdfver[6] != '.' ||
	pdfver[7] < '1' || pdfver[7] > '9') {
	pdf.flags |= 1 << BAD_PDF_VERSION;
	cli_dbgmsg("cli_pdf: bad pdf version: %.8s\n", pdfver);
    }
    if (pdfver != start || offset) {
	pdf.flags |= 1 << BAD_PDF_HEADERPOS;
	cli_dbgmsg("cli_pdf: PDF header is not at position 0: %ld\n",pdfver-start+offset);
    }
    offset += pdfver - start;

    /* find trailer and xref, don't fail if not found */
    map_off = (off_t)map->len - 2048;
    if (map_off < 0)
	map_off = 0;
    bytesleft = map->len - map_off;
    eofmap = fmap_need_off_once(map, map_off, bytesleft);
    if (!eofmap) {
	cli_errmsg("cli_pdf: mmap() failed (2)\n");
	return CL_EMAP;
    }
    eof = eofmap + bytesleft;
    for (q=&eofmap[bytesleft-5]; q > eofmap; q--) {
	if (memcmp(q, "%%EOF", 5) == 0)
	    break;
    }
    if (q <= eofmap) {
	pdf.flags |= 1 << BAD_PDF_TRAILER;
	cli_dbgmsg("cli_pdf: %%%%EOF not found\n");
    } else {
	const char *t;
	/*size = q - eofmap + map_off;*/
	q -= 9;
	for (;q > eofmap;q--) {
	    if (memcmp(q, "startxref", 9) == 0)
		break;
	}
	if (q <= eofmap) {
	    pdf.flags |= 1 << BAD_PDF_TRAILER;
	    cli_dbgmsg("cli_pdf: startxref not found\n");
	} else {
	    const char *enc;
	    for (t=q;t > eofmap; t--) {
		if (memcmp(t,"trailer",7) == 0)
		    break;
	    }

	    enc = cli_memstr(eofmap, bytesleft, "/Encrypt", 8);
	    if (enc) {
		pdf.flags |= 1 << ENCRYPTED_PDF;
		cli_dbgmsg("cli_pdf: encrypted pdf found, stream will probably fail to decompress!\n");
		pdf_parse_encrypt(&pdf, enc, eof - enc);
		pdf_parse_trailer(&pdf, eofmap, bytesleft);
	    }
	    q += 9;
	    while (q < eof && (*q == ' ' || *q == '\n' || *q == '\r')) { q++; }
	    xref = atol(q);
	    bytesleft = map->len - offset - xref;
	    if (bytesleft > 4096)
		bytesleft = 4096;
	    q = fmap_need_off_once(map, offset + xref, bytesleft);
	    if (!q || xrefCheck(q, q+bytesleft) == -1) {
		cli_dbgmsg("cli_pdf: did not find valid xref\n");
		pdf.flags |= 1 << BAD_PDF_TRAILER;
	    }
	}
    }
    size -= offset;

    pdf.size = size;
    pdf.map = fmap_need_off(map, offset, size);
    pdf.startoff = offset;
    if (!pdf.map) {
	cli_errmsg("cli_pdf: mmap() failed (3)\n");
	return CL_EMAP;
    }
    rc = run_pdf_hooks(&pdf, PDF_PHASE_PRE, -1, -1);
    if (rc) {
	cli_dbgmsg("cli_pdf: returning %d\n", rc);
	return rc == CL_BREAK ? CL_CLEAN : rc;
    }
    /* parse PDF and find obj offsets */
    while ((rc = pdf_findobj(&pdf)) > 0) {
	struct pdf_obj *obj = &pdf.objs[pdf.nobjs-1];
	cli_dbgmsg("cli_pdf: found %d %d obj @%ld\n", obj->id >> 8, obj->id&0xff, obj->start + offset);
    }
    if (pdf.nobjs)
	pdf.nobjs--;
    if (rc == -1)
	pdf.flags |= 1 << BAD_PDF_TOOMANYOBJS;

    /* must parse after finding all objs, so we can flag indirect objects */
    for (i=0;i<pdf.nobjs;i++) {
	struct pdf_obj *obj = &pdf.objs[i];
	pdf_parseobj(&pdf, obj);
    }

    pdf_handle_enc(&pdf);

    if (DETECT_ENCRYPTED &&
	(pdf.flags & (1 << ENCRYPTED_PDF)) &&
	!(pdf.flags & (1 << DECRYPTABLE_PDF))) {
	/* It is encrypted, and a password/key needs to be supplied to decrypt.
	 * This doesn't trigger for PDFs that are encrypted but don't need
	 * a password to decrypt */
	cli_append_virus(ctx, "Heuristics.Encrypted.PDF");
	rc = CL_VIRUS;
    }

    if (!rc)
	rc = run_pdf_hooks(&pdf, PDF_PHASE_PARSED, -1, -1);
    /* extract PDF objs */
    for (i=0;!rc && i<pdf.nobjs;i++) {
	struct pdf_obj *obj = &pdf.objs[i];
	rc = pdf_extract_obj(&pdf, obj);
    }

    if (pdf.flags & (1 << ENCRYPTED_PDF))
	pdf.flags &= ~ ((1 << BAD_FLATESTART) | (1 << BAD_STREAMSTART) |
	    (1 << BAD_ASCIIDECODE));

   if (pdf.flags && !rc) {
	cli_dbgmsg("cli_pdf: flags 0x%02x\n", pdf.flags);
	rc = run_pdf_hooks(&pdf, PDF_PHASE_END, -1, -1);
	if (!rc && (ctx->options & CL_SCAN_ALGORITHMIC)) {
	    if (pdf.flags & (1 << ESCAPED_COMMON_PDFNAME)) {
		/* for example /Fl#61te#44#65#63#6f#64#65 instead of /FlateDecode */
		cli_append_virus(ctx, "Heuristics.PDF.ObfuscatedNameObject");
		rc = cli_found_possibly_unwanted(ctx);
	    }
	}
#if 0
	/* TODO: find both trailers, and /Encrypt settings */
	if (pdf.flags & (1 << LINEARIZED_PDF))
	    pdf.flags &= ~ (1 << BAD_ASCIIDECODE);
	if (pdf.flags & (1 << MANY_FILTERS))
	    pdf.flags &= ~ (1 << BAD_ASCIIDECODE);
	if (!rc && (pdf.flags &
	    ((1 << BAD_PDF_TOOMANYOBJS) | (1 << BAD_STREAM_FILTERS) |
	     (1<<BAD_FLATE) | (1<<BAD_ASCIIDECODE)|
    	     (1<<UNTERMINATED_OBJ_DICT) | (1<<UNKNOWN_FILTER)))) {
	    rc = CL_EUNPACK;
	}
#endif
    }
    cli_dbgmsg("cli_pdf: returning %d\n", rc);
    free(pdf.objs);
    free(pdf.fileID);
    free(pdf.key);
    /* PDF hooks may abort, don't return CL_BREAK to caller! */
    return rc == CL_BREAK ? CL_CLEAN : rc;
}