// -------------------------------------------------------------------- // The Ipe document. // -------------------------------------------------------------------- /* 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 "ipedoc.h" #include "ipeiml.h" #include "ipestyle.h" #include "iperef.h" #include "ipepainter.h" #include "ipeutils.h" #include "ipefontpool.h" #include "ipepdfparser.h" #include "ipepdfwriter.h" #include "ipepswriter.h" // -------------------------------------------------------------------- /*! \class IpeDocument \ingroup doc \brief The model for an Ipe document. The IpeDocument class is the engine behind the Ipe program. It represents the contents of an Ipe document, and all the methods necessary to load, save, and edit it. It is independent of a specific user interface. IpeDocument's cannot be copied or assigned. Note that the IpeDocument owns the IpeRepository that defines the meaning of all object's attributes. Therefore, the document is the largest extent where IpeObject's make sense. You can have several documents at once, but you cannot move IpeObject's (or IpeStyleSheet's) from one to the other. If you need to do so, you'll have to externalize them to XML and internalize them into the other document. */ //! Constructor clears the boolean flags. IpeDocument::SProperties::SProperties() { iFullScreen = false; iCropBox = false; } //! Construct an empty document for filling by a client. /*! As constructed, it has no pages, A4 media, and only the standard style sheet. */ IpeDocument::IpeDocument() { iFontPool = 0; iEdited = false; iStyleSheet = IpeStyleSheet::Standard(&iRepository); iProperties.iMedia = IpeRect(IpeVector::Zero, IpeVector(595,842)); // A4 } //! Destructor. IpeDocument::~IpeDocument() { for (const_iterator it = begin(); it != end(); ++it) delete (*it); delete iStyleSheet; delete iFontPool; } //! Copy constructor is disabled: it panics. IpeDocument::IpeDocument(const IpeDocument &) : IpePageSeq() { assert(false); } //! Assignment operator is disabled: it panics. IpeDocument &IpeDocument::operator=(const IpeDocument &) { assert(false); return *this; } // --------------------------------------------------------------------- IpeString readLine(IpeDataSource &source) { IpeString s; int ch = source.GetChar(); while (ch != EOF && ch != '\n') { s += char(ch); ch = source.GetChar(); } return s; } //! Determine format of file in \a source. IpeDocument::TFormat IpeDocument::FileFormat(IpeDataSource &source) { IpeString s1 = readLine(source); IpeString s2 = readLine(source); if (s1.substr(0, 5) == "Dict() || obj->Dict()->Stream().size() == 0) return IpeBuffer(); return obj->Dict()->Stream(); } // -------------------------------------------------------------------- class IpePsSource : public IpeDataSource { public: IpePsSource(IpeDataSource &source) : iSource(source) { /* nothing */ } bool SkipToXml(); IpeString ReadLine(); IpeBuffer Image(int index) const; int GetNext() const; inline bool Deflated() const { return iDeflated; } virtual int GetChar(); private: IpeDataSource &iSource; std::vector iImages; bool iEos; bool iDeflated; }; int IpePsSource::GetChar() { int ch = iSource.GetChar(); if (ch == '\n') iSource.GetChar(); // remove '%' return ch; } IpeString IpePsSource::ReadLine() { IpeString s; int ch = iSource.GetChar(); while (ch != EOF && ch != '\n') { s += char(ch); ch = iSource.GetChar(); } iEos = (ch == EOF); return s; } IpeBuffer IpePsSource::Image(int index) const { if (1 <= index && index <= int(iImages.size())) return iImages[index - 1]; else return IpeBuffer(); } bool IpePsSource::SkipToXml() { iDeflated = false; IpeString s1 = ReadLine(); IpeString s2 = ReadLine(); if (s1.substr(0, 11) != "%!PS-Adobe-" || s2.substr(0, 11) != "%%Creator: ") return false; if (s2.substr(11, 6) == "Ipelib") { // the 'modern' file format of Ipe 6.0 preview 17 and later do { s1 = ReadLine(); if (s1.substr(0, 17) == "%%BeginIpeImage: ") { IpeLex lex(s1.substr(17)); int num, len; lex >> num >> len; if (num != int(iImages.size() + 1)) return false; (void) ReadLine(); // skip 'image' IpeBuffer buf(len); IpeA85Source a85(iSource); char *p = buf.data(); char *p1 = p + buf.size(); while (p < p1) { int ch = a85.GetChar(); if (ch == EOF) return false; *p++ = char(ch); } iImages.push_back(buf); } } while (!iEos && s1.substr(0, 13) != "%%BeginIpeXml"); iDeflated = (s1.substr(13, 14) == ": /FlateDecode"); } else { // the 'old' file format generated through pdftops do { s1 = ReadLine(); } while (!iEos && s1.substr(0, 10) != "%%EndSetup"); } if (iEos) return false; (void) iSource.GetChar(); // skip '%' before return true; } class IpePsStreamParser : public IpeImlParser { public: explicit IpePsStreamParser(IpeDataSource &source, IpePsSource &psSource, IpeRepository *rep); virtual IpeBuffer PdfStream(int objNum); private: IpePsSource &iPsSource; }; IpePsStreamParser::IpePsStreamParser(IpeDataSource &source, IpePsSource &psSource, IpeRepository *rep) : IpeImlParser(source, rep), iPsSource(psSource) { // nothing } IpeBuffer IpePsStreamParser::PdfStream(int objNum) { return iPsSource.Image(objNum); } // -------------------------------------------------------------------- IpeDocument *DoParse(IpeDocument *self, IpeImlParser &parser, int &reason) { int requires; if (!parser.ParseDocument(*self, requires) || requires > IPELIB_VERSION) { delete self; self = 0; if (requires > IPELIB_VERSION) reason = -requires; else reason = parser.ParsePosition(); } return self; } IpeDocument *DoParseXml(IpeDataSource &source, int &reason) { IpeDocument *self = new IpeDocument; IpeImlParser parser(source, self->Repository()); return DoParse(self, parser, reason); } IpeDocument *DoParsePs(IpeDataSource &source, int &reason) { IpePsSource psSource(source); reason = -3; // could not find Xml stream if (!psSource.SkipToXml()) return 0; IpeDocument *self = new IpeDocument; if (psSource.Deflated()) { IpeA85Source a85(psSource); IpeInflateSource source(a85); IpePsStreamParser parser(source, psSource, self->Repository()); return DoParse(self, parser, reason); } else { IpePsStreamParser parser(psSource, psSource, self->Repository()); return DoParse(self, parser, reason); } } IpeDocument *DoParsePdf(IpeDataSource &source, int &reason) { IpePdfFile loader; reason = -2; // could not parse if (!loader.Parse(source)) return 0; reason = -3; // not an Ipe document const IpePdfObj *obj = loader.Object(1); if (!obj || !obj->Dict()) return 0; const IpePdfObj *type = obj->Dict()->Get("Type", 0); if (!type || !type->Name() || type->Name()->Value() != "Ipe") return 0; IpeBuffer buffer = obj->Dict()->Stream(); IpeBufferSource xml(buffer); IpeDocument *self = new IpeDocument; if (obj->Dict()->Deflated()) { IpeInflateSource xml1(xml); IpePdfStreamParser parser(loader, xml1, self->Repository()); return DoParse(self, parser, reason); } else { IpePdfStreamParser parser(loader, xml, self->Repository()); return DoParse(self, parser, reason); } } //! Construct a document from an input stream. /*! Returns 0 if the stream couldn't be parsed, and a reason explaining that in \a reason. If \a reason is positive, it is a file (stream) offset where parsing failed. If \a reason is negative, it is an error code. If it is smaller than -60000, then it is the negative value of the Ipe version that created that file (and which exceeds the version of this Ipe). */ IpeDocument *IpeDocument::New(IpeDataSource &source, TFormat format, int &reason) { if (format == EXml) return DoParseXml(source, reason); if (format == EPdf) return DoParsePdf(source, reason); if (format == EPs || format == EEps) return DoParsePs(source, reason); return 0; } IpeDocument *IpeDocument::New(const char *fname, int &reason) { reason = -1; std::FILE *fd = std::fopen(fname, "rb"); if (!fd) return 0; IpeFileSource source(fd); TFormat format = FileFormat(source); std::rewind(fd); IpeDocument *self = New(source, format, reason); std::fclose(fd); return self; } // -------------------------------------------------------------------- //! Save in a stream. /*! Returns true if sucessful. Does not reset IsEdited(). */ bool IpeDocument::Save(IpeTellStream &stream, IpeString creator, TFormat format, uint flags, int compressLevel) const { if (format == EXml) { SaveAsXml(stream, creator); return true; } if (format == EPdf) { IpePdfWriter writer(stream, this, (flags & ENoShading), compressLevel); writer.EmbedFonts(iFontPool); writer.CreatePages(flags & ELastView); writer.CreateBookmarks(); if (!(flags & EExport)) { IpeString xmlData; IpeStringStream stream(xmlData); if (compressLevel > 0) { IpeDeflateStream dfStream(stream, compressLevel); // all bitmaps have been embedded and carry correct object number SaveAsXml(dfStream, creator, true); dfStream.Close(); writer.CreateXmlStream(xmlData, true); } else { SaveAsXml(stream, creator, true); writer.CreateXmlStream(xmlData, false); } } writer.CreateTrailer(creator); return true; } if (format == EPs || format == EEps) { IpePsWriter writer(stream, this, (format == EEps)); if (!writer.CreateHeader(creator)) return false; writer.CreatePages(flags & ELastView); if (!(flags & EExport)) writer.CreateXml(creator, compressLevel); writer.CreateTrailer(); return true; } return false; } bool IpeDocument::Save(const char *fname, IpeString creator, TFormat format, uint flags, int compressLevel) const { std::FILE *fd = std::fopen(fname, "wb"); if (!fd) return false; IpeFileStream stream(fd); bool result = Save(stream, creator, format, flags, compressLevel); std::fclose(fd); return result; } // -------------------------------------------------------------------- //! Save in XML format into an IpeStream. /*! You can set \a creator to set the \c creator attribute in the \c tag. */ void IpeDocument::SaveAsXml(IpeStream &stream, IpeString creator, bool usePdfBitmaps) const { stream << "\n"; IpeString info; IpeStringStream infoStr(info); infoStr << "\n"; if (info.size() > 10) stream << info; if (!iProperties.iPreamble.empty()) { stream << ""; stream.PutXmlString(iProperties.iPreamble); stream << "\n"; } if (iStyleSheet) iStyleSheet->SaveCascadeAsXml(stream); // save bitmaps IpeBitmapFinder bm; for (const_iterator it = begin(); it != end(); ++it) bm.ScanPage(*it); if (!bm.iBitmaps.empty()) { int id = 1; IpeBitmap prev; std::sort(bm.iBitmaps.begin(), bm.iBitmaps.end()); for (std::vector::iterator it = bm.iBitmaps.begin(); it != bm.iBitmaps.end(); ++it) { if (!it->Equal(prev)) { if (usePdfBitmaps) { it->SaveAsXml(stream, it->ObjNum(), it->ObjNum()); } else { it->SaveAsXml(stream, id); it->SetObjNum(id); } } else it->SetObjNum(prev.ObjNum()); // noop if prev == it prev = *it; ++id; } } // save pages IpePainter painter(iStyleSheet); for (const_iterator it = begin(); it != end(); ++it) (*it)->SaveAsXml(painter, stream); stream << "\n"; } // -------------------------------------------------------------------- //! Return true if document has been edited since last save. bool IpeDocument::IsEdited() const { if (iEdited) return true; for (const_iterator it = begin(); it != end(); ++it) { if ((*it)->IsEdited()) return true; } return false; } //! Set whether document has been edited. /*! Methods that can modify the document already set the flag, so you only need to call this when inserting or deleting pages, etc. When modifying an IpePage, rather call IpePage::SetEdited(true). Clients need to manually reset the edited flag when they save the document, or after constructing it during loading. Calling this with \c edited == \c false will call IpePage::SetEdited(false) for all pages. */ void IpeDocument::SetEdited(bool edited) { iEdited = edited; if (!edited) { for (iterator it = begin(); it != end(); ++it) (*it)->SetEdited(false); } } //! Set document properties. void IpeDocument::SetProperties(const SProperties &props) { iProperties = props; iEdited = true; } //! Replace the style sheet cascade. /*! The previous contents is not deleted (because this function is often used to insert style sheets into the cascade). Sets the edited flag. */ void IpeDocument::SetStyleSheet(IpeStyleSheet *sheet) { iStyleSheet = sheet; iEdited = true; } //! Check all symbolic attributes in the document. /*! This function verifies that all symbolic attributes in the document are defined in the style sheet. It appends to \a seq all symbolic attributes (in no particular order, but with not duplicates) that are NOT defined. Returns \c true if there are no undefined symbolic attributes in the document. */ bool IpeDocument::CheckStyle(IpeAttributeSeq &seq) const { for (const_iterator it = begin(); it != end(); ++it) { const IpePage *page = *it; for (IpePage::const_iterator it1 = page->begin(); it1 != page->end(); ++it1) { it1->Object()->CheckStyle(StyleSheet(), seq); } } return (seq.size() == 0); } bool IpeDocument::HasTrueTypeFonts() const { if (iFontPool) { for (IpeFontPool::const_iterator it = iFontPool->begin(); it != iFontPool->end(); ++it) { if (it->iType == IpeFont::ETrueType) return true; } } return false; } //! Update the font pool (after running Pdflatex). /*! Takes ownership of the font pool. */ void IpeDocument::SetFontPool(IpeFontPool *fontPool) { delete iFontPool; iFontPool = fontPool; } //! Load a style sheet and add at top of cascade. bool IpeDocument::AddStyleSheet(IpeDataSource &source) { IpeImlParser parser(source, Repository()); IpeStyleSheet *sheet = parser.ParseStyleSheet(); if (sheet) { sheet->SetCascade(GetStyleSheet()); SetStyleSheet(sheet); return true; } else return false; } //! Return total number of views in all pages. int IpeDocument::TotalViews() const { int views = 0; for (const_iterator it = begin(); it != end(); ++it) { int nviews = (*it)->CountViews(); views += (nviews > 0) ? nviews : 1; } return views; } //! Create a new empty page with standard settings. IpePage *IpeDocument::NewPage(int gridSize) { // Create one empty page IpePage *page = new IpePage; IpeAttribute bga = Repository()->MakeSymbol(IpeAttribute::ETemplate, "background"); IpeView view; if (StyleSheet()->FindTemplate(bga)) { IpeLayer bglayer("background"); bglayer.SetLocked(true); page->AddLayer(bglayer); IpeObject *obj = new IpeReference(StyleSheet(), bga); page->push_back(IpePgObject(IpePgObject::ENone, 0, obj)); view.AddLayer("background"); } page->AddLayer(IpeLayer("alpha")); view.AddLayer("alpha"); view.SetActive("alpha"); page->AddView(view); page->SetGridSize(gridSize); return page; } // --------------------------------------------------------------------