//
// Simple markdown to PDF converter example for PDFio.
//
// Copyright © 2024-2025 by Michael R Sweet.
//
// Licensed under Apache License v2.0.  See the file "LICENSE" for more
// information.
//
// Usage:
//
//   ./md2pdf FILENAME.md FILENAME.pdf
//   ./md2pdf FILENAME.md >FILENAME.pdf
//
// The generated PDF file is formatted for a "universal" paper size (8.27x11",
// the intersection of US Letter and ISO A4) with 1" top and bottom margins and
// 0.5" side margins.  The document title (if present) is centered at the top
// of the second and subsequent pages while the current heading and page number
// are provided at the bottom of each page.
//

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <math.h>
#ifdef _WIN32
#  include <io.h>
#else
#  include <unistd.h>
#endif // _WIN32
#include "mmd.h"
#include <pdfio.h>
#include <pdfio-content.h>


//
// Types...
//

typedef enum doccolor_e			// Document color enumeration
{
  DOCCOLOR_BLACK,			// #000
  DOCCOLOR_RED,				// #900
  DOCCOLOR_ORANGE,			// #CC0
  DOCCOLOR_BLUE,			// #00C
  DOCCOLOR_LTGRAY,			// #EEE
  DOCCOLOR_GRAY				// #555
} doccolor_t;

typedef enum docfont_e			// Document font enumeration
{
  DOCFONT_REGULAR,			// Roboto-Regular
  DOCFONT_BOLD,				// Roboto-Bold
  DOCFONT_ITALIC,			// Roboto-Italic
  DOCFONT_MONOSPACE,			// RobotoMono-Regular
  DOCFONT_MAX				// Number of fonts
} docfont_t;

typedef struct docimage_s		// Document image info
{
  const char	*url;			// Reference URL
  pdfio_obj_t	*obj;			// Image object
} docimage_t;

#define DOCIMAGE_MAX	1000		// Maximum number of images

typedef struct doclink_s		// Document link info
{
  const char	*url;			// Target URL
  pdfio_rect_t	box;			// Link box
} doclink_t;

#define DOCLINK_MAX	1000		// Maximum number of links per page

typedef struct docaction_s		// Document action info
{
  const char	*target;		// Target name
  pdfio_obj_t	*obj;			// Link object
} docaction_t;

#define DOCACTION_MAX	10000		// Maximum number of actions per document

typedef struct doctarget_s		// Document target info
{
  char		name[128];		// Target name
  size_t	page;			// Target page
  double	y;			// Target page position
} doctarget_t;

#define DOCTARGET_MAX	1000		// Maximum number of targets per document

typedef struct doctoc_s			// Document table-of-contents entry
{
  int		level;			// Level
  int		count;			// Total number of child entries
  pdfio_obj_t	*obj;			// Dictionary object
  pdfio_dict_t	*dict;			// Dictionary value
} doctoc_t;

#define DOCTOC_MAX	1000		// Maximum number of TOC entries

typedef struct docdata_s		// Document formatting data
{
  // State for the whole document
  pdfio_file_t	*pdf;			// PDF file
  pdfio_rect_t	media_box;		// Media (page) box
  pdfio_rect_t	crop_box;		// Crop box (for margins)
  pdfio_rect_t	art_box;		// Art box (for markdown content)
  pdfio_obj_t	*fonts[DOCFONT_MAX];	// Embedded fonts
  double	font_space;		// Unit width of a space
  size_t	num_images;		// Number of embedded images
  docimage_t	images[DOCIMAGE_MAX];	// Embedded images
  const char	*title;			// Document title
  char		*heading;		// Current document heading
  size_t	num_actions;		// Number of actions for this document
  docaction_t	actions[DOCACTION_MAX];	// Actions for this document
  size_t	num_targets;		// Number of targets for this document
  doctarget_t	targets[DOCTARGET_MAX];	// Targets for this document
  size_t	num_toc;		// Number of table-of-contents entries
  doctoc_t	toc[DOCTOC_MAX];	// Table-of-contents entries

  // State for the current page
  pdfio_stream_t *st;			// Current page stream
  double	y;			// Current position on page
  const char	*tag;			// Current block tag
  bool		in_table,		// Are we in a table?
		in_row;			// Are we in a table row?
  docfont_t	font;			// Current font
  double	fsize;			// Current font size
  doccolor_t	color;			// Current color
  pdfio_array_t	*annots_array;		// Annotations array (for links)
  pdfio_obj_t	*annots_obj;		// Annotations object (for links)
  size_t	num_links;		// Number of links for this page
  doclink_t	links[DOCLINK_MAX];	// Links for this page
} docdata_t;

typedef struct linefrag_s		// Line fragment
{
  mmd_type_t	type;			// Type of fragment
  double	x,			// X position of item
		width,			// Width of item
		height;			// Height of item
  size_t	imagenum;		// Image number
  const char	*text;			// Text string
  const char	*url;			// Link URL string
  bool		ws;			// Whitespace before text?
  docfont_t	font;			// Text font
  doccolor_t	color;			// Text color
} linefrag_t;

#define LINEFRAG_MAX	200		// Maximum number of fragments on a line

typedef struct tablecol_s		// Table column data
{
  double	min_width,		// Minimum required width of column
		max_width,		// Maximum required width of column
		width,			// Width of column
		left,			// Left edge
		right;			// Right edge
} tablecol_t;

#define TABLECOL_MAX	20		// Maximum number of table columns

typedef struct tablerow_s		// Table row
{
  mmd_t		*cells[TABLECOL_MAX];	// Cells in row
  double	height;			// Row height
} tablerow_t;

#define TABLEROW_MAX	1000		// Maximum number of table rows


//
// Macros...
//

#define in2pt(in) (in * 72.0)
#define mm2pt(mm) (mm * 72.0 / 25.4)


//
// Constants...
//

#define USE_TRUETYPE	0		// Set to 1 to use Roboto TrueType fonts

#if USE_TRUETYPE
#  define UNICODE_VALUE	true		// `true` for Unicode text, `false` for ISO-8859-1
static const char * const docfont_filenames[] =
{
  "Roboto-Regular.ttf",
  "Roboto-Bold.ttf",
  "Roboto-Italic.ttf",
  "RobotoMono-Regular.ttf"
};
#else
#  define UNICODE_VALUE	false		// `true` for Unicode text, `false` for ISO-8859-1
static const char * const docfont_filenames[] =
{
  "Helvetica",
  "Helvetica-Bold",
  "Helvetica-Oblique",
  "Courier"
};
#endif // USE_TRUETYPE

static const char * const docfont_names[] =
{
  "FR",
  "FB",
  "FI",
  "FM"
};

#define BQ_PADDING	18.0		// Padding for block quotes
#define BQ_THICKNESS	3.0		// Thickness of block quote bar

#define CODE_PADDING	4.5		// Padding for code blocks

#define IMAGE_PPI	100.0		// Pixels per inch for images

#define LINE_HEIGHT	1.4		// Multiplier for line height

#define LIST_PADDING	36.0		// Padding/indentation for lists

#define SIZE_BODY	11.0		// Size of body text (points)
#define SIZE_CODEBLOCK	10.0		// Size of code block text (points)
#define SIZE_HEADFOOT	9.0		// Size of header/footer text (points)
#define SIZE_HEADING_1	18.0		// Size of first level heading (points)
#define SIZE_HEADING_2	16.0		// Size of top level heading (points)
#define SIZE_HEADING_3	15.0		// Size of top level heading (points)
#define SIZE_HEADING_4	14.0		// Size of top level heading (points)
#define SIZE_HEADING_5	13.0		// Size of top level heading (points)
#define SIZE_HEADING_6	12.0		// Size of top level heading (points)
#define SIZE_TABLE	10.0		// Size of table text (points)

#define PAGE_WIDTH	mm2pt(210)	// Page width in points
#define PAGE_LENGTH	in2pt(11)	// Page length in points
#define PAGE_LEFT	in2pt(0.5)	// Left margin in points
#define PAGE_RIGHT	(PAGE_WIDTH - in2pt(0.5))
					// Right margin in points
#define PAGE_BOTTOM	in2pt(1.0)	// Bottom margin in points
#define PAGE_TOP	(PAGE_LENGTH - in2pt(1.0))
					// Top margin in points
#define PAGE_HEADER	(PAGE_LENGTH - in2pt(0.5))
					// Vertical position of header
#define PAGE_FOOTER	in2pt(0.5)	// Vertical position of footer

#define TABLE_PADDING	4.5		// Table padding value


//
// Functions...
//

static void	add_images(docdata_t *dd, mmd_t *doc);
static void	add_links(docdata_t *dd);
static pdfio_obj_t *find_image(docdata_t  *dd, const char *url, size_t *imagenum);
static void	format_block(docdata_t *dd, mmd_t *block, docfont_t deffont, double fsize, double left, double right, const char *leader);
static void	format_code(docdata_t *dd, mmd_t *block, double left, double right);
static void	format_doc(docdata_t *dd, mmd_t *doc, docfont_t deffont, double left, double right);
static void	format_table(docdata_t *dd, mmd_t *table, double left, double right);
static void	make_target_name(char *dst, const char *src, size_t dstsize);
static double	measure_cell(docdata_t *dd, mmd_t *cell, tablecol_t *col);
static mmd_t	*mmd_walk_next(mmd_t *top, mmd_t *node);
static void	new_page(docdata_t *dd);
static ssize_t	output_cb(void *output_cbdata, const void *buffer, size_t bytes);
static void	render_line(docdata_t *dd, double margin_left, double need_bottom, double lineheight, size_t num_frags, linefrag_t *frags);
static void	render_row(docdata_t *dd, size_t num_cols, tablecol_t *cols, tablerow_t *row);
static void	set_color(docdata_t *dd, doccolor_t color);
static void	set_font(docdata_t *dd, docfont_t font, double fsize);
static void	write_actions(docdata_t *dd);
static void	write_toc(docdata_t *dd);


//
// 'main()' - Convert markdown to PDF.
//

int					// O - Exit status
main(int  argc,				// I - Number of command-line arguments
     char *argv[])			// I - Command-line arguments
{
  docdata_t	dd;			// Document data
  docfont_t	fontface;		// Current font
  mmd_t		*doc;			// Markdown document
  const char	*value;			// Metadata value


  setbuf(stderr, NULL);

  // Get the markdown file from the command-line...
  if (argc < 2 || argc > 3)
  {
    fputs("Usage: md2pdf FILENAME.md [FILENAME.pdf]\n", stderr);
    fputs("       md2pdf FILENAME.md >FILENAME.pdf\n", stderr);
    return (1);
  }

  if ((doc = mmdLoad(/*root*/NULL, argv[1])) == NULL)
    return (1);

  // Initialize the document data
  memset(&dd, 0, sizeof(dd));

  dd.media_box.x2 = PAGE_WIDTH;
  dd.media_box.y2 = PAGE_LENGTH;

  dd.crop_box.x1 = PAGE_LEFT;
  dd.crop_box.y1 = PAGE_FOOTER;
  dd.crop_box.x2 = PAGE_RIGHT;
  dd.crop_box.y2 = PAGE_HEADER;

  dd.art_box.x1  = PAGE_LEFT;
  dd.art_box.y1  = PAGE_BOTTOM;
  dd.art_box.x2  = PAGE_RIGHT;
  dd.art_box.y2  = PAGE_TOP;

  if ((dd.title = mmdGetMetadata(doc, "title")) == NULL)
    dd.art_box.y2 = PAGE_HEADER;	// No header if there is no title

  if (argc == 2)
  {
    // Output a PDF file to the standard output...
#ifdef _WIN32
    setmode(1, O_BINARY);		// Force binary output on Windows
#endif // _WIN32

    if ((dd.pdf = pdfioFileCreateOutput(output_cb, /*output_cbdata*/NULL, /*version*/NULL, /*media_box*/NULL, /*crop_box*/NULL, /*error_cb*/NULL, /*error_data*/NULL)) == NULL)
      return (1);
  }
  else if ((dd.pdf = pdfioFileCreate(argv[2], /*version*/NULL, /*media_box*/NULL, /*crop_box*/NULL, /*error_cb*/NULL, /*error_data*/NULL)) == NULL)
  {
    return (1);
  }

  if ((value = mmdGetMetadata(doc, "author")) != NULL)
    pdfioFileSetAuthor(dd.pdf, value);

  if ((value = mmdGetMetadata(doc, "keywords")) != NULL)
    pdfioFileSetKeywords(dd.pdf, value);

  if ((value = mmdGetMetadata(doc, "subject")) != NULL)
    pdfioFileSetSubject(dd.pdf, value);
  else if ((value = mmdGetMetadata(doc, "copyright")) != NULL)
    pdfioFileSetSubject(dd.pdf, value);

  if (dd.title)
    pdfioFileSetTitle(dd.pdf, dd.title);

  // Add fonts...
  for (fontface = DOCFONT_REGULAR; fontface < DOCFONT_MAX; fontface ++)
  {
#if USE_TRUETYPE
    if ((dd.fonts[fontface] = pdfioFileCreateFontObjFromFile(dd.pdf, docfont_filenames[fontface], UNICODE_VALUE)) == NULL)
      return (1);
#else
    if ((dd.fonts[fontface] = pdfioFileCreateFontObjFromBase(dd.pdf, docfont_filenames[fontface])) == NULL)
      return (1);
#endif // USE_TRUETYPE
  }

  dd.font_space = pdfioContentTextMeasure(dd.fonts[DOCFONT_REGULAR], " ", 1.0);

  // Add images...
  add_images(&dd, doc);

  // Parse the markdown document...
  format_doc(&dd, doc, DOCFONT_REGULAR, dd.art_box.x1, dd.art_box.x2);

  // Close the PDF and return...
  if (dd.st)
  {
    pdfioStreamClose(dd.st);
    add_links(&dd);
  }

  write_actions(&dd);

  if (dd.num_toc > 0)
    write_toc(&dd);

  pdfioFileClose(dd.pdf);

  mmdFree(doc);

  return (0);
}


//
// 'add_images()' - Scan the markdown document for images.
//

static void
add_images(docdata_t *dd,		// I - Document data
           mmd_t     *doc)		// I - Markdown document
{
  mmd_t	*current,			// Current node
	*next;				// Next node


  // Scan the entire document for images...
  for (current = mmdGetFirstChild(doc); current; current = next)
  {
    // Get next node
    next = mmd_walk_next(doc, current);

    // Look for image nodes...
    if (mmdGetType(current) == MMD_TYPE_IMAGE)
    {
      const char	*url,		// URL for image
			*ext;		// Extension

      url = mmdGetURL(current);
      ext = strrchr(url, '.');

      if (!access(url, 0) && ext && (!strcmp(ext, ".png") || !strcmp(ext, ".jpg") || !strcmp(ext, ".jpeg")))
      {
        // Local JPEG or PNG file, so add it if we haven't already...
        size_t		i;		// Looping var

        for (i = 0; i < dd->num_images; i ++)
        {
          if (!strcmp(dd->images[i].url, url))
            break;
        }

        if (i >= dd->num_images && dd->num_images < DOCIMAGE_MAX)
        {
          dd->images[i].url = url;
          if ((dd->images[i].obj = pdfioFileCreateImageObjFromFile(dd->pdf, url, false)) != NULL)
	    dd->num_images ++;
        }
      }
    }
  }
}


//
// 'add_links()' - Add the page links, if any.
//

static void
add_links(docdata_t *dd)		// I - Document data
{
  size_t	i;			// Looping var
  doclink_t	*l;			// Current link
  pdfio_dict_t	*dict;			// Object dictionary
  pdfio_obj_t	*aobj,			// Annotation object
		*lobj;			// Link object
  pdfio_array_t	*border;		// Border values


  // Add any links we have...
  for (i = 0, l = dd->links; i < dd->num_links; i ++, l ++)
  {
    if (l->url[0] == '#')
    {
      // No remote action...
      aobj = NULL;
    }
    else
    {
      // Create the link action (remote URL)
      dict = pdfioDictCreate(dd->pdf);
      pdfioDictSetName(dict, "S", "URI");
      pdfioDictSetString(dict, "URI", pdfioStringCreate(dd->pdf, l->url));

      aobj = pdfioFileCreateObj(dd->pdf, dict);
      pdfioObjClose(aobj);
    }

    // Create the annotation object pointing to the action...
    dict = pdfioDictCreate(dd->pdf);
    pdfioDictSetName(dict, "Subtype", "Link");
    pdfioDictSetNumber(dict, "F", 4); // Print flag
    pdfioDictSetRect(dict, "Rect", &l->box);
    border = pdfioArrayCreate(dd->pdf);
    pdfioArrayAppendNumber(border, 0.0);
    pdfioArrayAppendNumber(border, 0.0);
    pdfioArrayAppendNumber(border, 0.0);
    pdfioDictSetArray(dict, "Border", border);

    lobj = pdfioFileCreateObj(dd->pdf, dict);

    if (l->url[0] == '#' && dd->num_actions < DOCACTION_MAX)
    {
      // Save this link action for later...
      docaction_t *a = dd->actions + dd->num_actions;
					// New action

      a->target = l->url + 1;
      a->obj    = lobj;

      dd->num_actions ++;
    }
    else if (aobj)
    {
      // Close out this link since we have a remote URL...
      pdfioDictSetObj(dict, "A", aobj);
      pdfioObjClose(lobj);
    }
    else
    {
      // Nothing that can be done for this one...
      pdfioObjClose(lobj);
    }

    pdfioArrayAppendObj(dd->annots_array, lobj);
  }

  // Close the Annots array object...
  pdfioObjClose(dd->annots_obj);

  // Reset links...
  dd->annots_array = NULL;
  dd->annots_obj   = NULL;
  dd->num_links    = 0;
}


//
// 'find_image()' - Find an image in the document.
//

static pdfio_obj_t *			// O - Image object or `NULL` if none
find_image(docdata_t  *dd,		// I - Document data
           const char *url,		// I - Image URL
           size_t     *imagenum)	// O - Image number
{
  size_t	i;			// Looping var


  // Look for a matching URL...
  for (i = 0; i < dd->num_images; i ++)
  {
    if (!strcmp(dd->images[i].url, url))
    {
      if (imagenum)
        *imagenum = i;

      return (dd->images[i].obj);
    }
  }

  // Not found, return NULL...
  if (imagenum)
    *imagenum = 0;

  return (NULL);
}


//
// 'format_block()' - Format a block of text
//

static void
format_block(docdata_t  *dd,		// I - Document data
             mmd_t      *block,		// I - Block to format
             docfont_t  deffont,	// I - Default font
             double     fsize,		// I - Size of font
             double     left,		// I - Left margin
             double     right,		// I - Right margin
             const char *leader)	// I - Leader text on the first line
{
  mmd_type_t	blocktype;		// Block type
  mmd_t		*current,		// Current node
		*next;			// Next node
  size_t	num_frags;		// Number of line fragments
  linefrag_t	frags[LINEFRAG_MAX],	// Line fragments
		*frag;			// Current fragment
  mmd_type_t	type;			// Current node type
  const char	*text,			// Current text
		*url;			// Current URL, if any
  bool		ws;			// Current whitespace
  pdfio_obj_t	*image;			// Current image, if any
  size_t	imagenum;		// Current image number
  doccolor_t	color = DOCCOLOR_BLACK;	// Current text color
  docfont_t	font = deffont;		// Current text font
  double	x,			// Current position
		width,			// Width of current fragment
		wswidth,		// Width of whitespace
		margin_left,		// Left margin
		need_bottom,		// Space needed after this block
		height,			// Height of current fragment
		lineheight;		// Height of current line


  blocktype  = mmdGetType(block);

  if (mmdGetNextSibling(block))
    need_bottom = 3.0 * SIZE_BODY * LINE_HEIGHT;
  else
    need_bottom = 0.0;

  if (leader)
  {
    // Add leader text on first line...
    frags[0].type     = MMD_TYPE_NORMAL_TEXT;
    frags[0].width    = pdfioContentTextMeasure(dd->fonts[deffont], leader, fsize);
    frags[0].height   = fsize;
    frags[0].x        = left - frags[0].width;
    frags[0].imagenum = 0;
    frags[0].text     = leader;
    frags[0].url      = NULL;
    frags[0].ws       = false;
    frags[0].font     = deffont;
    frags[0].color    = DOCCOLOR_BLACK;

    num_frags  = 1;
    lineheight = fsize * LINE_HEIGHT;
  }
  else
  {
    // No leader text...
    num_frags  = 0;
    lineheight = 0.0;
  }

  frag = frags + num_frags;

  switch (mmdGetType(block))
  {
    case MMD_TYPE_HEADING_1 :
        dd->tag = "H1";
        break;

    case MMD_TYPE_HEADING_2 :
        dd->tag = "H2";
        break;

    case MMD_TYPE_HEADING_3 :
        dd->tag = "H3";
        break;

    case MMD_TYPE_HEADING_4 :
        dd->tag = "H4";
        break;

    case MMD_TYPE_HEADING_5 :
        dd->tag = "H5";
        break;

    case MMD_TYPE_HEADING_6 :
        dd->tag = "H6";
        break;

    case MMD_TYPE_TABLE_HEADER_CELL :
        dd->tag = "TH";
        break;

    case MMD_TYPE_TABLE_BODY_CELL_LEFT :
    case MMD_TYPE_TABLE_BODY_CELL_CENTER :
    case MMD_TYPE_TABLE_BODY_CELL_RIGHT :
        dd->tag = "TD";
        break;

    default :
        dd->tag = "P";
        break;
  }

  if (dd->st)
    pdfioContentBeginMarked(dd->st, dd->tag, /*dict*/NULL);

  // Loop through the block and render lines...
  for (current = mmdGetFirstChild(block), x = left; current; current = next)
  {
    // Get information about the current node...
    type     = mmdGetType(current);
    text     = mmdGetText(current);
    image    = NULL;
    imagenum = 0;
    url      = mmdGetURL(current);
    ws       = mmdGetWhitespace(current);
    wswidth  = ws ? dd->font_space * fsize : 0.0;
    next     = mmd_walk_next(block, current);

    // Process the node...
    if (type == MMD_TYPE_IMAGE && url)
    {
      // Embed an image
      if ((image = find_image(dd, url, &imagenum)) == NULL)
        continue;

      // Image - treat as 100dpi
      width  = 72.0 * pdfioImageGetWidth(image) / IMAGE_PPI;
      height = 72.0 * pdfioImageGetHeight(image) / IMAGE_PPI;
      text   = NULL;

      if (width > (right - left))
      {
        // Too wide, scale to width...
        width  = right - left;
        height = width * pdfioImageGetHeight(image) / pdfioImageGetWidth(image);
      }
      else if (height > (dd->art_box.y2 - dd->art_box.y1))
      {
        // Too tall, scale to height...
        height = dd->art_box.y2 - dd->art_box.y1;
        width  = height * pdfioImageGetWidth(image) / pdfioImageGetHeight(image);
      }
    }
    else if (type == MMD_TYPE_HARD_BREAK && num_frags > 0)
    {
      if (blocktype == MMD_TYPE_TABLE_HEADER_CELL || blocktype == MMD_TYPE_TABLE_BODY_CELL_CENTER)
        margin_left = 0.5 * (right - x);
      else if (blocktype == MMD_TYPE_TABLE_BODY_CELL_RIGHT)
        margin_left = right - x;
      else
        margin_left = 0.0;

      render_line(dd, margin_left, need_bottom, lineheight, num_frags, frags);

      if (deffont == DOCFONT_ITALIC)
      {
        // Add an orange bar to the left of block quotes...
        set_color(dd, DOCCOLOR_ORANGE);
        pdfioContentPathRect(dd->st, left - BQ_PADDING, dd->y - (LINE_HEIGHT - 1.0) * fsize - BQ_THICKNESS, BQ_THICKNESS, lineheight + 2.0 * BQ_THICKNESS);
        pdfioContentFill(dd->st, /*even_odd*/false);
      }

      num_frags   = 0;
      frag        = frags;
      x           = left;
      lineheight  = 0.0;
      need_bottom = 0.0;

      continue;
    }
    else if (type == MMD_TYPE_CHECKBOX)
    {
      // Checkbox
      width = height = fsize;
    }
    else if (!text)
    {
      continue;
    }
    else
    {
      // Text fragment...
      if (type == MMD_TYPE_EMPHASIZED_TEXT)
	font = DOCFONT_ITALIC;
      else if (type == MMD_TYPE_STRONG_TEXT)
	font = DOCFONT_BOLD;
      else  if (type == MMD_TYPE_CODE_TEXT)
	font = DOCFONT_MONOSPACE;
      else
	font = deffont;

      if (type == MMD_TYPE_CODE_TEXT)
	color = DOCCOLOR_RED;
      else if (type == MMD_TYPE_LINKED_TEXT)
	color = DOCCOLOR_BLUE;
      else
	color = DOCCOLOR_BLACK;

      width  = pdfioContentTextMeasure(dd->fonts[font], text, fsize);
      height = fsize * LINE_HEIGHT;
    }

    // See if this node will fit on the current line...
    if ((num_frags > 0 && (x + width + wswidth) >= right) || num_frags == LINEFRAG_MAX)
    {
      // No, render this line and start over...
      if (blocktype == MMD_TYPE_TABLE_HEADER_CELL || blocktype == MMD_TYPE_TABLE_BODY_CELL_CENTER)
        margin_left = 0.5 * (right - x);
      else if (blocktype == MMD_TYPE_TABLE_BODY_CELL_RIGHT)
        margin_left = right - x;
      else
        margin_left = 0.0;

      render_line(dd, margin_left, need_bottom, lineheight, num_frags, frags);

      num_frags   = 0;
      frag        = frags;
      x           = left;
      lineheight  = 0.0;
      need_bottom = 0.0;

      if (deffont == DOCFONT_ITALIC)
      {
        // Add an orange bar to the left of block quotes...
        set_color(dd, DOCCOLOR_ORANGE);
        pdfioContentSave(dd->st);
        pdfioContentSetLineWidth(dd->st, 3.0);
        pdfioContentPathMoveTo(dd->st, left - 6.0, dd->y - (LINE_HEIGHT - 1.0) * fsize);
        pdfioContentPathLineTo(dd->st, left - 6.0, dd->y + fsize);
        pdfioContentStroke(dd->st);
        pdfioContentRestore(dd->st);
      }
    }

    // Add the current node to the fragment list
    if (num_frags == 0)
    {
      // No leading whitespace at the start of the line
      ws      = false;
      wswidth = 0.0;
    }

    frag->type       = type;
    frag->x          = x;
    frag->width      = width + wswidth;
    frag->height     = text ? fsize : height;
    frag->imagenum   = imagenum;
    frag->text       = text;
    frag->url        = url;
    frag->ws         = ws;
    frag->font       = font;
    frag->color      = color;

    num_frags ++;
    frag ++;
    x += width + wswidth;
    if (height > lineheight)
      lineheight = height;
  }

  if (num_frags > 0)
  {
    if (blocktype == MMD_TYPE_TABLE_HEADER_CELL || blocktype == MMD_TYPE_TABLE_BODY_CELL_CENTER)
      margin_left = 0.5 * (right - x);
    else if (blocktype == MMD_TYPE_TABLE_BODY_CELL_RIGHT)
      margin_left = right - x;
    else
      margin_left = 0.0;

    render_line(dd, margin_left, need_bottom, lineheight, num_frags, frags);

    if (deffont == DOCFONT_ITALIC)
    {
      // Add an orange bar to the left of block quotes...
      set_color(dd, DOCCOLOR_ORANGE);
      pdfioContentSave(dd->st);
      pdfioContentSetLineWidth(dd->st, 3.0);
      pdfioContentPathMoveTo(dd->st, left - 6.0, dd->y - (LINE_HEIGHT - 1.0) * fsize);
      pdfioContentPathLineTo(dd->st, left - 6.0, dd->y + fsize);
      pdfioContentStroke(dd->st);
      pdfioContentRestore(dd->st);
    }
  }

  pdfioContentEndMarked(dd->st);
  dd->tag = NULL;
}


//
// 'format_code()' - Format a code block...
//

static void
format_code(docdata_t *dd,		// I - Document data
            mmd_t     *block,		// I - Code block
            double    left,		// I - Left margin
            double    right)		// I - Right margin
{
  mmd_t		*code;			// Current code block
  double	lineheight;		// Line height


  // Compute line height...
  lineheight = SIZE_CODEBLOCK * LINE_HEIGHT;

  // Start a new page as needed...
  if (!dd->st)
    new_page(dd);

  dd->y -= lineheight + CODE_PADDING;

  if ((dd->y - lineheight) < dd->art_box.y1)
  {
    new_page(dd);

    dd->y -= lineheight + CODE_PADDING;
  }

  // Draw the top padding...
  set_color(dd, DOCCOLOR_LTGRAY);
  pdfioContentPathRect(dd->st, left - CODE_PADDING, dd->y + SIZE_CODEBLOCK, right - left + 2.0 * CODE_PADDING, CODE_PADDING);
  pdfioContentFillAndStroke(dd->st, false);

  // Start a code text block...
  dd->tag = "P";
  pdfioContentBeginMarked(dd->st, dd->tag, /*dict*/NULL);

  set_font(dd, DOCFONT_MONOSPACE, SIZE_CODEBLOCK);
  pdfioContentTextBegin(dd->st);
  pdfioContentTextMoveTo(dd->st, left, dd->y);

  for (code = mmdGetFirstChild(block); code; code = mmdGetNextSibling(code))
  {
    set_color(dd, DOCCOLOR_LTGRAY);
    pdfioContentPathRect(dd->st, left - CODE_PADDING, dd->y - (LINE_HEIGHT - 1.0) * SIZE_CODEBLOCK, right - left + 2.0 * CODE_PADDING, lineheight);
    pdfioContentFillAndStroke(dd->st, false);

    set_color(dd, DOCCOLOR_RED);
    pdfioContentTextShow(dd->st, UNICODE_VALUE, mmdGetText(code));
    dd->y -= lineheight;

    if (dd->y < dd->art_box.y1)
    {
      // End the current text block...
      pdfioContentTextEnd(dd->st);

      // Start a new page...
      new_page(dd);
      set_font(dd, DOCFONT_MONOSPACE, SIZE_CODEBLOCK);

      dd->y -= lineheight;

      pdfioContentTextBegin(dd->st);
      pdfioContentTextMoveTo(dd->st, left, dd->y);
    }
  }

  // End the current text block...
  pdfioContentTextEnd(dd->st);
  dd->y += lineheight;

  pdfioContentEndMarked(dd->st);
  dd->tag = NULL;

  // Draw the bottom padding...
  set_color(dd, DOCCOLOR_LTGRAY);
  pdfioContentPathRect(dd->st, left - CODE_PADDING, dd->y - CODE_PADDING - (LINE_HEIGHT - 1.0) * SIZE_CODEBLOCK, right - left + 2.0 * CODE_PADDING, CODE_PADDING);
  pdfioContentFillAndStroke(dd->st, false);
}


//
// 'format_doc()' - Format a document.
//

static void
format_doc(docdata_t *dd,		// I - Document data
           mmd_t     *doc,		// I - Document node to format
           docfont_t deffont,		// I - Default font
           double    left,		// I - Left margin
           double    right)		// I - Right margin
{
  int		i;			// Child number
  mmd_type_t	doctype;		// Document node type
  mmd_t		*current;		// Current node
  mmd_type_t	curtype;		// Current node type
  char		leader[32];		// Leader
  static const double heading_sizes[] =	// Heading font sizes
  {
    SIZE_HEADING_1,
    SIZE_HEADING_2,
    SIZE_HEADING_3,
    SIZE_HEADING_4,
    SIZE_HEADING_5,
    SIZE_HEADING_6
  };


  doctype = mmdGetType(doc);

  for (i = 1, current = mmdGetFirstChild(doc); current; i ++, current = mmdGetNextSibling(current))
  {
    switch (curtype = mmdGetType(current))
    {
      default :
          break;

      case MMD_TYPE_THEMATIC_BREAK :
          // Force a page break
          dd->y = dd->art_box.y1;
          break;

      case MMD_TYPE_BLOCK_QUOTE :
          format_doc(dd, current, DOCFONT_ITALIC, left + BQ_PADDING, right - BQ_PADDING);
          break;

      case MMD_TYPE_ORDERED_LIST :
      case MMD_TYPE_UNORDERED_LIST :
	  dd->y -= SIZE_BODY * LINE_HEIGHT;

          format_doc(dd, current, deffont, left + LIST_PADDING, right);
          break;

      case MMD_TYPE_LIST_ITEM :
          if (doctype == MMD_TYPE_ORDERED_LIST)
          {
            snprintf(leader, sizeof(leader), "%d. ", i);
            format_block(dd, current, deffont, SIZE_BODY, left, right, leader);
          }
          else
          {
            format_block(dd, current, deffont, SIZE_BODY, left, right, /*leader*/"• ");
          }
          break;

      case MMD_TYPE_HEADING_1 :
      case MMD_TYPE_HEADING_2 :
      case MMD_TYPE_HEADING_3 :
      case MMD_TYPE_HEADING_4 :
      case MMD_TYPE_HEADING_5 :
      case MMD_TYPE_HEADING_6 :
          // Update the current heading
          free(dd->heading);
          dd->heading = mmdCopyAllText(current);

          // Add a blank line before the heading...
	  dd->y -= heading_sizes[curtype - MMD_TYPE_HEADING_1] * LINE_HEIGHT;

	  // Format the heading...
          format_block(dd, current, DOCFONT_BOLD, heading_sizes[curtype - MMD_TYPE_HEADING_1], left, right, /*leader*/NULL);

          // Add the heading to the table-of-contents...
          if (dd->num_toc < DOCTOC_MAX)
          {
            doctoc_t *t = dd->toc + dd->num_toc;
					// New TOC
	    pdfio_array_t *dest;	// Destination array

            t->level = curtype - MMD_TYPE_HEADING_1;
            t->dict  = pdfioDictCreate(dd->pdf);
            t->obj   = pdfioFileCreateObj(dd->pdf, t->dict);
            dest     = pdfioArrayCreate(dd->pdf);

	    pdfioArrayAppendObj(dest, pdfioFileGetPage(dd->pdf, pdfioFileGetNumPages(dd->pdf) - 1));
	    pdfioArrayAppendName(dest, "XYZ");
	    pdfioArrayAppendNumber(dest, PAGE_LEFT);
	    pdfioArrayAppendNumber(dest, dd->y + heading_sizes[curtype - MMD_TYPE_HEADING_1] * LINE_HEIGHT);
	    pdfioArrayAppendNumber(dest, 0.0);

	    pdfioDictSetArray(t->dict, "Dest", dest);
	    pdfioDictSetString(t->dict, "Title", pdfioStringCreate(dd->pdf, dd->heading));

            dd->num_toc ++;
          }

	  // Add the heading to the list of link targets...
          if (dd->num_targets < DOCTARGET_MAX)
          {
            doctarget_t *t = dd->targets + dd->num_targets;
					// New target

            make_target_name(t->name, dd->heading, sizeof(t->name));
            t->page = pdfioFileGetNumPages(dd->pdf) - 1;
            t->y    = dd->y + heading_sizes[curtype - MMD_TYPE_HEADING_1] * LINE_HEIGHT;

            dd->num_targets ++;
          }
          break;

      case MMD_TYPE_PARAGRAPH :
          // Add a blank line before the paragraph...
	  dd->y -= SIZE_BODY * LINE_HEIGHT;

	  // Format the paragraph...
          format_block(dd, current, deffont, SIZE_BODY, left, right, /*leader*/NULL);
          break;

      case MMD_TYPE_TABLE :
          // Add a blank line before the paragraph...
	  dd->y -= SIZE_BODY * LINE_HEIGHT;

	  // Format the table...
          format_table(dd, current, left, right);
          break;

      case MMD_TYPE_CODE_BLOCK :
          // Add a blank line before the code block...
	  dd->y -= SIZE_BODY * LINE_HEIGHT;

	  // Format the code block...
          format_code(dd, current, left + CODE_PADDING, right - CODE_PADDING);
          break;
    }
  }
}


//
// 'format_table()' - Format a table...
//

static void
format_table(docdata_t *dd,		// I - Document data
             mmd_t     *table,		// I - Table node
             double    left,		// I - Left margin
             double    right)		// I - Right margin
{
  mmd_t		*current,		// Current node
		*next;			// Next node
  mmd_type_t	type;			// Node type
  size_t	col,			// Current column
		num_cols;		// Number of columns
  tablecol_t	cols[TABLECOL_MAX];	// Columns
  size_t	row,			// Current row
		num_rows;		// Number of rows
  tablerow_t	rows[TABLEROW_MAX],	// Rows
		*rowptr;		// Pointer to current row
  double	x,			// Current X position
		height,			// Height of cell
		format_width,		// Maximum format width of table
		table_width;		// Total width of table


  // Find all of the rows and columns in the table...
  memset(cols, 0, sizeof(cols));
  memset(rows, 0, sizeof(rows));

  for (num_cols = 0, num_rows = 0, rowptr = rows, current = mmdGetFirstChild(table); current && num_rows < TABLEROW_MAX; current = next)
  {
    next = mmd_walk_next(table, current);
    type = mmdGetType(current);

    if (type == MMD_TYPE_TABLE_ROW)
    {
      // Parse row...
      for (col = 0, current = mmdGetFirstChild(current); current && num_cols < TABLECOL_MAX; current = mmdGetNextSibling(current), col ++)
      {
        rowptr->cells[col] = current;

        measure_cell(dd, current, cols + col);

        if (col >= num_cols)
          num_cols = col + 1;
      }

      rowptr ++;
      num_rows ++;
    }
  }

  // Figure out the width of each column...
  for (col = 0, table_width = 0.0; col < num_cols; col ++)
  {
    cols[col].max_width += 2.0 * TABLE_PADDING;

    table_width += cols[col].max_width;
    cols[col].width = cols[col].max_width;
  }

  format_width = right - left - 2.0 * TABLE_PADDING * num_cols;

  if (table_width > format_width)
  {
    // Content too wide, try scaling the widths...
    double	avg_width,		// Average column width
		base_width,		// Base width
		remaining_width,	// Remaining width
		scale_width;		// Width for scaling
    size_t	num_remaining_cols = 0;	// Number of remaining columns

    // First mark any columns that are narrower than the average width...
    avg_width = format_width / num_cols;

    for (col = 0, base_width = 0.0, remaining_width = 0.0; col < num_cols; col ++)
    {
      if (cols[col].width > avg_width)
      {
        remaining_width += cols[col].width;
        num_remaining_cols ++;
      }
      else
      {
        base_width += cols[col].width;
      }
    }

    // Then proportionately distribute the remaining width to the other columns...
    format_width -= base_width;

    for (col = 0, table_width = 0.0; col < num_cols; col ++)
    {
      if (cols[col].width > avg_width)
        cols[col].width = cols[col].width * format_width / remaining_width;

      table_width += cols[col].width;
    }
  }

  // Calculate the margins of each column in preparation for formatting
  for (col = 0, x = left + TABLE_PADDING; col < num_cols; col ++)
  {
    cols[col].left  = x;
    cols[col].right = x + cols[col].width;

    x += cols[col].width + 2.0 * TABLE_PADDING;
  }

  // Calculate the height of each row and cell in preparation for formatting
  for (row = 0, rowptr = rows; row < num_rows; row ++, rowptr ++)
  {
    for (col = 0; col < num_cols; col ++)
    {
      height = measure_cell(dd, rowptr->cells[col], cols + col) + 2.0 * TABLE_PADDING;
      if (height > rowptr->height)
        rowptr->height = height;
    }
  }

  // Render each table row...
  dd->in_table = true;

  if (dd->st)
    pdfioContentBeginMarked(dd->st, "Table", /*dict*/NULL);

  for (row = 0, rowptr = rows; row < num_rows; row ++, rowptr ++)
    render_row(dd, num_cols, cols, rowptr);

  pdfioContentEndMarked(dd->st);

  dd->in_table = false;
}


//
// 'make_target_name()' - Convert text to a target name.
//

static void
make_target_name(char       *dst,	// I - Destination buffer
                 const char *src,	// I - Source text
                 size_t     dstsize)	// I - Size of destination buffer
{
  char	*dstptr = dst,			// Pointer into destination
	*dstend = dst + dstsize - 1;	// End of destination


  while (*src && dstptr < dstend)
  {
    if (isalnum(*src) || *src == '.' || *src == '-')
      *dstptr++ = tolower(*src);
    else if (*src == ' ')
      *dstptr++ = '-';

    src ++;
  }

  *dstptr = '\0';
}


//
// 'measure_cell()' - Measure the dimensions of a table cell.
//

static double				// O - Formatted height
measure_cell(docdata_t  *dd,		// I - Document data
             mmd_t      *cell,		// I - Cell node
             tablecol_t *col)		// O - Column data
{
  mmd_t		*current,		// Current node
		*next;			// Next node
  mmd_type_t	type;			// Node type
  const char	*text,			// Current text
		*url;			// Current URL, if any
  bool		ws;			// Current whitespace
  docfont_t	font;			// Current font
  double	x = 0.0,		// Current X position
		width,			// Width of node
		wswidth,		// Width of whitespace
		height,			// Height of node
		lineheight = 0.0,	// Height of line
		cellheight = 0.0;	// Height of cell


  for (current = mmdGetFirstChild(cell); current; current = next)
  {
    next    = mmd_walk_next(cell, current);
    type    = mmdGetType(current);
    text    = mmdGetText(current);
    url     = mmdGetURL(current);
    ws      = mmdGetWhitespace(current);
    wswidth = 0.0;

    if (type == MMD_TYPE_IMAGE && url)
    {
      // Embed an image
      pdfio_obj_t *image = find_image(dd, url, /*imagenum*/NULL);
					// Image object

      if (!image)
        continue;

      // Image - treat as 100dpi
      width  = 72.0 * pdfioImageGetWidth(image) / IMAGE_PPI;
      height = 72.0 * pdfioImageGetHeight(image) / IMAGE_PPI;

      if (col->width > 0.0 && width > col->width)
      {
	// Too wide, scale to width...
	width  = col->width;
	height = width * pdfioImageGetHeight(image) / pdfioImageGetWidth(image);
      }
      else if (height > (dd->art_box.y2 - dd->art_box.y1))
      {
	// Too tall, scale to height...
	height = dd->art_box.y2 - dd->art_box.y1;
	width  = height * pdfioImageGetWidth(image) / pdfioImageGetHeight(image);
      }
    }
    else if (type == MMD_TYPE_HARD_BREAK && x > 0.0)
    {
      // Hard break...
      if (x > col->max_width)
        col->max_width = x;

      cellheight += lineheight;
      x          = 0.0;
      lineheight = 0.0;
      continue;
    }
    else if (type == MMD_TYPE_CHECKBOX)
    {
      // Checkbox...
      width = height = SIZE_TABLE;
    }
    else if (!text)
    {
      continue;
    }
    else
    {
      // Text fragment...
      if (type == MMD_TYPE_EMPHASIZED_TEXT)
	font = DOCFONT_ITALIC;
      else if (type == MMD_TYPE_STRONG_TEXT)
	font = DOCFONT_BOLD;
      else  if (type == MMD_TYPE_CODE_TEXT)
	font = DOCFONT_MONOSPACE;
      else if (mmdGetType(cell) == MMD_TYPE_TABLE_HEADER_CELL)
        font = DOCFONT_BOLD;
      else
	font = DOCFONT_REGULAR;

      width  = pdfioContentTextMeasure(dd->fonts[font], text, SIZE_TABLE);
      height = SIZE_TABLE * LINE_HEIGHT;

      if (ws && x > 0.0)
	wswidth = pdfioContentTextMeasure(dd->fonts[font], " ", SIZE_TABLE);
    }

    if (width > col->min_width)
      col->min_width = width;

    // See if this node will fit on the current line
    if (col->width > 0.0 && (x + width + wswidth) >= col->width)
    {
      // No, record the new line...
      if (x > col->max_width)
        col->max_width = x;

      cellheight += lineheight;
      x          = 0.0;
      lineheight = 0.0;
      wswidth    = 0.0;
    }

    x += width + wswidth;

    if (height > lineheight)
      lineheight = height;
  }

  // Capture the last line's measurements...
  if (x > col->max_width)
    col->max_width = x;

  if (x > 0.0)
    cellheight += lineheight;

  // Return the total height...
  return (cellheight);
}


//
// 'mmd_walk_next()' - Find the next markdown node.
//

static mmd_t *				// O - Next node or `NULL` at end
mmd_walk_next(mmd_t *top,		// I - Top node
              mmd_t *node)		// I - Current node
{
  mmd_t	*next,				// Next node
	*parent;			// Parent node


  // Figure out the next node under "top"...
  if ((next = mmdGetFirstChild(node)) == NULL)
  {
    if ((next = mmdGetNextSibling(node)) == NULL)
    {
      if ((parent = mmdGetParent(node)) != top)
      {
	while ((next = mmdGetNextSibling(parent)) == NULL)
	{
	  if ((parent = mmdGetParent(parent)) == top)
	    break;
	}
      }
    }
  }

  return (next);
}


//
// 'new_page()' - Start a new page.
//

static void
new_page(docdata_t *dd)			// I - Document data
{
  pdfio_dict_t	*page_dict;		// Page dictionary
  docfont_t	fontface;		// Current font face
  size_t	i;			// Looping var
  char		temp[32];		// Temporary string
  double	width;			// Width of fragment


  // Close the current page...
  if (dd->st)
  {
    if (dd->tag)
    {
      // Close current tag and any row or table...
      pdfioContentEndMarked(dd->st);
      if (dd->in_row)
        pdfioContentEndMarked(dd->st);
      if (dd->in_table)
        pdfioContentEndMarked(dd->st);
    }

    pdfioStreamClose(dd->st);
    add_links(dd);
  }

  // Prep the new page...
  page_dict = pdfioDictCreate(dd->pdf);

  dd->annots_array = pdfioArrayCreate(dd->pdf);
  dd->annots_obj   = pdfioFileCreateArrayObj(dd->pdf, dd->annots_array);
  pdfioDictSetObj(page_dict, "Annots", dd->annots_obj);

  pdfioDictSetRect(page_dict, "MediaBox", &dd->media_box);
//  pdfioDictSetRect(page_dict, "CropBox", &dd->crop_box);
  pdfioDictSetRect(page_dict, "ArtBox", &dd->art_box);

  for (fontface = DOCFONT_REGULAR; fontface < DOCFONT_MAX; fontface ++)
    pdfioPageDictAddFont(page_dict, docfont_names[fontface], dd->fonts[fontface]);

  for (i = 0; i < dd->num_images; i ++)
    pdfioPageDictAddImage(page_dict, pdfioStringCreatef(dd->pdf, "I%u", (unsigned)i), dd->images[i].obj);

  dd->st    = pdfioFileCreatePage(dd->pdf, page_dict);
  dd->color = DOCCOLOR_BLACK;
  dd->font  = DOCFONT_MAX;
  dd->fsize = 0.0;
  dd->y     = dd->art_box.y2;

  // Add header/footer text
  pdfioContentBeginMarked(dd->st, "Artifact", /*dict*/NULL);
  set_color(dd, DOCCOLOR_GRAY);
  set_font(dd, DOCFONT_REGULAR, SIZE_HEADFOOT);

  if (pdfioFileGetNumPages(dd->pdf) > 1 && dd->title)
  {
    // Show title in header...
    width = pdfioContentTextMeasure(dd->fonts[DOCFONT_REGULAR], dd->title, SIZE_HEADFOOT);

    pdfioContentTextBegin(dd->st);
    pdfioContentTextMoveTo(dd->st, dd->crop_box.x1 + 0.5 * (dd->crop_box.x2 - dd->crop_box.x1 - width), dd->crop_box.y2 - SIZE_HEADFOOT);
    pdfioContentTextShow(dd->st, UNICODE_VALUE, dd->title);
    pdfioContentTextEnd(dd->st);

    pdfioContentPathMoveTo(dd->st, dd->crop_box.x1, dd->crop_box.y2 - 2 * SIZE_HEADFOOT * LINE_HEIGHT + SIZE_HEADFOOT);
    pdfioContentPathLineTo(dd->st, dd->crop_box.x2, dd->crop_box.y2 - 2 * SIZE_HEADFOOT * LINE_HEIGHT + SIZE_HEADFOOT);
    pdfioContentStroke(dd->st);
  }

  // Show page number and current heading...
  pdfioContentPathMoveTo(dd->st, dd->crop_box.x1, dd->crop_box.y1 + SIZE_HEADFOOT * LINE_HEIGHT);
  pdfioContentPathLineTo(dd->st, dd->crop_box.x2, dd->crop_box.y1 + SIZE_HEADFOOT * LINE_HEIGHT);
  pdfioContentStroke(dd->st);

  pdfioContentTextBegin(dd->st);
  snprintf(temp, sizeof(temp), "%u", (unsigned)pdfioFileGetNumPages(dd->pdf));
  if (pdfioFileGetNumPages(dd->pdf) & 1)
  {
    // Page number on right...
    width = pdfioContentTextMeasure(dd->fonts[DOCFONT_REGULAR], temp, SIZE_HEADFOOT);
    pdfioContentTextMoveTo(dd->st, dd->crop_box.x2 - width, dd->crop_box.y1);
  }
  else
  {
    // Page number on left...
    pdfioContentTextMoveTo(dd->st, dd->crop_box.x1, dd->crop_box.y1);
  }

  pdfioContentTextShow(dd->st, UNICODE_VALUE, temp);
  pdfioContentTextEnd(dd->st);

  if (dd->heading)
  {
    pdfioContentTextBegin(dd->st);

    if (pdfioFileGetNumPages(dd->pdf) & 1)
    {
      // Current heading on left...
      pdfioContentTextMoveTo(dd->st, dd->crop_box.x1, dd->crop_box.y1);
    }
    else
    {
      width = pdfioContentTextMeasure(dd->fonts[DOCFONT_REGULAR], dd->heading, SIZE_HEADFOOT);
      pdfioContentTextMoveTo(dd->st, dd->crop_box.x2 - width, dd->crop_box.y1);
    }

    pdfioContentTextShow(dd->st, UNICODE_VALUE, dd->heading);
    pdfioContentTextEnd(dd->st);
  }

  pdfioContentEndMarked(dd->st);

  if (dd->in_table)
    pdfioContentBeginMarked(dd->st, "Table", /*dict*/NULL);

  if (dd->in_row)
    pdfioContentBeginMarked(dd->st, "TR", /*dict*/NULL);

  if (dd->tag)
    pdfioContentBeginMarked(dd->st, dd->tag, /*dict*/NULL);
}


//
// 'output_cb()' - Write PDF data to the standard output...
//

static ssize_t				// O - Number of bytes written
output_cb(void       *output_cbdata,	// I - Callback data (not used)
          const void *buffer,		// I - Buffer to write
          size_t     bytes)		// I - Number of bytes to write
{
  (void)output_cbdata;

  return ((ssize_t)fwrite(buffer, 1, bytes, stdout));
}


//
// 'render_line()' - Render a line of text/graphics.
//

static void
render_line(docdata_t  *dd,		// I - Document data
            double     margin_left,	// I - Left margin
            double     need_bottom,	// I - How much space is needed after
            double     lineheight,	// I - Height of line
            size_t     num_frags,	// I - Number of line fragments
            linefrag_t *frags)		// I - Line fragments
{
  size_t	i;			// Looping var
  linefrag_t	*frag;			// Current line fragment
  bool		in_text = false;	// Are we in a text block?


  if (!dd->st)
    new_page(dd);

  dd->y -= lineheight;
  if ((dd->y - need_bottom) < dd->art_box.y1)
  {
    new_page(dd);

    dd->y -= lineheight;
  }

  for (i = 0, frag = frags; i < num_frags; i ++, frag ++)
  {
    if (frag->type == MMD_TYPE_CHECKBOX)
    {
      // Draw checkbox...
      set_color(dd, frag->color);

      if (in_text)
      {
	pdfioContentTextEnd(dd->st);
	in_text = false;
      }

      // Add box
      pdfioContentPathRect(dd->st, frag->x + 1.0 + margin_left, dd->y, frag->width - 3.0, frag->height - 3.0);

      if (frag->text)
      {
        // Add check
        pdfioContentPathMoveTo(dd->st, frag->x + 3.0 + margin_left, dd->y + 2.0);
        pdfioContentPathLineTo(dd->st, frag->x + frag->width - 4.0 + margin_left, dd->y + frag->height - 5.0);

        pdfioContentPathMoveTo(dd->st, frag->x + 3.0 + margin_left, dd->y + frag->height - 5.0);
        pdfioContentPathLineTo(dd->st, frag->x + frag->width - 4.0 + margin_left, dd->y + 2.0);
      }

      pdfioContentStroke(dd->st);
    }
    else if (frag->text)
    {
      // Draw text
      if (!in_text)
      {
	pdfioContentTextBegin(dd->st);
	pdfioContentTextMoveTo(dd->st, frag->x + margin_left, dd->y);

	in_text = true;
      }

      if (frag->ws && frag->font == DOCFONT_MONOSPACE)
      {
	set_font(dd, DOCFONT_REGULAR, frag->height);
	pdfioContentTextShow(dd->st, UNICODE_VALUE, " ");
      }

      set_color(dd, frag->color);
      set_font(dd, frag->font, frag->height);

      if (frag->font == DOCFONT_MONOSPACE)
	pdfioContentTextShow(dd->st, UNICODE_VALUE, frag->text);
      else
	pdfioContentTextShowf(dd->st, UNICODE_VALUE, "%s%s", frag->ws ? " " : "", frag->text);

      if (frag->url && dd->num_links < DOCLINK_MAX)
      {
        doclink_t *l = dd->links + dd->num_links;
					// Pointer to this link record

        if (!strcmp(frag->url, "@"))
        {
          // Use mapped text as link target...
          char	targetlink[129];	// Targeted link

          targetlink[0] = '#';
          make_target_name(targetlink + 1, frag->text, sizeof(targetlink) - 1);

          l->url = pdfioStringCreate(dd->pdf, targetlink);
        }
        else if (!strcmp(frag->url, "@@"))
        {
          // Use literal text as anchor...
          l->url = pdfioStringCreatef(dd->pdf, "#%s", frag->text);
        }
        else
        {
          // Use URL as-is...
          l->url = frag->url;
        }

        l->box.x1 = frag->x;
        l->box.y1 = dd->y;
        l->box.x2 = frag->x + frag->width;
        l->box.y2 = dd->y + frag->height;

        dd->num_links ++;
      }
    }
    else
    {
      // Draw image
      char	imagename[32];		// Current image name

      if (in_text)
      {
	pdfioContentTextEnd(dd->st);
	in_text = false;
      }

      snprintf(imagename, sizeof(imagename), "I%u", (unsigned)frag->imagenum);
      pdfioContentDrawImage(dd->st, imagename, frag->x + margin_left, dd->y, frag->width, frag->height);
    }
  }

  if (in_text)
    pdfioContentTextEnd(dd->st);
}


//
// 'render_row()' - Render a table row...
//

static void
render_row(docdata_t  *dd,		// I - Document data
           size_t     num_cols,		// I - Number of columns
           tablecol_t *cols,		// I - Column information
           tablerow_t *row)		// I - Row
{
  size_t	col;			// Current column
  double	row_y;			// Start of row
  docfont_t	deffont;		// Default font


  // Start a new page as needed...
  if (!dd->st)
    new_page(dd);
  else if ((dd->y - row->height) < dd->art_box.y1)
    new_page(dd);

  dd->in_row = true;
  pdfioContentBeginMarked(dd->st, "TR", /*dict*/NULL);

  if (mmdGetType(row->cells[0]) == MMD_TYPE_TABLE_HEADER_CELL)
  {
    // Header row, no border...
    deffont = DOCFONT_BOLD;
  }
  else
  {
    // Regular body row, add borders...
    deffont = DOCFONT_REGULAR;

    set_color(dd, DOCCOLOR_GRAY);
    pdfioContentPathRect(dd->st, cols[0].left - TABLE_PADDING, dd->y - row->height, cols[num_cols - 1].right - cols[0].left + 2.0 * TABLE_PADDING, row->height);
    for (col = 1; col < num_cols; col ++)
    {
      pdfioContentPathMoveTo(dd->st, cols[col].left - TABLE_PADDING, dd->y);
      pdfioContentPathLineTo(dd->st, cols[col].left - TABLE_PADDING, dd->y - row->height);
    }
    pdfioContentStroke(dd->st);
  }

  row_y = dd->y;

  for (col = 0; col < num_cols; col ++)
  {
    dd->y = row_y;

    format_block(dd, row->cells[col], deffont, SIZE_TABLE, cols[col].left, cols[col].right, /*leader*/NULL);
  }

  dd->y = row_y - row->height;

  pdfioContentEndMarked(dd->st);
  dd->in_row = false;
}


//
// 'set_color()' - Set the stroke and fill color as needed.
//

static void
set_color(docdata_t  *dd,		// I - Document data
          doccolor_t color)		// I - Document color
{
  if (color == dd->color)
    return;

  switch (color)
  {
    case DOCCOLOR_BLACK :
        pdfioContentSetFillColorDeviceGray(dd->st, 0.0);
        pdfioContentSetStrokeColorDeviceGray(dd->st, 0.0);
        break;
    case DOCCOLOR_RED :
        pdfioContentSetFillColorDeviceRGB(dd->st, 0.6, 0.0, 0.0);
        pdfioContentSetStrokeColorDeviceRGB(dd->st, 0.6, 0.0, 0.0);
        break;
    case DOCCOLOR_ORANGE :
        pdfioContentSetFillColorDeviceRGB(dd->st, 1.0, 0.5, 0.0);
        pdfioContentSetStrokeColorDeviceRGB(dd->st, 1.0, 0.5, 0.0);
        break;
    case DOCCOLOR_BLUE :
        pdfioContentSetFillColorDeviceRGB(dd->st, 0.0, 0.0, 0.8);
        pdfioContentSetStrokeColorDeviceRGB(dd->st, 0.0, 0.0, 0.8);
        break;
    case DOCCOLOR_LTGRAY :
        pdfioContentSetFillColorDeviceGray(dd->st, 0.933);
        pdfioContentSetStrokeColorDeviceGray(dd->st, 0.933);
        break;
    case DOCCOLOR_GRAY :
        pdfioContentSetFillColorDeviceGray(dd->st, 0.333);
        pdfioContentSetStrokeColorDeviceGray(dd->st, 0.333);
        break;
  }

  dd->color = color;
}


//
// 'set_font()' - Set the font typeface and size as needed.
//

static void
set_font(docdata_t *dd,			// I - Document data
         docfont_t font,		// I - Font
         double    fsize)		// I - Font size
{
  if (font == dd->font && fabs(fsize - dd->fsize) < 0.1)
    return;

  if (font == DOCFONT_MAX)
    return;

  pdfioContentSetTextFont(dd->st, docfont_names[font], fsize);

  if (fabs(fsize - dd->fsize) >= 0.1)
    pdfioContentSetTextLeading(dd->st, fsize * LINE_HEIGHT);

  dd->font  = font;
  dd->fsize = fsize;
}


//
// 'write_actions()' - Write remaining actions to the PDF file.
//

static void
write_actions(docdata_t *dd)		// I - Document data
{
  size_t	i, j;			// Looping vars
  docaction_t	*a;			// Current action
  doctarget_t	*t;			// Current target


  for (i = 0, a = dd->actions; i < dd->num_actions; i ++, a ++)
  {
    for (j = 0, t = dd->targets; j < dd->num_targets; j ++, t ++)
    {
      if (!strcmp(a->target, t->name))
        break;
    }

    if (j < dd->num_targets)
    {
      pdfio_array_t *dest = pdfioArrayCreate(dd->pdf);
					// Destination array

      pdfioArrayAppendObj(dest, pdfioFileGetPage(dd->pdf, t->page));
      pdfioArrayAppendName(dest, "XYZ");
      pdfioArrayAppendNumber(dest, PAGE_LEFT);
      pdfioArrayAppendNumber(dest, t->y);
      pdfioArrayAppendNumber(dest, 0.0);

      pdfioDictSetArray(pdfioObjGetDict(a->obj), "Dest", dest);
    }

    pdfioObjClose(a->obj);
  }
}


//
// 'write_toc()' - Write the table-of-contents outline.
//

static void
write_toc(docdata_t *dd)		// I - Document data
{
  size_t	i, j;			// Looping vars
  doctoc_t	*t,			// Table-of-contents entry
		*nt,			// Next entry
		*levels[6];		// Current entries for each level
  int		level;			// Current level
  pdfio_dict_t	*dict;			// Outline dictionary
  pdfio_obj_t	*obj;			// Outline object


  // Initialize the various TOC levels...
  levels[0] = levels[1] = levels[2] = levels[3] = levels[4] = levels[5] = NULL;

  // Scan the table of contents and finalize the dictionaries...
  for (i = 0, t = dd->toc; i < dd->num_toc; i ++, t ++)
  {
    // Set parent, previous, and next entries...
    if (t->level > 0 && levels[t->level - 1])
      pdfioDictSetObj(t->dict, "Parent", levels[t->level - 1]->obj);

    if (levels[t->level])
      pdfioDictSetObj(t->dict, "Prev", levels[t->level]->obj);

    for (j = i + 1, nt = t + 1; j < dd->num_toc; j ++, nt ++)
    {
      if (nt->level == t->level)
      {
	pdfioDictSetObj(t->dict, "Next", nt->obj);
        break;
      }
      else if (nt->level < t->level)
      {
        break;
      }
    }

    // First, last, and count...
    for (level = 0; level < t->level; level ++)
      levels[level]->count ++;

    levels[t->level] = t;

    if ((i + 1) < dd->num_toc && t[1].level > t->level)
      pdfioDictSetObj(t->dict, "First", t[1].obj);

    if ((i + 1) >= dd->num_toc)
    {
      // Close out all levels...
      for (level = t->level; level > 0; level --)
      {
        pdfioDictSetObj(levels[level - 1]->dict, "Last", levels[level]->obj);
	levels[level] = NULL;
      }
    }
    else if (t->level > t[1].level)
    {
      // Close out N levels...
      for (level = t->level; level > t[1].level; level --)
      {
        pdfioDictSetObj(levels[level - 1]->dict, "Last", levels[level]->obj);
	levels[level] = NULL;
      }
    }
  }

  // Create the top-level outline object...
  dict = pdfioDictCreate(dd->pdf);
  obj  = pdfioFileCreateObj(dd->pdf, dict);

  pdfioDictSetName(dict, "Type", "Outline");
  pdfioDictSetNumber(dict, "Count", dd->num_toc);
  pdfioDictSetObj(dict, "First", dd->toc[0].obj);

  // Close the objects for the entries...
  for (i = 0, t = dd->toc; i < dd->num_toc; i ++, t ++)
  {
    if (t->level == 0)
      pdfioDictSetObj(dict, "Last", t->obj);

    if (t->count)
    {
      // Set Count value...
      if (t->level == 0)
        pdfioDictSetNumber(t->dict, "Count", t->count);
      else
        pdfioDictSetNumber(t->dict, "Count", -t->count);
    }

    pdfioObjClose(t->obj);
  }

  // Close the outline object and add it to the document catalog...
  pdfioObjClose(obj);

  pdfioDictSetObj(pdfioFileGetCatalog(dd->pdf), "Outlines", obj);
}
