웹 어셈블리 이미지 처리 연습 기록 (1)
https://github.com/hyuckkim/Online-ModernArt-Maker
사실 온라인도 아니고 모던아트도 아니고 메이커도 아니지만 이름을 뭘로 해야 할지 모르겠어서 그냥 이렇게 적음.
사이트는 대충 로컬 이미지를 imagequant로 팔레트 png 이미지로 바꾸는 기능 / 각 팔레트 바꾸는 기능 이렇게 2개.
libimagequant (LIQ) — Image Quantization Library (pngquant.org)
동기
2학년 때 정보과제연구를 진행했었는데, 1학기 때 만든 게 '안녕' 이고 2학기 때 팔레트 이미지 분석을 했었다. 최종적으로 이미지의 팔레트를 바꾸는 사이트를 만들자고 생각했는데, 그 당시에는 blob과 웹 어셈블리의 존재를 몰라서 이미지를 서버로 보내서 처리하고 받아오고 해야 하는 줄 알았다.
유튜브에서 squoosh! 라는 사이트를 우연히 보게 됐다. 그 사이트에서 이미지를 팔레트 이미지로 바꾸는 기능을 보고 (png 이미지였다. png에 팔레트 기능이 있었던가?) 어케 했지 하고 찾아보니까 구글 오픈소스였다. 그렇게 imagequant랑 webassembly랑 사용 예시 전부 다 갖춰졌다. 그래서 이제 만드는 일만 남았다.
시작
mdn에서 rust-bindgen을 사용해봤다. 내용은 좋았으나 여기서 구버전 충돌 나고 typescript 오류나고 express 문제 생기고 했다. 이 때까지는 webassembly에 대한 개념이 일종의 서버사이드 프레임워크 정도로 생각했다.
하여튼 그래서 그냥 static 웹서버에서 webassembly를 쓰는 거에 대해 긴가민가 했다. 내가 쓰는 Edge 브라우저에서는 보안 설정을 켜면 webassembly가 아예 실행이 안 되는 문제가 있어서 더더욱. 오죽하면 github page에서 쓸 수 있냐는 이슈까지 (아주 잠깐이지만) 열었었다. 지금 생각하면 너무 창피하다.
rust-bindgen이 생성한 파일을 import 할 때도 문제가 있었다.
import * as rust from "";
이렇게 임포트를 하고서 꼭
rust.default();
default 함수를 실행해야 한다. 난 또 export default라길래 import 하면 자동으로 실행되는 줄 알았지.
하여튼 여기까지 해서 깃허브에 '10을 반환하는 웹어셈블리 함수'가 올라갔다.
라이브러리
구글에서 쓴 방식 거의 그대로 따라했다. GPL 위반인가? 저건 C++이고 이건 러스트니까 아니길 빈다.
squoosh/imagequant.cpp at dev · GoogleChromeLabs/squoosh (github.com)
다만 웹어셈블리에서 imagequant를 쓸 때 주의점이 있다. feature를 꺼야 한다. 기본 상태로는 멀티스레딩을 하는데 이게 웹어셈블리랑 상성이 안 좋은가 보다.
문제가 하나 있었다. 내가 원하는 건 팔레트로 저용량이 된 이미지지 색 수만 줄어든 24비트 이미지가 아니었다. 구글이 만든 대로는 cavas data를 주고 또다른 canvas data를 받는다. 나는 웹어셈블리에서 png 파일을 받길 원한다.
png 라이브러리를 찾아 봤으나 맘에 드는 게 없었다. 어차피 편하게 가는 게 목적이 아니라 공부 하려는게 목적이니까 한 번 직접 구현해보기로 했다.
png
png가 무엇인가. 인터넷 표준의 왕. 무압축의 신. 사진 보낼 때 제발 jpg 말고 이걸로 주세요. 심지어 자체 조사 결과에 따르면 로드 속도도 딱히 밀리지 않는다. 중요한 건 png에 팔레트 기능이 있다는 것이다. png 파일의 구조 뜯어보기로 했다.
png는 앞 8바이트에 png라면 무조건 가지는 정보를 써 두고 그 다음부터 반복이 시작된다.
내용 길이 (4) - 이름 (4) - 내용 (*) - crc(4)
내용은 deflate로 압축해야 하고, crc는 adler32를 사용한다. 헤더 몇 개만 있으면 구현 끝이다. 별로 안 어렵네?
코딩
헤더를 구조체로 만들었다. 길이랑 crc는 데이터 구할 때 한 번만 쓰니까 없애고, 이름과 내용만 받았다. 이름은 4바이트니까 문자열로 생성자 두고 [u8; 4], 4바이트 배열로 만들었다. 내용은 바이트 배열로 만들었다. 그리고 내용에 1바이트 / 4바이트 / 바이트 배열을 추가하는 함수를 만들었다. 마지막으로 deflate 압축은 라이브러리가 있다. 라이브러리로 쓱쓱싹싹 하면 된다.
다 만들어갈 때쯤에야 알았는데 팔레트에서 bitdepth가 무시된다는 게 팔레트쪽 PLTE를 말한다는 것을 깨달았다. bitvec도 처음 써보고 unsafe도 처음 써봤다.
픽셀 데이터는 가로줄마다 끊는다. 각 줄의 앞에 한 바이트가 더 들어가는데 이 바이트의 정보로 앞 픽셀과 뒤 픽셀의 관계를 정할 수 있다. 아무래도 데이터에 0이 많을수록 압축이 쉬우니까 같은 색은 이걸로 때우고 0 칠하라고 있는 것.
crc는 라이브러리 쓰는 것보다 그냥 자체 구현이 쉽다. 러스트로 옮겨도 아무 문제 없었으니까 그냥 직접 명세 보고 만들어 쓰자. crc 안맞으니까 로컬에선 괜찮은데 웹기반에서는 안 열렸다.
그리고 이 데이터들을 반환하면 웹 어셈블리 부분 끝이다.
타입스크립트
화면 상태가 3개다. 처음 / 이미지 받고 / 이미지 팔레트화 되고. 처음에는 개체들을 다 생성했는데 너무 장황해져서 그냥 html 문자열 부어서 생성하는 게 나았다. 이러나저러나 'as'를 꽤 많이 쓰게 됐다.
여기서는 별 문제 없었던 듯. 여기까지 만들고 잠깐 쉬었다.