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

stb_image_write: support for 16 bit png #1726

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

lyd405121
Copy link

Background

  • stb is my favorite picture saver and loader
  • But recently I found a small bug
  • stb_image_writer.h can not export 16 bit png with right image pitch(stride)
  • So I decided to do this fix

Why is this neccessary

  • In the area of computer graphics, single channel png is used for roughness , metal and height map
  • Especially for high precision height map is needed
  • And sometimes we want to produce an accurate depth map to training robot, 16 bit single channel png is also needed
1.mp4

Bug reproduce

  • I wrote a small test to demenstration it which is known as julia set pattern
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h" //the origin stb code
//#include "stb_image_write_my.h" // my fix

#define WID 1280
#define HGT 720

//https://www.shadertoy.com/view/wtBBWd
template<typename T>
void DrawJuliaSet(T* pImage, int nMax){
    float cx = -0.8f, cy = 0.2f;
    for (int i = 0; i < WID; i++) {
        for (int j = 0; j < HGT; j++) {
            float zx = float(i) / float(HGT) - 1.0f, zy = float(j) / float(HGT) - 0.5f;
            pImage[(i + j * WID)] = 0;
            while (sqrtf(zx * zx + zy * zy) < float(HGT) && pImage[(i + j * WID)] < nMax){
                float temp = zx * zx - zy * zy + cx;
                zy = 2.0f * zx * zy + cy;
                zx = temp;
                pImage[(i + j * WID)]++;
            }
        }
    }
}

int main()
{
    unsigned short* us_pixel= new unsigned short[WID * HGT];
    DrawJuliaSet(us_pixel, 65535);
    stbi_write_png("16bit.png", WID, HGT, 1, us_pixel, WID*sizeof(unsigned short));

    unsigned char* uc_pixel = new unsigned char[WID * HGT];
    DrawJuliaSet(uc_pixel, 255);
    stbi_write_png("8bit.png",  WID, HGT, 1,  uc_pixel, WID*sizeof(unsigned char));
    return 0;
}
  • When I use the origin code of stb, this is the 8bit result, works perfectly!

8bit

  • But when it turns to 16 bit, bug bites!
  • There is empty verticle line pass through the image, and whole picture is sterched

16bit

  • So I dive into the stb origin code and found two mistake
  • ① the png head for bit count is always 8
   STBIW_MEMMOVE(o,sig,8); o+= 8;
   stbiw__wp32(o, 13); // header length
   stbiw__wptag(o, "IHDR");
   stbiw__wp32(o, x);
   stbiw__wp32(o, y);
   *o++ = 8; //here is wrong
   *o++ = STBIW_UCHAR(ctype[n]);
   *o++ = 0;
   *o++ = 0;
   *o++ = 0;
   stbiw__wpcrc(&o,13);
  • ② using picture width*channel count instead of image stride
   filt = (unsigned char *) STBIW_MALLOC((x*n+1) * y); if (!filt) return 0; //better use stride_bytes instead of x*n
   line_buffer = (signed char *) STBIW_MALLOC(x * n); if (!line_buffer) { STBIW_FREE(filt); return 0; }
  • When I fix all this, I test again
  • 8 bit Julia set, is still perfect as before
  • And the 16 bit picture look exactly the same

16bit-my

More test

  • I also test 3 channel png with 16 bit
#define STB_IMAGE_WRITE_IMPLEMENTATION
//#include "stb_image_write.h"
#include "stb_image_write_my.h"

#define WID 256
#define HGT 256

template<typename T>
void DrawColor(T* pImage, int nMax) {
    for (int i = 0; i < WID; i++) {
        for (int j = 0; j < HGT; j++) {
            pImage[(i + j * WID) * 3 + 0] = T(float(i) / float(WID - 1) * float(nMax));
            pImage[(i + j * WID) * 3 + 1] = T(float(j) / float(HGT - 1) * float(nMax));
            pImage[(i + j * WID) * 3 + 2] = 0;
        }
    }
}

int main()
{
    unsigned short* us_pixel = new unsigned short[WID * HGT * 3];
    DrawColor(us_pixel, 65535);
    stbi_write_png("16bit-n3.png", WID, HGT, 3, us_pixel, WID * sizeof(unsigned short) * 3);

    unsigned char* uc_pixel = new unsigned char[WID * HGT * 3];
    DrawColor(uc_pixel, 255);
    stbi_write_png("8bit-n3.png", WID, HGT, 3, uc_pixel, WID * sizeof(unsigned char) * 3);
    return 0;
}
  • The 8-bit test produce the image with origin code:

8bit-n3

  • The 16-bit test produce the image below:

16bit-n3

  • The 8-bit test produce the image with my fix for stb:

8bit-n3-my

  • The 16-bit test produce the image with my fix for stb:

16bit-n3-my

For single channel 16 bit png
@lyd405121 lyd405121 changed the title [FIX] Fix for single channel 16 bit png writer [FIX] Fix for 16 bit png writer Dec 8, 2024
@lyd405121 lyd405121 changed the title [FIX] Fix for 16 bit png writer stb_image_witer: support for 16 bit png Dec 9, 2024
@lyd405121 lyd405121 changed the title stb_image_witer: support for 16 bit png stb_image_write: support for 16 bit png Dec 9, 2024
@lyd405121
Copy link
Author

What's left

  • This fix will not help the case if the picture is not a square and channel is more than one
  • For example the picture's width is 512, and the height is 256

16bit-n3my

Why not fix it

  • It needs to fix too much code in function "stbiw__encode_png_line"
  • The simplest way is to write a template function to handle with unsigned char and short
  • But I think it is only works for C++ compiler
  • So we should add stbiw__encode_png_line_16 like what's in stb_image.h do
  • And futhermore stbiw__paeth and STBIW_UCHAR should be rewritten or make a unsigned short version
  • I am too afraid to modify it , I have no confidence to make it right

This fix is still valuable

  • It will cover single channel with any width/height 16bit picture to export
  • It will cover multi-channel with width==height 16bit picture to export, and most of video games use square picture to improve cache-friendly code
  • Only few lines of code has been modified

@nothings
Copy link
Owner

nothings commented Dec 9, 2024

Releasing this wouldn't make any sense in practice because of the width=height restriction. It may be true that gamedev textures are this way, but (a) a more common use for this library is screenshots, which aren't square, and (b) more importantly it's too weird a restriction. End users won't read the documentation and notice the limitation and would report it as a bug, and we'd have to say it's not a bug, and they'd be annoyed. We either fully support 16-bit writing, or not at all.

@lyd405121
Copy link
Author

Releasing this wouldn't make any sense in practice because of the width=height restriction. It may be true that gamedev textures are this way, but (a) a more common use for this library is screenshots, which aren't square, and (b) more importantly it's too weird a restriction. End users won't read the documentation and notice the limitation and would report it as a bug, and we'd have to say it's not a bug, and they'd be annoyed. We either fully support 16-bit writing, or not at all.

  • Thanks for your reply
  • I am agree with you too, but It will take sometime
  • So if I can fix all size , channel ,bits(32bit too) of image,then it will make sense

Two solution

  • ① make a new entry function stbi_write_png_16 like stbi_load_16 in the saver of stb do
  • ② add new switch branch for 8/16/32 bit in the original function stbi_write_png,alloc unsigned char/short/int memory and postprocess as well.
  • For your opinion, which one is better?

@nothings
Copy link
Owner

nothings commented Dec 9, 2024

It should definitely be a separate function so it can be typesafe (take unsigned short *).

@lyd405121
Copy link
Author

Conclusion

  • After looking around my code,I found nothing wrong with it
  • I finally found maybe the app for displaying png not supporting 16 bit RGB picture is to blame

Prove

int main()
{
    char name[32];
    for (int i = 8; i < 1000; )
    {
        for (int j = 8; j < 1000;)
        {
            sprintf_s(name, "my_16bit_%dx%d.png", i,j);
            unsigned short* us_pixel = new unsigned short[i * j];
            DrawJuliaSet(us_pixel, 255,i,j);
            stbi_write_png(name, i, j, 1, us_pixel, i * sizeof(unsigned short));

            sprintf_s(name, "my_8bit_%dx%d.png", i, j);
            unsigned char* uc_pixel = new unsigned char[i * j];
            DrawJuliaSet(uc_pixel, 255, i, j);
            stbi_write_png(name, i, j, 1, uc_pixel, i * sizeof(unsigned char));

            sprintf_s(name, "my_16bit_n3_%dx%d.png", i, j);
            unsigned short* us_pixel3 = new unsigned short[i * j * 3];
            DrawColor(us_pixel3, 255, i, j);
            stbi_write_png(name, i, j, 3, us_pixel3, i * sizeof(unsigned short) * 3);

            sprintf_s(name, "my_8bit_n3_%dx%d.png", i, j);
            unsigned char* uc_pixel3 = new unsigned char[i * j * 3];
            DrawColor(uc_pixel3, 255, i, j);
            stbi_write_png(name, i, j, 3, uc_pixel3, i * sizeof(unsigned char) * 3);
            i = i + 15;
            j = j + 15;
        }
    }
    return 0;
}
  • So I make a huge test for all size of 16 bit RGB/Gray picture with color range in 0-255 instead of 0-65535
  • The result is all works perfectly without any change of my code
16bit-channel3.mp4
  • And I have tested many apps like gimp photoshop ifran-view
  • When eporxting images, there is no 16 bit option to choose
  • So I guess that when encounter 16-bit rgb picture, they will simply do an operation "&0xff" to get the tail of 16bit data

Why does they support

  • Maybe there is no monitor can support 65536×65536×65536 kinds of color
  • As far as I know, nowadays, 10 bit hdr monitor has most kinds of color support which is 1.07 billion

Overthrow my assumption

  • If there is a 3-channel 16bit picture which has a color range exceed 256
  • And it is support by ifran view or gimp,maybe I can explore the decode data of it

@nothings
Copy link
Owner

nothings commented Dec 9, 2024

Ok, that's good, but you can't change the API the way you have to infer the size of the pixels from the stride. Stride already has a different meaning.

The stride describes the number of bytes between successive rows of an image, sometimes called the pitch; it's not the length of a row. For example, if you have a 1024x1024 1-channel 1-byte image and you want to write out a 512x512 subregion of it, you call the writer with a pointer to the top left corner of the subregion, and you specify the stride as 1024, because that's the distance between rows of the 512x512 subimage in memory.

So you still need a separate interface for writing out 16-bit.

@nothings
Copy link
Owner

nothings commented Dec 9, 2024

Also, you can test the 16-bit output by using stb_image to read the 16-bit PNGs and verify they're the same pixels that you wrote out.

@lyd405121
Copy link
Author

Solid prove

  • I use stb to load the pitcture I export above
  • Here is my result

solid-prove

At last

  • So I should not touch the origin function as not many clients use 16 bit especially for screenshot
  • Just make a new one, is that right?

I have tested images file from 8*8 to 1280 *1280
@lyd405121
Copy link
Author

lyd405121 commented Dec 9, 2024

Seperate Function Done

  • I make a test from 8×8 to 1280×1280 size of picture
  • Looks all good
final-test.mp4

More info

  • I test 0-65535 range for 3 channel 16bit loader for stb

  • And found its low bit is swapped with high bit

  • But if I am using libpng,the value is ok
    libpng-vs-stb

  • When it turns to range 0-255,where every apps can display the 16bit 3channel picture normally

  • But stb still gets a swap

256

FYI

  • All of my test code is here
#define STB_IMAGE_WRITE_IMPLEMENTATION
#define STB_IMAGE_IMPLEMENTATION

#include "stb_image_write.h"
#include "stb_image.h"
#include "stdio.h"
#include "png.h"
#define PNG_BYTES_TO_CHECK 4     //libpng检查文件是否为png所需要BYTE数目

//https://www.shadertoy.com/view/wtBBWd
template<typename T>
void DrawJuliaSet(T* pImage, int nMax, int WID, int HGT){
    float cx = -0.8f, cy = 0.2f;
    for (int i = 0; i < WID; i++) {
        for (int j = 0; j < HGT; j++) {
            float zx = float(i) / float(HGT) - 1.0f, zy = float(j) / float(HGT) - 0.5f;
            pImage[(i + j * WID)] = 0;
            while (sqrtf(zx * zx + zy * zy) < float(HGT) && pImage[(i + j * WID)] < nMax){
                float temp = zx * zx - zy * zy + cx;
                zy = 2.0f * zx * zy + cy;
                zx = temp;
                pImage[(i + j * WID)]++;
            }
        }
    }
}

template<typename T>
void DrawColor(T* pImage, int nMax, int WID, int HGT) {
    for (int i = 0; i < HGT; i++) {
        for (int j = 0; j < WID; j++) {
            pImage[(j + i * WID) * 3 + 0] = T(float(j) / float(WID - 1) * float(nMax));
            pImage[(j + i * WID) * 3 + 1] = T(float(i) / float(HGT - 1) * float(nMax));
            pImage[(j + i * WID) * 3 + 2] = 0;
        }
    }
}

void ReadPng(const char* pFileName)
{

    //初始化各种结构
    png_structp png_ptr = { 0 };
    png_infop   info_ptr = { 0 };
    char        buf[PNG_BYTES_TO_CHECK] = { 0 };
    int         temp0 = 0;

    //创建png数据结构体
    png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, 0, 0, 0);
    //创建png文件信息结构体
    info_ptr = png_create_info_struct(png_ptr);
    setjmp(png_jmpbuf(png_ptr));

    //检测是否为png文件
    FILE* m_pFile = fopen(pFileName, "rb");
    if (NULL == m_pFile)
    {
        return;
    }

    temp0 = (int)fread(buf, 1, PNG_BYTES_TO_CHECK, m_pFile);   //读取四个字节判断是否为Png文件
    temp0 = png_sig_cmp((png_const_bytep)buf, (png_size_t)0, PNG_BYTES_TO_CHECK);
    if (temp0 != 0)
    {
        return;
    }

    //开始读文件
    rewind(m_pFile);                                              //重置文件指针
    png_init_io(png_ptr, m_pFile);                                //初始化png数据结构体
    png_read_png(png_ptr, info_ptr, PNG_TRANSFORM_EXPAND, 0);     //初始化png文件信息结构体

    //获取宽度,高度,位深,颜色类型
    int color_type;

    int nChannel = png_get_channels(png_ptr, info_ptr); //获取通道数
    color_type = png_get_color_type(png_ptr, info_ptr); //颜色类型


    int nPicWid = png_get_image_width(png_ptr, info_ptr);
    int nPicHgt = png_get_image_height(png_ptr, info_ptr);

    int nPicPitch = png_get_rowbytes(png_ptr, info_ptr);
    int nPicDepth = png_get_bit_depth(png_ptr, info_ptr) / 8;


    int nSize = static_cast<long long>(nPicPitch) * static_cast<long long>(nPicHgt);    // 计算图片的总像素点数量 

    png_bytepp row_pointers;// row_pointers里边就是rgba数据 
    row_pointers = png_get_rows(png_ptr, info_ptr);

    long long temp = 0;


    if (nChannel == 3 || color_type == PNG_COLOR_TYPE_RGB)//每个像素点占3个字节内存 
    {
        //由于png文件解析之后的数据按照RGB排列,所以这里直接赋值即可
        for (int i = 0; i < nPicHgt; i++)
        {
            for (int j = 0; j < nPicWid * 3; j = j + 3)
            {
                unsigned short* pData = (unsigned short*)(row_pointers[i] + j * nPicDepth); //存储解析后的数据
                printf("%d %d %d|", pData[0], pData[1], pData[2]);
            }
            printf("\n");
            printf("--------------next line-------------\n");
        }
    }

    //删除数据占用的内存
    png_destroy_read_struct(&png_ptr, &info_ptr, 0);
    fclose(m_pFile);
}

int main()
{
    char name[32];
    for (int i = 8; i <= 1280; )
    {
        for (int j = 8; j <= 1280;)
        {
            sprintf_s(name, "my_16bit_%dx%d.png", i,j);
            unsigned short* us_pixel = new unsigned short[i * j];
            DrawJuliaSet(us_pixel, 65535,i,j);
            stbi_write_png_16(name, i, j, 1, us_pixel, i * sizeof(unsigned short));

            sprintf_s(name, "my_8bit_%dx%d.png", i, j);
            unsigned char* uc_pixel = new unsigned char[i * j];
            DrawJuliaSet(uc_pixel, 255, i, j);
            stbi_write_png(name, i, j, 1, uc_pixel, i * sizeof(unsigned char));

            sprintf_s(name, "my_16bit_n3_%dx%d.png", i, j);
            unsigned short* us_pixel3 = new unsigned short[i * j * 3];
            DrawColor(us_pixel3, 255, i, j);
            stbi_write_png_16(name, i, j, 3, us_pixel3, i * sizeof(unsigned short) * 3);

            sprintf_s(name, "my_8bit_n3_%dx%d.png", i, j);
            unsigned char* uc_pixel3 = new unsigned char[i * j * 3];
            DrawColor(uc_pixel3, 255, i, j);
            stbi_write_png(name, i, j, 3, uc_pixel3, i * sizeof(unsigned char) * 3);
            i = i + 15;
            j = j + 15;
            printf("--------------%dX%d Done-------------\n", i,j);
        }
    }
    
    printf("****************test for stb*********************\n");
    int w, h, n;
    auto pData = stbi_load_16("my_16bit_n3_8x8.png", &w, &h, &n, 3);
    for (int i = 0; i < h; i++)
    {
        for (int j = 0; j < w; j++)
        {
            printf("%d %d %d|", pData[3 * (j + i * w) + 0], pData[3 * (j + i * w) + 1], pData[3 * (j + i * w) + 2]);
        }
        printf("\n");
        printf("--------------next line-------------\n");
    }

    printf("****************test for libpng*********************\n");
    ReadPng("my_16bit_n3_8x8.png");
    return 0;
}
  • Here is a 16bit channel3 1223x1223 png file

my_16bit_n3_1223x1223

  • You can download it from browser and preview it in any apps

files

@lyd405121
Copy link
Author

lyd405121 commented Jan 22, 2025

Final Prove

  • Recently, I have some time to check the last thing about 16 bit writer
  • And I totally figure out what's happening throughout the reader and writer in stb

Find a Competitors

The mistake in stb reader

  • But there is still a thing bites me
  • Just as you have said: Load 16 bit picture to see if the buffer is the same as before
  • The answer is NO!
    wrong
  • If I am using stb to read png, whether it is written by stb or libpng. the bit is always wrong
  • High byte has swapped by Low byte
  • But If I am using libpng to read the same png, all data is correct!

How to solve it

  • It is abvious that swapping high and low byte is the simplest way
    switch
  • Then I repeat my test
  • All the reader and writer, produce the same result
    right

Your opinion

  • My boss @nothings, Is there anything I have missed to make a wrong conclusion

@nothings
Copy link
Owner

Well, there are two problems with the conclusion that the 16-bit png stb_image reader code is wrong:

if the current code reverses the bytes in 16-bit PNGs, it is a bit surprising that nobody else has ever noticed or complained

The specification explicitly say the bytes in 16-bit samples are in big-endian order:

PNG images that are not indexed-colour images may have sample values with
a bit depth of 16. Such sample values are in network byte order (MSB first, LSB
second). PNG permits multi-sample pixels only with 8 and 16-bit samples, so
multiple samples of a single pixel are never packed into one byte.

The correct way to decode a 16-bit big-endian number is with (cur[0] << 8) | cur[1], not (cur[1] << 8) | cur[0].


So it seems like the most likely issue is that something is wrong with your testing methodology, although nothing jumps out at me. Another possibility is that something somewhere else in the stb_image pipeline is also byte-swapping the 16-bit pixels, but I'm not aware of anything that would do that. But the fix you suggest seems specifically wrong, given the specification.

@lyd405121
Copy link
Author

lyd405121 commented Jan 23, 2025

  • Absolutely I will never modified the stb_image.h, because there are some many users work with it
  • Maybe the loading code for libpng I wrote is wrong, lack of endian configure. I am sorry for making arbitrary conclusion.
  • But for the 16bit-writer which I added, I have demontrated its correctness. Same result for image viewer compare to libpng
  • Hope what I have done is not waste of your time, I just want to make contribution to this project since I have benifit from it so much without paying anything

@nothings
Copy link
Owner

No worries, I understand you're making a good faith effort here, I'm just not sure what to do with the info you're supplied about the reading problem.

@p0nce
Copy link

p0nce commented Jan 29, 2025

Well if it can help, this stb_image_write fork does implement 16-bit PNG support https://github.com/AuburnSounds/gamut/blob/main/source/gamut/codecs/stb_image_write.d

@lyd405121
Copy link
Author

Well if it can help, this stb_image_write fork does implement 16-bit PNG support https://github.com/AuburnSounds/gamut/blob/main/source/gamut/codecs/stb_image_write.d

Thanks for p0nce 🤗

  • This code let user choose endian option
  • The default writer option is big-endian,which is the seem as we do now
  • When define the marco “LittleEndian” it will swap the high and low bit
  • It maybe a good method,But the question is how the loader konw it is “LittleEndian”
  • I think it is better way to let users control the raw buffer, they actually know the right endian they want to use

More info

  • In my memory(three years ago,not clearly),the python picture loader PIL library has the same result as stb_load_16bit
  • At that time point, I used libpng write pre-reverse buffer, and my customer use PIL load it which was weird but work
  • And I use windows to write picture,while my custom works at ubuntu,maybe the windows io api is automaticlly little endian

@lyd405121
Copy link
Author

@lyd405121
Copy link
Author

Another Competitor

  • I use opencv-python to test the pictures what I have written
  • PIL failed with 16bit-channel3
import numpy as np
import cv2

#image = cv2.imread('libpng_16bit_n3_8x8.png', cv2.IMREAD_UNCHANGED)
image = cv2.imread('my_16bit_n3_8x8.png', cv2.IMREAD_UNCHANGED)

# 确保图像是16位深度
assert image.dtype == np.uint16, "图像不是16位深度"

# 输出每个像素的值
for i in range(image.shape[0]):
    for j in range(image.shape[1]):
        print(f"Pixel ({i},{j}): {image[i, j]}")

Amazing result

  • Print every pixel, I found that using opencv in python
  • The loading result is the same as stb_load_16
    pythonwithopencv
  • numpy is col-major, so the RGB was reversed as BGR
  • So, libpng is the one to be blame(or my test code generated by copilot is wrong), stb do exactly the png standard told
  • Case done I guess

@lyd405121
Copy link
Author

Final fix done

  • As libpng is not reliable, so my policy is to swap bit when mutiple channel is deteced
  • I read the png encode standard material for a bit, probably its'right
    right
  • Now using stb_load_16 read stbi_write_png_16's output will get the right data, but can not be exchanged with libpng, while single channel is fine.
  • And now stbi_write_png_16 can work with python-opencv perfectly which ai reseacher will benefit
    same

Good news

  • Now the RGB-16BIT picture written from stbi_write_png_16 can be viewed correctly
  • No more actifact as single channel png which means what I have said was wrong, viewers support 16bit color picture
  • It's a shame to waste your time for my mistake
    my_16bit_n3_863x863

Conclusion

  • Now stb_write can write 16bit picture for both single or multiple channel
  • They can be re-read correctly by stb_load_16
  • They can be viewed correctly by any viewers
  • They can be loaded correctly by python-opencv
  • Only single channel 16bit can be read correctly by libpng, multiple channel should be handled by users

@nothings
Copy link
Owner

nothings commented Feb 7, 2025

For reference: it looks like the libpng API requires the user to explicitly request PNG_TRANSFORM_SWAP_ENDIAN or call png_set_swap() when reading and writing 16-bit image data on little-endian platforms to actually get big-endian data into the file. If I'm correct, this is an absurdly stupid design. Since little-endian "won", it means most people are on little-endian machines, don't know they should do this, leave it out in both the reader and the writer, and it works to round trip for them (since both sides are wrong), but produces incorrect files (the files contain little-endian data), leaving us in a position where if you read/write with libpng and write/read with anything that follows the spec it breaks.

Our assumption that libpng was correct--which seemed pretty reasonable!--absolutely fucked us.

@lyd405121
Copy link
Author

lyd405121 commented Feb 8, 2025

Wake up

  • My lord, png_set_swap is the key
  • I am agree with you, this design is really awkward...😡
  • And more, only 16bit multiple channel should use png_set_swap,this logic is the same like my implement now

Conlusion

  • We do 8bit version picture as reference,every 16bit result looks the same as 8bit in picture viewers,in any pixel resolution
Resolution.mp4
  • Libpng can work with stb,only if you use png_set_swap correctly
    Exchange
  • Now stb wins libpng for simplicity both read and write,users need not pay attention to whether it is 16bit or multiple channel

Just in case

  • I put my final test code here,for anyone to examine
    Stb16Bit.zip
  • For googlers or maybe gpt training,I put my 16 bit libpng read/write code here,it may help.
void ReadByLIBPng(const char* pFileName)
{
    //初始化各种结构
    png_structp png_ptr = { 0 };
    png_infop   info_ptr = { 0 };
    char        buf[PNG_BYTES_TO_CHECK] = { 0 };
    int         temp0 = 0;

    //创建png数据结构体
    png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, 0, 0, 0);
    //创建png文件信息结构体
    info_ptr = png_create_info_struct(png_ptr);
    setjmp(png_jmpbuf(png_ptr));

    //检测是否为png文件
    FILE* m_pFile = fopen(pFileName, "rb");
    if (nullptr == m_pFile)
    {
        return;
    }

    temp0 = (int)fread(buf, 1, PNG_BYTES_TO_CHECK, m_pFile);   //读取四个字节判断是否为Png文件
    temp0 = png_sig_cmp((png_const_bytep)buf, (png_size_t)0, PNG_BYTES_TO_CHECK);
    if (temp0 != 0)
    {
        return;
    }

    //开始读文件
    rewind(m_pFile);                                              //重置文件指针
    png_init_io(png_ptr, m_pFile);                                //初始化png数据结构体

    png_read_info(png_ptr, info_ptr);
    if (png_get_bit_depth(png_ptr, info_ptr) == 16 && png_get_channels(png_ptr, info_ptr) > 1) {
        png_set_swap(png_ptr); // This is equivalent to using PNG_TRANSFORM_SWAP_ENDIAN
    }
    png_read_update_info(png_ptr, info_ptr);


    int color_type;
    int nChannel = png_get_channels(png_ptr, info_ptr); 
    color_type = png_get_color_type(png_ptr, info_ptr); 


    int nPicWid = png_get_image_width(png_ptr, info_ptr);
    int nPicHgt = png_get_image_height(png_ptr, info_ptr);
    int nPicDepth = png_get_bit_depth(png_ptr, info_ptr) / 8;
    png_bytep* row_pointers = (png_bytep*)malloc(sizeof(png_bytep) * png_get_image_height(png_ptr, info_ptr));
    for (size_t y = 0; y < png_get_image_height(png_ptr, info_ptr); y++) {
        row_pointers[y] = (png_byte*)malloc(png_get_rowbytes(png_ptr, info_ptr));
    }
    png_read_image(png_ptr, row_pointers);

    // Process the image data here
    for (size_t y = 0; y < png_get_image_height(png_ptr, info_ptr); y++) {
        free(row_pointers[y]);
    }
    free(row_pointers);

    png_destroy_read_struct(&png_ptr, &info_ptr, 0);
    fclose(m_pFile);
}

void WriteByLIBPng(const char* pFileName, int nWid, int nHgt, int nChannel,void* p, int nByteForChannel)
{
    void* pPixel = p;
    int width    = nWid;
    int height   = nHgt;
    int nBitDepth = 8 * nByteForChannel;


    //检测是否为png文件
    FILE* m_pFile = fopen(pFileName, "wb");
    if (nullptr == m_pFile)
    {
        return;
    }


    unsigned char* pData = (unsigned char*)pPixel;
    png_structp png_ptr;
    png_infop info_ptr;

    png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
    if (png_ptr == nullptr)
    {
        fclose(m_pFile);
        m_pFile = nullptr;
        return;
    }


    info_ptr = png_create_info_struct(png_ptr);
    if (info_ptr == nullptr)
    {
        png_destroy_write_struct(&png_ptr, nullptr);
        return;
    }
    png_init_io(png_ptr, m_pFile);

    switch (nChannel)
    {
    case 1:
        png_set_IHDR(png_ptr, info_ptr, width, height, nBitDepth, PNG_COLOR_TYPE_GRAY, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE);
        break;
    case 2:
        png_set_IHDR(png_ptr, info_ptr, width, height, nBitDepth, PNG_COLOR_TYPE_GRAY_ALPHA, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE);
        break;
    case 3:
        png_set_IHDR(png_ptr, info_ptr, width, height, nBitDepth, PNG_COLOR_TYPE_RGB, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE);
        break;
    case 4:
        png_set_IHDR(png_ptr, info_ptr, width, height, nBitDepth, PNG_COLOR_TYPE_RGB_ALPHA, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE);
        break;
    default:
        return;
    }


    png_colorp palette = (png_colorp)png_malloc(png_ptr, PNG_MAX_PALETTE_LENGTH * sizeof(png_color));
    if (!palette)
    {
        fclose(m_pFile);
        png_destroy_write_struct(&png_ptr, &info_ptr);
        return;
    }

    png_set_PLTE(png_ptr, info_ptr, palette, PNG_MAX_PALETTE_LENGTH);
    png_write_info(png_ptr, info_ptr);
    if (nBitDepth == 16 && nChannel > 1)
    {
        png_set_swap(png_ptr); // This is equivalent to using PNG_TRANSFORM_SWAP_ENDIAN
    }

    png_bytepp rows = (png_bytepp)png_malloc(png_ptr, height * sizeof(png_bytep));
    if (nullptr == rows)
    {
        png_write_end(png_ptr, info_ptr);
        png_free(png_ptr, palette);
        palette = nullptr;
        png_destroy_write_struct(&png_ptr, &info_ptr);
        fclose(m_pFile);
        return;
    }


    for (int i = 0; i < height; ++i)
    {
        rows[i] = (png_bytep)(pData + static_cast<long long>(i) * static_cast<long long>(width) * nChannel * nByteForChannel);
    }



    png_write_image(png_ptr, rows);
    delete[] rows;
    png_write_end(png_ptr, info_ptr);
    png_free(png_ptr, palette);
    palette = nullptr;
    png_destroy_write_struct(&png_ptr, &info_ptr);
    fclose(m_pFile);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants