<aside> 💾 OpenGL을 공부하던 도중, 직접 png 파일을 로드하고 싶다는 생각이 들었다. glfw 말고는 다른 라이브러리를 쓰지 않겠다는 마인드였다. 그리하여 공식 png api인 libpng를 사용해 보기로 했다. 물론 이것도 라이브러리이긴 하고, 만약 진짜 저수준으로 갔다면 이 png 파일도 libpng를 쓰지 않고 손수 파싱할 수도 있겠지만… 자동차 바퀴 만들려고 고무 원재료를 찾으러 가는 격인 것 같아 그만두었다. 언젠가는 해 보고야 말겠다.

</aside>

libpng가 제공하는 방법은 high-level api, low-level api 2개가 있다. 우선 고수준부터 보자. 필자는 C언어를 사용하여 설명하겠다. 혹시 최종적으로 어떻게 하는지가 궁금하면 저수준 API부분으로 바로 넘어가면 된다. 바로 출발해 보자.

여담으로, 저수준 작업을 할 때마다 우리가 평소에 얼마나 높은 수준으로 작업하고 있는지 새삼 놀란다. 저수준으로 한 발자국 가 보아도, 그 저수준은 더 깊은 저수준에 대한 추상화에 불과하다.

고수준 API

우선 필자가 작성한 코드를 빠르게 스윽 훑고 뒤에서 설명하겠다.

static unsigned char *read_png(const char *path,
        unsigned int *width, unsigned int *height, unsigned char *channels) {
    png_structp png_ptr = NULL;
    png_infop info_ptr = NULL;

    FILE *fp = fopen(path, "rb");
    if (!fp) {
        printf("failed to open file %s\\n", path);
        return NULL;
    }
    png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING,
            NULL, NULL, NULL);
    if (!png_ptr) {
        printf("failed to create read struct");
        fclose(fp);
        return NULL;
    }
    info_ptr = png_create_info_struct(png_ptr);
    if (!info_ptr) {
        printf("failed to create info struct\\n");
        png_destroy_read_struct(&png_ptr, NULL, NULL);
        fclose(fp);
        return NULL;
    }
    if (setjmp(png_jmpbuf(png_ptr))) {
        printf("an error occured\\n");
        png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
        fclose(fp);
        return NULL;
    }

    png_init_io(png_ptr, fp);
    png_read_png(png_ptr, info_ptr, PNG_TRANSFORM_IDENTITY, NULL);
    png_bytepp rows = png_get_rows(png_ptr, info_ptr);
    unsigned int w = png_get_image_width(png_ptr, info_ptr);
    unsigned int h = png_get_image_height(png_ptr, info_ptr);
    unsigned char ch = png_get_channels(png_ptr, info_ptr);
    *width = w;
    *height = h;
    *channels = ch;

// OpenGL 전용 부분
    unsigned char *buf = malloc(w * h * ch);
    if (!buf) {
        printf("malloc failed\\n");
        return NULL;
    }
    unsigned char *ptr = buf;
    size_t row_bytes = w * ch;
    for (size_t i = h; i-- > 0;) {
        for (size_t j = 0; j < row_bytes; ++j) {
            *ptr++ = rows[i][j];
        }
    }
// 부분 끝

    png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
    fclose(fp);

    return buf; // 코드 밖에서 free해주면 되겠다.
}

우선 OpenGL에게 넘겨줄 포맷으로 읽어들였기 때문에 조금 긴데, 핵심만 짚고 넘어가 보자.

우선 windows와 호환성을 위해 “rb”(read binary) 모드로 해당 png 파일을 fopen을 해 준다.

png_structppng_infop 가 중요한 struct다. 이름에서 제시하듯 둘 다 포인터 타입인데, typedef되어있을 뿐이다. png_structp는 struct png_struct_def *이다. 이 struct들 안의 내용은 직접 조작할 수 없도록 헤더에 포함되지 않고 숨겨져 있다. struct 안의 내용을 얻는 방법은 뒤에 설명하겠다. 이 둘은 다음의 함수들로 생성한다:

extern png_structp png_create_read_struct(png_const_charp user_png_ver, 
		png_voidp error_ptr, png_error_ptr error_fn, png_error_ptr warn_fn);

시그니처가 복잡해 보이는데, 맨 처음에 PNG_LIBPNG_VER_STRING 을 넘기고 나머지는 다 NULL을 넣으면 된다. 그 뒤에 3개는 에러 핸들링 콜백들인데 NULL로 넣으면 기본 콜백을 사용해 준다.

extern png_infop png_create_info_struct (png_const_structrp pnt_ptr);

방금 만들어 낸 png_structp를 넣어주면 된다. *(참고로 시그니처에 structrp로 되어있는것은 restrict 라서 그렇다.)

그다음에 setjmp로 에러 발생시 핸들링을 해 준다.