Skip to content

Multiple bugs with PNG cartridges #2042

@bztsrc

Description

@bztsrc
Contributor

Hi,

Very cool console you have! I like it very much! I only have issues with the PNG files. The way how TIC-80 currently handles them is unacceptable for several reasons:

  • First, it's not supported everywhere, only in studio and in surf (but not from command line for example).
  • Second, it does not work on systems with case-sensitive file systems (due to the LoadPngCart call depends on file extension!)
  • Third, and this is the biggest problem, a real no-go for me, is the way how TIC-80 embeds data generates CORRUPT png files! Tools are refusing to resize the image because of that, or worse, they might corrupt the cartridge data silently.

Here's a proposed solution that fixes all of these problems at once and as a side effect improves performance a lot. This renders the use of LoadPngCart completely unnecessary, it Just Works(tm) and gets the cartridge from a png file without actually decoding the data:

diff --git a/src/cart.c b/src/cart.c
index 9be32c7..1c6079d 100644
--- a/src/cart.c
+++ b/src/cart.c
@@ -87,6 +87,28 @@ void tic_cart_load(tic_cartridge* cart, const u8* buffer, s32 size)
 
 #define LOAD_CHUNK(to) memcpy(&to, ptr, MIN(sizeof(to), chunk->size ? retro_le_to_cpu16(chunk->size) : TIC_BANK_SIZE))
 
+    // check if the cartridge buffer is a PNG file
+    if (!memcmp(buffer, "\x89PNG", 4))
+    {
+        s32 siz;
+        const u8* ptr = buffer + 8;
+        while (ptr < end)
+        {
+            siz = ((ptr[0] << 24) | (ptr[1] << 16) | (ptr[2] << 8) | ptr[3]) + 12;
+            if (!memcmp(ptr + 4, "caRt", 4))
+            {
+                buffer = ptr + 8;
+                size = siz;
+                end = buffer + size;
+                break;
+            }
+            ptr += siz;
+        }
+        // error, no TIC-80 cartridge chunk in PNG???
+        if (ptr >= end)
+            return;
+    }
+
     // load palette chunk first
     {
         const u8* ptr = buffer;

The advantage is, this patch is extremely simple, dependency-free and it does not require to decode the png at all! It just iterates through png chunks without interpreting them.

I've chosen the chunk magic to be caRt. This isn't some foolishness, rather selected in accordance to the PNG Specification section 9.8. "Use of private chunks", and means ancillary, private, safe-to-copy chunk.

As for creating such VALID png files, here's an extremely simple command line tool to do that (I've written it deliberately in a way so that you can reuse the tic2png function in TIC-80). Again, this is totally dependency-free (note no png libraries used!), and does not require to decode nor to encode png data, all it does is just manipulating png chunks. It simply inserts a caRt chunk with the cartridge data as the last chunk (well, the one before the IEND chunk). It also takes care of the case if the png already had a caRt chunk.

/*
 * tic2png.c
 *
 * Copyright (C) 2022 bzt (bztsrc@gitlab) MIT license
 *
 * @brief Small dependency-free tool to embed a .tic cartridge into a .png file
 * Compilation: gcc tic2png.c -o tic2png
 */

#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

/* the png specification is very strict, *do not* change uppercase/lowercase */
#define TICMAGIC "caRt"

/**
 * Add a tic file into a png chunk
 */
int tic2png(uint8_t *tic, size_t ticsize, uint8_t **png, size_t *pngsize)
{
    int i, j;
    uint32_t crc, crc_table[256], size;
    uint8_t *buf, *in, *out, *end, *ptr = tic;
    size_t s;

    if(!tic || !png || !*png || memcmp(*png, "\x89PNG", 4) || !pngsize || *pngsize < 8+12+12) return 0;
    end = *png + *pngsize;
    buf = malloc(*pngsize + ticsize + 12);
    if(!buf) return 0;
    s = 8; in = *png; out = buf; memcpy(out, in, 8); in += 8; out += 8;
    /* copy all chunks, except tic chunk */
    while(in < end - 12 && memcmp(in + 4, "IEND", 4)) {
        size = ((in[0] << 24) | (in[1] << 16) | (in[2] << 8) | in[3]) + 12;
        if(memcmp(in + 4, TICMAGIC, 4)) { memcpy(out, in, size); out += size; s += size; }
        in += size;
    }
    /* add new tic chunk */
    s += 4;
    *out++ = (ticsize >> 24) & 0xff; *out++ = (ticsize >> 16) & 0xff; *out++ = (ticsize >>  8) & 0xff; *out++ = (ticsize >>  0) & 0xff;
    for(i = 0; i < 256; i++) {
        crc = (uint32_t)i; for(j = 0; j < 8; j++) { crc = crc & 1 ? 0xedb88320L ^ (crc >> 1) : (crc >> 1); crc_table[i] = crc;
    }
    crc = 0xffffffff;
    for(i = 0; i < 4; i++, s++, out++) { *out = TICMAGIC[i]; crc = crc_table[(crc ^ *out) & 0xff] ^ (crc >> 8); }
    for(; ticsize; ticsize--, s++, ptr++, out++) { *out = *ptr; crc = crc_table[(crc ^ *out) & 0xff] ^ (crc >> 8); }
    crc ^= 0xffffffff; s += 4;
    *out++ = (crc >> 24) & 0xff; *out++ = (crc >> 16) & 0xff; *out++ = (crc >>  8) & 0xff; *out++ = (crc >>  0) & 0xff;
    /* add end chunk */
    memcpy(out, in, 12);
    s += 12;
    /* replace original png buffer with the new one */
    free(*png);
    *png = buf;
    *pngsize = s;
    return 1;
}

/**
 * Read in one file into memory
 */
uint8_t *readfile(char *fn, size_t *size)
{
    FILE *f;
    uint8_t *buf = NULL;

    f = fopen(fn, "rb");
    if(f) {
        fseek(f, 0, SEEK_END);
        *size = ftell(f);
        fseek(f, 0, SEEK_SET);
        buf = malloc(*size);
        if(!buf) { fclose(f); fprintf(stderr, "tic2png: unable to allocate memory\r\n"); exit(1); }
        fread(buf, 1, *size, f);
        fclose(f);
    } else {
        fprintf(stderr, "tic2png: unable to read %s\r\n", fn);
        exit(1);
    }
    return buf;
}

/**
 * Main procedure
 */
int main(int argc, char **argv)
{
    FILE *f;
    int ret;
    size_t ticsize, pngsize, outsize;
    uint8_t *tic, *png, *out;

    /* check arguments */
    if(argc < 3) {
        printf("tic2png - by bzt MIT\r\n\r\n  %s <.tic file> <.png file>\r\n", argv[0]);
        return 1;
    }

    /* read in files */
    tic = readfile(argv[1], &ticsize);
    png = readfile(argv[2], &pngsize);

    /* add tic to png */
    if(!tic2png(tic, ticsize, &png, &pngsize)) {
        fprintf(stderr, "tic2png: unable to add png chunk\r\n");
        return 1;
    }

    /* save new png file */
    f = fopen(argv[2], "wb");
    if(f) {
        fwrite(png, 1, pngsize, f);
        fclose(f);
    } else {
        fprintf(stderr, "tic2png: unable to write %s\r\n", argv[2]);
        return 1;
    }
    return 0;
}

All these are licensed under MIT, feel free to use them in TIC-80.

Cheers,
bzt

Activity

joshgoebel

joshgoebel commented on Dec 5, 2022

@joshgoebel
Collaborator

is the way how TIC-80 embeds data generates CORRUPT png files

Could you please provide additional details here? This should not be the case. The algorithm that we use to embed the cartridge data in the 6 least significant bits of a 24-bit image should not create corrupt files.

bztsrc

bztsrc commented on Dec 5, 2022

@bztsrc
ContributorAuthor

Could you please provide additional details here?

Sure. Try running TIC-80 on Linux, and have cartridges with .PNG, .Png and .png extension. Won't work.

The algorithm that we use to embed the cartridge data in the 6 least significant bits of a 24-bit image should not create corrupt files.

That's PIOC-8, and not how TIC-80 works. TIC-80 stores 3 bits per channel (which is, unlike 2 bits noticeable by naked eye), and furthermore into a 32-bit image. Even if you don't resize the image (which obviously corrupts data silently), tools might decide to simply clear the RGB channel or just store random bytes there if the A channel is 0 (because in that case the pixel won't be visible anyway), meaning you'll get data corruption in the cartridge data at random bytes (believe me, I had to debug such a steganography bug, you wouldn't want to do that, it's a particularly nasty bug to catch).

There are other serious issues with this method too: you have to encode and decode the entire PNG image all the time, which is time consuming and wastes precious resources. Also your storage capacity is seriously limited, and TIC-80 already run out of that (hence the need for that hackish, already obsoleted CHUNK_CODE_ZIP, but if you really want to store wasm bytecode in CHUNK_BINARY chunks, no storage space will be enough).

And doing this makes absolutely no sense: the PNG Specification is pretty clear that you're allowed to put private chunks into images, these will be kept by various tools and editors (thanks to the safe-to-copy bit, so resizing, alpha etc. isn't a problem any more because cartridge data will be kept untouched), plus it allows storing cartridges up to 2^31 bytes, meaning all that Lua compression hassle can be easily avoided, and there's plenty of space for wasm bytecode too.

I understand that PICO-8 uses stegano, but that's not a reason to keep using a bad solution in TIC-80, right? Especially when the simpler solution so much much better (smaller code, lot faster, more stupid-proof).

Cheers,
bzt

joshgoebel

joshgoebel commented on Dec 5, 2022

@joshgoebel
Collaborator

not how TIC-80 works. TIC-80 stores 3 bits per channel (which is, unlike 2 bits noticeable by naked eye),

Same concept, different number of bits. :-) Your original message makes it sound like WE save corrupt PNG files that other tools can't read... what you're saying here is that other tools can CORRUPT our hidden data, which is an entirely different matter.

you have to encode and decode the entire PNG image all the time

I think that's just a price you pay for doing it the "cool" way... not a problem on most newer hardware.

but if you really want to store wasm bytecode in CHUNK_BINARY chunks, no storage space will be enough).

This is a much better point to me... I was wondering where all that extra data was going to go...

makes absolutely no sense: the PNG Specification is pretty clear

It might also be this is merely a carry over from the GIF days...did GIFs have data channels?

private chunks into images, these will be kept by various tools and editors

Some tools do strip these without asking, but I do agree for many cases binary data might be safer here than hidden in the main image...


I don't have a super strong opinion here. I think the existing encoding concept is kind of cool and the retro brain of mine lines the idea of "official" carts... once you start opening the carts with Photoshop and saving them, all bets are off... that's not an official cart anymore - one shouldn't expect it to work.

What kind of PNG do we save now if someone has 256kb of code or a 256kb BINARY chunk?. Are you saying it simply blows up and doesn't work. that we're already over the limit?

joshgoebel

joshgoebel commented on Dec 5, 2022

@joshgoebel
Collaborator

256 x 256 * 32 bit = 256kb total, ugh...

joshgoebel

joshgoebel commented on Dec 5, 2022

@joshgoebel
Collaborator

Your first two issues definitely sound like bugs/deficiencies we should fix, the last would really need a decision by @nesbox. But it does sound like we at least need a much larger cartridge bitmap if we're going to keep encoding data inside the graphic. So that alone may settle things.

bztsrc

bztsrc commented on Dec 5, 2022

@bztsrc
ContributorAuthor

Your first two issues definitely sound like bugs/deficiencies we should fix

I think simply looking for PNG magic instead of the file extension would do the trick.

256 x 256 * 32 bit = 256kb total, ugh...

See? But actually worse. 240 x 136 and one byte per pixel only. That's 32640 bytes, no more, hardly enough for a single CHUNK_CODE bank...

the last would really need a decision by @nesbox. But it does sound like we at least need a much larger cartridge bitmap if we're going to keep encoding data inside the graphic. So that alone may settle things.

Agreed. This is a change in png cartridge format that breaks compatibility (although it would be possible to use both solutions: if there's a caRt chunk, use that, otherwise fallback to the stegano method. On save only use the new chunk method. This way old cartridges would keep working.)

Cheers,
bzt

joshgoebel

joshgoebel commented on Dec 5, 2022

@joshgoebel
Collaborator

See? But actually worse. 240 x 136

Not sure what you mean, our cartridge PNGs are 256x256... and I thought you said 32-bit (24-bit + alpha)... so that's 256kb total (before storing any images, of course even less if we count available for encoding)... still very little though, not sure how it'd be working now for larger carts... maybe people just use .TIC and not .PNG?

otherwise fallback to the stegano method

The project is very into backwards compatibility, so that complexity probably isn't fully going away anytime soon even if we switched.

bztsrc

bztsrc commented on Dec 5, 2022

@bztsrc
ContributorAuthor

Not sure what you mean, our cartridge PNGs are 256x256...

I can only see 240 x 136 images here and here. Am I looking at the wrong place?

I thought you said 32-bit (24-bit + alpha)... so that's 256kb total

Not sure how it works exactly, but with one cart byte per carriage pixel that's 64k, and if all 3 bits used in every channel, then 2562564*3/8 = 98k. But hard to tell, the code is extremely messy. Why there is a loop and a trinary? It should be as simple as pixelchannel = (pixelchannel & ~mask) | (newbits & mask), where mask is 0b11 or 0b111. Anyway, still very little, not going to be future proof, agreed.

The project is very into backwards compatibility, so that complexity probably isn't fully going away anytime soon even if we switched.

That's okay. If new cartridges are saved with chunks instead of stegano, it would be perfectly fine with me.

Cheers,
bzt

joshgoebel

joshgoebel commented on Dec 6, 2022

@joshgoebel
Collaborator

I can only see 240 x 136 images here and here.

That's just a screen shot, not a cartridge. We had a whole thread about it: #755

There is some way to export cartridge PNGs...

bztsrc

bztsrc commented on Dec 6, 2022

@bztsrc
ContributorAuthor

There is some way to export cartridge PNGs...

Yeah, also someone really should convert all those screenshots on the website into png cartridges.

Anyway, here's a patch that adds caRt chunk support to TIC-80.

  • on save, it saves both the chunk and with steganography
  • on load, if chunk is found, it uses that, otherwise it fallbacks to steganography
diff --git a/src/ext/png.c b/src/ext/png.c
index 62a952e..4610a65 100644
--- a/src/ext/png.c
+++ b/src/ext/png.c
@@ -1,6 +1,7 @@
 // MIT License
 
 // Copyright (c) 2021 Vadim Grigoruk @nesbox // grigoruk@gmail.com
+// Copyright (c) 2022 bzt png chunk stuff
 
 // Permission is hereby granted, free of charge, to any person obtaining a copy
 // of this software and associated documentation files (the "Software"), to deal
@@ -28,6 +29,7 @@
 #include <png.h>
 #include "tic_assert.h"
 
+#define EXTRA_CHUNK "caRt"
 #define RGBA_SIZE sizeof(u32)
 
 png_buffer png_create(s32 size)
@@ -48,7 +50,7 @@ static void pngReadCallback(png_structp png, png_bytep out, png_size_t size)
     stream->pos += size;
 }
 
-png_img png_read(png_buffer buf)
+png_img png_read(png_buffer buf, png_buffer *cart)
 {
     png_img res = { 0 };
 
@@ -60,6 +62,7 @@ png_img png_read(png_buffer buf)
         PngStream stream = { .buffer = buf};
 
         png_set_read_fn(png, &stream, pngReadCallback);
+        png_set_keep_unknown_chunks(png, 2, NULL, 0);
         png_read_info(png, info);
 
         res.width = png_get_image_width(png, info);
@@ -100,6 +103,25 @@ png_img png_read(png_buffer buf)
 
         png_read_image(png, rows);
 
+        // Read in cartridge data from chunk if possible
+        if (cart)
+        {
+            png_unknown_chunkp unknowns = NULL;
+            int num_unknowns = png_get_unknown_chunks(ptr, info, &unknowns);
+
+            for(s32 i = 0; i < num_unknowns; i++)
+                if (!memcmp(unknowns[i].name, EXTRA_CHUNK, 5))
+                {
+                    cart->size = unknowns[i].size;
+                    cart->data = (u8*)malloc(cart->size);
+                    if (cart->data)
+                        memcpy(cart->data, unknowns[i].data, cart->size);
+                    else
+                        cart->size = 0;
+                    break;
+                }
+        }
+
         free(rows);
 
         png_destroy_read_struct(&png, &info, NULL);
@@ -121,13 +143,15 @@ static void pngWriteCallback(png_structp png, png_bytep data, png_size_t size)
 
 static void pngFlushCallback(png_structp png) {}
 
-png_buffer png_write(png_img src)
+png_buffer png_write(png_img src, png_buffer *cart)
 {
+    png_unknown_chunk unknowns = { 0 };
     png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
     png_infop info = png_create_info_struct(png);
 
     PngStream stream = {0};
     png_set_write_fn(png, &stream, pngWriteCallback, pngFlushCallback);
+    png_set_keep_unknown_chunks(png, 2, NULL, 0);
 
     // Output is 8bit depth, RGBA format.
     png_set_IHDR(
@@ -141,6 +165,15 @@ png_buffer png_write(png_img src)
         PNG_FILTER_TYPE_DEFAULT
     );
 
+    // Save cartridge data in a chunk too. This supports bigger cartridges than steganography
+    if (cart && cart->data && cart->size > 0){
+        memcpy(&unknowns.name, EXTRA_CHUNK, 5);
+        unknowns.data = cart->data;
+        unknowns.size = cart->size;
+        unknowns.location = PNG_AFTER_IDAT;
+        png_set_unknown_chunks(png, info, &unknowns, 1);
+    }
+
     png_write_info(png, info);
 
     png_bytep* rows = malloc(sizeof(png_bytep) * src.height);
@@ -188,8 +221,8 @@ static inline s32 ceildiv(s32 a, s32 b)
 }
 
 png_buffer png_encode(png_buffer cover, png_buffer cart)
-{    
-    png_img png = png_read(cover);
+{
+    png_img png = png_read(cover, NULL);
 
     const s32 cartBits = cart.size * BITS_IN_BYTE;
     const s32 coverSize = png.width * png.height * RGBA_SIZE - HEADER_SIZE;
@@ -206,7 +239,7 @@ png_buffer png_encode(png_buffer cover, png_buffer cart)
     for (s32 i = end; i < coverSize; i++)
         bitcpy(dst, i << 3, (const u8[]){rand()}, 0, header.bits);
 
-    png_buffer out = png_write(png);
+    png_buffer out = png_write(png, &cart);
 
     free(png.data);
 
@@ -215,8 +248,18 @@ png_buffer png_encode(png_buffer cover, png_buffer cart)
 
 png_buffer png_decode(png_buffer cover)
 {
-    png_img png = png_read(cover);
+    png_buffer cart = { 0 };
+    png_img png = png_read(cover, &cart);
+
+    // if we have a data from a png chunk, use that
+    if (cart.data && cart.size > 0)
+    {
+        if (png.data)
+            free(png.data);
+        return cart;
+    }
 
+    // otherwise fallback to steganography
     if (png.data)
     {
         Header header;
diff --git a/src/ext/png.h b/src/ext/png.h
index 930ab62..5c8b517 100644
--- a/src/ext/png.h
+++ b/src/ext/png.h
@@ -60,8 +60,8 @@ typedef struct
 
 png_buffer png_create(s32 size);
 
-png_img png_read(png_buffer buf);
-png_buffer png_write(png_img src);
+png_img png_read(png_buffer buf, png_buffer *cart);
+png_buffer png_write(png_img src, png_buffer *cart);
 
 png_buffer png_encode(png_buffer cover, png_buffer cart);
 png_buffer png_decode(png_buffer cover);
diff --git a/src/studio/screens/console.c b/src/studio/screens/console.c
index 58d3522..59f47fd 100644
--- a/src/studio/screens/console.c
+++ b/src/studio/screens/console.c
@@ -1747,7 +1747,7 @@ static void onImportTilesBase(Console* console, const char* name, const void* bu
     png_buffer png = {(u8*)buffer, size};
     bool error = true;
 
-    png_img img = png_read(png);
+    png_img img = png_read(png, NULL);
 
     if(img.data) SCOPE(free(img.data))
     {
@@ -1829,7 +1829,7 @@ static void onImport_screen(Console* console, const char* name, const void* buff
     png_buffer png = {(u8*)buffer, size};
     bool error = true;
 
-    png_img img = png_read(png);
+    png_img img = png_read(png, NULL);
 
     if(img.data) SCOPE(free(img.data))
     {
@@ -1948,7 +1948,7 @@ static void exportSprites(Console* console, const char* filename, tic_tile* base
         for(s32 i = 0; i < TIC_SPRITESHEET_SIZE * TIC_SPRITESHEET_SIZE; i++)
             img.values[i] = tic_rgba(&pal->colors[getSpritePixel(base, i % TIC_SPRITESHEET_SIZE, i / TIC_SPRITESHEET_SIZE)]);
 
-        png_buffer png = png_write(img);
+        png_buffer png = png_write(img, NULL);
 
         SCOPE(free(png.data))
         {
@@ -2258,7 +2258,7 @@ static void onExport_mapimg(Console* console, const char* param, const char* pat
                 }
         }
 
-        png_buffer png = png_write(img);
+        png_buffer png = png_write(img, NULL);
 
         SCOPE(free(png.data))
         {
@@ -2306,7 +2306,7 @@ static void onExport_screen(Console* console, const char* param, const char* nam
         for(s32 i = 0; i < TIC80_WIDTH * TIC80_HEIGHT; i++)
             img.values[i] = tic_rgba(&pal->colors[tic_tool_peek4(bank->screen.data, i)]);
 
-        png_buffer png = png_write(img);
+        png_buffer png = png_write(img, NULL);
 
         SCOPE(free(png.data))
         {
@@ -2404,7 +2404,7 @@ static CartSaveResult saveCartName(Console* console, const char* name)
                         };
 
                         png_buffer template = {(u8*)Cartridge, sizeof Cartridge};
-                        png_img img = png_read(template);
+                        png_img img = png_read(template, NULL);
 
                         // draw screen
                         {
@@ -2452,7 +2452,7 @@ static CartSaveResult saveCartName(Console* console, const char* name)
                                     ptr[CoverWidth * y + x] = tic_rgba(pal + tic_tool_peek4(screen, y * TIC80_WIDTH + x));
                         }
 
-                        cover = png_write(img);
+                        cover = png_write(img, NULL);
 
                         free(img.data);
                     }

Unrelated: I think src/studio/screens/console.c should be refactored a bit, on save it encodes / decodes the png multiple times without any good reason.

Cheers,
bzt

bztsrc

bztsrc commented on Dec 6, 2022

@bztsrc
ContributorAuthor

I think simply looking for PNG magic instead of the file extension would do the trick.

Concerning that, here's a patch (git diff fails here because of the indentation, actually the change in onLoadCommandConfirmed is straightforward and a lot smaller than the diff suggests):

diff --git a/src/studio/screens/console.c b/src/studio/screens/console.c
index 58d3522..40feabf 100644
--- a/src/studio/screens/console.c
+++ b/src/studio/screens/console.c
@@ -908,64 +908,52 @@ static void onLoadCommandConfirmed(Console* console)
                     onCartLoaded(console, name, section);
                 }
             }
-            else if(tic_tool_has_ext(param, PngExt) && tic_fs_exists(console->fs, param))
+            else if(tic_fs_exists(console->fs, param))
             {
                 png_buffer buffer;
                 buffer.data = tic_fs_load(console->fs, param, &buffer.size);
 
-                SCOPE(free(buffer.data))
+                if(buffer.data)
                 {
-                    tic_cartridge* cart = loadPngCart(buffer);
-
-                    if(cart) SCOPE(free(cart))
+                    if(!memcmp(buffer.data, "\x89PNG", 4))
                     {
-                        loadCartSection(console, cart, section);
-                        onCartLoaded(console, param, section);
-                    }
-                    else printError(console, "\npng cart loading error");
-                }
-            }
-            else
-            {
-                const char* name = param;
+                        tic_cartridge* cart = loadPngCart(buffer);
 
-#if defined(TIC80_PRO)
-                if(tic_project_ext(name))
-                {
-                    void* data = tic_fs_load(console->fs, name, &size);
-
-                    if(data) SCOPE(free(data))
+                        if(cart) SCOPE(free(cart))
+                        {
+                            loadCartSection(console, cart, section);
+                            onCartLoaded(console, param, section);
+                        }
+                        else printError(console, "\npng cart loading error");
+                    }
+                    else if(tic_project_ext(param))
                     {
+#if defined(TIC80_PRO)
                         tic_cartridge* cart = newCart();
 
                         SCOPE(free(cart))
                         {
-                            tic_project_load(name, data, size, cart);
+                            tic_project_load(param, buffer.data, buffer.size, cart);
                             loadCartSection(console, cart, section);
-                            onCartLoaded(console, name, section);
+                            onCartLoaded(console, param, section);
                         }
-                    }
-                    else printError(console, "\nproject loading error");
-
-                }
-                else printError(console, "\nfile not found");
 #else
-                if(tic_project_ext(name)) {
-                    printError(console, "\nproject loading error");
-                    printFront(console, "\nThis version only supports binary .png or .tic cartridges.");
-                    printLine(console);
-                    printFront(console, "\nTIC-80 ");
-                    consolePrint(console,"PRO",tic_color_light_blue);
-                    printFront(console, " is needed for text files.");
-                    printLine(console);
-                    printFront(console, "\nLearn more:\n");
-                    printLink(console, "https://tic80.com/pro");
-                } else {
-                    printError(console, "\ncart loading error");
-                }
-                
+                        printError(console, "\nproject loading error");
+                        printFront(console, "\nThis version only supports binary .png or .tic cartridges.");
+                        printLine(console);
+                        printFront(console, "\nTIC-80 ");
+                        consolePrint(console,"PRO",tic_color_light_blue);
+                        printFront(console, " is needed for text files.");
+                        printLine(console);
+                        printFront(console, "\nLearn more:\n");
+                        printLink(console, "https://tic80.com/pro");
 #endif
+                    }
+                    free(buffer.data);
+                }
+                else printError(console, "\ncart loading error");
             }
+            else printError(console, "\nfile not found");
         }
     }
     else
@@ -4189,7 +4177,7 @@ static bool cmdLoadCart(Console* console, const char* path)
         setCartName(console, cartName, path);
         tic_mem* tic = console->tic;
 
-        if(tic_tool_has_ext(cartName, PngExt))
+        if(!memcmp(data, "\x89PNG", 4))
         {
             tic_cartridge* cart = loadPngCart((png_buffer){data, size});
 
diff --git a/src/studio/screens/surf.c b/src/studio/screens/surf.c
index b45cab3..bfc3983 100644
--- a/src/studio/screens/surf.c
+++ b/src/studio/screens/surf.c
@@ -398,7 +398,7 @@ static void loadCover(Surf* surf)
             if(cart)
             {
 
-                if(tic_tool_has_ext(item->name, PngExt))
+                if(!memcmp(data, "\89PNG", 4))
                 {
                     tic_cartridge* pngcart = loadPngCart((png_buffer){data, size});
 
@@ -582,13 +582,12 @@ static void onPlayCart(void* data)
 static void loadCart(Surf* surf)
 {
     SurfItem* item = getMenuItem(surf);
+    s32 size = 0;
+    void* data = tic_fs_load(surf->fs, item->name, &size);
 
-    if(tic_tool_has_ext(item->name, PngExt))
+    if(data)
     {
-        s32 size = 0;
-        void* data = tic_fs_load(surf->fs, item->name, &size);
-
-        if(data)
+        if(!memcmp(data, "\89PNG", 4))
         {
             tic_cartridge* cart = loadPngCart((png_buffer){data, size});
 
@@ -597,6 +596,9 @@ static void loadCart(Surf* surf)
                 surf->anim.movie = resetMovie(&surf->anim.play);
                 free(cart);
             }
+        } else {
+            free(data);
+            surf->anim.movie = resetMovie(&surf->anim.play);
         }
     }
     else surf->anim.movie = resetMovie(&surf->anim.play);

Cheers,
bzt

bztsrc

bztsrc commented on Dec 7, 2022

@bztsrc
ContributorAuthor

@joshgoebel

There is some way to export cartridge PNGs...

I've added a neat feature to my converter. Besides of converting PICO-8 cartridges, it can now convert between .tic and .tic.png back and forth. It is blasing fast, just try feeding any .tic to the web based converter. But when used as a CLI tool, then you can mass-convert any number of .tic files into PNGs in batch.

Here are some examples from the website that I've converted. Should be 100% pixel correct to the version saved by the TIC-80 console:
searching_for_pixel tic
soda_adventure_v1_0 tic
van_helsing tic
the_myths_and_legends_of_bone_knight tic
20_seconds_on_the_moon tic
pong tic
These are all containing both caRt chunk and steganography encoded data, so fully backward and forward compatible.

Cheers,
bzt

bztsrc

bztsrc commented on Dec 8, 2022

@bztsrc
ContributorAuthor

I've added a PR for this. git push doesn't work for me on github, so I had to use the webeditor, pity me!

Anyway, there's no reason to keep this ticket open, but @nesbox I'd like to hear your opinion. I've created a PR and a tool to mass convert cartridges, what else is needed to make this happen?

Cheers,
bzt

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @joshgoebel@bztsrc

        Issue actions