// -------------------------------------------------------------------- // 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::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 cmapFonts; iDoc->StyleSheet()->AllCMaps(cmapFonts); std::map 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> \n" << "endcodespacerange\n" << "1 beginbfrange\n" << "<00> <" << 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 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 iSubPages; }; //! Create the bookmarks (PDF outline). void IpePdfWriter::CreateBookmarks() { // first collect all information std::vector
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::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 << " <>"; } else { // one view only! iStream << count << " <

>"; } 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::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"; } // --------------------------------------------------------------------