← 블로그 목록

[Core2026] Loader 이슈 처리 사례

@Nor-s
  • #ThorVG
  • #Core
목차

IMPORTANT

Static PNG Loader 이슈 처리 사례를 통해 Image Loader 이슈 작업 방식에 대해 알아봅시다.

1. Loader & LoaderMgr

1.1 Build Option

ThorVG의 Loader는 포맷별 모듈로 분리됩니다.

option('loaders',
   type: 'array',
   choices: ['', 'svg', 'png', 'jpg', 'lottie', 'ttf', 'otf', 'webp', 'media', 'all'],
   value: ['svg', 'lottie', 'ttf'],
   description: 'Enable File Loaders in thorvg')

-Dloaders=로 빌드에 포함할 파일 포맷을 선택합니다.

option('static',
   type: 'boolean',
   value: false,
   description: 'Force to use static linking modules in thorvg')

-Dstatic=false이면 시스템에 설치된 라이브러리를 우선 사용합니다.

NOTE

static 옵션을 꺼도 external lib이 존재하지 않으면 static 모듈이 선택됩니다.

PNG Loader의 선택 조건입니다.

if png_loader
    if get_option('static')
        subdir('png')
    else
        subdir('external_png')
        if not png_dep.found()
            subdir('png')
        endif
    endif
endif
설정과 환경선택되는 구현
static=trueThorVG 커스텀 Static PNG Loader
static=false, 시스템 libpng oExternal PNG Loader
static=false, 시스템 libpng xStatic PNG Loader로 fallback

1.2 경량 엔진 관점에서의 이점

  • 필요한 포맷만 빌드에 포함
  • 시스템 라이브러리가 있으면 외부 구현 사용
  • 불필요한 코드 제외로 바이너리 크기 절감

1.3 Loader Manager와 Loader들

Loader 클래스는 디코더 wrapper가 아니라 모든 포맷이 따르는 공통 Base, LoaderMgr 에 의해 생성, 공유, 소멸됩니다.

Loader
├─ ImageLoader ── w, h, playable, paint(), bitmap()
│  ├─ BitmapLoader ── RenderSurface
│  │  └─ PngLoader (+ Task)
│  └─ AnimLoader ── frame, duration, segment
└─ FontLoader ── glyph, metrics, transform
struct Loader
{
    INLIST_ITEM(Loader);

    // Use either hashkey(data) or hashpath(path)
    uintptr_t hashkey = 0;
    char* hashpath = nullptr;

    FileType type;               // current loader file type
    atomic<uint16_t> sharing{};  // reference count
    Ownership owner = Ownership::Borrow;
    bool readied = false;        // read done already
    bool cached = false;         // cached for sharing

    //...
};
  • open(): 입력 처리 가능 여부 확인 (파일 존재 여부, 파일 오픈여부 등 유효성)
  • read(): Open한 데이터를 한 번만 읽음
    • TaskScheduler 에 요청 (실제 디코딩 시작, 멀티스레딩)
      • 비동기 처리는 PngLoader처럼 Task를 함께 상속한 구현에서만 사용
    • 엔진에 사용할 비트맵 데이터로 변환
  • sharing: cache hit, Picture::duplicate()

LoaderMgr

LoaderMgr::loader("poster.png")

    ├─ 같은 path의 live cache가 있는가? ── yes ─► 같은 Loader 공유

    └─ no
       ├─ extension → FileType::Png
       ├─ _find(Png) → new PngLoader
       ├─ PngLoader::open(path, ops)로 실제 입력 검증
       ├─ 성공하면 path cache에 넣고 반환
       └─ InvalidArguments라면 컴파일된 다른 후보를 순서대로 확인
  • LoaderMgr: Loader의 생성·검증·공유·폐기
  • _activeLoaders -> Loader 객체의 공유 목록 (전역)
    • 메모리 cache key: pointer 주소

2. Static PNG Loader

  • External: 시스템의 외부 라이브러리 사용
  • Static: 자체 구현 또는 필요한 부분만 가져온 오픈소스 사용

2.1 Static Loader 구현을 위한, 오픈소스 도입 히스토리

External 구현에 libpng를 도입한 뒤, Static 구현에는 LodePNG를 통합했습니다.

  • LodePNG 전체가 아닌 디코더 부분만 통합
  • 사용하지 않는 기능 제거
  • ThorVG의 색 공간과 메모리 정책에 맞게 수정
Color Space 커스텀사용하지 않는 기능 제거메모리 할당 커스텀
시점변경의미
2021-04libpng 기반 PNG loadingExternal PNG Loader의 출발점
2021-10static JPG/PNG loader 준비static/external 선택 구조 마련
2021-10LodePNG 기반 static loader필요한 decoder subset만 통합
2021-11RGBA/BGRA 경로 보정출력 pixel 표현을 ThorVG에 맞춤
2023-08강제 색 채널 변환 되돌림renderer 공통 변환과 역할 중복 제거
2025-02지정 메모리 할당자 도입tvg::malloc/free로 메모리 경계 통일
2026-07PR #4568iCCP profile을 읽어 sRGB로 변환

2.2 PngLoader

  • PngLoader: BitmapLoaderTask 상속
  • PNG 디코딩: TaskScheduler를 통한 비동기 처리

open

두 가지 경로가 있습니다.

  • 파일 경로 open: Picture::load()에서 사용
  • 메모리 data open: Lottie 에서 PNG를 base64로 인코딩한 뒤 JSON에 포함시켜 전달할 때 사용

두 함수는 실제 디코딩은 수행하지 않으며 역할은 다음과 같습니다.

  • 디코더 라이브러리에서 width, height, 포맷 정보를 query하여 관리
  • width, height를 통해 디코딩된 데이터가 저장될 버퍼 준비

read

void PngLoader::run(unsigned tid)
{
    auto width = static_cast<unsigned>(w);
    auto height = static_cast<unsigned>(h);

    state.info_raw.colortype = LCT_RGBA;   // request this image format

    if (lodepng_decode(&surface.buf8, &width, &height, &state, data, size)) {
        TVGERR("PNG", "Failed to decode image");
    }

    if (state.info_png.iccp_defined) {
        lodepng_toSrgb(surface.buf8, width, height, &state.info_png);
    }

    surface.stride = width;
    surface.w = width;
    surface.h = height;
    surface.cs = ColorSpace::ABGR8888S;
    surface.channelSize = sizeof(uint32_t);
}

bool PngLoader::read()
{
    if (!data || w == 0 || h == 0) return false;
    if (!Loader::read()) return true;

    TaskScheduler::request(this);
    return true;
}

실제 디코딩은 TaskScheduler에 요청합니다.

  • surface .buf8 에 비트맵 정보를 저장하며 SW, GL, WG 각 렌더 백엔드에서 사용

NOTE

몇몇 Loader는 TaskScheduler를 사용하지 않음 (Thread Safe 와 관련있을 것으로 추정)

PngLoader::bitmap()done()으로 worker 완료를 기다린 뒤 surface를 반환합니다.

RenderSurface* PngLoader::bitmap()
{
    done();
    return BitmapLoader::bitmap();
}
  • gl, wg → texture

Picture::update

WG Prepare -> Texture

GL Prepare -> Texture

3. Loader 이슈 처리 사례

  • 사례: Static PNG Loader의 색상 불일치하다는 이슈
  • 현상: 동일한 PNG의 렌더링 결과에서 색상 차이 발생
IssueExpected
색상 프로필이 적용되지 않은 ThorVG 렌더링 결과
색상 프로필이 적용된 기대 결과

3.1 이슈 파일 분석하기

가설: ThorVG가 이슈 파일의 PNG 스펙 일부를 지원하지 않음, 별도의 툴 -> 파일 내부 조사

pngcheck로 이슈 파일의 청크를 확인합니다.

// 색상 차이가 발생하는 이슈 케이스
➜ png-color git:(nor-s/png-color) ✗ pngcheck -v image-test.png
File: image-test.png (19816 bytes)
  chunk IHDR at offset 0x0000c, length 13
    458 x 442 image, 32-bit RGB+alpha, non-interlaced
  chunk iCCP at offset 0x00025, length 3151
    profile name = ICC Profile, compression method = 0 (deflate)
    compressed profile = 3138 bytes
  chunk cICP at offset 0x00c80, length 4
:   Display P3
    White x = 0.3127 y = 0.329, Red x = 0.68 y = 0.32
    Green x = 0.265 y = 0.69, Blue x = 0.15 y = 0.06
    Full range
  chunk eXIf at offset 0x00c90, length 138: EXIF metadata, big-endian (MM) format
  chunk pHYs at offset 0x00d26, length 9: 5669x5669 pixels/meter (144 dpi)
  chunk iTXt at offset 0x00d3b, length 470, keyword: XML:com.adobe.xmp
    uncompressed, no language tag
    no translated keyword, 449 bytes of UTF-8 text
  chunk iDOT at offset 0x00f1d, length 28: illegal unknown, public chunk
ERRORS DETECTED in image-test.png

정상 파일의 청크 구성입니다.

// 색상 차이가 없는 케이스
File: deps.png (3943610 bytes)
  chunk IHDR at offset 0x0000c, length 13
    2182 x 13604 image, 32-bit RGB+alpha, non-interlaced
  chunk bKGD at offset 0x00025, length 6
    red = 0x00ff, green = 0x00ff, blue = 0x00ff
  chunk IDAT at offset 0x00037, length 8192
    zlib: deflated, 32K window, default compression
  chunk IDAT at offset 0x02043, length 8192
  chunk IDAT at offset 0x0404f, length 8192
  chunk IDAT at offset 0x0605b, length 8192
  • 차이점: 이슈 파일에는 iCCP, cICP 색상 정보 청크가 포함됨

NOTE

PNG(Portable Network Graphics)는 무손실 압축 방식을 지원하는 단일 이미지 파일 포맷입니다. 파일 안의 일련의 청크로 이미지 정보를 전달합니다.

필수 청크

  • IHDR: width, height, bit depth, color type 등
  • IDAT: 이미지 데이터
  • PLTE: 색상 목록
  • IEND: 마지막 청크 표시

색과 관련된 보조 청크

  • cICP: ITU-T H.273의 색 공간, transfer function, matrix coefficient를 지정
  • iCCP: 압축된 ICC color profile
  • sRGB: 표준 sRGB color space가 사용되었음을 표시
  • cHRM + gAMA: 원색과 white point의 색도 좌표, gamma 지정
  • 원인: 당시 ThorVG의 compact LodePNG에는 색상 profile 변환 경로가 없었음
    • 필수 청크 통합/보조 청크 제외 -> 미지원 된 기능

3.2 PNG 색과 관련된 보조 청크

NOTE

용어 정리

  • 색 공간: RGB 숫자가 어떤 실제 색을 뜻하는지 정한 규칙
  • 색 프로필: 이미지의 색 공간 특성을 기록한 데이터
  • iCCP: 압축된 ICC profile을 저장하는 PNG 청크
  • ICC Profile: 색 입력 장치나 색 출력 장치의 특성을 기록한 데이터 집합
  • 같은 RGB 값도 색 공간에 따라 실제 색이 달라짐
  • 디자인 도구는 색상 profile을 PNG 보조 청크에 저장
  • IDAT의 RGB 값만으로는 제작자가 의도한 색을 재현할 수 없음
/*
Color profile related chunk types: cICP, iCCP, sRGB, gAMA, cHRM, sBIT

LodePNG does not apply any color conversions on pixels in the encoder or
decoder and does not interpret these color profile values. It merely passes
on the information. If you wish to use color profiles and convert colors,
a separate color management library should be used. There is also a limited
library for this in lodepng_util.h.

Only one color information source should be handled, in this priority:
1: cICP
2: iCCP
3: sRGB
4: gAMA and cHRM
*/

cICP, iCCP, sRGB, gAMA, cHRM은 PNG 색상 profile 관련 청크입니다.

  • 한가지 중 선택해서 사용
  • 우선순위: cICP > iCCP > sRGB > gAMA + cHRM

iCCP

  • 기존 RGB 를 일정 수학적 변환으로 sRGB로 변환하는 정보를 가지고 있음
  • 색상 변환을 위한 변수
    • rXYZ, gXYZ, bXYZ로 source RGB → XYZ matrix 구성
    • rTRC, gTRC, bTRC curve로 encoded RGB를 linear RGB로 복원
    • wtpt와 optional chad tag로 white point와 chromatic adaptation 처리

3.3 LodePNG 작업과 PR #4568

lodepng_util.h의 iCCP 처리 코드에서 ThorVG에 필요한 부분만 적용했습니다.

PR: loader/png: added sRGB conversion for embedded ICC profiles #4568

적용 전적용 후macOS Preview

진행한 작업들

1459 라인인 오픈소스를 최종적으로 456 라인으로 코드 정리

  • 전체적인 코드흐름을 읽고 필요한 부분만 통합
  • ThorVG 의 Math 함수들 사용 (ex. 행렬 연산)
  • 실제적인 사용 사례가 적은 코드 제거 (Grayscale)
  • ThorVG 의 메모리 함수 사용
  • 중복된 로직 제거
  • LodePNG 에서 테스트 되지 않은 영역 제거

4. 결론

이슈 파일 분석
→ 포맷 스펙 분석
→ 문제 해결 방향 정하기
→ 오픈소스 코드 분석과 필요한 부분 통합
  • 포맷과 파일 스펙을 먼저 확인
  • 재현 테스트로 해결 기준 고정 (최종 Display 색 일치)
  • 오픈소스 내부 구현을 이해하고 필요한 범위만 통합