export/wick/WickFile.js

/*
 * Copyright 2019 WICKLETS LLC
 *
 * This file is part of Wick Engine.
 *
 * Wick Engine 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 3 of the License, or
 * (at your option) any later version.
 *
 * Wick Engine 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 Wick Engine.  If not, see <https://www.gnu.org/licenses/>.
 */


/**
 * Utility class for creating and parsing wick files.
 */
Wick.WickFile = class {
    /**
     * Generate some metadata for debugging wick projects.
     * @returns {object}
     */
    static generateMetaData() {
        return {
            wickengine: Wick.version,
            lastModified: +new Date(),
            platform: {
                name: platform.name,
                version: platform.version,
                product: platform.product,
                manufacturer: platform.manufacturer,
                layout: platform.layout,
                os: {
                    architecture: platform.os.architecture,
                    family: platform.os.family,
                    version: platform.os.version,
                },
                description: platform.description,
            }
        };
    }

    /**
     * Create a project from a wick file.
     * @param {File} wickFile - Wick file containing project data.
     * @param {function} callback - Function called when the project is created.
     * @param {string} format - The format to return. Can be 'blob' or 'base64'.
     */
    static fromWickFile(wickFile, callback, format) {

        if (!format) {
            format = 'blob';
        }
        if (format !== 'blob' && format !== 'base64') {
            console.error('WickFile.toWickFile: invalid format: ' + format);
            return;
        }

        var zip = new JSZip();

        zip.loadAsync(wickFile, { base64: format === 'base64' }).then((contents) => {
            contents.files['project.json'].async('text')
                .then(projectJSON => {

                    var projectData = JSON.parse(projectJSON);

                    if (!projectData.objects) {
                        // No metadata! This is a pre 1.0.9a project. Convert it.
                        console.log('Wick.WickFile: Converting old project format.');
                        projectData = Wick.WickFile.Alpha.convertJsonProject(projectData);
                    }

                    projectData.assets = [];

                    for (var uuid in projectData.objects) {
                        var data = projectData.objects[uuid];
                        var object = Wick.Base.fromData(data);
                        Wick.ObjectCache.addObject(object);
                    }

                    var project = Wick.Base.fromData(projectData.project);
                    Wick.ObjectCache.addObject(project);

                    var loadedAssetCount = 0;
                    let corruptedFiles = []; // Store a list of all files that are now missing. 

                    // Immediately end if the project has no assets.
                    if (project.getAssets().length === 0) {
                        this._prepareProject(project);
                        callback(project);
                    } else {
                        // Make a copy of the assets, as we may get rid of some mid process.
                        let allAssets = project.getAssets().concat([]);

                        allAssets.forEach(assetData => {
  
                            var assetFile = contents.files['assets/' + assetData.uuid + '.' + assetData.fileExtension];

                            /**
                             * Checks if we've loaded all assets, logs an error if an error occurred 
                             * while loading asset files.
                             */
                            var checkProjectLoad = () => {
                                loadedAssetCount++;
                                if (loadedAssetCount === allAssets.length) {
                                    // Throw an error if any corrupted files were found.
                                    project.errorOccured && corruptedFiles.length > 0 && project.errorOccured("Corrupted Files Were Deleted: " + corruptedFiles);
                                    
                                    this._prepareProject(project);
                                    callback(project);
                                }
                            }

                            

                            if (!assetFile) {
                                // Try removing the asset from the project here.
                                assetData.removeAllInstances();
                                project.removeAsset(assetData);
                                corruptedFiles.push(assetData.filename);

                                checkProjectLoad();
                                return;
                            }

                            assetFile.async('base64')
                                .then(assetFileData => {
                                    var assetSrc = 'data:' + assetData.MIMEType + ';base64,' + assetFileData;
                                    Wick.FileCache.addFile(assetSrc, assetData.uuid);
                                }).catch(e => {
                                    console.log('Error loading asset file.');
                                    console.log(e);
                                    callback(null);
                                }).finally(() => {
                                    assetData.load(() => {
                                        checkProjectLoad();
                                    });
                                });
                        });
                    }
                });
        }).catch((e) => {
            console.log('Error loading project zip.')
            console.log(e);
            callback(null);
        });
    }

    /**
     * Create a wick file from the project.
     * @param {Wick.Project} project - the project to create a wick file from
     * @param {function} callback - Function called when the file is created. Contains the file as a parameter.
     * @param {string} format - The format to return. Can be 'blob' or 'base64'.
     */
    static toWickFile(project, callback, format) {
        if (!format) {
            format = 'blob';
        }
        if (format !== 'blob' && format !== 'base64') {
            console.error('WickFile.toWickFile: invalid format: ' + format);
            return;
        }

        var zip = new JSZip();

        // Create assets folder
        var assetsFolder = zip.folder("assets");

        // Populate assets folder with files
        project.getAssets().filter(asset => {
            return asset instanceof Wick.ImageAsset ||
                asset instanceof Wick.SoundAsset ||
                asset instanceof Wick.FontAsset ||
                asset instanceof Wick.ClipAsset ||
                asset instanceof Wick.SVGAsset;
        }).forEach(asset => {
            // Create file from asset dataurl, add it to assets folder
            var fileExtension = asset.MIMEType.split('/')[1];
            var filename = asset.uuid;
            var data = asset.src.split(',')[1];
            assetsFolder.file(filename + '.' + fileExtension, data, { base64: true });
        });

        var objectCacheSerialized = {};
        Wick.ObjectCache.getActiveObjects(project).forEach(object => {
            objectCacheSerialized[object.uuid] = object.serialize();
        });

        var projectSerialized = project.serialize();

        for (var uuid in objectCacheSerialized) {
            if (objectCacheSerialized[uuid].classname === 'Project') {
                delete objectCacheSerialized[uuid];
            }
        }

        // Remove some extra data that we don't actually want to save
        // Clear selection:
        for (var uuid in objectCacheSerialized) {
            var object = objectCacheSerialized[uuid];
            if (object.classname === 'Selection') {
                object.selectedObjects = [];
            }
        }
        // Set focus to root
        for (var uuid in objectCacheSerialized) {
            var object = objectCacheSerialized[uuid];
            if (projectSerialized.children.indexOf(uuid) !== -1 && object.classname === 'Clip') {
                projectSerialized.focus = uuid;
            }
        }
        // Reset all playhead positions
        for (var uuid in objectCacheSerialized) {
            var object = objectCacheSerialized[uuid];
            if (object.classname === 'Timeline') {
                object.playheadPosition = 1;
            }
        }

        // Add project json to root directory of zip file
        var projectData = {
            project: projectSerialized,
            objects: objectCacheSerialized,
        };
        zip.file("project.json", JSON.stringify(projectData, null, 2));

        zip.generateAsync({
            type: format,
            compression: "DEFLATE",
            compressionOptions: {
                level: 9
            },
        }).then(callback);
    }

    /* Make any small backwards compatibility fixes needed */
    static _prepareProject(project) {
        // 1.16+ projects don't allow gaps between frames.
        Wick.ObjectCache.getAllObjects().filter(object => {
            return object instanceof Wick.Timeline;
        }).forEach(timeline => {
            var oldFrameGapFillMethod = timeline.fillGapsMethod;
            timeline.fillGapsMethod = 'blank_frames';
            timeline.resolveFrameGaps();
            timeline.fillGapsMethod = oldFrameGapFillMethod;
        });
    }
}