/* pdfgen.c

   Copyright 2009 Taco Hoekwater <taco@luatex.org>

   This file is part of LuaTeX.

   LuaTeX is free software; you can redistribute it and/or modify it under
   the terms of the GNU General Public License as published by the Free
   Software Foundation; either version 2 of the License, or (at your
   option) any later version.

   LuaTeX is distributed in the hope that it will be useful, but WITHOUT
   ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
   FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
   License for more details.

   You should have received a copy of the GNU General Public License along
   with LuaTeX; if not, see <http://www.gnu.org/licenses/>. */

static const char __svn_version[] =
    "$Id: pdfgen.c 3025 2009-08-18 22:42:49Z hhenkel $"
    "$URL: http://foundry.supelec.fr/svn/luatex/tags/beta-0.43.0/source/texk/web2c/luatexdir/pdf/pdfgen.c $";

#include "ptexlib.h"
#include <ctype.h>
#include "md5.h"

/* for tokenlist_to_cstring */

#define is_hex_char isxdigit

#define check_nprintf(size_get, size_want) \
    if ((unsigned)(size_get) >= (unsigned)(size_want)) \
        pdftex_fail ("snprintf failed: file %s, line %d", __FILE__, __LINE__);

PDF static_pdf = NULL;

static char *jobname_cstr = NULL;

/* commandline interface */
integer pdf_output_option;
integer pdf_output_value;
integer pdf_draftmode_option;
integer pdf_draftmode_value;

halfword pdf_info_toks;         /* additional keys of Info dictionary */
halfword pdf_catalog_toks;      /* additional keys of Catalog dictionary */
halfword pdf_catalog_openaction;
halfword pdf_names_toks;        /* additional keys of Names dictionary */
halfword pdf_trailer_toks;      /* additional keys of Trailer dictionary */
boolean is_shipping_page;       /* set to |shipping_page| when |ship_out| starts */

/* init_pdf_struct() is called early, only once, from maincontrol.c */

PDF init_pdf_struct(PDF pdf)
{
    assert(pdf == NULL);
    pdf = xmalloc(sizeof(pdf_output_file));
    memset(pdf, 0, sizeof(pdf_output_file));

    pdf->o_mode = OMODE_NONE;   /* will be set by fix_o_mode() */
    pdf->o_status = ST_INITIAL;

    pdf->os_obj = xmalloc(pdf_os_max_objs * sizeof(os_obj_data));
    pdf->os_buf_size = inf_pdf_os_buf_size;
    pdf->os_buf = xmalloc(pdf->os_buf_size * sizeof(unsigned char));
    pdf->op_buf_size = inf_pdf_op_buf_size;
    pdf->op_buf = xmalloc(pdf->op_buf_size * sizeof(unsigned char));

    pdf->buf_size = pdf->op_buf_size;
    pdf->buf = pdf->op_buf;

    /* Sometimes it is neccesary to allocate memory for PDF output that cannot
       be deallocated then, so we use |mem| for this purpose. */
    pdf->mem_size = inf_pdf_mem_size;   /* allocated size of |mem| array */
    pdf->mem = xmalloc(pdf->mem_size * sizeof(int));
    pdf->mem_ptr = 1;           /* the first word is not used so we can use zero as a value for testing
                                   whether a pointer to |mem| is valid  */
    pdf->pstruct = NULL;

    pdf->posstruct = xmalloc(sizeof(posstructure));
    pdf->posstruct->pos.h = 0;
    pdf->posstruct->pos.v = 0;
    pdf->posstruct->dir = dir_TL_;

    pdf->obj_tab_size = inf_obj_tab_size;       /* allocated size of |obj_tab| array */
    pdf->obj_tab = xmalloc((pdf->obj_tab_size + 1) * sizeof(obj_entry));
    memset(pdf->obj_tab, 0, sizeof(obj_entry));

    pdf->minor_version = -1;    /* unset */
    pdf->decimal_digits = 4;
    pdf->gamma = 65536;
    pdf->image_gamma = 65536;
    pdf->image_hicolor = 1;
    pdf->image_apply_gamma = 0;
    pdf->objcompresslevel = 0;
    pdf->compress_level = 0;
    pdf->draftmode = 0;
    pdf->inclusion_copy_font = 1;
    pdf->replace_font = 0;
    pdf->pk_resolution = 0;
    pdf->pk_scale_factor = 0;

    init_dest_names(pdf);
    pdf->resources = NULL;

    init_pdf_pagecalculations(pdf);

    return pdf;
}

static void pdf_shipout_begin(boolean shipping_page)
{
    pos_stack_used = 0;         /* start with empty stack */

    page_mode = shipping_page;
    if (shipping_page) {
        colorstackpagestart();
    }
}

static void pdf_shipout_end(boolean shipping_page)
{
    if (pos_stack_used > 0) {
        pdftex_fail("%u unmatched \\pdfsave after %s shipout",
                    (unsigned int) pos_stack_used,
                    ((shipping_page) ? "page" : "form"));
    }
}

/*
  We use |pdf_get_mem| to allocate memory in |mem|
*/

integer pdf_get_mem(PDF pdf, integer s)
{                               /* allocate |s| words in |mem| */
    integer a;
    integer ret;
    if (s > sup_pdf_mem_size - pdf->mem_ptr)
        overflow("PDF memory size (pdf_mem_size)", pdf->mem_size);
    if (pdf->mem_ptr + s > pdf->mem_size) {
        a = 0.2 * pdf->mem_size;
        if (pdf->mem_ptr + s > pdf->mem_size + a) {
            pdf->mem_size = pdf->mem_ptr + s;
        } else if (pdf->mem_size < sup_pdf_mem_size - a) {
            pdf->mem_size = pdf->mem_size + a;
        } else {
            pdf->mem_size = sup_pdf_mem_size;
        }
        pdf->mem = xreallocarray(pdf->mem, int, pdf->mem_size);
    }
    ret = pdf->mem_ptr;
    pdf->mem_ptr = pdf->mem_ptr + s;
    return ret;
}

/*
|fix_o_mode| freezes |pdf->o_mode| as soon as anything goes through
the backend, be it \.{PDF}, \.{DVI}, or \.{Lua}.
*/

#define pdf_output int_par(pdf_output_code)

void fix_o_mode(PDF pdf)
{
    static int fixed_pdf_output = 0;
    if (pdf->o_mode == OMODE_NONE) {
        if (pdf_output > 0) {
            if (pdf_output == 2009)
                pdf->o_mode = OMODE_LUA;
            else
                pdf->o_mode = OMODE_PDF;
        } else
            pdf->o_mode = OMODE_DVI;
        fixed_pdf_output = pdf_output;
    } else if (pdf_output != fixed_pdf_output) {
        pdf_error("setup",
                  "\\pdfoutput can only be changed before anything is written to the output");
    }
}

/*
This ensures that |pdfminorversion| is set only before any bytes have
been written to the generated \.{PDF} file. Here also all variables for
\.{PDF} output are initialized, the \.{PDF} file is opened by |ensure_pdf_open|,
and the \.{PDF} header is written.
*/

void fix_pdf_minorversion(PDF pdf)
{
    if (pdf->minor_version < 0) {       /* unset */
        if ((pdf_minor_version < 0) || (pdf_minor_version > 9)) {
            char *hlp[] = { "The pdfminorversion must be between 0 and 9.",
                "I changed this to 4.", NULL
            };
            char msg[256];
            (void) snprintf(msg, 255,
                            "LuaTeX error (illegal pdfminorversion %d)",
                            (int) pdf_minor_version);
            tex_error(msg, hlp);
            pdf_minor_version = 4;
        }
        pdf->minor_version = pdf_minor_version;
    } else {
        /* Check that variables for \.{PDF} output are unchanged */
        if (pdf->minor_version != int_par(pdf_minor_version_code))
            pdf_error("setup",
                      "\\pdfminorversion cannot be changed after data is written to the PDF file");
        if (pdf->draftmode != int_par(pdf_draftmode_code))
            pdf_error("setup",
                      "\\pdfdraftmode cannot be changed after data is written to the PDF file");
    }
    if (pdf->draftmode != 0) {
        pdf->compress_level = 0;        /* re-fix it, might have been changed inbetween */
        pdf->objcompresslevel = 0;
    }
}

/*
The PDF buffer is flushed by calling |pdf_flush|, which checks the
variable |zip_write_state| and will compress the buffer before flushing if
neccesary. We call |pdf_begin_stream| to begin a stream  and |pdf_end_stream|
to finish it. The stream contents will be compressed if compression is turn on.
*/

/* writepdf() always writes by fwrite() */

static void write_pdf(PDF pdf, integer a, int b)
{
    (void) fwrite((char *) &pdf->buf[a], sizeof(pdf->buf[a]),
                  (int) ((b) - (a) + 1), pdf->file);
}

void pdf_flush(PDF pdf)
{                               /* flush out the |pdf_buf| */

    off_t saved_pdf_gone;
    if (!pdf->os_mode) {
        saved_pdf_gone = pdf->gone;
        switch (pdf->zip_write_state) {
        case no_zip:
            if (pdf->ptr > 0) {
                if (pdf->draftmode == 0)
                    write_pdf(pdf, 0, pdf->ptr - 1);
                pdf->gone += pdf->ptr;
                pdf->last_byte = pdf->buf[pdf->ptr - 1];
            }
            break;
        case zip_writing:
            if (pdf->draftmode == 0)
                write_zip(pdf, false);
            break;
        case zip_finish:
            if (pdf->draftmode == 0)
                write_zip(pdf, true);
            pdf->zip_write_state = no_zip;
            break;
        }
        pdf->ptr = 0;
        if (saved_pdf_gone > pdf->gone)
            pdf_error("file size",
                      "File size exceeds architectural limits (pdf_gone wraps around)");
    }
}


/* switch between PDF stream and object stream mode */
void pdf_os_switch(PDF pdf, boolean pdf_os)
{
    if (pdf_os && pdf->os_enable) {
        if (!pdf->os_mode) {    /* back up PDF stream variables */
            pdf->op_ptr = pdf->ptr;
            pdf->ptr = pdf->os_ptr;
            pdf->buf = pdf->os_buf;
            pdf->buf_size = pdf->os_buf_size;
            pdf->os_mode = true;        /* switch to object stream */
        }
    } else {
        if (pdf->os_mode) {     /* back up object stream variables */
            pdf->os_ptr = pdf->ptr;
            pdf->ptr = pdf->op_ptr;
            pdf->buf = pdf->op_buf;
            pdf->buf_size = pdf->op_buf_size;
            pdf->os_mode = false;       /* switch to PDF stream */
        }
    }
}

/* create new \.{/ObjStm} object if required, and set up cross reference info */
void pdf_os_prepare_obj(PDF pdf, integer i, integer pdf_os_level)
{
    pdf_os_switch(pdf, ((pdf_os_level > 0)
                        && (pdf->objcompresslevel >= pdf_os_level)));
    if (pdf->os_mode) {
        if (pdf->os_cur_objnum == 0) {
            pdf->os_cur_objnum = pdf_new_objnum(pdf);
            pdf->obj_ptr--;     /* object stream is not accessible to user */
            pdf->os_cntr++;     /* only for statistics */
            pdf->os_idx = 0;
            pdf->ptr = 0;       /* start fresh object stream */
        } else {
            pdf->os_idx++;
        }
        obj_os_idx(pdf, i) = pdf->os_idx;
        obj_offset(pdf, i) = pdf->os_cur_objnum;
        pdf->os_obj[pdf->os_idx].num = i;
        pdf->os_obj[pdf->os_idx].off = pdf->ptr;
    } else {
        obj_offset(pdf, i) = pdf_offset(pdf);
        obj_os_idx(pdf, i) = -1;        /* mark it as not included in object stream */
    }
}

/* low-level buffer checkers */

/* check that |s| bytes more fit into |pdf_os_buf|; increase it if required */
static void pdf_os_get_os_buf(PDF pdf, integer s)
{
    integer a;
    if (s > sup_pdf_os_buf_size - pdf->ptr)
        overflow("PDF object stream buffer", pdf->os_buf_size);
    if (pdf->ptr + s > pdf->os_buf_size) {
        a = 0.2 * pdf->os_buf_size;
        if (pdf->ptr + s > pdf->os_buf_size + a)
            pdf->os_buf_size = pdf->ptr + s;
        else if (pdf->os_buf_size < sup_pdf_os_buf_size - a)
            pdf->os_buf_size = pdf->os_buf_size + a;
        else
            pdf->os_buf_size = sup_pdf_os_buf_size;
        pdf->os_buf =
            xreallocarray(pdf->os_buf, unsigned char, pdf->os_buf_size);
        pdf->buf = pdf->os_buf;
        pdf->buf_size = pdf->os_buf_size;
    }
}

/* make sure that there are at least |n| bytes free in PDF buffer */
void pdf_room(PDF pdf, integer n)
{
    if (pdf->os_mode && (n + pdf->ptr > pdf->buf_size))
        pdf_os_get_os_buf(pdf, n);
    else if ((!pdf->os_mode) && (n > pdf->buf_size))
        overflow("PDF output buffer", pdf->op_buf_size);
    else if ((!pdf->os_mode) && (n + pdf->ptr > pdf->buf_size))
        pdf_flush(pdf);
}


/* print out a character to PDF buffer; the character will be printed in octal
 * form in the following cases: chars <= 32, backslash (92), left parenthesis
 * (40) and  right parenthesis (41)
 */

#define pdf_print_escaped(c)                                            \
  if ((c)<=32||(c)=='\\'||(c)=='('||(c)==')'||(c)>127) {                \
    pdf_room(pdf,4);                                                    \
    pdf_quick_out(pdf,'\\');                                                \
    pdf_quick_out(pdf,'0' + (((c)>>6) & 0x3));                              \
    pdf_quick_out(pdf,'0' + (((c)>>3) & 0x7));                              \
    pdf_quick_out(pdf,'0' + ( (c)     & 0x7));                              \
  } else {                                                              \
    pdf_out(pdf,(c));                                                       \
  }

void pdf_print_char(PDF pdf, int c)
{
    if (c > 255)
        return;
    pdf_print_escaped(c);
}

void pdf_print_wide_char(PDF pdf, int c)
{
    char hex[5];
    snprintf(hex, 5, "%04X", c);
    pdf_room(pdf, 4);
    pdf_quick_out(pdf, hex[0]);
    pdf_quick_out(pdf, hex[1]);
    pdf_quick_out(pdf, hex[2]);
    pdf_quick_out(pdf, hex[3]);
} void pdf_puts(PDF pdf, const char *s)
{

    size_t l = strlen(s);
    if (l < (size_t) pdf->buf_size) {
        pdf_room(pdf, l);
        while (*s != '\0')
            pdf_quick_out(pdf, *s++);
    } else {
        while (*s != '\0')
            pdf_out(pdf, *s++);
    }
}

__attribute__ ((format(printf, 2, 3)))
void pdf_printf(PDF pdf, const char *fmt, ...)
{
    va_list args;
    va_start(args, fmt);
    if (pdf->printf_buf == NULL) {
        pdf->printf_buf = xtalloc(PRINTF_BUF_SIZE, char);
    }
    (void) vsnprintf(pdf->printf_buf, PRINTF_BUF_SIZE, fmt, args);
    pdf_puts(pdf, pdf->printf_buf);
    va_end(args);
}


/* print out a string to PDF buffer */

void pdf_print(PDF pdf, str_number s)
{
    if (s < number_chars) {
        assert(s < 256);
        pdf_out(pdf, s);
    } else {
        register pool_pointer j = str_start_macro(s);
        while (j < str_start_macro(s + 1)) {
            pdf_out(pdf, str_pool[j++]);
        }
    }
}

/* print out a integer to PDF buffer */

void pdf_print_int(PDF pdf, longinteger n)
{
    register integer k = 0;     /*  current digit; we assume that $|n|<10^{23}$ */
    integer dig[24];
    if (n < 0) {
        pdf_out(pdf, '-');
        if (n < -0x7FFFFFFF) {  /* need to negate |n| more carefully */
            register longinteger m;
            k++;
            m = -1 - n;
            n = m / 10;
            m = (m % 10) + 1;
            if (m < 10) {
                dig[0] = m;
            } else {
                dig[0] = 0;
                n++;
            }
        } else {
            n = -n;
        }
    }
    do {
        dig[k++] = n % 10;
        n = n / 10;
    } while (n != 0);
    pdf_room(pdf, k);
    while (k-- > 0) {
        pdf_quick_out(pdf, '0' + dig[k]);
    }
}


/* print $m/10^d$ as real */
void pdf_print_real(PDF pdf, integer m, integer d)
{
    if (m < 0) {
        pdf_out(pdf, '-');
        m = -m;
    };
    pdf_print_int(pdf, m / ten_pow[d]);
    m = m % ten_pow[d];
    if (m > 0) {
        pdf_out(pdf, '.');
        d--;
        while (m < ten_pow[d]) {
            pdf_out(pdf, '0');
            d--;
        }
        while (m % 10 == 0)
            m = m / 10;
        pdf_print_int(pdf, m);
    }
}

/* print out |s| as string in PDF output */

void pdf_print_str(PDF pdf, char *s)
{
    char *orig = s;
    int l = strlen(s) - 1;      /* last string index */
    if (l < 0) {
        pdf_puts(pdf, "()");
        return;
    }
    /* the next is not really safe, the string could be "(a)xx(b)" */
    if ((s[0] == '(') && (s[l] == ')')) {
        pdf_puts(pdf, s);
        return;
    }
    if ((s[0] != '<') || (s[l] != '>') || odd((l + 1))) {
        pdf_out(pdf, '(');
        pdf_puts(pdf, s);
        pdf_out(pdf, ')');
        return;
    }
    s++;
    while (is_hex_char(*s))
        s++;
    if (s != orig + l) {
        pdf_out(pdf, '(');
        pdf_puts(pdf, orig);
        pdf_out(pdf, ')');
        return;
    }
    pdf_puts(pdf, orig);        /* it was a hex string after all  */
}


/* begin a stream */
void pdf_begin_stream(PDF pdf)
{
    assert(pdf->os_mode == false);
    pdf_puts(pdf, "/Length           \n");
    pdf->seek_write_length = true;      /* fill in length at |pdf_end_stream| call */
    pdf->stream_length_offset = pdf_offset(pdf) - 11;
    pdf->stream_length = 0;
    pdf->last_byte = 0;
    if (pdf->compress_level > 0) {
        pdf_puts(pdf, "/Filter /FlateDecode\n");
        pdf_puts(pdf, ">>\n");
        pdf_puts(pdf, "stream\n");
        pdf_flush(pdf);
        pdf->zip_write_state = zip_writing;
    } else {
        pdf_puts(pdf, ">>\n");
        pdf_puts(pdf, "stream\n");
        pdf_save_offset(pdf);
    }
}

/* end a stream */
void pdf_end_stream(PDF pdf)
{
    if (pdf->zip_write_state == zip_writing)
        pdf->zip_write_state = zip_finish;
    else
        pdf->stream_length = pdf_offset(pdf) - pdf_saved_offset(pdf);
    pdf_flush(pdf);
    if (pdf->seek_write_length)
        write_stream_length(pdf, pdf->stream_length, pdf->stream_length_offset);
    pdf->seek_write_length = false;
    if (pdf->last_byte != pdf_new_line_char)
        pdf_out(pdf, pdf_new_line_char);
    pdf_puts(pdf, "endstream\n");
    pdf_end_obj(pdf);
}

void pdf_remove_last_space(PDF pdf)
{
    if ((pdf->ptr > 0) && (pdf->buf[pdf->ptr - 1] == ' '))
        pdf->ptr--;
}


/*
To print |scaled| value to PDF output we need some subroutines to ensure
accurary.
*/

#define max_integer 0x7FFFFFFF  /* $2^{31}-1$ */

/* scaled value corresponds to 100in, exact, 473628672 */
scaled one_hundred_inch = 7227 * 65536;

/* scaled value corresponds to 1in (rounded to 4736287) */
scaled one_inch = (7227 * 65536 + 50) / 100;

    /* scaled value corresponds to 1truein (rounded!) */
scaled one_true_inch = (7227 * 65536 + 50) / 100;

/* scaled value corresponds to 100bp */
scaled one_hundred_bp = (7227 * 65536) / 72;

/* scaled value corresponds to 1bp (rounded to 65782) */
scaled one_bp = ((7227 * 65536) / 72 + 50) / 100;

/* $10^0..10^9$ */
integer ten_pow[10] = {
    1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000
};

/*
The function |divide_scaled| divides |s| by |m| using |dd| decimal
digits of precision. It is defined in C because it is a good candidate
for optimizations that are not possible in pascal.
*/

scaled round_xn_over_d(scaled x, integer n, integer d)
{
    boolean positive;           /* was |x>=0|? */
    unsigned t, u, v;           /* intermediate quantities */
    if (x >= 0) {
        positive = true;
    } else {
        x = -(x);
        positive = false;
    }
    t = (x % 0100000) * n;
    u = (x / 0100000) * n + (t / 0100000);
    v = (u % d) * 0100000 + (t % 0100000);
    if (u / d >= 0100000)
        arith_error = true;
    else
        u = 0100000 * (u / d) + (v / d);
    v = v % d;
    if (2 * v >= (unsigned) d)
        u++;
    if (positive)
        return u;
    else
        return (-u);
}

#define lround(a) (long) floor((a) + 0.5)

void pdf_print_bp(PDF pdf, scaled s)
{                               /* print scaled as |bp| */
    pdffloat a;
    pdfstructure *p = pdf->pstruct;
    assert(p != NULL);
    a.m = lround(s * p->k1);
    a.e = pdf->decimal_digits;
    print_pdffloat(pdf, a);
}

void pdf_print_mag_bp(PDF pdf, scaled s)
{                               /* take |mag| into account */
    pdffloat a;
    pdfstructure *p = pdf->pstruct;
    prepare_mag();
    if (int_par(mag_code) != 1000)
        a.m = lround(s * (long) int_par(mag_code) / 1000.0 * p->k1);
    else
        a.m = lround(s * p->k1);
    a.e = pdf->decimal_digits;
    print_pdffloat(pdf, a);
}

#define set_p_or_return(a) do {                 \
        p = a;                                  \
        if (p==NULL) {                          \
            a = item;                           \
            return;                             \
        }                                       \
    } while (0)


/**********************************************************************/

void reset_resource_lists(pdf_resource_struct * p)
{
    p->obj_list = NULL;
    p->font_list = NULL;
    p->xform_list = NULL;
    p->ximage_list = NULL;
    p->dest_list = NULL;
    p->link_list = NULL;
    p->annot_list = NULL;
    p->bead_list = NULL;
    p->text_procset = false;
    p->image_procset = 0;
}

void append_object_list(PDF pdf, pdf_obj_type t, integer f)
{
    pdf_object_list *p;
    pdf_object_list *item = xmalloc(sizeof(pdf_object_list));
    item->link = NULL;
    item->info = f;
    switch (t) {
    /* *INDENT-OFF* */
    case obj_type_obj:    set_p_or_return(pdf->resources->obj_list);    break;
    case obj_type_font:   set_p_or_return(pdf->resources->font_list);   break;
    case obj_type_xform:  set_p_or_return(pdf->resources->xform_list);  break;
    case obj_type_ximage: set_p_or_return(pdf->resources->ximage_list); break;
    case obj_type_dest:   set_p_or_return(pdf->resources->dest_list);   break;
    case obj_type_link:   set_p_or_return(pdf->resources->link_list);   break;
    case obj_type_annot:  set_p_or_return(pdf->resources->annot_list);  break;
    case obj_type_bead:   set_p_or_return(pdf->resources->bead_list);   break;
    default:              assert(0);                         break;
    /* *INDENT-ON* */
    }
    while (p->link != NULL)
        p = p->link;
    p->link = item;
    return;
}

/* return zero on failure, or the object  */
pdf_object_list *lookup_object_list(PDF pdf, pdf_obj_type t, integer f)
{
    pdf_object_list *p;
    switch (t) {
    /* *INDENT-OFF* */
    case obj_type_obj:    p = pdf->resources->obj_list; break;
    case obj_type_font:   p = pdf->resources->font_list; break;
    case obj_type_xform:  p = pdf->resources->xform_list; break;
    case obj_type_ximage: p = pdf->resources->ximage_list; break;
    case obj_type_dest:   p = pdf->resources->dest_list; break;
    case obj_type_link:   p = pdf->resources->link_list; break;
    case obj_type_annot:  p = pdf->resources->annot_list; break;
    case obj_type_bead:   p = pdf->resources->bead_list; break;
    default:              return NULL;  break;  /* shouldnt happen */
    /* *INDENT-ON* */
    }
    while (p != NULL) {
        if (p->info == f)
            return p;
        p = p->link;
    }
    return NULL;
}

static void flush_object_list(PDF pdf, pdf_obj_type t)
{
    pdf_object_list *q, *p;
    switch (t) {
    /* *INDENT-OFF* */
    case obj_type_obj:    p = pdf->resources->obj_list; break;
    case obj_type_font:   p = pdf->resources->font_list; break;
    case obj_type_xform:  p = pdf->resources->xform_list; break;
    case obj_type_ximage: p = pdf->resources->ximage_list; break;
    case obj_type_dest:   p = pdf->resources->dest_list; break;
    case obj_type_link:   p = pdf->resources->link_list; break;
    case obj_type_annot:  p = pdf->resources->annot_list; break;
    case obj_type_bead:   p = pdf->resources->bead_list; break;
    default:              return;  break;
    /* *INDENT-ON* */
    }
    while (p != NULL) {
        q = p;
        p = p->link;
        free(q);
    }
    switch (t) {
    /* *INDENT-OFF* */
    case obj_type_obj:    pdf->resources->obj_list = NULL; break;
    case obj_type_font:   pdf->resources->font_list = NULL; break;
    case obj_type_xform:  pdf->resources->xform_list = NULL; break;
    case obj_type_ximage: pdf->resources->ximage_list = NULL; break;
    case obj_type_dest:   pdf->resources->dest_list = NULL; break;
    case obj_type_link:   pdf->resources->link_list = NULL; break;
    case obj_type_annot:  pdf->resources->annot_list = NULL; break;
    case obj_type_bead:   pdf->resources->bead_list = NULL; break;
    default:              break; /* cant happen */
    /* *INDENT-ON* */
    }
}

void flush_resource_lists(PDF pdf)
{
    flush_object_list(pdf, obj_type_font);
    flush_object_list(pdf, obj_type_obj);
    flush_object_list(pdf, obj_type_xform);
    flush_object_list(pdf, obj_type_ximage);
    /* the following lists are nonzero only if shipping_page */
    flush_object_list(pdf, obj_type_annot);
    flush_object_list(pdf, obj_type_link);
    flush_object_list(pdf, obj_type_dest);
    flush_object_list(pdf, obj_type_bead);
}

/**********************************************************************/

/* Subroutines to print out various PDF objects */

/* print out an integer with fixed width; used for outputting cross-reference table */
void pdf_print_fw_int(PDF pdf, longinteger n, integer w)
{
    integer k;                  /* $0\le k\le23$ */
    integer dig[24];
    k = 0;
    do {
        dig[k] = n % 10;
        n = n / 10;
        k++;
    } while (k != w);
    pdf_room(pdf, k);
    while (k-- > 0)
        pdf_quick_out(pdf, '0' + dig[k]);
}

/* print out an integer as a number of bytes; used for outputting \.{/XRef} cross-reference stream */
void pdf_out_bytes(PDF pdf, longinteger n, integer w)
{
    integer k;
    integer bytes[8];           /* digits in a number being output */
    k = 0;
    do {
        bytes[k] = n % 256;
        n = n / 256;
        k++;
    } while (k != w);
    pdf_room(pdf, k);
    while (k-- > 0)
        pdf_quick_out(pdf, bytes[k]);
}

/* print out an entry in dictionary with integer value to PDF buffer */

void pdf_int_entry(PDF pdf, char *s, integer v)
{
    pdf_printf(pdf, "/%s ", s);
    pdf_print_int(pdf, v);
} void pdf_int_entry_ln(PDF pdf, char *s, integer v)
{

    pdf_int_entry(pdf, s, v);
    pdf_print_nl(pdf);
}

/* print out an indirect entry in dictionary */
void pdf_indirect(PDF pdf, char *s, integer o)
{
    pdf_printf(pdf, "/%s %d 0 R", s, (int) o);
} void pdf_indirect_ln(PDF pdf, char *s, integer o)
{

    pdf_indirect(pdf, s, o);
    pdf_print_nl(pdf);
}

/* print out |s| as string in PDF output */
void pdf_print_str_ln(PDF pdf, char *s)
{
    pdf_print_str(pdf, s);
    pdf_print_nl(pdf);
}

/* print out an entry in dictionary with string value to PDF buffer */
void pdf_str_entry(PDF pdf, char *s, char *v)
{
    if (v == 0)
        return;
    pdf_printf(pdf, "/%s ", s);
    pdf_print_str(pdf, v);
}

void pdf_str_entry_ln(PDF pdf, char *s, char *v)
{
    if (v == 0)
        return;
    pdf_str_entry(pdf, s, v);
    pdf_print_nl(pdf);
}

void pdf_print_toks(PDF pdf, halfword p)
{
    int len = 0;
    char *s = tokenlist_to_cstring(p, true, &len);
    if (len > 0)
        pdf_puts(pdf, s);
    xfree(s);
}


void pdf_print_toks_ln(PDF pdf, halfword p)
{
    int len = 0;
    char *s = tokenlist_to_cstring(p, true, &len);
    if (len > 0) {
        pdf_puts(pdf, s);
        pdf_print_nl(pdf);
    }
    xfree(s);
}

/* prints a rect spec */
void pdf_print_rect_spec(PDF pdf, halfword r)
{
    pdf_print_mag_bp(pdf, pdf_ann_left(r));
    pdf_out(pdf, ' ');
    pdf_print_mag_bp(pdf, pdf_ann_bottom(r));
    pdf_out(pdf, ' ');
    pdf_print_mag_bp(pdf, pdf_ann_right(r));
    pdf_out(pdf, ' ');
    pdf_print_mag_bp(pdf, pdf_ann_top(r));
}

/* output a rectangle specification to PDF file */
void pdf_rectangle(PDF pdf, halfword r)
{
    prepare_mag();
    pdf_puts(pdf, "/Rect [");
    pdf_print_rect_spec(pdf, r);
    pdf_puts(pdf, "]\n");
}

static void init_pdf_outputparameters(PDF pdf)
{
    assert(pdf->o_mode == OMODE_PDF);
    pdf->draftmode = fix_int(pdf_draftmode, 0, 1);
    pdf->compress_level = fix_int(pdf_compress_level, 0, 9);
    pdf->decimal_digits = fix_int(pdf_decimal_digits, 0, 4);
    pdf->gamma = fix_int(pdf_gamma, 0, 1000000);
    pdf->image_gamma = fix_int(pdf_image_gamma, 0, 1000000);
    pdf->image_hicolor = fix_int(pdf_image_hicolor, 0, 1);
    pdf->image_apply_gamma = fix_int(pdf_image_apply_gamma, 0, 1);
    pdf->objcompresslevel = fix_int(pdf_objcompresslevel, 0, 3);
    pdf->inclusion_copy_font = fix_int(pdf_inclusion_copy_font, 0, 1);
    pdf->replace_font = fix_int(pdf_replace_font, 0, 1);
    pdf->pk_resolution = fix_int(pdf_pk_resolution, 72, 8000);
    if ((pdf->minor_version >= 5) && (pdf->objcompresslevel > 0)) {
        pdf->os_enable = true;
    } else {
        if (pdf->objcompresslevel > 0) {
            pdf_warning("Object streams",
                        "\\pdfobjcompresslevel > 0 requires \\pdfminorversion > 4. Object streams disabled now.",
                        true, true);
            pdf->objcompresslevel = 0;
        }
        pdf->os_enable = false;
    }
    if (pdf->pk_resolution == 0)        /* if not set from format file or by user */
        pdf->pk_resolution = pk_dpi;    /* take it from \.{texmf.cnf} */
    pdf->pk_scale_factor =
        divide_scaled(72, pdf->pk_resolution, 5 + pdf->decimal_digits);
    if (!callback_defined(read_pk_file_callback)) {
        if (pdf_pk_mode != null) {
            char *s = tokenlist_to_cstring(pdf_pk_mode, true, NULL);
            kpseinitprog("PDFTEX", pdf->pk_resolution, s, nil);
            xfree(s);
        } else {
            kpseinitprog("PDFTEX", pdf->pk_resolution, nil, nil);
        }
        if (!kpsevarvalue("MKTEXPK"))
            kpsesetprogramenabled(kpsepkformat, 1, kpsesrccmdline);
    }
    set_job_id(pdf, int_par(year_code),
               int_par(month_code), int_par(day_code), int_par(time_code));
    if ((pdf_unique_resname > 0) && (pdf->resname_prefix == NULL))
        pdf->resname_prefix = get_resname_prefix(pdf);
}

/**********************************************************************/

/* Checks that we have a name for the generated PDF file and that it's open. */

static void ensure_pdf_open(PDF pdf)
{
    if (pdf->file_name != NULL)
        return;
    if (job_name == 0)
        open_log_file();
    pack_job_name(".pdf");
    if (pdf->draftmode == 0) {
        while (!lua_b_open_out(pdf->file))
            prompt_file_name("file name for output", ".pdf");
    }
    pdf->file = name_file_pointer;      /* hm ? */
    pdf->file_name = xstrdup(makecstring(make_name_string()));
}

void ensure_pdf_header_written(PDF pdf)
{
    static boolean header_written = false;      /* kludge, should be in pdf */
    if (!header_written) {
        assert(pdf->o_mode == OMODE_PDF);
        ensure_pdf_open(pdf);
        /* Initialize variables for \.{PDF} output */
        fix_pdf_minorversion(pdf);
        init_pdf_outputparameters(pdf);
        /* Write \.{PDF} header */
        pdf_printf(pdf, "%%PDF-1.%d\n", pdf->minor_version);
        pdf_out(pdf, '%');
        pdf_out(pdf, 'P' + 128);
        pdf_out(pdf, 'T' + 128);
        pdf_out(pdf, 'E' + 128);
        pdf_out(pdf, 'X' + 128);
        pdf_print_nl(pdf);
        header_written = true;
    }
}

/* begin a PDF dictionary object */
void pdf_begin_dict(PDF pdf, integer i, integer pdf_os_level)
{
    ensure_pdf_header_written(pdf);
    pdf_os_prepare_obj(pdf, i, pdf_os_level);
    if (!pdf->os_mode) {
        pdf_printf(pdf, "%d 0 obj <<\n", (int) i);
    } else {
        if (pdf->compress_level == 0)
            pdf_printf(pdf, "%% %d 0 obj\n", (int) i);  /* debugging help */
        pdf_puts(pdf, "<<\n");
    }
}

/* begin a new PDF dictionary object */
void pdf_new_dict(PDF pdf, integer t, integer i, integer pdf_os)
{
    pdf_create_obj(pdf, t, i);
    pdf_begin_dict(pdf, pdf->obj_ptr, pdf_os);
}

/* end a PDF dictionary object */
void pdf_end_dict(PDF pdf)
{
    if (pdf->os_mode) {
        pdf_puts(pdf, ">>\n");
        if (pdf->os_idx == pdf_os_max_objs - 1)
            pdf_os_write_objstream(pdf);
    } else {
        pdf_puts(pdf, ">> endobj\n");
    }
}


/*
Write out an accumulated object stream.
First the object number and byte offset pairs are generated
and appended to the ready buffered object stream.
By this the value of \.{/First} can be calculated.
Then a new \.{/ObjStm} object is generated, and everything is
copied to the PDF output buffer, where also compression is done.
When calling this procedure, |pdf_os_mode| must be |true|.
*/

void pdf_os_write_objstream(PDF pdf)
{
    halfword i, j, p, q;
    if (pdf->os_cur_objnum == 0)        /* no object stream started */
        return;
    p = pdf->ptr;
    i = 0;
    j = 0;
    while (i <= pdf->os_idx) {  /* assemble object number and byte offset pairs */
        pdf_printf(pdf, "%d %d", (int) pdf->os_obj[i].num,
                   (int) pdf->os_obj[i].off);
        if (j == 9) {           /* print out in groups of ten for better readability */
            pdf_out(pdf, pdf_new_line_char);
            j = 0;
        } else {
            pdf_out(pdf, ' ');
            j++;
        }
        i++;
    }
    pdf->buf[pdf->ptr - 1] = pdf_new_line_char; /* no risk of flush, as we are in |pdf_os_mode| */
    q = pdf->ptr;
    pdf_begin_dict(pdf, pdf->os_cur_objnum, 0); /* switch to PDF stream writing */
    pdf_puts(pdf, "/Type /ObjStm\n");
    pdf_printf(pdf, "/N %d\n", (int) (pdf->os_idx + 1));
    pdf_printf(pdf, "/First %d\n", (int) (q - p));
    pdf_begin_stream(pdf);
    pdf_room(pdf, q - p);       /* should always fit into the PDF output buffer */
    i = p;
    while (i < q) {             /* write object number and byte offset pairs */
        pdf_quick_out(pdf, pdf->os_buf[i]);
        i++;
    }
    i = 0;
    while (i < p) {
        q = i + pdf->buf_size;
        if (q > p)
            q = p;
        pdf_room(pdf, q - i);
        while (i < q) {         /* write the buffered objects */
            pdf_quick_out(pdf, pdf->os_buf[i]);
            i++;
        }
    }
    pdf_end_stream(pdf);
    pdf->os_cur_objnum = 0;     /* to force object stream generation next time */
}

/* begin a PDF object */
void pdf_begin_obj(PDF pdf, integer i, integer pdf_os_level)
{
    ensure_pdf_header_written(pdf);
    pdf_os_prepare_obj(pdf, i, pdf_os_level);
    if (!pdf->os_mode) {
        pdf_printf(pdf, "%d 0 obj\n", (int) i);
    } else if (pdf->compress_level == 0) {
        pdf_printf(pdf, "%% %d 0 obj\n", (int) i);      /* debugging help */
    }
}

/* begin a new PDF object */
void pdf_new_obj(PDF pdf, integer t, integer i, integer pdf_os)
{
    pdf_create_obj(pdf, t, i);
    pdf_begin_obj(pdf, pdf->obj_ptr, pdf_os);
}


/* end a PDF object */
void pdf_end_obj(PDF pdf)
{
    if (pdf->os_mode) {
        if (pdf->os_idx == pdf_os_max_objs - 1)
            pdf_os_write_objstream(pdf);
    } else {
        pdf_puts(pdf, "endobj\n");      /* end a PDF object */
    }
}


void write_stream_length(PDF pdf, integer length, longinteger offset)
{
    if (jobname_cstr == NULL)
        jobname_cstr = xstrdup(makecstring(job_name));
    if (pdf->draftmode == 0) {
        xfseeko(pdf->file, (off_t) offset, SEEK_SET, jobname_cstr);
        fprintf(pdf->file, "%li", (long int) length);
        xfseeko(pdf->file, pdf_offset(pdf), SEEK_SET, jobname_cstr);
    }
}


/* Converts any string given in in in an allowed PDF string which can be
 * handled by printf et.al.: \ is escaped to \\, parenthesis are escaped and
 * control characters are octal encoded.
 * This assumes that the string does not contain any already escaped
 * characters!
 */
char *convertStringToPDFString(const char *in, int len)
{
    static char pstrbuf[MAX_PSTRING_LEN];
    char *out = pstrbuf;
    int i, j, k;
    char buf[5];
    j = 0;
    for (i = 0; i < len; i++) {
        check_buf(j + sizeof(buf), MAX_PSTRING_LEN);
        if (((unsigned char) in[i] < '!') || ((unsigned char) in[i] > '~')) {
            /* convert control characters into oct */
            k = snprintf(buf, sizeof(buf),
                         "\\%03o", (unsigned int) (unsigned char) in[i]);
            check_nprintf(k, sizeof(buf));
            out[j++] = buf[0];
            out[j++] = buf[1];
            out[j++] = buf[2];
            out[j++] = buf[3];
        } else if ((in[i] == '(') || (in[i] == ')')) {
            /* escape paranthesis */
            out[j++] = '\\';
            out[j++] = in[i];
        } else if (in[i] == '\\') {
            /* escape backslash */
            out[j++] = '\\';
            out[j++] = '\\';
        } else {
            /* copy char :-) */
            out[j++] = in[i];
        }
    }
    out[j] = '\0';
    return pstrbuf;
}

/* Converts any string given in in in an allowed PDF string which is
 * hexadecimal encoded;
 * sizeof(out) should be at least lin*2+1.
 */

static void convertStringToHexString(const char *in, char *out, int lin)
{
    int i, j, k;
    char buf[3];
    j = 0;
    for (i = 0; i < lin; i++) {
        k = snprintf(buf, sizeof(buf),
                     "%02X", (unsigned int) (unsigned char) in[i]);
        check_nprintf(k, sizeof(buf));
        out[j++] = buf[0];
        out[j++] = buf[1];
    }
    out[j] = '\0';
}

/* Compute the ID string as per PDF1.4 9.3:
  <blockquote>
    File identifers are defined by the optional ID entry in a PDF file's
    trailer dictionary (see Section 3.4.4, "File Trailer"; see also
    implementation note 105 in Appendix H). The value of this entry is an
    array of two strings. The first string is a permanent identifier based
    on the contents of the file at the time it was originally created, and
    does not change when the file is incrementally updated. The second
    string is a changing identifier based on the file's contents at the
    time it was last updated. When a file is first written, both
    identifiers are set to the same value. If both identifiers match when a
    file reference is resolved, it is very likely that the correct file has
    been found; if only the first identifier matches, then a different
    version of the correct file has been found.
        To help ensure the uniqueness of file identifiers, it is recommend
    that they be computed using a message digest algorithm such as MD5
    (described in Internet RFC 1321, The MD5 Message-Digest Algorithm; see
    the Bibliography), using the following information (see implementation
    note 106 in Appendix H):
    - The current time
    - A string representation of the file's location, usually a pathname
    - The size of the file in bytes
    - The values of all entries in the file's document information
      dictionary (see Section 9.2.1,  Document Information Dictionary )
  </blockquote>
  This stipulates only that the two IDs must be identical when the file is
  created and that they should be reasonably unique. Since it's difficult
  to get the file size at this point in the execution of pdfTeX and
  scanning the info dict is also difficult, we start with a simpler
  implementation using just the first two items.
 */
void print_ID(PDF pdf, char *file_name)
{
    time_t t;
    size_t size;
    char time_str[32];
    md5_state_t state;
    md5_byte_t digest[16];
    char id[64];
    char pwd[4096];
    /* start md5 */
    md5_init(&state);
    /* get the time */
    t = time(NULL);
    size = strftime(time_str, sizeof(time_str), "%Y%m%dT%H%M%SZ", gmtime(&t));
    md5_append(&state, (const md5_byte_t *) time_str, size);
    /* get the file name */
    if (getcwd(pwd, sizeof(pwd)) == NULL)
        pdftex_fail("getcwd() failed (%s), (path too long?)", strerror(errno));
    md5_append(&state, (const md5_byte_t *) pwd, strlen(pwd));
    md5_append(&state, (const md5_byte_t *) "/", 1);
    md5_append(&state, (const md5_byte_t *) file_name, strlen(file_name));
    /* finish md5 */
    md5_finish(&state, digest);
    /* write the IDs */
    convertStringToHexString((char *) digest, id, 16);
    pdf_printf(pdf, "/ID [<%s> <%s>]", id, id);
}

/* Print the /CreationDate entry.

  PDF Reference, third edition says about the expected date format:
  <blockquote>
    3.8.2 Dates

      PDF defines a standard date format, which closely follows that of
      the international standard ASN.1 (Abstract Syntax Notation One),
      defined in ISO/IEC 8824 (see the Bibliography). A date is a string
      of the form

        (D:YYYYMMDDHHmmSSOHH'mm')

      where

        YYYY is the year
        MM is the month
        DD is the day (01-31)
        HH is the hour (00-23)
        mm is the minute (00-59)
        SS is the second (00-59)
        O is the relationship of local time to Universal Time (UT),
          denoted by one of the characters +, -, or Z (see below)
        HH followed by ' is the absolute value of the offset from UT
          in hours (00-23)
        mm followed by ' is the absolute value of the offset from UT
          in minutes (00-59)

      The apostrophe character (') after HH and mm is part of the syntax.
      All fields after the year are optional. (The prefix D:, although also
      optional, is strongly recommended.) The default values for MM and DD
      are both 01; all other numerical fields default to zero values.  A plus
      sign (+) as the value of the O field signifies that local time is
      later than UT, a minus sign (-) that local time is earlier than UT,
      and the letter Z that local time is equal to UT. If no UT information
      is specified, the relationship of the specified time to UT is
      considered to be unknown. Whether or not the time zone is known, the
      rest of the date should be specified in local time.

      For example, December 23, 1998, at 7:52 PM, U.S. Pacific Standard
      Time, is represented by the string

        D:199812231952-08'00'
  </blockquote>

  The main difficulty is get the time zone offset. strftime() does this in ISO
  C99 (e.g. newer glibc) with %z, but we have to work with other systems (e.g.
  Solaris 2.5).
*/
#define TIME_STR_SIZE 30        /* minimum size for time_str is 24: "D:YYYYmmddHHMMSS+HH'MM'" */
static void makepdftime(PDF pdf)
{
    struct tm lt, gmt;
    size_t size;
    int i, off, off_hours, off_mins;
    time_t t = pdf->start_time;
    char *time_str = pdf->start_time_str;

    /* get the time */
    lt = *localtime(&t);
    size = strftime(time_str, TIME_STR_SIZE, "D:%Y%m%d%H%M%S", &lt);
    /* expected format: "YYYYmmddHHMMSS" */
    if (size == 0) {
        /* unexpected, contents of time_str is undefined */
        time_str[0] = '\0';
        return;
    }

    /* correction for seconds: %S can be in range 00..61,
       the PDF reference expects 00..59,
       therefore we map "60" and "61" to "59" */
    if (time_str[14] == '6') {
        time_str[14] = '5';
        time_str[15] = '9';
        time_str[16] = '\0';    /* for safety */
    }

    /* get the time zone offset */
    gmt = *gmtime(&t);

    /* this calculation method was found in exim's tod.c */
    off = 60 * (lt.tm_hour - gmt.tm_hour) + lt.tm_min - gmt.tm_min;
    if (lt.tm_year != gmt.tm_year) {
        off += (lt.tm_year > gmt.tm_year) ? 1440 : -1440;
    } else if (lt.tm_yday != gmt.tm_yday) {
        off += (lt.tm_yday > gmt.tm_yday) ? 1440 : -1440;
    }

    if (off == 0) {
        time_str[size++] = 'Z';
        time_str[size] = 0;
    } else {
        off_hours = off / 60;
        off_mins = abs(off - off_hours * 60);
        i = snprintf(&time_str[size], 9, "%+03d'%02d'", off_hours, off_mins);
        check_nprintf(i, 9);
    }
    pdf->start_time = t;
}

void init_start_time(PDF pdf)
{
    assert(pdf);
    if (pdf->start_time == 0) {
        pdf->start_time = time((time_t *) NULL);
        pdf->start_time_str = xtalloc(TIME_STR_SIZE, char);
        makepdftime(pdf);
    }
}

void print_creation_date(PDF pdf)
{
    init_start_time(pdf);
    pdf_printf(pdf, "/CreationDate (%s)\n", pdf->start_time_str);
}

void print_mod_date(PDF pdf)
{
    init_start_time(pdf);
    pdf_printf(pdf, "/ModDate (%s)\n", pdf->start_time_str);
}

char *getcreationdate(PDF pdf)
{
    assert(pdf);
    init_start_time(pdf);
    return pdf->start_time_str;
}

void remove_pdffile(PDF pdf)
{
    if (pdf != NULL) {
        if (!kpathsea_debug && pdf->file_name && (pdf->draftmode != 0)) {
            xfclose(pdf->file, pdf->file_name);
            remove(pdf->file_name);
        }
    }
}

static void realloc_fb(PDF pdf)
{
    if (pdf->fb_array == NULL) {
        pdf->fb_limit = SMALL_ARRAY_SIZE;
        pdf->fb_array = xtalloc(pdf->fb_limit, char);
        pdf->fb_ptr = pdf->fb_array;
    } else if ((size_t) (pdf->fb_ptr - pdf->fb_array + 1) > pdf->fb_limit) {
        size_t last_ptr_index = pdf->fb_ptr - pdf->fb_array;
        pdf->fb_limit *= 2;
        if ((size_t) (pdf->fb_ptr - pdf->fb_array + 1) > pdf->fb_limit)
            pdf->fb_limit = pdf->fb_ptr - pdf->fb_array + 1;
        xretalloc(pdf->fb_array, pdf->fb_limit, char);
        pdf->fb_ptr = pdf->fb_array + last_ptr_index;
    }
}

integer fb_offset(PDF pdf)
{
    return pdf->fb_ptr - pdf->fb_array;
}

void fb_seek(PDF pdf, integer offset)
{
    pdf->fb_ptr = pdf->fb_array + offset;
}


void fb_putchar(PDF pdf, eight_bits b)
{
    if ((size_t) (pdf->fb_ptr - pdf->fb_array + 1) > pdf->fb_limit)
        realloc_fb(pdf);
    *(pdf->fb_ptr)++ = b;
}

void fb_flush(PDF pdf)
{
    char *p;
    integer n;
    for (p = pdf->fb_array; p < pdf->fb_ptr;) {
        n = pdf->buf_size - pdf->ptr;
        if (pdf->fb_ptr - p < n)
            n = pdf->fb_ptr - p;
        memcpy(pdf->buf + pdf->ptr, p, (unsigned) n);
        pdf->ptr += n;
        if (pdf->ptr == pdf->buf_size)
            pdf_flush(pdf);
        p += n;
    }
    pdf->fb_ptr = pdf->fb_array;
}

void fb_free(PDF pdf)
{
    xfree(pdf->fb_array);
}


#define ZIP_BUF_SIZE  32768

#define check_err(f, fn)                        \
  if (f != Z_OK)                                \
    pdftex_fail("zlib: %s() failed (error code %d)", fn, f)

void write_zip(PDF pdf, boolean finish)
{
    int err;
    int level = pdf->compress_level;
    assert(level > 0);
    /* This was just to suppress the filename report in |pdftex_fail|
       but zlib errors are rare enough (especially now that the
       compress level is fixed) that I don't care about the slightly
       ugly error message that could result.
     */
    /* cur_file_name = NULL; */
    if (pdf->stream_length == 0) {
        if (pdf->zipbuf == NULL) {
            pdf->zipbuf = xtalloc(ZIP_BUF_SIZE, char);
            pdf->c_stream.zalloc = (alloc_func) 0;
            pdf->c_stream.zfree = (free_func) 0;
            pdf->c_stream.opaque = (voidpf) 0;
            check_err(deflateInit(&pdf->c_stream, level), "deflateInit");
        } else {
            check_err(deflateReset(&pdf->c_stream), "deflateReset");
        }
        pdf->c_stream.next_out = (Bytef *) pdf->zipbuf;
        pdf->c_stream.avail_out = ZIP_BUF_SIZE;
    }
    assert(pdf->zipbuf != NULL);
    pdf->c_stream.next_in = pdf->buf;
    pdf->c_stream.avail_in = pdf->ptr;
    for (;;) {
        if (pdf->c_stream.avail_out == 0) {
            pdf->gone += xfwrite(pdf->zipbuf, 1, ZIP_BUF_SIZE, pdf->file);
            pdf->last_byte = pdf->zipbuf[ZIP_BUF_SIZE - 1];     /* not needed */
            pdf->c_stream.next_out = (Bytef *) pdf->zipbuf;
            pdf->c_stream.avail_out = ZIP_BUF_SIZE;
        }
        err = deflate(&pdf->c_stream, finish ? Z_FINISH : Z_NO_FLUSH);
        if (finish && err == Z_STREAM_END)
            break;
        check_err(err, "deflate");
        if (!finish && pdf->c_stream.avail_in == 0)
            break;
    }
    if (finish) {
        if (pdf->c_stream.avail_out < ZIP_BUF_SIZE) {   /* at least one byte has been output */
            pdf->gone +=
                xfwrite(pdf->zipbuf, 1, ZIP_BUF_SIZE - pdf->c_stream.avail_out,
                        pdf->file);
            pdf->last_byte =
                pdf->zipbuf[ZIP_BUF_SIZE - pdf->c_stream.avail_out - 1];
        }
        xfflush(pdf->file);
    }
    pdf->stream_length = pdf->c_stream.total_out;
}

void zip_free(PDF pdf)
{
    if (pdf->zipbuf != NULL) {
        check_err(deflateEnd(&pdf->c_stream), "deflateEnd");
        free(pdf->zipbuf);
    }
}

void pdf_error(char *t, char *p)
{
    normalize_selector();
    print_err("LuaTeX error");
    if (t != NULL) {
        tprint(" (");
        tprint(t);
        tprint(")");
    }
    tprint(": ");
    if (p != NULL)
        tprint(p);
    succumb();
}

void pdf_warning(char *t, char *p, boolean prepend_nl, boolean append_nl)
{
    if (prepend_nl)
        print_ln();
    tprint("LuaTeX warning");
    if (t != NULL) {
        tprint(" (");
        tprint(t);
        tprint(")");
    }
    tprint(": ");
    if (p != NULL)
        tprint(p);
    if (append_nl)
        print_ln();
    if (history == spotless)
        history = warning_issued;
}

/**********************************************************************/
/* Use check_o_mode() in the backend-specific "Implement..." chunks */

void check_o_mode(PDF pdf, char *s, int o_modes, boolean strict)
{

    char warn_string[100];
    char *m;
    output_mode o_mode;

    /* in warn mode (strict == false):
       only check, don't do fix_o_mode() here! pdf->o_mode is left
       in possibly wrong state until real output, ok.
     */

    if (pdf->o_mode == OMODE_NONE) {
        if (pdf_output > 0) {
            if (pdf_output == 2009)
                o_mode = OMODE_LUA;
            else
                o_mode = OMODE_PDF;
        } else
            o_mode = OMODE_DVI;
    } else
        o_mode = pdf->o_mode;
    if ((o_mode & o_modes) == 0) {      /* warning or error */
        switch (o_mode) {
        case OMODE_DVI:
            m = "DVI";
            break;
        case OMODE_PDF:
            m = "PDF";
            break;
        case OMODE_LUA:
            m = "Lua";
            break;
        default:
            assert(0);
        }
        snprintf(warn_string, 99, "not allowed in %s mode (\\pdfpoutput = %d)",
                 m, pdf_output);
        if (strict)
            pdf_error(s, warn_string);
        else
            pdf_warning(s, warn_string, true, true);
    } else {
        if (strict) {
            fix_o_mode(pdf);
            switch (o_mode) {
            case OMODE_DVI:
                ensure_dvi_header_written(pdf);
                break;
            case OMODE_PDF:
                ensure_pdf_header_written(pdf);
                break;
            case OMODE_LUA:
                break;
            default:
                assert(0);
            }
        }
    }
}

/**********************************************************************/

void set_job_id(PDF pdf, int year, int month, int day, int time)
{
    char *name_string, *format_string, *s;
    size_t slen;
    int i;

    if (pdf->job_id_string != NULL)
        return;

    name_string = xstrdup(makecstring(job_name));
    format_string = xstrdup(makecstring(format_ident));
    make_pdftex_banner();
    slen = SMALL_BUF_SIZE +
        strlen(name_string) + strlen(format_string) + strlen(pdftex_banner);
    s = xtalloc(slen, char);
    /* The Web2c version string starts with a space.  */
    i = snprintf(s, slen,
                 "%.4d/%.2d/%.2d %.2d:%.2d %s %s %s",
                 year, month, day, time / 60, time % 60,
                 name_string, format_string, pdftex_banner);
    check_nprintf(i, slen);
    pdf->job_id_string = xstrdup(s);
    xfree(s);
    xfree(name_string);
    xfree(format_string);
} char *get_resname_prefix(PDF pdf)
{

/*     static char name_str[] = */
/* "!\"$&'*+,-.0123456789:;=?@ABCDEFGHIJKLMNOPQRSTUVWXYZ\\" */
/* "^_`abcdefghijklmnopqrstuvwxyz|~"; */
    static char name_str[] =
        "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
    static char prefix[7];      /* make a tag of 6 chars long */
    unsigned long crc;
    short i;
    size_t base = strlen(name_str);
    crc = crc32(0L, Z_NULL, 0);
    crc = crc32(crc, (Bytef *) pdf->job_id_string, strlen(pdf->job_id_string));
    for (i = 0; i < 6; i++) {
        prefix[i] = name_str[crc % base];
        crc /= base;
    }
    prefix[6] = '\0';
    return prefix;
}

#define mag int_par(mag_code)

void pdf_begin_page(PDF pdf, boolean shipping_page)
{
    scaled form_margin = one_bp;
    ensure_pdf_header_written(pdf);
    init_pdf_pagecalculations(pdf);

    if (pdf->resources == NULL)
        pdf->resources = xmalloc(sizeof(pdf_resource_struct));
    reset_resource_lists(pdf->resources);
    pdf->resources->last_resources = pdf_new_objnum(pdf);

    if (shipping_page) {
        pdf->last_page = get_obj(pdf, obj_type_page, total_pages + 1, 0);
        set_obj_aux(pdf, pdf->last_page, 1);    /* mark that this page has been created */
        pdf_new_dict(pdf, obj_type_others, 0, 0);
        pdf->last_stream = pdf->obj_ptr;
        pdf->last_thread = null;
    } else {
        pdf_begin_dict(pdf, pdf_cur_form, 0);
        pdf->last_stream = pdf_cur_form;

        /* Write out Form stream header */
        pdf_printf(pdf, "/Type /XObject\n");
        pdf_printf(pdf, "/Subtype /Form\n");
        if (obj_xform_attr(pdf, pdf_cur_form) != null) {
            pdf_print_toks_ln(pdf, obj_xform_attr(pdf, pdf_cur_form));
            delete_token_ref(obj_xform_attr(pdf, pdf_cur_form));
            set_obj_xform_attr(pdf, pdf_cur_form, null);
        }
        pdf_printf(pdf, "/BBox [");
        pdf_print_bp(pdf, -form_margin);
        pdf_out(pdf, ' ');
        pdf_print_bp(pdf, -form_margin);
        pdf_out(pdf, ' ');
        pdf_print_bp(pdf, cur_page_size.h + form_margin);
        pdf_out(pdf, ' ');
        pdf_print_bp(pdf, cur_page_size.v + form_margin);
        pdf_printf(pdf, "]\n");
        pdf_printf(pdf, "/FormType 1\n");
        pdf_printf(pdf, "/Matrix [1 0 0 1 0 0]\n");
        pdf_indirect_ln(pdf, "Resources", pdf->resources->last_resources);
    }
    /* Start stream of page/form contents */
    pdf_begin_stream(pdf);
    if (shipping_page) {
        /* Adjust transformation matrix for the magnification ratio */
        if (mag != 1000) {
            pdf_print_real(pdf, mag, 3);
            pdf_printf(pdf, " 0 0 ");
            pdf_print_real(pdf, mag, 3);
            pdf_printf(pdf, " 0 0 cm\n");
        }
    }
    pdf_shipout_begin(shipping_page);

    if (shipping_page)
        pdf_out_colorstack_startpage(pdf);
}

#define pdf_page_attr equiv(pdf_page_attr_loc)
#define pdf_page_resources equiv(pdf_page_resources_loc)

void pdf_end_page(PDF pdf, boolean shipping_page)
{
    integer j, ff;
    pdf_resource_struct *res_p = pdf->resources;
    pdf_resource_struct local_resources;
    pdf_object_list *ol;
    scaledpos save_cur_page_size;       /* to save |cur_page_size| during flushing pending forms */


    /* Finish stream of page/form contents */
    pdf_goto_pagemode(pdf);
    pdf_shipout_end(shipping_page);
    pdf_end_stream(pdf);

    if (shipping_page) {
        /* Write out page object */

        pdf_begin_dict(pdf, pdf->last_page, 1);
        pdf->last_pages = pdf_do_page_divert(pdf, pdf->last_page, 0);
        pdf_printf(pdf, "/Type /Page\n");
        pdf_indirect_ln(pdf, "Contents", pdf->last_stream);
        pdf_indirect_ln(pdf, "Resources", res_p->last_resources);
        pdf_printf(pdf, "/MediaBox [0 0 ");
        pdf_print_mag_bp(pdf, cur_page_size.h);
        pdf_out(pdf, ' ');
        pdf_print_mag_bp(pdf, cur_page_size.v);
        pdf_printf(pdf, "]\n");
        if (pdf_page_attr != null)
            pdf_print_toks_ln(pdf, pdf_page_attr);
        pdf_indirect_ln(pdf, "Parent", pdf->last_pages);
        if (pdf->img_page_group_val > 0) {
            pdf_printf(pdf, "/Group %d 0 R\n", pdf->img_page_group_val);
            pdf->img_page_group_val = 0;
        }
        /* Generate array of annotations or beads in page */
        if ((res_p->annot_list != NULL)
            || (res_p->link_list != NULL)) {
            pdf_printf(pdf, "/Annots [ ");
            ol = res_p->annot_list;
            while (ol != NULL) {
                pdf_print_int(pdf, ol->info);
                pdf_printf(pdf, " 0 R ");
                ol = ol->link;
            }
            ol = res_p->link_list;
            while (ol != NULL) {
                pdf_print_int(pdf, ol->info);
                pdf_printf(pdf, " 0 R ");
                ol = ol->link;
            }
            pdf_printf(pdf, "]\n");
        }
        print_beads_list(pdf);
        pdf_end_dict(pdf);

    }
    /* Write out resource lists */
    /* Write out pending raw objects */
    if ((ol = res_p->obj_list) != NULL) {
        while (ol != NULL) {
            if (!is_obj_written(pdf, ol->info))
                pdf_write_obj(pdf, ol->info);
            ol = ol->link;
        }
    }

    /* Write out pending forms */
    /* When flushing pending forms we need to save and restore resource lists
       (|font_list|, |pdf_obj_list|, |pdf_xform_list| and |pdf_ximage_list|),
       which are also used by page shipping.
       Saving and restoring |cur_page_size| is needed for proper
       writing out pending PDF marks. */
    if ((ol = res_p->xform_list) != NULL) {
        while (ol != NULL) {
            if (!is_obj_written(pdf, ol->info)) {
                pdf_cur_form = ol->info;
                save_cur_page_size = cur_page_size;
                pdf->resources = &local_resources;
                ship_out(pdf, obj_xform_box(pdf, pdf_cur_form), false);
                cur_page_size = save_cur_page_size;
                /* Restore resource lists */
                pdf->resources = res_p;
            }
            ol = ol->link;
        }
    }

    /* Write out pending images */
    if ((ol = res_p->ximage_list) != NULL) {
        while (ol != NULL) {
            if (!is_obj_written(pdf, ol->info))
                pdf_write_image(pdf, ol->info);
            ol = ol->link;
        }
    }

    if (shipping_page) {
        /* Write out pending PDF marks */
        /* Write out PDF annotations */
        if ((ol = res_p->annot_list) != NULL) {
            while (ol != NULL) {
                j = obj_annot_ptr(pdf, ol->info);       /* |j| points to |pdf_annot_node| */
                pdf_begin_dict(pdf, ol->info, 1);
                pdf_printf(pdf, "/Type /Annot\n");
                pdf_print_toks_ln(pdf, pdf_annot_data(j));
                pdf_rectangle(pdf, j);
                pdf_end_dict(pdf);
                ol = ol->link;
            }
        }

        /* Write out PDF link annotations */
        if ((ol = res_p->link_list) != NULL) {
            while (ol != NULL) {
                j = obj_annot_ptr(pdf, ol->info);
                pdf_begin_dict(pdf, ol->info, 1);
                pdf_printf(pdf, "/Type /Annot\n");
                if (pdf_action_type(pdf_link_action(j)) != pdf_action_user)
                    pdf_printf(pdf, "/Subtype /Link\n");
                if (pdf_link_attr(j) != null)
                    pdf_print_toks_ln(pdf, pdf_link_attr(j));
                pdf_rectangle(pdf, j);
                if (pdf_action_type(pdf_link_action(j)) != pdf_action_user)
                    pdf_printf(pdf, "/A ");
                write_action(pdf, pdf_link_action(j));
                pdf_end_dict(pdf);
                ol = ol->link;
            }
            /* Flush |pdf_start_link_node|'s created by |append_link| */
            ol = res_p->link_list;
            while (ol != NULL) {
                j = obj_annot_ptr(pdf, ol->info);
                /* nodes with |subtype = pdf_link_data_node| were created by |append_link| and
                   must be flushed here, as they are not linked in any list */
                if (subtype(j) == pdf_link_data_node)
                    flush_node(j);
                ol = ol->link;
            }
        }

        /* Write out PDF mark destinations */
        write_out_pdf_mark_destinations(pdf);
        /* Write out PDF bead rectangle specifications */
        print_bead_rectangles(pdf);

    }
    /* Write out resources dictionary */
    pdf_begin_dict(pdf, res_p->last_resources, 1);
    /* Print additional resources */
    if (shipping_page) {
        if (pdf_page_resources != null)
            pdf_print_toks_ln(pdf, pdf_page_resources);
    } else {
        if (obj_xform_resources(pdf, pdf_cur_form) != null) {
            pdf_print_toks_ln(pdf, obj_xform_resources(pdf, pdf_cur_form));
            delete_token_ref(obj_xform_resources(pdf, pdf_cur_form));
            set_obj_xform_resources(pdf, pdf_cur_form, null);
        }
    }

    /* Generate font resources */
    if ((ol = res_p->font_list) != NULL) {
        pdf_printf(pdf, "/Font << ");
        while (ol != NULL) {
            pdf_printf(pdf, "/F");
            set_ff(ol->info);
            pdf_print_int(pdf, ff);
            pdf_print_resname_prefix(pdf);
            pdf_out(pdf, ' ');
            pdf_print_int(pdf, pdf_font_num(ff));
            pdf_printf(pdf, " 0 R ");
            ol = ol->link;
        }
        pdf_printf(pdf, ">>\n");
        res_p->text_procset = true;
    }

    /* Generate XObject resources */
    if ((res_p->xform_list != NULL)
        || (res_p->ximage_list != NULL)) {
        pdf_printf(pdf, "/XObject << ");
        if ((ol = res_p->xform_list) != NULL) {
            while (ol != NULL) {
                pdf_printf(pdf, "/Fm");
                pdf_print_int(pdf, obj_info(pdf, ol->info));
                pdf_print_resname_prefix(pdf);
                pdf_out(pdf, ' ');
                pdf_print_int(pdf, ol->info);
                pdf_printf(pdf, " 0 R ");
                ol = ol->link;
            }
        }
        if ((ol = res_p->ximage_list) != NULL) {
            while (ol != null) {
                pdf_printf(pdf, "/Im");
                pdf_print_int(pdf, image_index(obj_data_ptr(pdf, ol->info)));
                pdf_print_resname_prefix(pdf);
                pdf_out(pdf, ' ');
                pdf_print_int(pdf, ol->info);
                pdf_printf(pdf, " 0 R ");
                update_image_procset(obj_data_ptr(pdf, ol->info));
                ol = ol->link;
            }
        }
        pdf_printf(pdf, ">>\n");
    }

    /* Generate ProcSet */
    pdf_printf(pdf, "/ProcSet [ /PDF");
    if (res_p->text_procset)
        pdf_printf(pdf, " /Text");
    if (check_image_b(res_p->image_procset))
        pdf_printf(pdf, " /ImageB");
    if (check_image_c(res_p->image_procset))
        pdf_printf(pdf, " /ImageC");
    if (check_image_i(res_p->image_procset))
        pdf_printf(pdf, " /ImageI");
    pdf_printf(pdf, " ]\n");

    pdf_end_dict(pdf);

    /* In the end of shipping out a page we reset all the lists holding objects
       have been created during the page shipping. */

    flush_resource_lists(pdf);
}

/**********************************************************************/

/* Finishing the PDF output file. */

#define pdf_pages_attr equiv(pdf_pages_attr_loc)
#define pdf_gen_tounicode int_par(pdf_gen_tounicode_code)

/*
Destinations that have been referenced but don't exists have
|obj_dest_ptr=null|. Leaving them undefined might cause troubles for
PDF browsers, so we need to fix them.
*/

void pdf_fix_dest(PDF pdf, integer k)
{
    if (obj_dest_ptr(pdf, k) != null)
        return;
    pdf_warning("dest", NULL, false, false);
    if (obj_info(pdf, k) < 0) {
        tprint("name{");
        print(-obj_info(pdf, k));
        tprint("}");
    } else {
        tprint("num");
        print_int(obj_info(pdf, k));
    }
    tprint(" has been referenced but does not exist, replaced by a fixed one");
    print_ln();
    print_ln();
    pdf_begin_obj(pdf, k, 1);
    pdf_out(pdf, '[');
    pdf_print_int(pdf, pdf->head_tab[obj_type_page]);
    pdf_printf(pdf, " 0 R /Fit]\n");
    pdf_end_obj(pdf);
}

void print_pdf_pages_attr(PDF pdf)
{
    if (pdf_pages_attr != null)
        pdf_print_toks_ln(pdf, pdf_pages_attr);
}

/*
If the same keys in a dictionary are given several times, then it is not
defined which value is choosen by an application.  Therefore the keys
|/Producer| and |/Creator| are only set if the token list
|pdf_info_toks| converted to a string does not contain these key strings.
*/

boolean substr_of_str(char *s, char *t)
{
    if (strstr(t, s) == NULL)
        return false;
    return true;
}

void pdf_print_info(PDF pdf, integer luatex_version, str_number luatex_revision)
{                               /* print info object */
    boolean creator_given, producer_given, creationdate_given, moddate_given,
        trapped_given;
    char *s = NULL;
    int len = 0;
    pdf_new_dict(pdf, obj_type_others, 0, 3);   /* keep Info readable unless explicitely forced */
    creator_given = false;
    producer_given = false;
    creationdate_given = false;
    moddate_given = false;
    trapped_given = false;
    if (pdf_info_toks != 0) {
        s = tokenlist_to_cstring(pdf_info_toks, true, &len);
        creator_given = substr_of_str("/Creator", s);
        producer_given = substr_of_str("/Producer", s);
        creationdate_given = substr_of_str("/CreationDate", s);
        moddate_given = substr_of_str("/ModDate", s);
        trapped_given = substr_of_str("/Trapped", s);
    }
    if (!producer_given) {
        /* Print the Producer key */
        pdf_printf(pdf, "/Producer (LuaTeX-");
        pdf_print_int(pdf, luatex_version / 100);
        pdf_out(pdf, '.');
        pdf_print_int(pdf, luatex_version % 100);
        pdf_out(pdf, '.');
        pdf_print(pdf, luatex_revision);
        pdf_printf(pdf, ")\n");

    }
    if (pdf_info_toks != null) {
        if (len > 0) {
            pdf_puts(pdf, s);
            pdf_print_nl(pdf);
            xfree(s);
        }
        delete_token_ref(pdf_info_toks);
        pdf_info_toks = null;
    }
    if (!creator_given)
        pdf_str_entry_ln(pdf, "Creator", "TeX");
    if (!creationdate_given) {
        print_creation_date(pdf);
    }
    if (!moddate_given) {
        print_mod_date(pdf);
    }
    if (!trapped_given) {
        pdf_printf(pdf, "/Trapped /False\n");
    }
    pdf_str_entry_ln(pdf, "PTEX.Fullbanner", pdftex_banner);
    pdf_end_dict(pdf);
}

void build_free_object_list(PDF pdf)
{
    integer k, l;
    l = 0;
    set_obj_fresh(pdf, l);      /* null object at begin of list of free objects */
    for (k = 1; k <= pdf->sys_obj_ptr; k++) {
        if (!is_obj_written(pdf, k)) {
            set_obj_link(pdf, l, k);
            l = k;
        }
    }
    set_obj_link(pdf, l, 0);
}

/*
Now the finish of PDF output file. At this moment all Page objects
are already written completely to PDF output file.
*/

void finish_pdf_file(PDF pdf, integer luatex_version,
                     str_number luatex_revision)
{
    boolean res;
    integer i, j, k;
    integer root, outlines, threads, names_tree;
    integer xref_offset_width;
    integer callback_id = callback_defined(stop_run_callback);

    if (total_pages == 0) {
        if (callback_id == 0) {
            tprint_nl("No pages of output.");
        } else if (callback_id > 0) {
            res = run_callback(callback_id, "->");
        }
        if (pdf->gone > 0)
            garbage_warning();
    } else {
        if (pdf->draftmode == 0) {
            pdf_flush(pdf);     /* to make sure that the output file name has been already created */
            flush_jbig2_page0_objects(pdf);     /* flush page 0 objects from JBIG2 images, if any */
            /* Check for non-existing pages */
            k = pdf->head_tab[obj_type_page];
            while (obj_aux(pdf, k) == 0) {
                pdf_warning("dest", "Page ", false, false);
                print_int(obj_info(pdf, k));
                tprint(" has been referenced but does not exist!");
                print_ln();
                print_ln();
                k = obj_link(pdf, k);
            }
            pdf->head_tab[obj_type_page] = k;

            /* Check for non-existing destinations */
            k = pdf->head_tab[obj_type_dest];
            while (k != 0) {
                pdf_fix_dest(pdf, k);
                k = obj_link(pdf, k);
            }

            /* Output fonts definition */
            for (k = 1; k <= max_font_id(); k++) {
                if (font_used(k) && (pdf_font_num(k) < 0)) {
                    i = -pdf_font_num(k);
                    assert(pdf_font_num(i) > 0);
                    for (j = font_bc(k); j <= font_ec(k); j++)
                        if (quick_char_exists(k, j) && pdf_char_marked(k, j))
                            pdf_mark_char(i, j);
                    if ((pdf_font_attr(i) == 0) && (pdf_font_attr(k) != 0)) {
                        set_pdf_font_attr(i, pdf_font_attr(k));
                    } else if ((pdf_font_attr(k) == 0)
                               && (pdf_font_attr(i) != 0)) {
                        set_pdf_font_attr(k, pdf_font_attr(i));
                    } else if ((pdf_font_attr(i) != 0)
                               && (pdf_font_attr(k) != 0)
                               &&
                               (!str_eq_str
                                (pdf_font_attr(i), pdf_font_attr(k)))) {
                        pdf_warning("\\pdffontattr", "fonts ", false, false);
                        print_font_identifier(i);
                        tprint(" and ");
                        print_font_identifier(k);
                        tprint
                            (" have conflicting attributes; I will ignore the attributes assigned to ");
                        print_font_identifier(i);
                        print_ln();
                        print_ln();
                    }
                }
            }
            pdf->gen_tounicode = pdf_gen_tounicode;
            k = pdf->head_tab[obj_type_font];
            while (k != 0) {
                f = obj_info(pdf, k);
                assert(pdf_font_num(f) > 0);
                do_pdf_font(pdf, k, f);
                k = obj_link(pdf, k);
            }
            write_fontstuff(pdf);

            pdf->last_pages = output_pages_tree(pdf);
            /* Output outlines */
            outlines = print_outlines(pdf);

            /* Output name tree */
            /* The name tree is very similiar to Pages tree so its construction should be
               certain from Pages tree construction. For intermediate node |obj_info| will be
               the first name and |obj_link| will be the last name in \.{\\Limits} array.
               Note that |pdf_dest_names_ptr| will be less than |obj_ptr|, so we test if
               |k < pdf_dest_names_ptr| then |k| is index of leaf in |dest_names|; else
               |k| will be index in |obj_tab| of some intermediate node.
             */
            names_tree = output_name_tree(pdf);

            /* Output article threads */
            if (pdf->head_tab[obj_type_thread] != 0) {
                pdf_new_obj(pdf, obj_type_others, 0, 1);
                threads = pdf->obj_ptr;
                pdf_out(pdf, '[');
                k = pdf->head_tab[obj_type_thread];
                while (k != 0) {
                    pdf_print_int(pdf, k);
                    pdf_printf(pdf, " 0 R ");
                    k = obj_link(pdf, k);
                }
                pdf_remove_last_space(pdf);
                pdf_printf(pdf, "]\n");
                pdf_end_obj(pdf);
                k = pdf->head_tab[obj_type_thread];
                while (k != 0) {
                    out_thread(pdf, k);
                    k = obj_link(pdf, k);
                }
            } else {
                threads = 0;
            }

            /* Output the catalog object */
            pdf_new_dict(pdf, obj_type_others, 0, 1);
            root = pdf->obj_ptr;
            pdf_printf(pdf, "/Type /Catalog\n");
            pdf_indirect_ln(pdf, "Pages", pdf->last_pages);
            if (threads != 0)
                pdf_indirect_ln(pdf, "Threads", threads);
            if (outlines != 0)
                pdf_indirect_ln(pdf, "Outlines", outlines);
            if (names_tree != 0)
                pdf_indirect_ln(pdf, "Names", names_tree);
            if (pdf_catalog_toks != null) {
                pdf_print_toks_ln(pdf, pdf_catalog_toks);
                delete_token_ref(pdf_catalog_toks);
                pdf_catalog_toks = null;
            }
            if (pdf_catalog_openaction != 0)
                pdf_indirect_ln(pdf, "OpenAction", pdf_catalog_openaction);
            pdf_end_dict(pdf);

            pdf_print_info(pdf, luatex_version, luatex_revision);       /* last candidate for object stream */

            if (pdf->os_enable) {
                pdf_os_switch(pdf, true);
                pdf_os_write_objstream(pdf);
                pdf_flush(pdf);
                pdf_os_switch(pdf, false);
                /* Output the cross-reference stream dictionary */
                pdf_new_dict(pdf, obj_type_others, 0, 0);
                if ((obj_offset(pdf, pdf->sys_obj_ptr) / 256) > 16777215)
                    xref_offset_width = 5;
                else if (obj_offset(pdf, pdf->sys_obj_ptr) > 16777215)
                    xref_offset_width = 4;
                else if (obj_offset(pdf, pdf->sys_obj_ptr) > 65535)
                    xref_offset_width = 3;
                else
                    xref_offset_width = 2;
                /* Build a linked list of free objects */
                build_free_object_list(pdf);
                pdf_printf(pdf, "/Type /XRef\n");
                pdf_printf(pdf, "/Index [0 ");
                pdf_print_int(pdf, pdf->obj_ptr);
                pdf_printf(pdf, "]\n");
                pdf_int_entry_ln(pdf, "Size", pdf->obj_ptr);
                pdf_printf(pdf, "/W [1 ");
                pdf_print_int(pdf, xref_offset_width);
                pdf_printf(pdf, " 1]\n");
                pdf_indirect_ln(pdf, "Root", root);
                pdf_indirect_ln(pdf, "Info", pdf->obj_ptr - 1);
                if (pdf_trailer_toks != null) {
                    pdf_print_toks_ln(pdf, pdf_trailer_toks);
                    delete_token_ref(pdf_trailer_toks);
                    pdf_trailer_toks = null;
                }
                print_ID(pdf, pdf->file_name);
                pdf_print_nl(pdf);
                pdf_begin_stream(pdf);
                for (k = 0; k <= pdf->sys_obj_ptr; k++) {
                    if (!is_obj_written(pdf, k)) {      /* a free object */
                        pdf_out(pdf, 0);
                        pdf_out_bytes(pdf, obj_link(pdf, k), xref_offset_width);
                        pdf_out(pdf, 255);
                    } else if (obj_os_idx(pdf, k) == -1) {      /* object not in object stream */
                        pdf_out(pdf, 1);
                        pdf_out_bytes(pdf, obj_offset(pdf, k),
                                      xref_offset_width);
                        pdf_out(pdf, 0);
                    } else {    /* object in object stream */
                        pdf_out(pdf, 2);
                        pdf_out_bytes(pdf, obj_offset(pdf, k),
                                      xref_offset_width);
                        pdf_out(pdf, obj_os_idx(pdf, k));
                    }
                }
                pdf_end_stream(pdf);

                pdf_flush(pdf);
            } else {
                /* Output the |obj_tab| */
                /* Build a linked list of free objects */
                build_free_object_list(pdf);

                pdf_save_offset(pdf);
                pdf_printf(pdf, "xref\n");
                pdf_printf(pdf, "0 ");
                pdf_print_int_ln(pdf, pdf->obj_ptr + 1);
                pdf_print_fw_int(pdf, obj_link(pdf, 0), 10);
                pdf_printf(pdf, " 65535 f \n");
                for (k = 1; k <= pdf->obj_ptr; k++) {
                    if (!is_obj_written(pdf, k)) {
                        pdf_print_fw_int(pdf, obj_link(pdf, k), 10);
                        pdf_printf(pdf, " 00000 f \n");
                    } else {
                        pdf_print_fw_int(pdf, obj_offset(pdf, k), 10);
                        pdf_printf(pdf, " 00000 n \n");
                    }
                }

            }

            /* Output the trailer */
            if (!pdf->os_enable) {
                pdf_printf(pdf, "trailer\n");
                pdf_printf(pdf, "<< ");
                pdf_int_entry_ln(pdf, "Size", pdf->sys_obj_ptr + 1);
                pdf_indirect_ln(pdf, "Root", root);
                pdf_indirect_ln(pdf, "Info", pdf->sys_obj_ptr);
                if (pdf_trailer_toks != null) {
                    pdf_print_toks_ln(pdf, pdf_trailer_toks);
                    delete_token_ref(pdf_trailer_toks);
                    pdf_trailer_toks = null;
                }
                print_ID(pdf, pdf->file_name);
                pdf_printf(pdf, " >>\n");
            }
            pdf_printf(pdf, "startxref\n");
            if (pdf->os_enable)
                pdf_print_int_ln(pdf, obj_offset(pdf, pdf->sys_obj_ptr));
            else
                pdf_print_int_ln(pdf, pdf_saved_offset(pdf));
            pdf_printf(pdf, "%%%%EOF\n");

            pdf_flush(pdf);
            if (callback_id == 0) {
                tprint_nl("Output written on ");
                tprint(pdf->file_name);
                tprint(" (");
                print_int(total_pages);
                tprint(" page");
                if (total_pages != 1)
                    print_char('s');
                tprint(", ");
                print_int(pdf_offset(pdf));
                tprint(" bytes).");
            } else if (callback_id > 0) {
                res = run_callback(callback_id, "->");
            }
        }
        libpdffinish(pdf);
        if (pdf->draftmode == 0)
            b_close(pdf->file);
        else
            pdf_warning(NULL,
                        "\\pdfdraftmode enabled, not changing output pdf",
                        true, true);
    }

    if (callback_id == 0) {
        if (log_opened) {
            fprintf(log_file,
                    "\nPDF statistics: %d PDF objects out of %d (max. %d)\n",
                    (int) pdf->obj_ptr, (int) pdf->obj_tab_size,
                    (int) sup_obj_tab_size);
            if (pdf->os_cntr > 0) {
                fprintf(log_file,
                        " %d compressed objects within %d object stream%s\n",
                        (int) ((pdf->os_cntr - 1) * pdf_os_max_objs +
                               pdf->os_idx + 1), (int) pdf->os_cntr,
                        (pdf->os_cntr > 1 ? "s" : ""));
            }
            fprintf(log_file, " %d named destinations out of %d (max. %d)\n",
                    (int) pdf->dest_names_ptr, (int) pdf->dest_names_size,
                    (int) sup_dest_names_size);
            fprintf(log_file,
                    " %d words of extra memory for PDF output out of %d (max. %d)\n",
                    (int) pdf->mem_ptr, (int) pdf->mem_size,
                    (int) sup_pdf_mem_size);
        }
    }
}

void scan_pdfcatalog(PDF pdf)
{
    halfword p;
    scan_pdf_ext_toks();
    pdf_catalog_toks = concat_tokens(pdf_catalog_toks, def_ref);
    if (scan_keyword("openaction")) {
        if (pdf_catalog_openaction != 0) {
            pdf_error("ext1", "duplicate of openaction");
        } else {
            check_o_mode(pdf, "\\pdfcatalog", OMODE_PDF, true);
            p = scan_action(pdf);
            pdf_new_obj(pdf, obj_type_others, 0, 1);
            pdf_catalog_openaction = pdf->obj_ptr;
            write_action(pdf, p);
            pdf_end_obj(pdf);
            delete_action_ref(p);
        }
    }
}
