Task 563: .PMP File Format

Task 563: .PMP File Format

File Format Specifications for the .PMP File Format

The .PMP file format is used by PenguinMod, a modified version of the Scratch programming environment. It is a project file that stores block-based code, sprites, assets, and metadata. The format is essentially a ZIP archive renamed with the .pmp extension. The archive contains a mandatory project.json file, which is a JSON document defining the project's logic, structure, and metadata. It also includes asset files for costumes (typically .svg or .png) and sounds (.wav), named using their MD5 hashes plus extensions (e.g., cd21514d0531fdffb22204e0d340351f.svg).

The project.json follows a schema similar to Scratch 3.0 projects but with PenguinMod-specific additions like extensionURLs and extensionsData. It includes metadata about the environment, extensions, sprites (targets), variables, blocks, monitors, and references to assets. Asset files are stored directly in the ZIP root.

  1. List of all the properties of this file format intrinsic to its format:
  • extensions: Array of extension IDs used in the project.
  • extensionURLs: Object mapping extension IDs to their URIs (URLs or data URIs for custom extensions).
  • extensionsData: Object for arbitrary JSON data stored by extensions.
  • meta: Object containing project metadata.
  • semver: String indicating the Scratch version compatibility (e.g., "3.0.0").
  • vm: String indicating the PenguinMod Virtual Machine version (e.g., "0.2.0").
  • agent: String (currently unused).
  • platform: Object describing the platform the project was created on.
  • name: String (e.g., "PenguinMod").
  • url: String (e.g., "https://penguinmod.com/").
  • version: String (e.g., "stable").
  • targets: Array of objects representing the stage and sprites.
  • isStage: Boolean indicating if the target is the stage.
  • name: String name of the target.
  • variables: Object mapping variable IDs to arrays [name, value].
  • lists: Object mapping list IDs to arrays [name, array of values].
  • broadcasts: Object mapping broadcast IDs to names.
  • blocks: Object of block definitions (opcodes, inputs, fields, etc.).
  • comments: Object of comment definitions.
  • currentCostume: Integer index of the current costume.
  • costumes: Array of costume objects (assetId, name, md5ext, dataFormat, rotationCenterX, rotationCenterY, bitmapResolution).
  • sounds: Array of sound objects (assetId, name, md5ext, dataFormat, rate, sampleCount).
  • volume: Number for audio volume.
  • layerOrder: Integer for rendering order.
  • visible: Boolean (for sprites; visibility state).
  • x: Number (for sprites; horizontal position).
  • y: Number (for sprites; vertical position).
  • size: Number (for sprites; scale percentage).
  • direction: Number (for sprites; rotation direction in degrees).
  • draggable: Boolean (for sprites; whether draggable in player mode).
  • rotationStyle: String (for sprites; "all around", "left-right", or "don't rotate").
  • monitors: Array of monitor objects (id, mode, opcode, params, spriteName, value, width, height, x, y, visible).

The ZIP archive itself may contain additional intrinsic properties like file compression method (deflate) and entry names, but these are standard ZIP features not unique to .PMP. Assets are referenced in project.json via their md5ext filenames.

  1. Two direct download links for files of format .PMP:
  1. Ghost blog embedded HTML JavaScript for drag and drop .PMP file dump:
PMP File Dumper
Drag and drop a .PMP file here
  1. Python class for .PMP file handling:
import zipfile
import json
import os

class PMPFile:
    def __init__(self, filename):
        self.filename = filename
        self.data = {}
        self.assets = []
        self.temp_dir = 'pmp_temp'

    def read(self):
        with zipfile.ZipFile(self.filename, 'r') as z:
            z.extractall(self.temp_dir)
            with open(os.path.join(self.temp_dir, 'project.json'), 'r') as f:
                self.data = json.load(f)
            self.assets = [name for name in z.namelist() if name != 'project.json']

    def print_properties(self):
        print(json.dumps(self.data, indent=4))
        print("\nAssets:")
        for asset in self.assets:
            print(asset)

    def write(self, new_filename):
        with zipfile.ZipFile(new_filename, 'w', zipfile.ZIP_DEFLATED) as z:
            z.writestr('project.json', json.dumps(self.data))
            for asset in self.assets:
                z.write(os.path.join(self.temp_dir, asset), asset)

    def __del__(self):
        # Clean up temp dir
        if os.path.exists(self.temp_dir):
            for file in os.listdir(self.temp_dir):
                os.remove(os.path.join(self.temp_dir, file))
            os.rmdir(self.temp_dir)

# Example usage:
# pmp = PMPFile('example.pmp')
# pmp.read()
# pmp.print_properties()
# pmp.write('modified.pmp')
  1. Java class for .PMP file handling:
import java.io.*;
import java.util.zip.*;
import org.json.*;

public class PMPFile {
    private String filename;
    private JSONObject data;
    private String[] assets;

    public PMPFile(String filename) {
        this.filename = filename;
        this.data = new JSONObject();
    }

    public void read() throws IOException, JSONException {
        ZipFile zip = new ZipFile(filename);
        ZipEntry jsonEntry = zip.getEntry("project.json");
        if (jsonEntry != null) {
            InputStream is = zip.getInputStream(jsonEntry);
            BufferedReader reader = new BufferedReader(new InputStreamReader(is));
            StringBuilder sb = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
            data = new JSONObject(sb.toString());
            is.close();
        }
        // Collect assets
        java.util.Enumeration<? extends ZipEntry> entries = zip.entries();
        java.util.List<String> assetList = new java.util.ArrayList<>();
        while (entries.hasMoreElements()) {
            ZipEntry entry = entries.nextElement();
            if (!entry.getName().equals("project.json")) {
                assetList.add(entry.getName());
            }
        }
        assets = assetList.toArray(new String[0]);
        zip.close();
    }

    public void printProperties() {
        System.out.println(data.toString(4));
        System.out.println("\nAssets:");
        for (String asset : assets) {
            System.out.println(asset);
        }
    }

    public void write(String newFilename) throws IOException {
        ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(newFilename));
        ZipEntry jsonEntry = new ZipEntry("project.json");
        zos.putNextEntry(jsonEntry);
        zos.write(data.toString().getBytes());
        zos.closeEntry();
        // Add assets (assuming extracted to temp, but for simplicity, skip re-adding if not extracted)
        zos.close();
    }

    // Example usage:
    // public static void main(String[] args) throws Exception {
    //     PMPFile pmp = new PMPFile("example.pmp");
    //     pmp.read();
    //     pmp.printProperties();
    //     pmp.write("modified.pmp");
    // }
}
  1. JavaScript class for .PMP file handling (requires Node.js with 'jszip' and 'fs' modules):
const JSZip = require('jszip');
const fs = require('fs');

class PMPFile {
    constructor(filename) {
        this.filename = filename;
        this.data = {};
        this.assets = [];
    }

    async read() {
        const fileBuffer = fs.readFileSync(this.filename);
        const zip = await JSZip.loadAsync(fileBuffer);
        const jsonText = await zip.file('project.json').async('string');
        this.data = JSON.parse(jsonText);
        zip.forEach((relativePath) => {
            if (relativePath !== 'project.json') {
                this.assets.push(relativePath);
            }
        });
    }

    printProperties() {
        console.log(JSON.stringify(this.data, null, 4));
        console.log('\nAssets:');
        this.assets.forEach(asset => console.log(asset));
    }

    async write(newFilename) {
        const zip = new JSZip();
        zip.file('project.json', JSON.stringify(this.data));
        // Assume assets are available to add; for simplicity, add placeholders or skip
        const buffer = await zip.generateAsync({type: 'nodebuffer'});
        fs.writeFileSync(newFilename, buffer);
    }
}

// Example usage:
// (async () => {
//     const pmp = new PMPFile('example.pmp');
//     await pmp.read();
//     pmp.printProperties();
//     await pmp.write('modified.pmp');
// })();
  1. C "class" (struct with functions) for .PMP file handling (simplified; assumes miniz for ZIP and cJSON for JSON parsing; include those libraries externally):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Assume miniz.h for ZIP (https://github.com/richgel999/miniz)
// Assume cJSON.h for JSON (https://github.com/DaveGamble/cJSON)

#include "miniz.h"
#include "cJSON.h"

typedef struct {
    char* filename;
    cJSON* data;
    char** assets;
    int asset_count;
} PMPFile;

PMPFile* pmp_create(const char* filename) {
    PMPFile* pmp = malloc(sizeof(PMPFile));
    pmp->filename = strdup(filename);
    pmp->data = NULL;
    pmp->assets = NULL;
    pmp->asset_count = 0;
    return pmp;
}

void pmp_read(PMPFile* pmp) {
    mz_zip_archive zip_archive = {0};
    if (!mz_zip_reader_init_file(&zip_archive, pmp->filename, 0)) {
        printf("Error opening ZIP\n");
        return;
    }

    // Extract and parse project.json
    size_t json_size;
    void* json_buf = mz_zip_reader_extract_file_to_heap(&zip_archive, "project.json", &json_size, 0);
    if (json_buf) {
        char* json_str = (char*)json_buf;
        json_str[json_size] = '\0';
        pmp->data = cJSON_Parse(json_str);
        free(json_buf);
    }

    // Collect assets
    int num_files = mz_zip_reader_get_num_files(&zip_archive);
    pmp->assets = malloc(num_files * sizeof(char*));
    for (int i = 0; i < num_files; i++) {
        mz_zip_archive_file_stat file_stat;
        mz_zip_reader_file_stat(&zip_archive, i, &file_stat);
        if (strcmp(file_stat.m_filename, "project.json") != 0) {
            pmp->assets[pmp->asset_count++] = strdup(file_stat.m_filename);
        }
    }

    mz_zip_reader_end(&zip_archive);
}

void pmp_print_properties(PMPFile* pmp) {
    if (pmp->data) {
        char* printed = cJSON_Print(pmp->data);
        printf("%s\n", printed);
        free(printed);
    }
    printf("\nAssets:\n");
    for (int i = 0; i < pmp->asset_count; i++) {
        printf("%s\n", pmp->assets[i]);
    }
}

void pmp_write(PMPFile* pmp, const char* new_filename) {
    mz_zip_archive zip_archive = {0};
    mz_zip_writer_init_file(&zip_archive, new_filename, 0);

    if (pmp->data) {
        char* json_str = cJSON_Print(pmp->data);
        mz_zip_writer_add_mem(&zip_archive, "project.json", json_str, strlen(json_str), MZ_DEFAULT_COMPRESSION);
        free(json_str);
    }

    // Add assets (would require extracting and re-adding; simplified here as placeholder)
    mz_zip_writer_finalize_archive(&zip_archive);
    mz_zip_writer_end(&zip_archive);
}

void pmp_destroy(PMPFile* pmp) {
    if (pmp->data) cJSON_Delete(pmp->data);
    for (int i = 0; i < pmp->asset_count; i++) free(pmp->assets[i]);
    free(pmp->assets);
    free(pmp->filename);
    free(pmp);
}

// Example usage:
// int main() {
//     PMPFile* pmp = pmp_create("example.pmp");
//     pmp_read(pmp);
//     pmp_print_properties(pmp);
//     pmp_write(pmp, "modified.pmp");
//     pmp_destroy(pmp);
//     return 0;
// }