File indexing completed on 2024-04-28 16:08:39

0001 /***************************************************************************
0002  *   Copyright (C) 2005-2014 by Linuxstopmotion contributors;              *
0003  *   see the AUTHORS file for details.                                     *
0004  *                                                                         *
0005  *   This program is free software; you can redistribute it and/or modify  *
0006  *   it under the terms of the GNU General Public License as published by  *
0007  *   the Free Software Foundation; either version 2 of the License, or     *
0008  *   (at your option) any later version.                                   *
0009  *                                                                         *
0010  *   This program is distributed in the hope that it will be useful,       *
0011  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
0012  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
0013  *   GNU General Public License for more details.                          *
0014  *                                                                         *
0015  *   You should have received a copy of the GNU General Public License     *
0016  *   along with this program; if not, write to the                         *
0017  *   Free Software Foundation, Inc.,                                       *
0018  *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
0019  ***************************************************************************/
0020 
0021 #include "projectserializer.h"
0022 
0023 #include "src/foundation/logger.h"
0024 #include "src/domain/filenamevisitor.h"
0025 #include "src/domain/animation/scene.h"
0026 #include "src/domain/animation/frame.h"
0027 #include "src/domain/animation/sound.h"
0028 #include "src/domain/animation/animationimpl.h"
0029 #include "src/domain/animation/workspacefile.h"
0030 #include "src/presentation/frontends/frontend.h"
0031 
0032 #include <libxml/tree.h>
0033 
0034 #include <assert.h>
0035 #include <fcntl.h>
0036 #include <stdio.h>
0037 #include <libtar.h>
0038 #include <cstring>
0039 #include <errno.h>
0040 #include <iosfwd>
0041 #include <map>
0042 #include <string>
0043 #include <set>
0044 
0045 // for older libtars
0046 #define UNCONST(s) const_cast<char*>(s)
0047 
0048 FileException::FileException(const char* functionName, int errorno) {
0049     snprintf(buffer, sizeof(buffer), "%s(): %s", functionName,
0050             strerror(errorno));
0051 }
0052 
0053 const char* FileException::what() const throw() {
0054     return buffer;
0055 }
0056 
0057 class ProjectFileCreationException : public FileException {
0058 public:
0059     ProjectFileCreationException(const char* functionName, int errorno)
0060             : FileException(functionName, errorno) {
0061     }
0062 };
0063 
0064 class ProjectFileReadException : public FileException {
0065 public:
0066     ProjectFileReadException(const char* functionName, int errorno)
0067             : FileException(functionName, errorno) {
0068     }
0069 };
0070 
0071 ProjectSerializer::ProjectSerializer() : projectFile(0) {
0072     LIBXML_TEST_VERSION;
0073 }
0074 
0075 ProjectSerializer::~ProjectSerializer() {
0076     resetProjectFile();
0077 }
0078 
0079 class TarFileRead {
0080     TAR* tar;
0081 public:
0082     TarFileRead(const char* filename) {
0083         if (tar_open(&tar, UNCONST(filename), 0, O_RDONLY, 0, 0 | 0) == -1) {
0084             throw ProjectFileReadException("tar_open", errno);
0085         }
0086     }
0087     ~TarFileRead() {
0088     }
0089     bool next() {
0090         int ret = th_read(tar);
0091         if (ret == 0)
0092             return true;
0093         if (ret < 0)
0094             throw ProjectFileReadException("th_read", errno);
0095         return false;
0096     }
0097     const char *regularFileFilename() const {
0098         return TH_ISREG(tar)? th_get_pathname(tar) : 0;
0099     }
0100     void extract(std::string& filename) {
0101         filename.c_str();
0102         if (tar_extract_regfile(tar, &filename[0]) < 0) {
0103             throw ProjectFileReadException("th_extract_regfile", errno);
0104         }
0105     }
0106     void finish() {
0107         if (tar_close(tar) < 0)
0108             throw ProjectFileReadException("tar_close", errno);
0109         tar = 0;
0110     }
0111 };
0112 
0113 class StoFileRead {
0114     TarFileRead tar;
0115     static bool isProjectFile(const char* filename) {
0116         if (filename) {
0117             const char* slash1 = strchr(filename, '/');
0118             if (slash1) {
0119                 ++slash1;
0120                 if (!strchr(slash1, '/')) {
0121                     const char* dot = strrchr(slash1, '.');
0122                     if (dot && strcmp(dot, ".dat") == 0) {
0123                         return true;
0124                     }
0125                 }
0126             }
0127         }
0128         return false;
0129     }
0130     // returns the basename portion of the argument if it is an image or sound,
0131     // null otherwise
0132     static const char* imageOrSoundFile(const char* filename) {
0133         static const char images[] = "images";
0134         static const char sounds[] = "sounds";
0135         if (filename) {
0136             const char* slash1 = strchr(filename, '/');
0137             if (slash1) {
0138                 ++slash1;
0139                 const char* slash2 = strchr(slash1, '/');
0140                 if (slash2) {
0141                     std::ptrdiff_t dirLen = slash2 - slash1;
0142                     ++slash2;
0143                     if ((strncmp(images, slash1, dirLen) == 0
0144                             || strncmp(sounds, slash1, dirLen))
0145                             && strchr(slash2, '/') == 0) {
0146                         return slash2;
0147                     }
0148                 }
0149             }
0150         }
0151         return 0;
0152     }
0153 public:
0154     StoFileRead(const char* filename) : tar(filename) {
0155     }
0156     ~StoFileRead() {
0157     }
0158     void unpack() {
0159         while (tar.next()) {
0160             const char* fn = tar.regularFileFilename();
0161             const char* base = imageOrSoundFile(fn);
0162             if (base) {
0163                 // image or sound
0164                 WorkspaceFile wf(base);
0165                 std::string path = wf.path();
0166                 tar.extract(path);
0167             } else if (isProjectFile(fn)) {
0168                 WorkspaceFile dat(WorkspaceFile::currentModelFile);
0169                 std::string path = dat.path();
0170                 tar.extract(path);
0171             } else if (fn) {
0172                 Logger::get().logWarning(
0173                         "Did not know what to do with tarred file %s", fn);
0174             }
0175         }
0176     }
0177     void finish() {
0178         tar.finish();
0179     }
0180 };
0181 
0182 void readSto(const char* filename) {
0183     StoFileRead sto(filename);
0184     sto.unpack();
0185     sto.finish();
0186 }
0187 
0188 // appends .sto to filename if appropriate to make a project filename
0189 void ProjectSerializer::setProjectFile(const char *filename) {
0190     static const char appendix[] = ".sto";
0191     const size_t len = strlen(filename);
0192     char* newProjectFile = new char[len + sizeof(appendix)];
0193     strcpy(newProjectFile, filename);
0194     const char *dotPtr = strrchr(filename, '.');
0195     if (dotPtr == NULL || strcmp(dotPtr, ".sto") != 0) {
0196         strcat(newProjectFile, ".sto");
0197     }
0198     delete[] projectFile;
0199     projectFile = newProjectFile;
0200 }
0201 
0202 bool ProjectSerializer::openDat(std::vector<Scene*>& out, const char* filename) {
0203     xmlDocPtr doc = xmlReadFile(filename, NULL, 0);
0204     if (!doc) {
0205         Logger::get().logWarning("Couldn't load XML file");
0206         return false;
0207     }
0208     xmlNodePtr rootNode = xmlDocGetRootElement(doc);
0209     out.clear();
0210     getAttributes(rootNode, out);
0211     xmlFreeDoc(doc);
0212     xmlCleanupParser();
0213     return true;
0214 }
0215 
0216 std::vector<Scene*> ProjectSerializer::openSto(const char *filename) {
0217     assert(filename != NULL);
0218     setProjectFile(filename);
0219     readSto(projectFile);
0220     WorkspaceFile dat(WorkspaceFile::currentModelFile);
0221     std::vector<Scene*> sVect;
0222     openDat(sVect, dat.path());
0223     return sVect;
0224 }
0225 
0226 class UniqueVisitor: public FileNameVisitor {
0227     std::set<std::string> visited;
0228     FileNameVisitor* del;
0229 public:
0230     UniqueVisitor(FileNameVisitor* delegate) :
0231             del(delegate) {
0232     }
0233     ~UniqueVisitor() {
0234     }
0235     void visitImage(const char* path) {
0236         std::string p(path);
0237         if (visited.find(p) == visited.end()) {
0238             visited.insert(p);
0239             del->visitImage(path);
0240         }
0241     }
0242     void visitSound(const char* path) {
0243         std::string p(path);
0244         if (visited.find(p) == visited.end()) {
0245             visited.insert(p);
0246             del->visitSound(path);
0247         }
0248     }
0249 };
0250 
0251 std::string getProjectName(const char* filename) {
0252     const char* nameBegin = strrchr(filename, '/');
0253     nameBegin = !nameBegin? filename : nameBegin + 1;
0254     const char* nameEnd = strrchr(filename, '.');
0255     if (!nameEnd || nameEnd < nameBegin)
0256         nameEnd = filename + strlen(filename);
0257     return std::string(nameBegin, nameEnd);
0258 }
0259 
0260 class TarPath {
0261     std::string path;
0262     std::string::size_type length;
0263 public:
0264     TarPath(const std::string& rootDir, const char* ext)
0265             : path(rootDir), length(0) {
0266         path.append(ext);
0267         length = path.length();
0268     }
0269     /**
0270      * Returns the argument prefixed with the rootDir and ext supplied
0271      * to the constructor. The result is valid until the next call of getPath
0272      * or this object is destroyed.
0273      */
0274     const char* getPath(const char* fname) {
0275         path.resize(length);
0276         path.append(fname);
0277         return path.c_str();
0278     }
0279 };
0280 
0281 class TarFileWrite {
0282     TAR* tar;
0283 public:
0284     TarFileWrite(const char* tarFilename) {
0285         if (-1== tar_open(&tar, UNCONST(tarFilename), NULL,
0286                     O_WRONLY | O_CREAT | O_TRUNC, 0644, 0)) {
0287             throw ProjectFileCreationException("tar_open", errno);
0288         }
0289     }
0290     ~TarFileWrite() {
0291     }
0292     void add(const char* realPath, const char* storedPath) {
0293         if (-1 == tar_append_file(tar, UNCONST(realPath), UNCONST(storedPath))) {
0294             throw ProjectFileCreationException("tar_append_file", errno);
0295         }
0296     }
0297     void eof() {
0298         if (tar_close(tar) < 0) {
0299             throw ProjectFileCreationException("tar_close", errno);
0300         }
0301         tar = 0;
0302     }
0303 };
0304 
0305 class StoFileWrite {
0306     TarFileWrite tar;
0307     std::string name;
0308     TarPath imagePath;
0309     TarPath soundPath;
0310     std::set<std::string> alreadyPacked;
0311 public:
0312     StoFileWrite(const char* stoFilename)
0313             : tar(stoFilename),
0314               name(getProjectName(stoFilename)),
0315               imagePath(name, "/images/"),
0316               soundPath(name, "/sounds/") {
0317     }
0318     void addProjectFile(const char* projectFilename) {
0319         std::string projectFileName = name;
0320         projectFileName.append("/");
0321         projectFileName.append(name);
0322         projectFileName.append(".dat");
0323         tar.add(projectFilename, projectFileName.c_str());
0324     }
0325     void addImage(const char* realName, const char* baseName) {
0326         std::string bn(baseName);
0327         if (alreadyPacked.insert(bn).second)
0328             tar.add(realName, imagePath.getPath(baseName));
0329     }
0330     void addSound(const char* realName, const char* baseName) {
0331         std::string bn(baseName);
0332         if (alreadyPacked.insert(bn).second)
0333             tar.add(realName, soundPath.getPath(baseName));
0334     }
0335     void finish() {
0336         tar.eof();
0337     }
0338 };
0339 
0340 void writeSto(const char* filename, const char* xmlProjectFile,
0341             const AnimationImpl& anim) {
0342     StoFileWrite sto(filename);
0343     sto.addProjectFile(xmlProjectFile);
0344     int sceneCount = anim.sceneCount();
0345     for (int i = 0; i != sceneCount; ++i) {
0346         const Scene* scene = anim.getScene(i);
0347         int frameCount = scene->getSize();
0348         for (int j = 0; j != frameCount; ++j) {
0349             const Frame* frame = scene->getFrame(j);
0350             sto.addImage(frame->getImagePath(), frame->getBasename());
0351             int soundCount = frame->soundCount();
0352             for (int k = 0; k != soundCount; ++k) {
0353                 const Sound* sound = frame->getSound(k);
0354                 sto.addSound(sound->getSoundPath(), sound->getBasename());
0355             }
0356         }
0357     }
0358     sto.finish();
0359 }
0360 
0361 /**
0362  * Save the current file, putting the current state into the new model file.
0363  * The caller is responsible for clearing the command log and then renaming
0364  * the new model file to be the current model file.
0365  * @param filename The filename to save.
0366  * @param anim The animation to save.
0367  * @param frontend The UI for reporting progress and errors.
0368  */
0369 void ProjectSerializer::save(const char *filename,
0370         const AnimationImpl& anim, Frontend *frontend) {
0371     assert(filename != NULL);
0372     assert(frontend != NULL);
0373 
0374     setProjectFile(filename);
0375 
0376     xmlDocPtr doc = xmlNewDoc(BAD_CAST "1.0");
0377     xmlCreateIntSubset(doc, BAD_CAST "smil",
0378             BAD_CAST "-//W3C//DTD SMIL 2.0//EN",
0379             BAD_CAST "http://www.w3.org/2001/SMIL20/SMIL20.dtd");
0380 
0381     xmlNodePtr rootNode = xmlNewNode(NULL, BAD_CAST "smil");
0382     xmlNewProp(rootNode, BAD_CAST "xmlns",
0383             BAD_CAST "http://www.w3.org/2001/SMIL20/Language");
0384     xmlNewProp(rootNode, BAD_CAST "xml:lang", BAD_CAST "en");
0385     xmlNewProp(rootNode, BAD_CAST "title", BAD_CAST "Stopmotion");
0386     xmlDocSetRootElement(doc, rootNode);
0387 
0388     setAttributes(rootNode, anim, frontend);
0389 
0390     WorkspaceFile newDat(WorkspaceFile::newModelFile);
0391     saveDOMToFile(doc, newDat.path());
0392 
0393     xmlFreeDoc(doc);
0394     xmlCleanupParser();
0395 
0396     // Write out new.dat file. The recovery system will ignore it until...
0397     writeSto(projectFile, newDat.path(), anim);
0398     int numElem = anim.sceneCount();
0399     frontend->updateProgress(numElem);
0400     frontend->hideProgress();
0401 }
0402 
0403 void ProjectSerializer::setAttributes(xmlNodePtr rootNode,
0404         const AnimationImpl& anim, Frontend *frontend) {
0405     xmlNodePtr node = NULL;
0406     const Frame *frame = NULL;
0407     const Sound *sound = NULL;
0408 
0409     xmlNodePtr scenes = xmlNewChild(rootNode, NULL, BAD_CAST "scenes", NULL);
0410 
0411     int numScenes = anim.sceneCount();
0412     frontend->showProgress(Frontend::savingScenesToDisk, numScenes);
0413     for (int i = 0; i < numScenes; ++i) {
0414         frontend->updateProgress(i);
0415         // Scenes
0416         node = xmlNewChild(scenes, NULL, BAD_CAST "seq", NULL);
0417 
0418         // Images
0419         xmlNodePtr images = xmlNewChild(node, NULL, BAD_CAST "images", NULL);
0420         const Scene* scene = anim.getScene(i);
0421         int numFrames = scene->getSize();
0422         for (int j = 0; j < numFrames; ++j) {
0423             frame = scene->getFrame(j);
0424             const char *filename = frame->getBasename();
0425             node = xmlNewChild(images, NULL, BAD_CAST "img", NULL);
0426             xmlNewProp(node, BAD_CAST "src", BAD_CAST filename);
0427 
0428             // Sounds
0429             int numSounds = frame->soundCount();
0430             if (numSounds > 0) {
0431                 xmlNodePtr sounds = xmlNewChild(node, NULL, BAD_CAST "sounds",
0432                         NULL);
0433                 for (int k = 0; k < numSounds; ++k) {
0434                     sound = frame->getSound(k);
0435                     filename = sound->getBasename();
0436                     node = xmlNewChild(sounds, NULL, BAD_CAST "audio", NULL);
0437                     xmlNewProp(node, BAD_CAST "src", BAD_CAST filename);
0438                     xmlNewProp(node, BAD_CAST "alt",
0439                             BAD_CAST frame->getSoundName(i));
0440                 }
0441             }
0442         }
0443     }
0444 }
0445 
0446 void ProjectSerializer::getAttributes(xmlNodePtr node,
0447         std::vector<Scene*>& sVect) {
0448     xmlNodePtr currNode = NULL;
0449     for (currNode = node; currNode; currNode = currNode->next) {
0450         if (currNode->type == XML_ELEMENT_NODE) {
0451             char *nodeName = (char*) currNode->name;
0452             // We either have a image node or a sound node
0453             if (strcmp(nodeName, "img") == 0
0454                     || strcmp(nodeName, "audio") == 0) {
0455                 char *filename = (char*) xmlGetProp(currNode, BAD_CAST "src");
0456                 if (filename != NULL) {
0457                     // The node is a image node
0458                     if (strcmp(nodeName, "img") == 0) {
0459                         WorkspaceFile wf(filename);
0460                         Frame *f = new Frame(wf);
0461                         Scene *s = sVect.back();
0462                         s->addSavedFrame(f);
0463                     }
0464                     // The node is a sound node
0465                     else {
0466                         Scene *s = sVect.back();
0467                         int frameNum = s->getSize() - 1;
0468                         WorkspaceFile wf(filename);
0469                         int soundNum = s->soundCount(frameNum);
0470                         s->newSound(frameNum, wf);
0471                         char *soundName = (char*) xmlGetProp(currNode,
0472                                 BAD_CAST "alt");
0473                         if (soundName != NULL) {
0474                             s->setSoundName(frameNum, soundNum, soundName);
0475                             xmlFree((xmlChar*) soundName);
0476                         }
0477                     }
0478                     xmlFree((xmlChar*) filename);
0479                 }
0480             }
0481             // The node is a scene node
0482             else if (strcmp((char*) currNode->name, "seq") == 0) {
0483                 Scene *s = new Scene();
0484                 sVect.push_back(s);
0485             }
0486         }
0487         getAttributes(currNode->children, sVect);
0488     }
0489 }
0490 
0491 void ProjectSerializer::saveDOMToFile(xmlDocPtr doc, const char* filename) {
0492     int ret = xmlSaveFormatFile(filename, doc, 1);
0493     if (ret == -1) {
0494         throw ProjectFileCreationException("xmlSaveFormatFile", errno);
0495     }
0496 }
0497 
0498 const char* ProjectSerializer::getProjectFile() {
0499     return projectFile;
0500 }
0501 
0502 void ProjectSerializer::resetProjectFile(const char* filename) {
0503     char* copy = 0;
0504     if (filename) {
0505         size_t len = strlen(filename);
0506         copy = new char[len + 1];
0507         strncpy(copy, filename, len + 1);
0508     }
0509     delete[] projectFile;
0510     projectFile = copy;
0511 }
0512 
0513 void ProjectSerializer::resetProjectFile() {
0514     resetProjectFile(0);
0515 }