webgl 캔버스의 png export 불가능 #173
chae-dahee
started this conversation in
Blog
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
webgl png export
thorvg.web의 이슈를 #139 "lottie: can't export to png with webgl canvas" 분석한다.
브라우저 렌더링 동작, WASM 바인딩, /thorvg 상위 라이브러리의 save2png 기능에 걸친 의존 구조
원인 구조, 경로 대비를 먼저 정리한다.
요약
WebGL 백엔드에서
save2png가 빈 이미지를 만든다.원인은 브라우저가 프레임 렌더 후 drawing buffer를 비우기 때문이고, 현재
save2png가 그 비워진 캔버스를 직접 읽는 방식(canvas.toBlob())이라서다.해결 방향은 캔버스를 읽지 않고
save2gif처럼 ThorVG saver로 저장하는 것으로 보인다. 현재 saver(PNG)가 상위 라이브러리 core에 없어서, thorvg#3737의 core 선행작업에 이어서 진행해야 할 것 이다.기술적으로는 web 전용 encoder를 도입하는 우회도 가능하나, 근본적인 해결은 아니다.
왜 WebGL 에서만 실패하지?
렌더링엔진
브라우저는 WebGL 캔버스에 렌더링한 뒤 기본적으로 drawing buffer를 flush(clear)한다. 이는 성능을 위한 브라우저의 기본 최적화다. 그 결과 렌더가 끝난 시점에 캔버스 픽셀을 읽으려 하면 이미 비워진 버퍼를 읽게 된다.
Software(CPU) 렌더러는 CPU 메모리 버퍼에 그리므로 문제가 없다. 이 이슈는 WebGL 백엔드에서만 재현된다. viewer(thorvg.view #124), web(thorvg.web #139)에서 동일하게 확인되었다.
참고 자료: WebGL2 Fundamentals — Taking a Screenshot of the Canvas
코드 관점
save2png와save2gif가 서로 다른 데이터 소스를 쓴다save2png(packages/lottie-player/src/lottie-player.ts:58-70)this.canvas!.toBlob(...): HTML 캔버스 픽셀을 직접 읽는다. WebGL 버퍼가 비워지면 빈 이미지가 나온다.save2gif(packages/lottie-player/src/lottie-player.ts:76-101)즉, save2png를 toBlob 방식(캔버스 읽기)에서 save2gif 방식(파일 읽기)으로 전환하는 것이 필요하다.
#124 이슈: save2gif 코멘트 방향
가장 간단한 해법은 Emscripten의
GL_TESTING옵션(내부적으로preserveDrawingBuffer를 켬)을 활성화하는 것이다. WebGL 컨텍스트가 렌더 후에도 버퍼를 보존하므로 캔버스를 읽을 수 있게 된다.그러나
벤치마크의 성능차이와 별개로 브라우저의 기본 동작을 우회하는 의도는 과연 적합한가?
save2gif 패턴: 캔버스를 읽지 않음
원본 데이터를 SW 렌더러로 재로드해 ThorVG saver 로 인코딩하고, 그 결과 파일을 되읽는다
C++ 부분 : wasm/lottie-player/tvgWasmLottieAnimation.cpp
save(data, mimetype)가 mimetype으로 분기하고(:434),"gif"면save2Gif를 호출한다(:441).save2Gif화면 캔버스의 상태와 독립적으로, 원본 데이터를 새 Animation으로 로드해
Saver로 저장한다. 저장 대상은 Emscripten 가상 파일시스템(MEMFS)의output.gif다.JS 부분 회수 경로 : packages/lottie-player/src/lottie-player.ts:76-101`
회수 방식: C++ saver가 MEMFS에 파일로 저장 → JS가
FS_readFile로 그 파일을 읽음 → Blob 다운로드캔버스 픽셀을 읽는 단계가 없으므로 WebGL 버퍼 flush 문제의 영향을 받지 않는다.
PNG saver 방식 : 정적 이미지
GIF와 PNG는 저장 대상 타입이 다르다. ThorVG의 saver 시그니처를 보면 두 오버로드로 갈린다(
inc/thorvg.h).save(Animation*, ...): 애니메이션 전체를 fps와 함께 저장한다.save(Paint*, ...): 단일 정적 이미지 Paintcore 상위 작업 #3737
save2gif 패턴을 PNG에 적용하려면 ThorVG core에 PNG saver가 있어야 하는데, 현재 core에는 없다.
thorvg/src/savers/:gif/만 있고png/는 없다.thorvg/meson_options.txtsavers 선택지 :['', 'gif', 'all']뿐, PNG saver 모듈 없다.thorvg/tools/svg2png/-lodepng.cppsvg2png.cpp가SwCanvas::gen()→canvas->target(buffer, ...)→lodepng::encode(...)패턴즉, core 에서 구현이 필요한 기능이다.
thorvg/thorvg#3737 "support png saver"
thorvg/savers/png지원을 고려하자. PNG 참고 자료는thorvg/tools/svg2png에 있다.save(Paint*)오버로드 방향PR #3874 구현 close
thorvg/thorvg#3874 "saver/png: support png saver" CLOSED(2026-06-04)
StaticPNG saver를savers/png에 추가하되, 인코딩 함수를loaders/png/tvgLodePng에서 가져왔다.png_loader = png_loader or png_saver로 묶여, lodepng 구현이 loader 모듈 아래 있는 채로 saver가 그것을 끌어쓰는 의존이 생겼다.common/tvgPngCodec.h/.cpppng/tvgLodePng → common/tvgPngCodec이슈 관계
web(#139)과 viewer(#124)는 증상 쪽이고, 해결은 core(#3737)에 PNG saver를 세우는 것. web에서 자체 encoder로 우회하는 길이 기술적으로 아주 막혀 있는 것은 아니지만, 이중 구현이라 근본 해결이 되지 못한다.
core PNG saver 구현 후 web
save()에 PNG 형식을 노출한다. (tvgWasmLottieAnimation.cpp:434의save()가 현재"gif"만 분기 →"png"분기 추가,save2Png구현)save2png()가 캔버스를 읽지 않고 MEMFS의output.png를FS_readFile로 읽어 내려받도록 변경한다. (현재의canvas.toBlob()방식 폐기)core PNG saver 구현방향
PR #3874 참고
현재 core 코드(savers/gif, loaders/png, renderer/tvgSaver.cpp) codec 배치, saver 방식, 빌드 배선이며, 주요 핵심은 codec 배치
1. codec 배치 : 통합 (common/tvgPngCodec)
lodepng를
common/tvgPngCodec.h/.cpp로 통합해 loader와 saver가 공유한다.THORVG_PNG_LOADER_SUPPORTTHORVG_PNG_SAVER_SUPPORTloaders/png/tvgLodePng → common/tvgPngCodec로 옮기는 리팩터 커밋2. saver 방식
GifSaver의 SW 렌더 패턴(
SwCanvas::gen()→target()→update()/draw()/sync())을 정적 이미지용으로 축약src/savers/png/tvgPngSaver.{h,cpp}추가struct PngSaver : SaveModule, Tasksave(Animation*, ...): GifSaver의 Paint 스텁과 대칭으로 "지원 안 함" 처리한다.save(Paint*, bg, filename, quality):paint->bounds()로 뷰 크기를 구하고(GifSaver :128-139처럼 음수 공간 컷오프와 zero-size 방어를 둔다),Task로 넘겨run()에서SwCanvas에bg와paint를 add한 뒤 한 번만 렌더해 CPU 버퍼를 만들고, 그 버퍼를tvgPngCodec의 인코드 함수로 파일에 기록한다.quality는 lodepng 압축 레벨로 매핑할지 우선 무시할지는 정책 확정이 필요하다.Paint를 받는 부분까지.save2Png바인딩 : 현재 화면에서 사용 중인 scene의Paint를 saver에 전달하도록 설계한다.3. 빌드 배선
FileType::Png는 이미 enum에 정의돼 있어(src/common/tvgCommon.h:76) loader용 값을 saver 분기에서 재사용한다.meson_options.txt의 savers 선택지(:18)를['', 'gif', 'png', 'all']로 확장src/savers/meson.build에if png_saver / subdir('png')를 추가src/savers/png/meson.build는 gif의 것을 그대로 본뜬다.THORVG_PNG_SAVER_SUPPORT매크로 정의tvgSaver.cpp에#ifdef로tvgPngSaver.hinclude_find(FileType)(src/renderer/tvgSaver.cpp:50)에case FileType::Png분기_find(const char* filename)(:82)에!strcmp(ext, "png")분기질문 및 확인사항
All reactions