Introduction
諸事情により「zip形式の複数画像(jpg)をまとめたファイル」を簡単に見るための
Chrome Extensionを作成してます。
エクスプローラ風のダイアログでGoogle Drive上のzipファイルを指定すると
そのファイルをダウンロードしてzipファイルからindex指定でファイルを順番に取得するような処理を実装してます。
JSZipとかつかってjsだけでzipを展開して
画像を表示してもよいのですが、せっかくなのでRustでつくったWASMを実行してみました。
Environment
- MacBook Pro (14-inch, M3, 2023)
- OS : MacOS 14.5
- Rust : 1.79.0
- wasm-pack 0.12.1
- gh : 2.49.2
Try
まずはCargoでWASM用プロジェクトを作成します。
このWASMではzipファイルを操作するための関数を定義します。
% cargo new zip_extractor --lib
Cargo.tomlはこんな感じです。
解凍やランダムアクセス処理はzip crate使います。
その他wasm-bindgenとかもろもろ。
[package]
name = "zip_extractor"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[features]
default = ["console_error_panic_hook"]
[dependencies]
zip = { version = "0.5.13", default-features = false, features = [ "deflate" ]}
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
console_error_panic_hook = { version = "0.1.6", optional = true }
wasm-bindgen = "0.2.92"
wasm-bindgen-futures = "0.4.42"
serde-wasm-bindgen = "0.6.5"
[dependencies.web-sys]
version = "0.3"
features = ["FileReader", "File","console"]
[dependencies.log]
version = "0.4"
[dependencies.console_log]
version = "0.2"
[profile.release]
opt-level = "z"
lto = true
WASMでは2つの関数を定義します。
1つはzipバイナリをうけとって、アーカイブされているファイル名一覧を返す、
list_files_in_zip関数。
&[u8]でzipバイナリを受けとり、zip crateをつかって
アーカイブされているファイル一覧を取得します。
#[wasm_bindgen]
pub fn list_files_in_zip(data: &[u8]) -> Result<JsValue, JsValue> {
info!("Received data of length: {}", data.len());
let cursor = Cursor::new(data);
let mut zip = ZipArchive::new(cursor).map_err(|_| JsValue::from_str("Failed to read ZIP archive"))?;
let mut file_names = Vec::new();
for i in 0..zip.len() {
let file = zip.by_index(i).map_err(|_| JsValue::from_str("Failed to read file from ZIP archive"))?;
let file_name = match String::from_utf8(file.name_raw().to_vec()) {
Ok(name) => name,
Err(_) => {
info!("Failed to decode file name as UTF-8");
return Err(JsValue::from_str("Failed to decode file name as UTF-8"));
}
};
file_names.push(file_name);
}
Ok(to_value(&file_names).unwrap_or(JsValue::from_str("[]")))
}
こちらはzipバイナリとindex指定でファイルを取得するための関数です。
by_indexを使い、index指定でほしいファイルを取得しています。
だいたい実行速度は20ms〜30msくらいでした。
#[wasm_bindgen]
pub fn extract_index_from_zip(data: &[u8], index: usize) -> Result<JsValue, JsValue> {
info!("Received data of length: {}", data.len());
info!("Looking for file at index: {}", index);
let cursor = Cursor::new(data);
let mut zip = ZipArchive::new(cursor).map_err(|_| JsValue::from_str("Failed to read ZIP archive"))?;
let mut file = zip.by_index(index).map_err(|_| {
info!("File not found at index: {}", index);
JsValue::from_str("File not found in ZIP archive")
})?;
let mut file_data = Vec::new();
file.read_to_end(&mut file_data).map_err(|_| JsValue::from_str("Failed to read file data"))?;
info!("Found file at index: {}", index);
Ok(to_value(&file_data).map_err(|_| JsValue::from_str("Failed to convert file data to JsValue"))?)
}
WASMモジュール全文はこちら。
use wasm_bindgen::prelude::*;
use zip::read::ZipArchive;
use std::io::{Cursor, Read};
use serde_wasm_bindgen::to_value;
use log::info;
#[wasm_bindgen(start)]
pub fn main() {
console_log::init_with_level(log::Level::Debug).expect("error initializing log");
console_error_panic_hook::set_once();
}
#[wasm_bindgen]
pub fn extract_index_from_zip(data: &[u8], index: usize) -> Result<JsValue, JsValue> {
info!("Received data of length: {}", data.len());
info!("Looking for file at index: {}", index);
let cursor = Cursor::new(data);
let mut zip = ZipArchive::new(cursor).map_err(|_| JsValue::from_str("Failed to read ZIP archive"))?;
// Use by_index to find the file directly by its index
let mut file = zip.by_index(index).map_err(|_| {
info!("File not found at index: {}", index);
JsValue::from_str("File not found in ZIP archive")
})?;
let mut file_data = Vec::new();
file.read_to_end(&mut file_data).map_err(|_| JsValue::from_str("Failed to read file data"))?;
info!("Found file at index: {}", index);
Ok(to_value(&file_data).map_err(|_| JsValue::from_str("Failed to convert file data to JsValue"))?)
}
#[wasm_bindgen]
pub fn list_files_in_zip(data: &[u8]) -> Result<JsValue, JsValue> {
info!("Received data of length: {}", data.len());
let cursor = Cursor::new(data);
let mut zip = ZipArchive::new(cursor).map_err(|_| JsValue::from_str("Failed to read ZIP archive"))?;
let mut file_names = Vec::new();
for i in 0..zip.len() {
let file = zip.by_index(i).map_err(|_| JsValue::from_str("Failed to read file from ZIP archive"))?;
let file_name = match String::from_utf8(file.name_raw().to_vec()) {
Ok(name) => name,
Err(_) => {
info!("Failed to decode file name as UTF-8");
return Err(JsValue::from_str("Failed to decode file name as UTF-8"));
}
};
file_names.push(file_name);
}
Ok(to_value(&file_names).unwrap_or(JsValue::from_str("[]")))
}
あとはwasmpackでビルドします。
target webを忘れずに。
% wasm-pack build --target web
[INFO]: 🎯 Checking for the Wasm target...
[INFO]: 🌀 Compiling to Wasm...
Finished `release` profile [optimized] target(s) in 0.02s
[INFO]: Optimizing wasm binaries with `wasm-opt`...
[INFO]: Optional fields missing from Cargo.toml: 'description', 'repository', and 'license'. These are not necessary, but recommended
[INFO]: ✨ Done in 0.43s
[INFO]: 📦 Your wasm pkg is ready to publish at /・・・/pkg.
ビルドは成功すると、pkgディレクトリに各種ファイル(wasmとかjs)が生成されます。
Chrome Extensionから使う
あとは生成されたwasmファイル郡を
Chrome Extension用ディレクトリにコピーすれば使用できます。
ExtensionのソースはGenAIに聞けばいくらでも答えてくれるので、
関係ありそうなところだけ抜粋します。
manifest.jsonは以下のような感じです。
content_security_policyでWASMを使えるように設定しておきます。
{
"manifest_version": 3,
"name": "Hoge Viewer",
"version": "1.0",
"permissions": ["identity", "downloads", "storage"],
"host_permissions": ["https://www.googleapis.com/"],
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "images/icon16.png",
"48": "images/icon48.png",
"128": "images/icon128.png"
}
},
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'"
}
}
popupから使用されるスクリプトでは
WASM関数のimportを初期化を行います。
//popup.js
import init, { list_files_in_zip, extract_index_from_zip } from '../pkg/zip_extractor.js';
document.addEventListener('DOMContentLoaded', async () => {
await init();
});
Google Driveからzipをダウンロードする関数は下記のような感じです。
GCPのコンソールとmanifest.jsonで
OAuth2の設定を適切にしておく必要があります。
ダウンロードしたzipをUint8Arrayでwrapして
WASMのlist_files_in_zipに渡します。
var fileList;
var zipData;
function downloadAndExtractZip(fileId) {
chrome.identity.getAuthToken({ interactive: true }, (token) => {
fetch(`https://www.googleapis.com/drive/v3/files/${fileId}?alt=media`, {
headers: {
Authorization: 'Bearer ' + token,
},
})
.then((response) => response.arrayBuffer())
.then((arrayBuffer) => {
zipData = new Uint8Array(arrayBuffer);
const fileListData = list_files_in_zip(zipData);
// インデックスとファイル名をセットにしてオブジェクトに保存
fileList = fileListData.map((name, index) => ({ index, name }));
})
.catch((error) => {
console.error('Error:', error);
});
});
}
この関数はindexを指定してzipファイルから
任意の位置にある画像ファイルを取得します。
function showImage(index) {
try {
const fileIndex = fileList[currentIndex].index;
const fileData = extract_index_from_zip(zipData, index);
const blob = new Blob([new Uint8Array(fileData)], { type: 'image/jpeg' });
const url = URL.createObjectURL(blob);
//popup.htmlのimageに設定
document.getElementById('image-viewer').src = url;
} catch (error) {
console.error('Error extracting file:', error);
}
}
これで、Chrome Extensionでzipに圧縮された複数画像を
Google Driveからダウンロードして表示するビューアが実現できます。
Summary
今回はRustでzip操作するWASMを実装して
Chrome Extensionから使ってみました。
wasmpackが楽すぎます。
また、wee_allocとか使えばWASMのサイズを削減できたりするので、
もっと使いやすくなるかもしれません。