WebAssembly

Image Converter WASM

Package konversi gambar berbasis Rust WebAssembly untuk browser dan Node.js, dipublikasikan sebagai package npm.

WebAssembly Rust Aplikasi Web

Tautan Repositori

GitHub: https://github.com/walujanle/image-converter-wasm

GitLab: https://gitlab.com/walujanle/image-converter-wasm

Tautan Package

npm: https://www.npmjs.com/package/@walujanle/image-converter-wasm

Tautan Unduh

Package: https://links.leonardwalujan.eu.org/lw/image-converter-wasm-latest-package

Checksum Package (SHA-256): https://links.leonardwalujan.eu.org/lw/image-converter-wasm-latest-package-checksum

Kode Sumber: https://links.leonardwalujan.eu.org/lw/image-converter-wasm-latest-source-code

Penjelasan Singkat

Image Converter WASM adalah engine konversi gambar yang ditulis dengan Rust, dikompilasi ke WebAssembly, lalu dipaketkan untuk aplikasi JavaScript. Package ini dipublikasikan di npm dengan nama @walujanle/image-converter-wasm.

Kontraknya sengaja dibuat kecil: Anda mengirim byte gambar sebagai Uint8Array, konversi berjalan di memori, lalu hasil encoding dikembalikan sebagai Uint8Array. Package ini tidak pernah membuka file picker, tidak membuat tombol download, tidak mengunggah apa pun, dan tidak menulis ke folder mana pun. Semua itu tetap menjadi milik aplikasi yang memakainya. Pemisahan inilah yang membuat package ini bisa dipakai di aplikasi frontend, di web worker, atau di skrip Node.js tanpa pernah berebut siapa yang mengatur file.

Format output yang didukung adalah JPEG, PNG, WebP, dan AVIF. Inputnya bisa jpg, jpeg, png, webp, avif, heic, atau heif. HEIC dan HEIF hanya bisa dibaca. Anda bisa mengonversi dari keduanya, tetapi package ini tidak melakukan encoding HEIC, dan ia menolak dengan error yang jelas alih-alih diam-diam mengembalikan format lain.

Alasan Saya Membuat Project Ini

Saya membuat ini karena ingin melewati jalurnya sendiri: dari kode Rust, menjadi WebAssembly, sampai menjadi sesuatu yang bisa dipasang frontend lewat npm dan dipanggil seperti dependency biasa. Membaca soal jalur itu tidak sama dengan benar-benar merilisnya.

Alasan kedua adalah pekerjaan gambarnya sendiri. Saya ingin berada lebih dekat ke engine dibanding memakai UI konverter biasa: format output, kualitas, kompresi PNG, mode lossless WebP, resize, crop, penanganan aspect ratio, metadata, dan pelaporan progress. Tujuannya bukan membuat layanan online converter yang generik, melainkan engine konversi yang bisa dipanggil sebuah website sementara frontend tetap memegang kendali penuh atas cara file dipilih, ditampilkan, diunduh, di-cache, atau diunggah.

Alasan ketiga adalah dua runtime-nya. Engine yang sama harus bekerja di browser dan di Node.js, dan itu memaksa saya jujur soal mana yang layak berada di dalam modul WASM dan mana yang seharusnya berada di wrapper-nya.

Jadi project ini akhirnya menjadi jembatan praktis antara tiga hal yang ingin saya pahami dengan benar: image processing dengan Rust, packaging WebAssembly, dan integrasi frontend melalui npm.

Tech Stack yang Digunakan

  • Rust untuk conversion engine, dikompilasi ke wasm32-unknown-unknown.
  • wasm-bindgen untuk boundary JavaScript.
  • Entry wrapper ESM dan CommonJS, keduanya memakai satu file logic bersama supaya tidak bisa saling menyimpang.
  • TypeScript declaration untuk API JavaScript publik.
  • Distribusi lewat npm, dengan target Node.js 18 atau lebih baru.
  • image untuk decoding JPEG, PNG, dan WebP.
  • heic dengan fitur av1 untuk decoding HEIC, HEIF, dan AVIF.
  • jpeg-encoder untuk output JPEG.
  • png untuk output PNG.
  • zenwebp untuk output WebP.
  • ravif untuk output AVIF.
  • fast_image_resize untuk proses resize.
  • img-parts, kamadak-exif, dan little_exif untuk penyuntingan container dan parsing metadata.

Seluruh jalur decoding murni Rust. Tidak ada library C di dependency tree dan tidak ada fallback codec JavaScript, dan hanya dengan cara itulah jalur input HEIC dan AVIF bisa bertahan di target WebAssembly.

Package ini memakai lisensi AGPL-3.0-only. Ini bukan pilihan gaya: binary WASM yang didistribusikan memuat dependency dengan jalur lisensi AGPL atau komersial, sehingga package-nya harus mengikuti.

Model Package

Package ini bukan satu file WebAssembly besar. Proses build menghasilkan modul WASM terpisah untuk setiap target output:

  • modul JPEG
  • modul PNG
  • modul WebP
  • modul AVIF

Wrapper JavaScript hanya memuat modul yang sesuai dengan format output yang diminta, dan baru saat diminta. Satu hal yang perlu disebut apa adanya: setiap modul tetap membawa decoder set lengkap, karena format output mana pun bisa menerima input mana pun yang didukung. Pemisahan ini memangkas bobot encoder, bukan bobot decoder.

Package npm yang dihasilkan menyediakan dua gaya module:

  • index.js untuk import ESM.
  • index.cjs untuk require CommonJS.
  • index.d.ts untuk pengguna TypeScript.
  • shared.js untuk option mapping dan urutan pemanggilan yang dipakai kedua entry.
  • formats/<format>/esm/index_bg.wasm untuk penggunaan ESM di browser dan bundler.
  • formats/<format>/cjs/index_bg.wasm untuk penggunaan CommonJS.

Di Node.js, wrapper membaca byte WASM langsung dari file yang ikut dipaketkan. Di build browser, wrapper memakai URL asset WASM yang dihasilkan, kecuali aplikasi memberikan wasmSources secara eksplisit — dan opsi itu memang disediakan untuk framework yang tidak mau menyajikan file WASM dari node_modules.

API Publik

API JavaScript publiknya sengaja dibuat kecil. Enam fungsi sudah mencakup semuanya.

init(options?)

Menyiapkan konfigurasi wrapper dan bisa melakukan preload modul output tertentu, sehingga konversi pertama tidak menjadi pihak yang menanggung waktu startup modul. Fungsi ini juga menerima custom WASM source, baik per format maupun satu source untuk semuanya, bagi framework yang membutuhkan file WASM disajikan dari folder public atau static.

convertImage(fileBytes, ext, options)

Mengonversi satu buffer input dan mengembalikan byte hasil encoding.

convertImageWithInfo(fileBytes, ext, options, onProgress?)

Konversi yang sama, tetapi juga mengembalikan lebar dan tinggi final serta menerima callback progress. Nilai progress dikirim dari 0 sampai 1.

getImageDimensions(fileBytes, ext, format?)

Membaca dimensi gambar tanpa menjalankan konversi penuh.

extractMetadata(fileBytes, ext, format?)

Membaca metadata EXIF, XMP, IPTC, dan ICC yang ada di dalam buffer di memori.

getProjectVersion(format?)

Mengembalikan versi project yang tertanam di modul WASM yang sedang dimuat.

Opsi Konversi

Opsi yang tersedia menutup kebutuhan nyata caller di browser maupun Node.js:

  • format: format output, yaitu Jpeg, Png, WebP, atau Avif.
  • quality: berlaku untuk JPEG, WebP lossy, dan AVIF. Nilai defaultnya 75 dan dibatasi pada rentang 1100.
  • pngCompressed: kompresi PNG yang lebih tinggi.
  • lossless: mode lossless untuk WebP.
  • resize: mengaktifkan proses resize.
  • targetWidth dan targetHeight: ukuran target.
  • resizeLockAspectRatio: menjaga aspect ratio asli saat resize. Aktif secara default.
  • crop: mengaktifkan crop berbasis persen.
  • cropTop, cropBottom, cropLeft, dan cropRight: persentase crop dari 0 sampai 100. Nilai negatif, di luar rentang, NaN, dan tak hingga akan ditolak, bukan diam-diam dijadikan nol.
  • keepMetadata: mempertahankan metadata yang didukung pada output.
  • intent: kebijakan kualitas, yaitu Balanced, Archive, atau Social.

Hanya format yang wajib. Sisanya punya default di sisi engine, jadi mengirim opsi sebagian adalah cara pemakaian yang normal, bukan kasus khusus.

intent adalah opsi yang paling saya sukai, karena ia memindahkan satu keputusan dari caller ke engine. Daripada setiap aplikasi host membuat tafsirnya sendiri tentang arti “quality 85” untuk empat codec yang berbeda, intent cukup menyebut tujuannya dan engine yang memilih chroma, kecepatan encoder, serta kebijakan metadata yang cocok:

IntentKualitasChroma JPEGSpeed AVIFWebPPNGMetadata
Balancedsesuai input4:4:4 mulai 854tuning lossysesuai permintaantidak diubah
Archivesesuai inputselalu 4:4:42near-lossless mulai 95selalu maksimaltidak diubah
Socialdibatasi 90selalu subsampled6tuning lossysesuai permintaanfield privat dihapus

Balanced adalah default dan berperilaku sebagaimana yang diharapkan. Archive rela mengeluarkan waktu encoding demi menjaga fidelity. Social bergerak ke arah sebaliknya: membatasi kualitas, melakukan subsampling chroma, meng-encode dengan cepat, dan membuang metadata yang biasanya tidak ingin ikut terpublikasi bersama sebuah foto.

Nilai kualitas diteruskan apa adanya ke encoder. Tidak ada kurva kedua yang diam-diam memetakan ulang angka yang Anda kirim, jadi nilai yang Anda tulis adalah nilai yang diterima codec.

Cara Kerja

Alurnya dimulai di wrapper JavaScript. Wrapper menormalkan format output, menormalkan extension input, memetakan nama opsi JavaScript ke bentuk JSON yang dipakai Rust, lalu memuat modul WASM yang tepat. Hanya itu tugasnya. Semua keputusan konversi yang sebenarnya berada di Rust, sehingga kedua entry wrapper tidak mungkin berbeda perilaku.

Di sisi Rust, extension diubah menjadi satu nilai ternormalisasi sekali saja di boundary, dan semua keputusan routing berikutnya membaca nilai yang sama itu. Pipeline lalu menolak output HEIC, memulai deadline konversi, dan memvalidasi input: ukuran file, extension, dan magic bytes, sebelum apa pun di-decode. Gerbang yang sama berjalan di setiap entry point yang menerima byte dari caller, termasuk ekstraksi metadata dan pembacaan dimensi, bukan hanya pada konversi.

Setelah itu barulah pemeriksaan pixel budget, lalu decoding. JPEG, PNG, dan WebP melewati crate image. HEIC, HEIF, dan AVIF melewati decoder heic yang murni Rust. Input AVIF secara khusus memang harus lewat jalur itu, karena decoder AVIF yang umum bergantung pada library C yang sama sekali tidak bisa dibangun untuk WebAssembly.

Orientasi EXIF diterapkan setelah decode, sehingga foto yang tersimpan dalam keadaan terputar oleh kamera keluar dalam posisi benar. Tiga input ISOBMFF dilewati pada tahap ini, karena decoder-nya sudah membetulkan orientasi dan menerapkannya dua kali justru akan memutar gambar ke posisi yang salah.

Ketika pelestarian metadata diminta, metadata diekstrak sebelum transformasi apa pun. ICC color profile dibaca di jalur terpisah, supaya fidelity warna tetap terjaga bahkan saat penyalinan metadata dimatikan. Jika intent-nya Social, field privat dibuang pada tahap ini, sebelum encoding dan bukan sesudahnya, sehingga pemeriksaan integritas metadata milik engine membandingkan hasilnya dengan payload yang sudah dibersihkan dan tidak pernah menganggap penghapusan yang disengaja sebagai metadata yang hilang.

Selanjutnya engine menjalankan crop opsional, resize opsional, memeriksa ulang pixel budget terhadap dimensi final, lalu meng-encode. Hasilnya ditulis ke memori dan dikembalikan ke JavaScript. Tidak ada konsep path file di layer ini, dan memang tidak pernah ada.

Batasannya tetap dan terdokumentasi:

BatasNilai
Ukuran file maksimal256 MB
Dimensi maksimal per sisi16384
Pixel maksimal per frame8192 × 8192
Timeout konversi300 detik

Pixel budget adalah batas yang biasanya tersentuh lebih dulu, dan alasannya spesifik. WebAssembly berjalan di ruang alamat 32-bit yang oleh browser dipotong jauh di bawah batas teoretisnya, sementara pipeline menahan lebih dari satu frame sekaligus. Gambar yang terlalu besar harus kembali sebagai error yang bisa ditangkap dan ditampilkan aplikasi, bukan sebagai modul yang gugur dan tidak bisa diapa-apakan.

Pelaporan progress bersifat berbasis tahap. Ia memberi tahu bagian pipeline mana yang sedang berjalan, dan itulah yang dibutuhkan sebuah progress bar. Ini bukan penghitung byte atau scanline dari dalam codec, dan dokumentasinya menyebut hal itu terang-terangan alih-alih menyiratkan ketelitian yang tidak ada.

Perilaku Metadata

Ekstraksi dan pelestarian adalah dua hal berbeda. extractMetadata(...) melaporkan apa yang ada di dalam byte sumber. keepMetadata: true meminta pipeline menuliskan metadata yang didukung ke output.

Yang bisa dibawa masing-masing container output:

Format OutputEXIFXMPIPTCICC
JPEGYaYaYaYa
PNGYaYaYaYa
WebPYaYaTidakYa
AVIFYaYaTidakTidak

JPEG menulis EXIF dan XMP lewat segment APP1, IPTC lewat Photoshop APP13, dan ICC lewat APP2. PNG menulis metadata ke chunk PNG dan tambahan memproyeksikan sebagian field ke text chunk, supaya properti file di sistem operasi menampilkan sesuatu yang berguna. WebP menulis chunk EXIF, XMP, dan ICC di container RIFF; IPTC tidak punya chunk WebP native di package ini. AVIF mempertahankan EXIF dan XMP, dengan XMP disisipkan melalui penyuntingan box ISOBMFF secara langsung, dan tidak menjanjikan embedding ICC di sisi output.

Untuk input AVIF dan HEIC, metadata dibaca dari struktur ISOBMFF: item metadata untuk EXIF dan XMP, ditambah ICC dari box colr dengan tipe profile prof atau rICC. Bisa membaca ICC dari sumber tidak sama dengan bisa menuliskannya ke setiap output, dan tabel di atas adalah versi jujurnya.

Intent Social adalah tempat metadata berubah menjadi fitur privasi. Intent ini membuang koordinat GPS beserta altitude, timestamp, map datum, serta telemetri arah, kecepatan, dan track yang biasanya menyertainya; nomor seri kamera dan lensa, termasuk namespace XMP milik vendor yang diam-diam menduplikasinya; nama pemilik kamera; image unique ID; dan dataset lokasi IPTC untuk kota, sublocation, provinsi, kode negara, dan negara. Data kepengarangan, hak cipta, deskripsi, dan eksposur tetap dipertahankan, karena itulah field yang biasanya memang ingin dibawa seorang fotografer bersama gambarnya. ICC profile tidak disentuh; sebuah profil warna tidak menceritakan Anda ada di mana atau memotret dengan body apa.

Setiap field tersebut dideklarasikan satu kali, di dalam satu registry yang dibaca bersama oleh jalur EXIF, XMP, IPTC, dan text PNG. Bagian itulah yang akan saya pertahankan kalau ditanya saat review: janji privasi yang tersebar di beberapa daftar yang dirawat manual adalah janji yang cepat atau lambat berhenti benar di salah satu daftarnya.

Contoh Penggunaan

import { convertImage, init } from "@walujanle/image-converter-wasm";

await init({ preload: ["Jpeg"] });

const input = new Uint8Array(await file.arrayBuffer());
const output = await convertImage(input, ".png", {
  format: "Jpeg",
  quality: 82,
  resize: true,
  targetWidth: 1600,
  resizeLockAspectRatio: true,
  keepMetadata: true,
  intent: "Social",
});

const blob = new Blob([output], { type: "image/jpeg" });

Extension dinormalkan, jadi png, .png, dan PNG sama-sama diterima selama byte-nya memang format tersebut.

Ketika Anda juga membutuhkan dimensi dan progress bar:

const result = await convertImageWithInfo(
  input,
  "heic",
  { format: "Avif", quality: 70, intent: "Archive" },
  (progress) => setPercent(Math.round(progress * 100)),
);

console.log(result.width, result.height);

Pada aplikasi browser, kode seperti ini sebaiknya berada di jalur client-side: client component, hook yang khusus browser, atau web worker. Kode server-side rendering tidak boleh mengasumsikan API file browser tersedia.

Catatan Build dan Release

Proses build-nya adalah skrip Node, sehingga Linux, macOS, dan Windows menjalankan langkah yang identik. build_wasm.sh dan build_wasm.bat hanyalah shim tipis di atas scripts/build.mjs, dan ketiganya menerima argumen yang sama. Skrip ini mengompilasi satu modul output dalam satu waktu, menjalankan wasm-bindgen, menyusun package npm di folder pkg/, lalu menyalin README npm ke tempatnya.

Build pertama meminta detail package Anda:

./build_wasm.sh --author "Your Name" --scope yourscope

Setelah itu nilainya diingat di file lokal yang di-gitignore, jadi build berikutnya cukup ./build_wasm.sh. Anda juga bisa membangun satu modul berdasarkan namanya, memaksa nama package tanpa scope, atau menghapus nilai yang tersimpan. Di CI, nilai yang sama bisa datang dari environment variable, dan nilai dari environment tidak pernah ditulis ke disk. Kalau tidak ada yang terkonfigurasi dan tidak ada terminal yang terhubung, build gagal dengan pesan yang jelas alih-alih menggantung di prompt yang tidak bisa dijawab siapa pun.

Dua perintah lain menopang proses rilis. node scripts/verify.mjs menjalankan seluruh checklist source — formatting, clippy dengan warning diperlakukan sebagai error, test suite, pemeriksaan terpisah per format output, dan pemeriksaan release WebAssembly — sebagai satu perintah. Setelah itu node test-npm.js melakukan smoke test terhadap package yang benar-benar dibangun, melalui entry ESM dan CommonJS, untuk semua format dan semua intent.

Repository ini sengaja menyimpan dua README:

  • README.md untuk maintainer yang membaca source repository.
  • js-wrapper/README.npm.md untuk orang yang memasang package dari npm.

Keduanya melayani pembaca yang benar-benar berbeda. Maintainer butuh detail build dan arsitektur; pengguna package butuh instalasi, API, runtime, dan catatan bundler. Menggabungkannya hanya akan membuat keduanya lebih buruk.

Ada juga jalur build threaded yang opsional. Jalur itu baru layak diaktifkan ketika host environment memang siap untuk WebAssembly threaded, yang berarti SharedArrayBuffer beserta header COOP dan COEP.

Batasan

  • HEIC dan HEIF hanya bisa di-decode. Tidak ada output HEIC, dan belum akan ada sampai muncul encoder murni Rust yang lolos audit lisensi.
  • TIFF bukan format input atau output publik di sini. Parsing TIFF internalnya ada untuk membaca payload EXIF, bukan untuk menjadi codec TIFF.
  • Konversinya berjalan di memori, jadi gambar besar memang membutuhkan memori nyata. Pixel budget 8192 × 8192 adalah batas praktisnya, dan angka itu lebih rendah daripada kesan yang diberikan batas per sisi.
  • Pemilihan file, preview, penyimpanan, dan download adalah tugas aplikasi host.
  • Progress bersifat berbasis tahap, bukan internal codec.
  • Output AVIF tidak menyematkan ICC profile.
  • Output WebP dan AVIF tidak membawa IPTC, karena container tersebut tidak punya jalur IPTC native di package ini.
  • Build WASM threaded membutuhkan dukungan dari host: SharedArrayBuffer, COOP, dan COEP.
  • Edge runtime bukan target yang didokumentasikan.

Lisensi

Image Converter WASM dirilis dengan lisensi AGPL-3.0-only.

Anda bisa mempelajari, memodifikasi, dan mendistribusikan ulang project ini sesuai ketentuan AGPL. Aplikasi yang mendistribusikan package ini atau menyediakannya melalui jaringan perlu memenuhi kewajiban AGPL atau mengatur jalur lisensi kompatibel yang terpisah. Alasan lisensinya tidak bisa begitu saja menjadi MIT cukup konkret: binary WASM yang didistribusikan memuat heic dan zenwebp, keduanya ditawarkan sebagai AGPL atau komersial, dan heic adalah decoder di balik seluruh input HEIC, HEIF, dan AVIF, sehingga kewajiban itu menjangkau setiap modul di dalam package.

Proyek Terbaru