이제 나는 웹 어셈블리의 구조도 알고 러스트도 알고 타입스크립트도 알고 png 파일 생긴 모양도 안다. 신난다.
구현해야 할 기능은 웹 어셈브리 쪽에서는 2개가 남았는데, 이미지가 팔레트 이미지인지 인식하는 기능과 팔레트 정보를 수정하는 기능이다.
팔레트 확인
이미지를 받아서 팔레트를 확인하고 팔레트가 있으면 팔레트 내용을 반환하고 팔레트가 없으면 빈 배열을 반환하는 함수를 만들었다.
#[wasm_bindgen]
pub fn read_palette(data: Uint8ClampedArray) -> Uint8ClampedArray {
let datavec = data.to_vec();
let mut i: usize = 8;
if datavec[0..8] != [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A] {
Uint8ClampedArray::new_with_length(0)
}
else {
loop {
let length = merge_to_u32(&datavec[i..i+4]).unwrap();
i += 4;
if &datavec[i..i+4] == b"IDAT" {
break Uint8ClampedArray::new_with_length(0);
}
if &datavec[i..i+4] == b"PLTE" {
i += 4;
let colors = Uint8ClampedArray::new_with_length(length);
for j in 0..length {
colors.set_index(j, datavec[i + j as usize]);
}
break colors;
}
i += 4 + length as usize + 4;
}
}
}
png 이미지는 무조건 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A 여덟 바이트로 시작해야 한다. 이게 아니면 png가 아니라는 뜻이다.
그 다음 바이트 부터는 길이 4바이트 - 청크이름 4바이트 - 내용 {길이}바이트 - crc 4바이트가 반복된다.
png 구조에서 IDAT 청크는 PLTE 청크보다 뒤에 와야 한다. IDAT가 먼저 나오면 팔레트가 없다는 뜻이다.
PLTE를 찾았으면 PLTE 청크 내용을 그대로 반환한다.
팔레트 수정
파일을 읽어 {index}번 팔레트를 {r}{g}{b}로 변경해 파일을 반환하는 함수이다.
#[wasm_bindgen]
pub fn change_palette(data: Uint8ClampedArray, index: u8, r: u8, g: u8, b: u8)
-> Uint8ClampedArray {
let datavec = data.to_vec();
let mut i: usize = 8;
if datavec[0..8] != [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A] {
Uint8ClampedArray::new_with_length(0)
}
else {
loop {
let length = merge_to_u32(&datavec[i..i+4]).unwrap();
i += 4;
if &datavec[i..i+4] == b"IDAT" {
break Uint8ClampedArray::new_with_length(0);
}
if &datavec[i..i+4] == b"PLTE" {
let mut newvec = datavec[i..i + length as usize + 4].to_vec();
let j = 4 + (index * 3) as usize;
newvec[j + 0] = r;
newvec[j + 1] = g;
newvec[j + 2] = b;
let result = Uint8ClampedArray::new_with_length(datavec.len() as u32);
for ii in 0..i {
result.set_index(ii as u32, datavec[ii]);
}
for ii in 0..newvec.len() {
result.set_index((i + ii) as u32, newvec[ii]);
}
let crc = w3crc::W3Crc::make_crc_table();
let crc = crc.crc(&newvec).to_be_bytes();
for ii in 0..4 {
result.set_index((i + newvec.len() + ii) as u32, crc[ii]);
}
for ii in (i + newvec.len() + 4)..datavec.len() {
result.set_index(ii as u32, datavec[ii]);
}
break result;
}
i += 4 + length as usize + 4;
}
}
}
PLTE 청크를 찾을 때 까지는 기존이랑 같다.
PLTE 헤더를 찾으면 팔레트 번호의 rgb를 수정한다. 생각해보니까 팔레트 개수랑 비교하는 오류 검사가 없지만 러스트에서 (웹어셈블리에서) 오류가 날 거니까 상관 없겠군.
crc 체크를 다시 해 줘야 한다. 왜냐하면 내용이 바뀌었기 때문이다.
for ii in ...이 구문 진짜 추해 보인다.
typescript
function colornizeIfPaletted(data: ArrayBuffer, blob: Blob) {
var colors = rust.read_palette(new Uint8ClampedArray(data));
if (colors.length != 0) {
pixeldata = blob;
createNextInterface(splitColors(colors));
}
}
이미지를검사해서 팔레트가 있으면 그걸로 이미지 ui를 고쳤다.
function createNextInterface(colors) {
var value = {
div: document.createElement('div'),
button: document.createElement('input'),
colors: new Array(),
addColor: (self, newcover) => {
self.div.insertAdjacentElement("beforeend", newcover);
self.colors.push(newcover);
},
};
value.div.id = 'palettemenu';
value.button.id = 'menubutton';
value.button.type = 'button';
value.button.value = '다운로드';
value.button.addEventListener("click", downloadPressed);
bgElement.insertAdjacentElement("beforeend", value.div);
value.div.insertAdjacentElement("beforeend", value.button);
value.button.ariaLabel = `파일 팔레트화가 완료되었습니다. 아래에 변경할 수 있는 색 목록이 있습니다. 색 목록을 변경한 뒤 이 버튼을 눌러주세요.`;
value.button.focus();
value.button.addEventListener('focusout', function () {
this.ariaLabel = `다운로드 버튼. 색 목록을 변경한 뒤 이 버튼을 눌러주세요.`;
});
var i = 0;
colors.forEach(e => {
var newcover = makeNewcover(e, i);
value.addColor(value, newcover);
i++;
});
pixelui = value;
return value;
}
각 팔레트 색마다 input color 요소를 추가했다.
element 추가할 때 insertAdjacentElement보다 insertAdjacentHTML 쓰는게 가독성이 더 좋은 것 같다...
접근성
aria-label 속성 사용 - 접근성 | MDN (mozilla.org)
이런 게 있어서 사용해봤다.
버튼마다 설명 길게 넣고, 3초정도 걸리는 파일 팔레트화 끝났을 때 설명 넣고, 맨 아래 깃허브 설명으로 안 넘어가게 설명을 넣었다.
ariaLabel을 바꾸고 그거에 focus를 주면 네레이터가 읽는 느낌. 다 읽은 뒤에 또 바꾸고 싶으면 focusout에다가 ariaLabel을 또 주면 된다.
여러가지 기술과 꼼수를 익힐 수 있었던 시간이었다.
다음에는 닷넷이나 데이터베이스 쓰는 거 만들어야지.