// --------------------------------------------------------------------
// Creating PDF output
// --------------------------------------------------------------------
/*

    This file is part of the extensible drawing editor Ipe.
    Copyright (C) 1993-2004  Otfried Cheong

    Ipe 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.

    As a special exception, you have permission to link Ipe with the
    CGAL library and distribute executables, as long as you follow the
    requirements of the Gnu General Public License in regard to all of
    the software in the executable aside from CGAL.

    Ipe 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 General Public
    License for more details.

    You should have received a copy of the GNU General Public License
    along with Ipe; if not, you can find it at
    "http://www.gnu.org/copyleft/gpl.html", or write to the Free
    Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

*/

#include "ipeimage.h"
#include "ipetext.h"
#include "ipevisitor.h"
#include "ipepainter.h"
#include "ipegroup.h"
#include "iperef.h"
#include "ipeutils.h"

#include "ipepdfwriter.h"
#include "ipefontpool.h"

typedef std::vector<IpeBitmap>::const_iterator BmIter;

// --------------------------------------------------------------------

class IpePdfPainter : public IpePainter {
public:
  IpePdfPainter(const IpeStyleSheet *style, IpeStream &stream);
  virtual ~IpePdfPainter() { }

  virtual void Rect(const IpeRect &re);

protected:
  virtual void DoPush();
  virtual void DoPop();
  virtual void DoNewPath();
  virtual void DoMoveTo(const IpeVector &v);
  virtual void DoLineTo(const IpeVector &v);
  virtual void DoCurveTo(const IpeVector &v1, const IpeVector &v2,
			 const IpeVector &v3);
  virtual void DoClosePath();
  virtual void DoDrawPath();
  virtual void DoDrawBitmap(IpeBitmap bitmap);
  virtual void DoDrawText(const IpeText *text);

private:
  void DrawColor(IpeAttribute color, const char *gray, const char *rgb);
  void DrawAttributes();

private:
  IpeStream &iStream;
  // iActiveState records the attribute settings that have been
  // recorded in the PDF output, to avoid repeating them
  // over and over again.
  StateSeq iActiveState;
};

IpePdfPainter::IpePdfPainter(const IpeStyleSheet *style, IpeStream &stream)
  : IpePainter(style), iStream(stream)
{
  State state;
  state.iStroke = IpeAttribute::Black();
  state.iFill = IpeAttribute::Black();
  state.iDashStyle = IpeAttribute::Solid();
  state.iLineCap = IpeAttribute(IpeAttribute::ELineCap, false, 0);
  state.iLineJoin = IpeAttribute(IpeAttribute::ELineJoin, false, 0);
  iActiveState.push_back(state);
}

void IpePdfPainter::Rect(const IpeRect &re)
{
  iStream << Matrix() * re.Min() << " "
	  << Matrix().Linear() * (re.Max() - re.Min()) << " re\n";
}

void IpePdfPainter::DoNewPath()
{
  DrawAttributes();
}

void IpePdfPainter::DoMoveTo(const IpeVector &v)
{
  iStream << Matrix() * v << " m\n";
}

void IpePdfPainter::DoLineTo(const IpeVector &v)
{
  iStream << Matrix() * v << " l\n";
}

void IpePdfPainter::DoCurveTo(const IpeVector &v1, const IpeVector &v2,
			      const IpeVector &v3)
{
  iStream << Matrix() * v1 << " "
	  << Matrix() * v2 << " "
	  << Matrix() * v3 << " c\n";
}

void IpePdfPainter::DoClosePath()
{
  iStream << "h ";
}

void IpePdfPainter::DoPush()
{
  State state = iActiveState.back();
  iActiveState.push_back(state);
  iStream << "q ";
}

void IpePdfPainter::DoPop()
{
  iActiveState.pop_back();
  iStream << "Q\n";
}

void IpePdfPainter::DrawColor(IpeAttribute color, const char *gray,
			      const char *rgb)
{
  if (color.IsNullOrVoid())
    return;
  assert(color.IsAbsolute());
  IpeColor col = StyleSheet()->Repository()->ToColor(color);
  if (col.IsGray())
    iStream << col.iRed << " " << gray << "\n";
  else
    iStream << col << " " << rgb << "\n";
}

void IpePdfPainter::DrawAttributes()
{
  State &s = iState.back();
  State &sa = iActiveState.back();
  const IpeRepository *rep = StyleSheet()->Repository();
  if (s.iDashStyle && s.iDashStyle != sa.iDashStyle) {
    sa.iDashStyle = s.iDashStyle;
    if (s.iDashStyle.IsSolid())
      iStream << "[] 0 d\n";
    else
      iStream << rep->String(s.iDashStyle) << " d\n";
  }
  if (s.iLineWidth && s.iLineWidth != sa.iLineWidth) {
    sa.iLineWidth = s.iLineWidth;
    iStream << rep->String(s.iLineWidth) << " w\n";
  }
  if (s.iLineCap && s.iLineCap != sa.iLineCap) {
    sa.iLineCap = s.iLineCap;
    iStream << int(s.iLineCap.Index()) << " J\n";
  }
  if (s.iLineJoin && s.iLineJoin != sa.iLineJoin) {
    sa.iLineJoin = s.iLineJoin;
    iStream << int(s.iLineJoin.Index()) << " j\n";
  }
  if (!s.iStroke.IsNullOrVoid() && s.iStroke != sa.iStroke) {
    sa.iStroke = s.iStroke;
    DrawColor(s.iStroke, "G", "RG");
  }
  if (!s.iFill.IsNullOrVoid() && s.iFill != sa.iFill) {
    sa.iFill = s.iFill;
    DrawColor(s.iFill, "g", "rg");
    /*
    if (s.iFill.IsNullOrVoid())
      return;
    assert(s.iFill.IsAbsolute());
    IpeColor col = StyleSheet()->Repository()->ToColor(s.iFill);
    iStream << col.iRed << " " << col.iGreen << " "
    << col.iBlue << " /P1 scn\n";
    */
  }
}

void IpePdfPainter::DoDrawPath()
{
  bool noStroke = Stroke().IsNullOrVoid();
  bool noFill = Fill().IsNullOrVoid();
  bool eofill = !WindRule() || !WindRule().Index();
  if (noStroke && noFill)
    iStream << "n\n"; // no op path
  else if (noStroke)
    iStream << (eofill ? "f*\n" : "f\n"); // fill only
  else if (noFill)
    iStream << "S\n"; // stroke only
  else
    iStream << (eofill ? "B*\n" : "B\n"); // fill and then stroke
}

void IpePdfPainter::DoDrawBitmap(IpeBitmap bitmap)
{
  if (bitmap.ObjNum() < 0)
    return;
  iStream << Matrix() << " cm /Image"
	  << bitmap.ObjNum() << " Do\n";
}

void IpePdfPainter::DoDrawText(const IpeText *text)
{
  const IpeText::XForm *xf = text->GetXForm();
  if (xf) {
    iStream << Matrix() << " cm " ;
    iStream << IpeMatrix(xf->iStretch.iX, 0, 0, xf->iStretch.iY, 0, 0)
	    << " cm ";
    iStream.PutRaw(xf->iStream.data(), xf->iStream.size());
    iStream << "\n";
  }
}

// --------------------------------------------------------------------

/*! \class IpePdfWriter
  \brief Create PDF file.

  This class is responsible for the creation of a PDF file from the
  Ipe data. You have to create an IpePdfWriter first, providing a file
  that has been opened for (binary) writing and is empty.  Then call
  EmbedFonts() to embed the IpeFontPool, EmbedBitmaps() to embed the
  bitmaps, and CreatePages() to embed the pages.
  \c CreateXmlStream embeds a stream with the XML representation of
  the Ipe document. Finally, call \c CreateTrailer to complete the PDF
  document, and close the file.

  Some reserved PDF object numbers:

    - 0: Must be left empty (a PDF restriction).
    - 1: Ipe XML stream.
    - 2: Parent of all pages objects.

*/

//! Create PDF writer operating on this (open and empty) file.
/*! If \a noShading is \c true, no background shading will be applied
  to pages (if such a shading is defined in the document's style
  sheet. */
IpePdfWriter::IpePdfWriter(IpeTellStream &stream, const IpeDocument *doc,
			   bool noShading, int compression)
  : iStream(stream), iDoc(doc)
{
  iCompressLevel = compression;
  iObjNum = 3;  // 0 - 2 are reserved
  iXmlStreamNum = -1; // no XML stream yet
  iResourceNum = -1;
  iShadingNum = -1;

  iStream << "%PDF-1.3\n";

  IpeShading s = iDoc->StyleSheet()->FindShading();
  if (!noShading && s.iType) {
    iShadingNum = StartObject();
    iStream << "<<\n"
	    << " /ShadingType " << int(s.iType) << "\n"
	    << " /ColorSpace /DeviceRGB\n";
    switch (s.iType) {
    case IpeShading::EAxial:
      iStream << " /Coords [" << s.iV[0] << " " << s.iV[1] << "]\n";
      break;
    case IpeShading::ERadial:
      iStream << " /Coords [" << s.iV[0] << " " << s.iRadius[0]
	      << " " << s.iV[1] << " " << s.iRadius[1] << "]\n";
    default:
      break;
    }
    iStream << " /Function << /FunctionType 2 /Domain [ 0 1 ] /N 1\n"
	    << "     /C1 [" << s.iColor[0] << "]\n"
	    << "     /C0 [" << s.iColor[1]<< "] >>\n"
	    << " /Extend [" << (s.iExtend[0] ? "true " : "false ")
	    << (s.iExtend[1] ? "true " : "false ") << "]\n"
	    << ">> endobj\n";
  }

  /*
  const int patSize = 8;
  const int imgSize = 1;

  StartObject(); // object 1 -> image for the pattern
  iStream << "<<\n";
  iStream << "/Type /XObject\n";
  iStream << "/Subtype /Image\n";
  iStream << "/ImageMask true\n";
  iStream << "/Width " << patSize << "\n";
  iStream << "/Height " << patSize << "\n";
  iStream << "/BitsPerComponent 1\n";
  iStream << "/Length " << (patSize * patSize) / 8 << "\n>> stream\n";
  for (int y = 0; y < patSize; ++y) {
    char byte = (y & 1) ? char(0xaa) : 0x55;
    for (int x = 0; x < patSize / 8; ++x)
      iStream.PutChar(byte);
  }
  iStream << "\nendstream endobj\n";

  StartObject(); // object 2 -> pattern
  iStream << "<<\n";
  iStream << "/Type /Pattern\n";
  iStream << "/PatternType 1\n";
  iStream << "/PaintType 2\n";
  iStream << "/TilingType 1\n";
  //iStream << "/BBox [0 0 " << imgSize << " " << imgSize << "]\n";
  //iStream << "/XStep " << imgSize << "\n";
  //iStream << "/YStep " << imgSize << "\n";
  iStream << "/BBox [0 0 1 1] /XStep 1.0 /YStep 1.0\n";
  iStream << "/Resources << /ProcSet [ /PDF ]\n";
  iStream << "  /XObject << /Image 1 0 R >> >>\n";
  iStream << "/Matrix [" << imgSize << " 0 0 " << imgSize << " 0 0]\n";
  iStream << "/Length 9\n>> stream\n";
  iStream << "/Image Do\nendstream endobj\n";
  */
}

//! Destructor.
IpePdfWriter::~IpePdfWriter()
{
  // nothing
}

/*! Write the beginning of the next object: "no 0 obj " and save
  information about file position. Default argument uses next unused
  object number.  Returns number of new object. */
int IpePdfWriter::StartObject(int objnum)
{
  if (objnum < 0)
    objnum = iObjNum++;
  iXref[objnum] = iStream.Tell();
  iStream << objnum << " 0 obj ";
  return objnum;
}

/*! Write all fonts to the PDF file, and fill in their object numbers.
  Embeds no fonts if \c pool is 0, but must be called nevertheless. */
void IpePdfWriter::EmbedFonts(const IpeFontPool *pool)
{
  // no fonts embedded?
  if (!pool)
    return;

  // get fonts that require a cmap
  std::vector<IpeString> cmapFonts;
  iDoc->StyleSheet()->AllCMaps(cmapFonts);

  std::map<int, int> fontNumber;

  for (IpeFontPool::const_iterator font = pool->begin();
       font != pool->end(); ++font) {
    int fontDescriptor = -1;
    if (!font->iFontDescriptor.empty()) {
      int streamId = StartObject();
      iStream << "<<\n" << font->iStreamDict;
      CreateStream(font->iStreamData.data(), font->iStreamData.size(), false);
      fontDescriptor = StartObject();
      iStream << "<<\n" << font->iFontDescriptor
	      << streamId << " 0 R\n" << ">> endobj\n";
    }
    int cmap = -1;
    int j = font->iName.size() - 2;
    if (j > 0 && std::find(cmapFonts.begin(), cmapFonts.end(),
			   font->iName.substr(0, j)) != cmapFonts.end()) {
      IpeString fpage = font->iName.substr(j);
      // int page = std::strtol(fpage.CString(), 0, 16);
      IpeString s;
      IpeStringStream ss(s);
      ss << "/CIDInit /ProcSet findresource begin\n"
	 << "12 dict begin\n"
	 << "begincmap\n"
	 << "/CIDSystemInfo\n"
	 << "<< /Registry (Adobe)\n"
	 << "/Ordering (UCS)\n"
	 << "/Supplement 0\n"
	 << ">> def\n"
	 << "/CMapName /Adobe.Identity.UCS def\n"
	 << "/CMapType 2 def\n"
	 << "1 begincodespacerange\n"
	 << "<00> <FF>\n"
	 << "endcodespacerange\n"
	 << "1 beginbfrange\n"
	 << "<00> <FF> <" << fpage << "00>\n"
	 << "endbfrange\n"
	 << "endcmap\n"
	 << "CMapName currentdict /CMap defineresource pop\n"
	 << "end\n"
	 << "end\n";
      cmap = StartObject();
      iStream << "<<\n";
      CreateStream(s.data(), s.size(), false);
    }
    int objectNumber = StartObject();
    fontNumber[font->iLatexNumber] = objectNumber;
    iStream << "<<\n" << font->iFontDict;
    if (fontDescriptor >= 0)
      iStream << "/FontDescriptor " << fontDescriptor << " 0 R\n";
    if (cmap >= 0)
      iStream << "/ToUnicode " << cmap << " 0 R\n";
    iStream << ">> endobj\n";
  }
  iResourceNum = StartObject();
  iStream << "<< ";
  for (IpeFontPool::const_iterator font = pool->begin();
       font != pool->end(); ++font) {
    iStream << "/F" << font->iLatexNumber << " "
	    << fontNumber[font->iLatexNumber] << " 0 R ";
  }
  iStream << ">> endobj\n";
}

//! Write a stream.
/*! Write a stream, either plain or compressed, depending on compress
  level.  Object must have been created with dictionary start having
  been written.
  If \a preCompressed is true, the data is already deflated.
*/
void IpePdfWriter::CreateStream(const char *data, int size, bool preCompressed)
{
  if (preCompressed) {
    iStream << "/Length " << size << " /Filter /FlateDecode >>\nstream\n";
    iStream.PutRaw(data, size);
    iStream << "\nendstream endobj\n";
    return;
  }

  if (iCompressLevel > 0) {
    int deflatedSize;
    IpeBuffer deflated = IpeDeflateStream::Deflate(data, size, deflatedSize,
						   iCompressLevel);
    iStream << "/Length " << deflatedSize
	    << " /Filter /FlateDecode >>\nstream\n";
    iStream.PutRaw(deflated.data(), deflatedSize);
    iStream << "\nendstream endobj\n";
  } else {
    iStream << "/Length " << size << " >>\nstream\n";
    iStream.PutRaw(data, size);
    iStream << "endstream endobj\n";
  }
}

// --------------------------------------------------------------------

void IpePdfWriter::EmbedBitmap(IpeBitmap bitmap)
{
  int objnum = StartObject();
  iStream << "<<\n";
  iStream << "/Type /XObject\n";
  iStream << "/Subtype /Image\n";
  iStream << "/Width " << bitmap.Width() << "\n";
  iStream << "/Height " << bitmap.Height() << "\n";
  switch (bitmap.ColorSpace()) {
  case IpeBitmap::EDeviceGray:
    iStream << "/ColorSpace /DeviceGray\n";
    break;
  case IpeBitmap::EDeviceRGB:
    iStream << "/ColorSpace /DeviceRGB\n";
    break;
  case IpeBitmap::EDeviceCMYK:
    iStream << "/ColorSpace /DeviceCMYK\n";
    // apparently JPEG CMYK images need this decode array??
    // iStream << "/Decode [1 0 1 0 1 0 1 0]\n";
    break;
  }
  switch (bitmap.Filter()) {
  case IpeBitmap::EFlateDecode:
    iStream << "/Filter /FlateDecode\n";
    break;
  case IpeBitmap::EDCTDecode:
    iStream << "/Filter /DCTDecode\n";
    break;
  default:
    // no filter
    break;
  }
  iStream << "/BitsPerComponent " << bitmap.BitsPerComponent() << "\n";
  iStream << "/Length " << bitmap.Size() << "\n>> stream\n";
  iStream.PutRaw(bitmap.Data(), bitmap.Size());
  iStream << "\nendstream endobj\n";
  bitmap.SetObjNum(objnum);
}

// --------------------------------------------------------------------

IpeRect IpePdfWriter::PaintView(IpeStream &stream, const IpePage *page,
				int view)
{
  // XXX pattern
  // stream << "/Cs1 cs\n";
  if (iShadingNum >= 0)
    stream << "/Sh sh\n";
  IpePdfPainter painter(iDoc->StyleSheet(), stream);
  IpeBBoxPainter bboxPainter(iDoc->StyleSheet());
  std::vector<bool> layers;
  page->MakeLayerTable(layers, view, false);
  for (IpePage::const_iterator it = page->begin(); it != page->end(); ++it) {
    if (layers[it->Layer()]) {
      it->Object()->Draw(painter);
      it->Object()->Draw(bboxPainter);
    }
  }
  return bboxPainter.BBox();
}

//! Create contents and page stream for this page view.
void IpePdfWriter::CreatePageView(int pno, int view)
{
  const IpePage *page = (*iDoc)[pno];
  // Find bitmaps to embed
  IpeBitmapFinder bm;
  bm.ScanPage(page);
  // Embed them
  for (BmIter it = bm.iBitmaps.begin(); it != bm.iBitmaps.end(); ++it) {
    BmIter it1 = std::find(iBitmaps.begin(), iBitmaps.end(), *it);
    if (it1 == iBitmaps.end()) {
      // look again, more carefully
      for (it1 = iBitmaps.begin();
	   it1 != iBitmaps.end() && !it1->Equal(*it); ++it1)
	;
      if (it1 == iBitmaps.end())
	EmbedBitmap(*it); // not yet embedded
      else
	it->SetObjNum(it1->ObjNum()); // identical IpeBitmap is embedded
      iBitmaps.push_back(*it);
    }
  }
  // Create page stream
  IpeString pagedata;
  IpeStringStream sstream(pagedata);
  IpeRect bbox;
  if (iCompressLevel > 0) {
    IpeDeflateStream dfStream(sstream, iCompressLevel);
    bbox = PaintView(dfStream, page, view);
    dfStream.Close();
  } else
    bbox = PaintView(sstream, page, view);

  int contentsobj = StartObject();
  iStream << "<<\n";
  CreateStream(pagedata.data(), pagedata.size(), (iCompressLevel > 0));
  int pageobj = StartObject();
  iStream << "<<\n";
  iStream << "/Type /Page\n";
  iStream << "/Contents " << contentsobj << " 0 R\n";
  // iStream << "/Rotate 0\n";
  iStream << "/Resources <<\n  /ProcSet [ /PDF ";
  if (iResourceNum >= 0)
    iStream << "/Text";
  if (!bm.iBitmaps.empty())
    iStream << "/ImageB /ImageC";
  iStream << " ]\n";
  if (iResourceNum >= 0)
    iStream << "  /Font " << iResourceNum << " 0 R\n";
  if (iShadingNum >= 0)
    iStream << "  /Shading << /Sh " << iShadingNum << " 0 R >>\n";
  if (!bm.iBitmaps.empty()) {
    iStream << "  /XObject << ";
    for (BmIter it = bm.iBitmaps.begin(); it != bm.iBitmaps.end(); ++it) {
      // mention each PDF object only once
      BmIter it1;
      for (it1 = bm.iBitmaps.begin();
	   it1 != it && it1->ObjNum() != it->ObjNum(); it1++)
	;
      if (it1 == it)
	iStream << "/Image" << it->ObjNum() << " " << it->ObjNum() << " 0 R ";
    }
    iStream << ">>\n";
  }

  // XXX pattern
  /*
  iStream << "/Pattern << /P1 2 0 R >>\n";
  iStream << "/ColorSpace << /Cs1 [/Pattern /DeviceRGB] >>\n";
  */
  iStream << "  >>\n";
  page->View(view).PageDictionary(iStream);
  iStream << "/MediaBox [" << iDoc->Properties().iMedia << "]\n";
  if (iDoc->Properties().iCropBox && !bbox.IsEmpty())
    iStream << "/CropBox [" << bbox << "]\n";
  if (!bbox.IsEmpty())
    iStream << "/ArtBox [" << bbox << "]\n";
  iStream << "/Parent 2 0 R\n";
  iStream << ">> endobj\n";
  iPageObjectNumbers.push_back(pageobj);
}

#if 0
//! Create PDF pages presenting this Ipe page.
void IpePdfWriter::CreatePage(int page)
{
  for (int view = 0; view < (*iDoc)[page]->CountViews(); ++view)
    CreatePageView(page, view);
}
#endif

//! Create all PDF pages.
void IpePdfWriter::CreatePages(bool lastView)
{
  for (uint page = 0; page < iDoc->size(); ++page) {
    int nViews = (*iDoc)[page]->CountViews();
    if (lastView)
      CreatePageView(page, lastView - 1);
    else
      for (int view = 0; view < nViews; ++view)
	CreatePageView(page, view);
  }
}

//! Create a stream containing the XML data.
void IpePdfWriter::CreateXmlStream(IpeString xmldata, bool preCompressed)
{
  iXmlStreamNum = StartObject(1);
  iStream << "<<\n/Type /Ipe\n";
  CreateStream(xmldata.data(), xmldata.size(), preCompressed);
}

//! Write a PDF string object to the PDF stream.
void IpePdfWriter::WriteString(IpeString text)
{
  iStream << "(";
  for (int i = 0; i < text.size(); ++i) {
    char ch = text[i];
    switch (ch) {
    case '(':
    case ')':
    case '\\':
      iStream << "\\";
      // fall through
    default:
      iStream << ch;
      break;
    }
  }
  iStream << ")";
}

// --------------------------------------------------------------------

struct Section {
  int iPage;
  int iObjNum;
  std::vector<int> iSubPages;
};

//! Create the bookmarks (PDF outline).
void IpePdfWriter::CreateBookmarks()
{
  // first collect all information
  std::vector<Section> sections;
  for (uint pg = 0; pg < iDoc->size(); ++pg) {
    IpeString s = (*iDoc)[pg]->Section(0);
    IpeString ss = (*iDoc)[pg]->Section(1);
    if (!s.empty()) {
      Section sec;
      sec.iPage = pg;
      sections.push_back(sec);
    }
    if (!sections.empty() && !ss.empty())
      sections.back().iSubPages.push_back(pg);
  }
  iBookmarks = -1;
  if (sections.empty())
    return;
  // reserve outline object
  iBookmarks = iObjNum++;
  // assign object numbers
  for (uint s = 0; s < sections.size(); ++s) {
    sections[s].iObjNum = iObjNum++;
    iObjNum += sections[s].iSubPages.size(); // leave space for subsections
  }
  // embed root
  StartObject(iBookmarks);
  iStream << "<<\n/First " << sections[0].iObjNum << " 0 R\n"
	  << "/Count " << int(sections.size()) << "\n"
	  << "/Last " << sections.back().iObjNum << " 0 R\n>> endobj\n";
  for (uint s = 0; s < sections.size(); ++s) {
    int count = sections[s].iSubPages.size();
    int obj = sections[s].iObjNum;
    // embed section
    StartObject(obj);
    iStream << "<<\n/Title ";
    WriteString((*iDoc)[sections[s].iPage]->Section(0));
    iStream << "\n/Parent " << iBookmarks << " 0 R\n"
	    << "/Dest [ " << iPageObjectNumbers[sections[s].iPage]
	    << " 0 R /XYZ null null null ]\n";
    if (s > 0)
      iStream << "/Prev " << sections[s-1].iObjNum << " 0 R\n";
    if (s < sections.size() - 1)
      iStream << "/Next " << sections[s+1].iObjNum << " 0 R\n";
    if (count > 0)
      iStream << "/Count " << -count << "\n"
	      << "/First " << (obj + 1) << " 0 R\n"
	      << "/Last " << (obj + count) << " 0 R\n";
    iStream << ">> endobj\n";
    // using ids obj + 1 .. obj + count for the subsections
    for (int ss = 0; ss < count; ++ss) {
      int pageNo = sections[s].iSubPages[ss];
      StartObject(obj + ss + 1);
      iStream << "<<\n/Title ";
      WriteString((*iDoc)[pageNo]->Section(1));
      iStream << "\n/Parent " << obj << " 0 R\n"
	      << "/Dest [ " << iPageObjectNumbers[pageNo]
	      << " 0 R /XYZ null null null ]\n";
      if (ss > 0)
	iStream << "/Prev " << (obj + ss) << " 0 R\n";
      if (ss < count - 1)
	iStream << "/Next " << (obj + ss + 2) << " 0 R\n";
      iStream << ">> endobj\n";
    }
  }
}

// --------------------------------------------------------------------

//! Create the root objects and trailer of the PDF file.
void IpePdfWriter::CreateTrailer(IpeString creator)
{
  const IpeDocument::SProperties &props = iDoc->Properties();
  // Create /Pages
  StartObject(2);
  iStream << "<<\n" << "/Type /Pages\n";
  iStream << "/Count " << int(iPageObjectNumbers.size()) << "\n";
  iStream << "/Kids [ ";
  for (std::vector<int>::const_iterator it = iPageObjectNumbers.begin();
       it != iPageObjectNumbers.end(); ++it)
    iStream << (*it) << " 0 R ";
  iStream << "]\n>> endobj\n";
  // Create /Catalog
  int catalogobj = StartObject();
  iStream << "<<\n/Type /Catalog\n/Pages 2 0 R\n";
  if (props.iFullScreen)
    iStream << "/PageMode /FullScreen\n";
  if (iBookmarks >= 0) {
    if (!props.iFullScreen)
      iStream << "/PageMode /UseOutlines\n";
    iStream << "/Outlines " << iBookmarks << " 0 R\n";
  }
  if (iDoc->TotalViews() > 1) {
    iStream << "/PageLabels << /Nums [ ";
    int count = 0;
    for (int page = 0; page < int(iDoc->size()); ++page) {
      int nviews = (*iDoc)[page]->CountViews();
      if (nviews > 1) {
	iStream << count << " <</S /D /P (" << (page + 1) << "-)>>";
      } else { // one view only!
	iStream << count << " <</P (" << (page + 1) << ")>>";
      }
      count += nviews;
    }
    iStream << "] >>\n";
  }
  iStream << ">> endobj\n";
  // Create /Info
  int infoobj = StartObject();
  iStream << "<<\n";
  iStream << "/Creator (" << creator << ")\n";
  iStream << "/Producer (" << creator << ")\n";
  if (!props.iTitle.empty()) {
    iStream << "/Title ";
    WriteString(props.iTitle);
    iStream << "\n";
  }
  if (!props.iAuthor.empty()) {
    iStream << "/Author ";
    WriteString(props.iAuthor);
    iStream << "\n";
  }
  if (!props.iSubject.empty()) {
    iStream << "/Subject ";
    WriteString(props.iSubject);
    iStream << "\n";
  }
  if (!props.iKeywords.empty()) {
    iStream << "/Keywords ";
    WriteString(props.iKeywords);
    iStream << "\n";
  }
  iStream << "/CreationDate (" << props.iCreated << ")\n";
  iStream << "/ModDate (" << props.iModified << ")\n";
  iStream << ">> endobj\n";
  // Create Xref
  int xrefpos = iStream.Tell();
  iStream << "xref\n0 " << iObjNum << "\n";
  for (int obj = 0; obj < iObjNum; ++obj) {
    std::map<int, long>::const_iterator it = iXref.find(obj);
    char s[12];
    if (it == iXref.end()) {
      std::sprintf(s, "%010d", obj);
      iStream << s << " 00000 f \n"; // note the final space!
    } else {
      std::sprintf(s, "%010ld", iXref[obj]);
      iStream << s << " 00000 n \n"; // note the final space!
    }
  }
  iStream << "trailer\n<<\n";
  iStream << "/Size " << iObjNum << "\n";
  iStream << "/Root " << catalogobj << " 0 R\n";
  iStream << "/Info " << infoobj << " 0 R\n";
  iStream << ">>\nstartxref\n" << int(xrefpos) << "\n%%EOF\n";
}

// --------------------------------------------------------------------
