Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add write support for 16-bit PNG #763

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 77 additions & 18 deletions stb_image_write.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,22 @@

USAGE:

There are five functions, one for each image file format:
There are six functions, one for each image file format:

int stbi_write_png(char const *filename, int w, int h, int comp, const void *data, int stride_in_bytes);
int stbi_write_png_16(char const *filename, int w, int h, int comp, const void *data, int stride_in_bytes);
int stbi_write_bmp(char const *filename, int w, int h, int comp, const void *data);
int stbi_write_tga(char const *filename, int w, int h, int comp, const void *data);
int stbi_write_jpg(char const *filename, int w, int h, int comp, const void *data, int quality);
int stbi_write_hdr(char const *filename, int w, int h, int comp, const float *data);

void stbi_flip_vertically_on_write(int flag); // flag is non-zero to flip data vertically

There are also five equivalent functions that use an arbitrary write function. You are
There are also six equivalent functions that use an arbitrary write function. You are
expected to open/close your file-equivalent before and after calling these:

int stbi_write_png_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data, int stride_in_bytes);
int stbi_write_png_16_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data, int stride_in_bytes);
int stbi_write_bmp_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data);
int stbi_write_tga_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data);
int stbi_write_hdr_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const float *data);
Expand Down Expand Up @@ -173,6 +175,7 @@ extern int stbi_write_force_png_filter;

#ifndef STBI_WRITE_NO_STDIO
STBIWDEF int stbi_write_png(char const *filename, int w, int h, int comp, const void *data, int stride_in_bytes);
STBIWDEF int stbi_write_png_16(char const *filename, int w, int h, int comp, const void *data, int stride_in_bytes);
STBIWDEF int stbi_write_bmp(char const *filename, int w, int h, int comp, const void *data);
STBIWDEF int stbi_write_tga(char const *filename, int w, int h, int comp, const void *data);
STBIWDEF int stbi_write_hdr(char const *filename, int w, int h, int comp, const float *data);
Expand All @@ -186,6 +189,7 @@ STBIWDEF int stbiw_convert_wchar_to_utf8(char *buffer, size_t bufferlen, const w
typedef void stbi_write_func(void *context, void *data, int size);

STBIWDEF int stbi_write_png_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data, int stride_in_bytes);
STBIWDEF int stbi_write_png_16_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data, int stride_in_bytes);
STBIWDEF int stbi_write_bmp_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data);
STBIWDEF int stbi_write_tga_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data);
STBIWDEF int stbi_write_hdr_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const float *data);
Expand Down Expand Up @@ -340,8 +344,11 @@ static void stbi__end_write_file(stbi__write_context *s)

#endif // !STBI_WRITE_NO_STDIO

typedef unsigned short stbiw_uint16;
typedef int stb_image_write_test_uint16[sizeof(stbiw_uint16)==2 ? 1 : -1];

typedef unsigned int stbiw_uint32;
typedef int stb_image_write_test[sizeof(stbiw_uint32)==4 ? 1 : -1];
typedef int stb_image_write_test_uint32[sizeof(stbiw_uint32)==4 ? 1 : -1];

static void stbiw__writefv(stbi__write_context *s, const char *fmt, va_list v)
{
Expand Down Expand Up @@ -1035,7 +1042,7 @@ static unsigned char stbiw__paeth(int a, int b, int c)
}

// @OPTIMIZE: provide an option that always forces left-predict or paeth predict
static void stbiw__encode_png_line(unsigned char *pixels, int stride_bytes, int width, int height, int y, int n, int filter_type, signed char *line_buffer)
static void stbiw__encode_png_line(unsigned char *pixels, int stride_bytes, int width, int height, int y, int n, int pixel_bytes, int filter_type, signed char *line_buffer)
{
static int mapping[] = { 0,1,2,3,4 };
static int firstmap[] = { 0,1,0,5,6 };
Expand All @@ -1050,8 +1057,8 @@ static void stbiw__encode_png_line(unsigned char *pixels, int stride_bytes, int
return;
}

// first loop isn't optimized since it's just one pixel
for (i = 0; i < n; ++i) {
// first loop isn't optimized since it's just one pixel
for (i = 0; i < pixel_bytes; ++i) {
switch (type) {
case 1: line_buffer[i] = z[i]; break;
case 2: line_buffer[i] = z[i] - z[i-signed_stride]; break;
Expand All @@ -1062,23 +1069,32 @@ static void stbiw__encode_png_line(unsigned char *pixels, int stride_bytes, int
}
}
switch (type) {
case 1: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - z[i-n]; break;
case 2: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - z[i-signed_stride]; break;
case 3: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - ((z[i-n] + z[i-signed_stride])>>1); break;
case 4: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - stbiw__paeth(z[i-n], z[i-signed_stride], z[i-signed_stride-n]); break;
case 5: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - (z[i-n]>>1); break;
case 6: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - stbiw__paeth(z[i-n], 0,0); break;
case 1: for (i=pixel_bytes; i < width*n; ++i) line_buffer[i] = z[i] - z[i-pixel_bytes]; break;
case 2: for (i=pixel_bytes; i < width*n; ++i) line_buffer[i] = z[i] - z[i-signed_stride]; break;
case 3: for (i=pixel_bytes; i < width*n; ++i) line_buffer[i] = z[i] - ((z[i-pixel_bytes] + z[i-signed_stride])>>1); break;
case 4: for (i=pixel_bytes; i < width*n; ++i) line_buffer[i] = z[i] - stbiw__paeth(z[i-pixel_bytes], z[i-signed_stride], z[i-signed_stride-pixel_bytes]); break;
case 5: for (i=pixel_bytes; i < width*n; ++i) line_buffer[i] = z[i] - (z[i-pixel_bytes>>1]); break;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a bug here, the code went from z[...]>>1 to z[...>>1 right before 'break']. I'm trying to fix this by changing the code back to using 'n' so that less code will be changed. I.e. change passed in parameter n to "channel_count", then say 'int n = pixel_bytes', and then most of the code is unchanged.

While doing that, though, I realized that the current code still loops i up to width * n, not width * pixel_bytes. That can't possibly be right, can it? But if it's wrong, then half of the image would have failed to encode, which I think you would have noticed.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@WearerOfShoes , were you able to take a look at this comment by Sean? I'm very interested in being able to save into any type deeper than 8 bits...

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@WearerOfShoes, I'm very interested in this, too...

Copy link

@jonfryd jonfryd Jul 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nothings: So it took me a while to figure out, but apart from the right-shift bug you pointed out, the code is actually correct with regards to width * n vs width * pixel_bytes in the for-loops because width has already been multiplied by comp_bytes (i.e. 1 for 8-bit PNGs, 2 for 16-bit PNGs) in stbiw__write_png_to_mem_core():

   // all calculations in PNG are based on bytes in 16-bit mode,
   // so just treated it as a twice-as-wide 8-bit image works in most cases
   // exceptions are patches up by sending along comp_bytes
   x *= comp_bytes;

I also ran the tests in image_write_test and all generated PNGs are seemingly correct.

(BTW, I can make a new PR based on a fork of @WearerOfShoes work so we can finish this if you like)

case 6: for (i=pixel_bytes; i < width*n; ++i) line_buffer[i] = z[i] - stbiw__paeth(z[i-pixel_bytes], 0,0); break;
}
}

STBIWDEF unsigned char *stbi_write_png_to_mem(const unsigned char *pixels, int stride_bytes, int x, int y, int n, int *out_len)
static unsigned char *stbiw__write_png_to_mem_core(const unsigned char *pixels, int stride_bytes, int x, int y, int n, int comp_bytes, int *out_len)
{
int force_filter = stbi_write_force_png_filter;
int ctype[5] = { -1, 0, 4, 2, 6 };
unsigned char sig[8] = { 137,80,78,71,13,10,26,10 };
unsigned char *out,*o, *filt, *zlib;
signed char *line_buffer;
int j,zlen;
int k;
int pixel_bytes;

// all calculations in PNG are based on bytes in 16-bit mode,
// so just treated it as a twice-as-wide 8-bit image works in most cases
// exceptions are patches up by sending along comp_bytes
x *= comp_bytes;

pixel_bytes = n * comp_bytes;

if (stride_bytes == 0)
stride_bytes = x * n;
Expand All @@ -1093,11 +1109,11 @@ STBIWDEF unsigned char *stbi_write_png_to_mem(const unsigned char *pixels, int s
int filter_type;
if (force_filter > -1) {
filter_type = force_filter;
stbiw__encode_png_line((unsigned char*)(pixels), stride_bytes, x, y, j, n, force_filter, line_buffer);
stbiw__encode_png_line((unsigned char*)(pixels), stride_bytes, x, y, j, n, pixel_bytes, force_filter, line_buffer);
} else { // Estimate the best filter by running through all of them:
int best_filter = 0, best_filter_val = 0x7fffffff, est, i;
for (filter_type = 0; filter_type < 5; filter_type++) {
stbiw__encode_png_line((unsigned char*)(pixels), stride_bytes, x, y, j, n, filter_type, line_buffer);
stbiw__encode_png_line((unsigned char*)(pixels), stride_bytes, x, y, j, n, pixel_bytes, filter_type, line_buffer);

// Estimate the entropy of the line using this filter; the less, the better.
est = 0;
Expand All @@ -1110,10 +1126,19 @@ STBIWDEF unsigned char *stbi_write_png_to_mem(const unsigned char *pixels, int s
}
}
if (filter_type != best_filter) { // If the last iteration already got us the best filter, don't redo it
stbiw__encode_png_line((unsigned char*)(pixels), stride_bytes, x, y, j, n, best_filter, line_buffer);
stbiw__encode_png_line((unsigned char*)(pixels), stride_bytes, x, y, j, n, pixel_bytes, best_filter, line_buffer);
filter_type = best_filter;
}
}
if (comp_bytes == 2) {
// flip endian of 16-bit words in line buffer on LE machines (this is effectively a noop on BE)
unsigned char *line_buf_uint8 = (unsigned char *)line_buffer;
stbiw_uint16 *line_buf_uint16 = (stbiw_uint16 *)line_buffer;
for (k = 0; k < x*n/2; k++) {
stbiw_uint16 swapped = (((stbiw_uint16)line_buf_uint8[k*2]) << 8) | ((stbiw_uint16)line_buf_uint8[k*2+1]);
line_buf_uint16[k] = swapped;
}
}
// when we get here, filter_type contains the filter type, and line_buffer contains the data
filt[j*(x*n+1)] = (unsigned char) filter_type;
STBIW_MEMMOVE(filt+j*(x*n+1)+1, line_buffer, x*n);
Expand All @@ -1132,9 +1157,9 @@ STBIWDEF unsigned char *stbi_write_png_to_mem(const unsigned char *pixels, int s
STBIW_MEMMOVE(o,sig,8); o+= 8;
stbiw__wp32(o, 13); // header length
stbiw__wptag(o, "IHDR");
stbiw__wp32(o, x);
stbiw__wp32(o, x/comp_bytes);
stbiw__wp32(o, y);
*o++ = 8;
*o++ = comp_bytes == 2 ? 16 : 8;
*o++ = STBIW_UCHAR(ctype[n]);
*o++ = 0;
*o++ = 0;
Expand All @@ -1157,6 +1182,17 @@ STBIWDEF unsigned char *stbi_write_png_to_mem(const unsigned char *pixels, int s
return out;
}

STBIWDEF unsigned char *stbi_write_png_to_mem(const unsigned char *pixels, int stride_bytes, int x, int y, int n, int *out_len)
{
return stbiw__write_png_to_mem_core(pixels, stride_bytes, x, y, n, 1, out_len);
}

STBIWDEF unsigned char *stbi_write_png_16_to_mem(const unsigned char *pixels, int stride_bytes, int x, int y, int n, int *out_len)
{
return stbiw__write_png_to_mem_core(pixels, stride_bytes, x, y, n, 2, out_len);
}


#ifndef STBI_WRITE_NO_STDIO
STBIWDEF int stbi_write_png(char const *filename, int x, int y, int comp, const void *data, int stride_bytes)
{
Expand All @@ -1172,6 +1208,20 @@ STBIWDEF int stbi_write_png(char const *filename, int x, int y, int comp, const
STBIW_FREE(png);
return 1;
}
STBIWDEF int stbi_write_png_16(char const *filename, int x, int y, int comp, const void *data, int stride_bytes)
{
FILE *f;
int len;
unsigned char *png = stbi_write_png_16_to_mem((const unsigned char *) data, stride_bytes, x, y, comp, &len);
if (png == NULL) return 0;

f = stbiw__fopen(filename, "wb");
if (!f) { STBIW_FREE(png); return 0; }
fwrite(png, 1, len, f);
fclose(f);
STBIW_FREE(png);
return 1;
}
#endif

STBIWDEF int stbi_write_png_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data, int stride_bytes)
Expand All @@ -1183,6 +1233,15 @@ STBIWDEF int stbi_write_png_to_func(stbi_write_func *func, void *context, int x,
STBIW_FREE(png);
return 1;
}
STBIWDEF int stbi_write_png_16_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data, int stride_bytes)
{
int len;
unsigned char *png = stbi_write_png_16_to_mem((const unsigned char *) data, stride_bytes, x, y, comp, &len);
if (png == NULL) return 0;
func(context, png, len);
STBIW_FREE(png);
return 1;
}


/* ***************************************************************************
Expand Down
53 changes: 53 additions & 0 deletions tests/image_test.c
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,19 @@ void dummy_write(void *context, void *data, int len)
memcpy(dummy, data, len);
}

typedef struct {
unsigned char data[64*1024];
int len;
} data_t;

void write_data(void *context, void *data, int len)
{
data_t *d = (data_t *)context;
assert(d->len + len <= sizeof(d->data));
memcpy(&d->data[d->len], data, len);
d->len += len;
}

extern void image_write_test(void);

int main(int argc, char **argv)
Expand Down Expand Up @@ -169,5 +182,45 @@ int main(int argc, char **argv)
}
printf("Tested %d files.\n", i);
}

// test 16-bit png
{
int i;
char **files = stb_readdir_files("pngsuite/16bit");
for (i=0; i < stb_arr_len(files); ++i) {
int n;
int w2,h2,n2;
char **failed = NULL;
stbi_us *data;
stbi_us *data2;
data_t compressed_png;
compressed_png.len = 0;
printf(".");
//printf("%s\n", files[i]);
data = stbi_load_16(files[i], &w, &h, &n, 0); if (data) ; else stb_arr_push(failed, "&n");
if (data) {
stbi_write_png_16_to_func(write_data,&compressed_png, w, h, n, data, w*n*2);
data2 = stbi_load_16_from_memory(compressed_png.data, compressed_png.len, &w2, &h2, &n2, 0);
if (data2) ; else stb_arr_push(failed, "Couldn't load");

if (memcmp(data, data2, w*h*n*2) != 0) printf("FAILED: %s data differs\n", files[i]);
if (w != w2) printf("FAILED: %s w differs: %d vs %d\n", files[i], w, w2);
if (h != h2) printf("FAILED: %s h differs: %d vs %d\n", files[i], h, h2);
if (n != n2) printf("FAILED: %s n differs: %d vs %d\n", files[i], n, n2);

free(data2);
}
if (failed) {
int j;
printf("FAILED: ");
for (j=0; j < stb_arr_len(failed); ++j)
printf("%s ", failed[j]);
printf(" -- %s\n", files[i]);
}
free(data);
}
printf("Tested %d 16-bit PNG files.\n", i);
}

return 0;
}
7 changes: 7 additions & 0 deletions tests/image_write_test.c
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ void image_write_test(void)
// make a RGB version of the template image
// use red on blue to detect R<->B swaps
unsigned char img6x5_rgb[6*5*3];
unsigned short img6x5_rgb_16[6*5*3];
float img6x5_rgbf[6*5*3];
int i;

Expand All @@ -31,12 +32,17 @@ void image_write_test(void)
img6x5_rgb[i*3 + 1] = 0;
img6x5_rgb[i*3 + 2] = on ? 0 : 255;

img6x5_rgb_16[i*3 + 0] = on ? 65535 : 0;
img6x5_rgb_16[i*3 + 1] = 0;
img6x5_rgb_16[i*3 + 2] = on ? 0 : 65535;

img6x5_rgbf[i*3 + 0] = on ? 1.0f : 0.0f;
img6x5_rgbf[i*3 + 1] = 0.0f;
img6x5_rgbf[i*3 + 2] = on ? 0.0f : 1.0f;
}

stbi_write_png("output/wr6x5_regular.png", 6, 5, 3, img6x5_rgb, 6*3);
stbi_write_png_16("output/wr6x5_regular_16.png", 6, 5, 3, img6x5_rgb_16, 2*6*3);
stbi_write_bmp("output/wr6x5_regular.bmp", 6, 5, 3, img6x5_rgb);
stbi_write_tga("output/wr6x5_regular.tga", 6, 5, 3, img6x5_rgb);
stbi_write_jpg("output/wr6x5_regular.jpg", 6, 5, 3, img6x5_rgb, 95);
Expand All @@ -45,6 +51,7 @@ void image_write_test(void)
stbi_flip_vertically_on_write(1);

stbi_write_png("output/wr6x5_flip.png", 6, 5, 3, img6x5_rgb, 6*3);
stbi_write_png_16("output/wr6x5_flip_16.png", 6, 5, 3, img6x5_rgb_16, 2*6*3);
stbi_write_bmp("output/wr6x5_flip.bmp", 6, 5, 3, img6x5_rgb);
stbi_write_tga("output/wr6x5_flip.tga", 6, 5, 3, img6x5_rgb);
stbi_write_jpg("output/wr6x5_flip.jpg", 6, 5, 3, img6x5_rgb, 95);
Expand Down