Pendahuluan
Pada konferensi Google I/O di San Francisco tanggal 10 Mei 2011, kami mengumumkan bahwa bahasa Go tersedia dalam Google App Engine. Go adalah bahasa pertama dalam App Engine yang di compile langsung ke kode mesin, yang membuatnya menjadi pilihan yang bagus untuk pekerjaan-pekerjaan yang membutuhkan CPU seperti manipulasi gambar.
Dengan semangat, kami mendemokan sebuah program bernama Moustachio yang mempermudah mengubah sebuah gambar seperti berikut:
dengan menambahkan sebuah kumis sehingga menghasilkan:
Semua pemrosesan grafis, termasuk penambahan kumis seperti contoh di atas, dilakukan oleh sebuah program Go yang berjalan di App Engine. (Sumber kode tersedia di proyek appengine-go.)
Walaupun semua gambar di web umumnya adalah JPEG, ada banyak format lain yang beredar, dan akan lebih baik lagi bagi Moustachio untuk dapat menerima gambar yang diunggah dalam beberapa format tersebut. Dekoder untuk JPEG dan PNG telah ada dalam pustaka gambar Go, namun format GIF tidak tersedia, jadi kami memutuskan untuk menulis sebuah dekoder GIF sebelum pengumuman tersebut. Dekoder GIF tersebut berisi beberapa bagian yang memperlihatkan bagaimana interface pada Go membuat beberapa permasalahan menjadi mudah untuk diselesaikan. Selanjutnya blog ini menjelaskan beberapa contoh penggunaan interface pada Go.
Format GIF
Pertama, tur singkat dari format GIF. Berkas gambar GIF adalah palette, yaitu, setiap nilai piksel adalah sebuah indeks ke sebuah peta warna yang ada di dalam berkas. Format GIF diciptakan pada saat layar tidak lebih dari 8 bit per piksel, dan sebuah peta warna digunakan untuk mengonversi sekumpulan nilai piksel tersebut menjadi tiga nilai RGB (merah, hijau, biru) untuk ditampilkan ke layar. (Hal ini terbalik dengan JPEG, misalnya, yang tidak memiliki peta warna karena penulisan JPEG merepresentasikan sinyal warna yang berbeda secara terpisah.)
Sebuah gambar GIF dapat berisi dari 1 sampai 8 bit per piksel, secara inklusif, namun 8 bit per piksel adalah yang paling umum.
Secara sederhana, sebuah berkas GIF berisi header yang mendefinisikan kedalaman piksel dan dimensi gambar, peta warna (256 RGB rangkap tiga untuk sebuah gambar 8-bit), dan data piksel. Data piksel disimpan sebagai urutan bit-bit satu-dimensi, dikompres menggunakan algoritme LZW, yang cukup efektif untuk grafik buatan komputer walaupun tidak cukup bagus untuk gambar foto. Data yang dikompres kemudian dibagi menjadi blok-blok yang panjangnya dibatasi oleh sebuah byte yang merepresentasikan jumlahnya (0-255) diikuti dengan data:
Membaca data piksel
Untuk membaca data piksel GIF dengan Go, kita dapat menggunakan dekompresi LZW
dari paket compress/lzw
.
Paket tersebut memiliki fungsi NewReader
yang mengembalikan sebuah objek
yang "memenuhi pembacaan dengan melakukan dekompresi data yang dibaca dari r":
func NewReader(r io.Reader, order Order, litWidth int) io.ReadCloser
Argumen order
mendefinisikan urutan penulisan bit dan argumen litWidth
yaitu ukuran word dalam bit, yang mana dalam berkas GIF berarti kedalaman
piksel, biasanya 8.
Namun kita tidak bisa mengirim berkas input sebagai argumen pertama dari
NewReader
karena dekompresi membutuhkan seurutan byte tetapi data GIF
berbentuk seurutan blok-blok yang harus dibuka terlebih dahulu.
Untuk mengatasi masalah ini, kita dapat membungkus input io.Reader
dengan
semacam kode untuk membuka blok tersebut, dan membuat kode tersebut
mengimplementasikan Reader
kembali.
Dengan kata lain, kita simpan kode yang membuka blok ke dalam method Read
dari tipe yang baru, yang kita sebut blockReader
.
Berikut struktur data dari blockReader
.
type blockReader struct { r reader // Sumber input; mengimplementasikan io.Reader dan io.ByteReader. slice []byte // Buffer dari data yang belum dibaca. tmp [256]byte // Penyimpanan untuk slice. }
Pembaca, r, akan menjadi sumber dari data gambar, bisa jadi sebuah berkas
atau koneksi HTTP.
Field slice
dan tmp
akan digunakan untuk mengatur pembukaan blok.
Berikut seluruh method Read
.
Kode berikut adalah contoh bagus dari penggunaan slice dan array dalam Go.
1 func (b *blockReader) Read(p []byte) (int, os.Error) { 2 if len(p) == 0 { 3 return 0, nil 4 } 5 if len(b.slice) == 0 { 6 blockLen, err := b.r.ReadByte() 7 if err != nil { 8 return 0, err 9 } 10 if blockLen == 0 { 11 return 0, os.EOF 12 } 13 b.slice = b.tmp[0:blockLen] 14 if _, err = io.ReadFull(b.r, b.slice); err != nil { 15 return 0, err 16 } 17 } 18 n := copy(p, b.slice) 19 b.slice = b.slice[n:] 20 return n, nil 21 }
Baris 2-4 adalah pemeriksaan: jika tidak ada tempat untuk menyimpan data, kembalikan nol. Hal ini seharusnya tidak pernah terjadi, namun lebih baik berjaga-jaga.
Baris 5 memeriksa apakah ada data yang tersisa dari pemanggilan sebelumnya
dengan memeriksa panjang dari b.slice
.
Jika tidak ada yang tersisa, slice akan memiliki panjang nol dan kita harus
membaca blok selanjutnya dari r
.
Sebuah blok GIF dimulai dengan sebuah byte yang berisi jumlah byte dalam blok, yang dibaca pada baris 6. Jika jumlahnya nol, GIF mendefinisikannya sebagai blok terakhir, sehingga kita dapat mengembalikan EOF pada baris 11.
Sekarang kita tahu harus membaca sejumlah blockLen
byte, jadi kita isi
b.slice
dengan byte dari b.tmp
dan menggunakan fungsi bantuan
io.ReadFull
untuk membaca keseluruhan blok data.
Fungsi tersebut akan mengembalikan error jika tidak bisa membaca sejumlah
blockLen
, yang seharusnya tidak pernah terjadi.
Jika tidak error kita punya sejumlah blockLen
yang siap dibaca di dalam
b.slice
.
Baris 18-19 menyalin data dari b.slice
ke buffer yang dikirim.
Kita mengimplementasikan Read
, bukan ReadFull
, sehingga kita boleh
mengembalikan jumlah byte yang kurang dari jumlah yang diminta.
Caranya cukup mudah: kita cukup menyalin data dari b.slice
ke buffer
yang dikirim (p
), dan kembalian dari copy
yaitu jumlah byte yang disalin.
Kemudian kita potong b.slice
untuk menghapus n
byte yang telah disalin,
siap untuk pemanggilan selanjutnya.
Teknik yang sangat bagus dalam pemrograman Go yaitu menggabungkan sebuah slice
(b.slice
) dengan sebuah array (b.tmp
).
Dalam kasus ini, artinya tipe blockReader
tidak pernah melakukan alokasi.
Hal ini juga berarti kita tidak perlu menyimpan penghitungan (karena secara
implisit ada sebagai panjang dari slice), dan fungsi bawaan copy
menjamin
operasi penyalinan tidak pernah lebih dari panjang yang tersedia.
(Untuk lebih lanjut tentang slice, lihat
artikel berikut dari Blog Go.)
Dengan tipe blockReader
, kita dapat membaca blok-blok dari seurutan data
gambar hanya dengan membungkus pembaca input, katakanlah sebuah berkas,
seperti berikut:
deblockingReader := &blockReader{r: imageFile}
Pembungkusan tersebut menjadikan seurutan gambar GIF yang terbagi dalam
blok-blok menjadi urutan byte sederhana yang dapat diakses dengan memanggil
method Read
pada blockReader
.
Merangkai semuanya
Dengan implementasi blockReader
dan kompresi LZW yang tersedia dalam
pustaka, kita punya semua bagian-bagian yang dibutuhkan untuk mendekode
seurutan data gambar.
Kita rangkai semuanya dengan kode berikut:
lzwr := lzw.NewReader(&blockReader{r: d.r}, lzw.LSB, int(litWidth)) if _, err = io.ReadFull(lzwr, m.Pix); err != nil { break }
Itu saja.
Baris pertama membuat sebuah blockReader
dan mengirim ke lzw.NewReader
untuk membuat sebuah pendekompresi.
Di sini d.r
adalah io.Reader
yang menyimpan data gambar,
lzw.LSB
mendefinisikan urutan byte untuk dekompresi LZW, dan litWidth
adalah kedalaman piksel.
Dari pendekompresi, baris kedua memanggil io.ReadFull
untuk mendekompresi
data dan menyimpannya ke dalam gambar, m.Pix
.
Saat ReadFull
selesai, data gambar telah didekompresi dan disimpan dalam
gambar, m
, siap untuk ditampilkan.
Kode tersebut bekerja untuk pertama kalinya.
Kita dapat menghilangkan variabel sementara lzwr
dengan memindahkan
pemanggilan NewReader
ke dalam daftar argumen untuk ReadFull
, seperti saat
kita membuat blockReader
di dalam pemanggilan NewReader
, namun hal
tersebut terlalu memampatkan banyak kode dalam satu baris.
Kesimpulan
Interface pada Go mempermudah membangun perangkat lunak dengan merangkai
bagian-bagian seperti contoh di atas membentuk suatu struktur data.
Dalam contoh di atas, kita mengimplementasikan pembacaan GIF dengan merangkai
sebuah pembaca blok dan sebuah pendekompresi menggunakan interface
io.Reader
, analogi ini sama dengan pipeline pada Unix.
Dan juga, kita menulis pembuka blok sebagai sebuah implementasi (implisit)
dari sebuah interface Reader
, tanpa membutuhkan deklarasi atau kode tambahan
supaya sesuai dengan pipeline (jalur) pemrosesan.
Sangat sulit mengimplementasikan dekoder dengan singkat namun tetap bersih dan
aman dalam kebanyakan bahasa pemrograman, namun mekanisme interface ditambah
dengan beberapa konvensi membuatnya tampak natural dalam Go.
Implementasi ini layak mendapatkan gambar lain, kali ini sebuah GIF: