Paket image/draw pada Go

Nigel Tao
29 September 2011

Catatan: blog ini membutuhkan pengetahuan dasar tentang konsep dan model gambar dalam domain komputer.

Pendahuluan

Paket image/draw mendefinisikan hanya satu operasi: menulis gambar sumber ke gambar tujuan, lewat sebuah gambar mask yang opsional. Operasi ini banyak gunanya dan dapat melakukan sejumlah pekerjaan manipulasi gambar yang umum secara elegan dan efisien.

Komposisi gambar dilakukan per piksel dengan gaya pustaka grafik Plan 9 dan ekstensi X Render. Model dari paket image/draw berdasarkan makalah klasik "Compositing Digital Images" oleh Porter dan Duff, dengan parameter tambahan mask: dst = (src IN mask) OP dst. Untuk mask yang sepenuhnya transparan, ia sama dengan formula asli Porter-Duff: dst = src OP dst. Dalam Go, gambar mask yang bernilai nil sama dengan gambar mask yang ukurannya tak terbatas dan sepenuhnya transparan.

Makalah Porter-Duff menjabarkan 12 operator komposisi, namun dengan mask yang eksplisit, hanya 2 darinya yang dibutuhkan: sumber-terhadap-tujuan dan sumber. Dalam Go, operator-operator tersebut direpresentasikan oleh konstan Over dan Src. Operator Over melakukan pelapisan dari gambar sumber terhadap gambar tujuan: perubahan terhadap gambar tujuan lebih sedikit bilamana sumber (setelah masking) lebih transparan (yaitu, memiliki nilai alpha yang kecil). Operator Src menyalin sumber (setelah masking) tanpa memperhatikan isi asli dari gambar tujuan. Untuk gambar sumber dan mask yang sepenuhnya transparan, kedua operator tersebut menghasilkan keluaran yang sama, namun menggunakan operator Src biasanya lebih cepat.

Penyelarasan Geometris

Komposisi membutuhkan pengasosiasian piksel tujuan dengan piksel sumber dan mask. Hal ini membutuhkan gambar tujuan, sumber, dan mask, dan sebuah operator komposisi, namun ia juga harus menspesifikasikan petak mana dari setiap gambar yang akan digunakan. Tidak setiap operasi gambar harus menulis ke seluruh petak tujuan: saat memperbarui gambar animasi, akan lebih efisien bila hanya menulis bagian dari gambar yang telah berubah. Tidak setiap operasi gambar harus membaca seluruh petak sumber: saat menggunakan sebuah sprite yang menggabungkan banyak gambar-gambar kecil menjadi satu gambar besar, hanya sebuah bagian gambar yang diperlukan. Tidak setiap operasi gambar harus membaca seluruh mask: gambar mask yang berupa kumpulan glyph dari fon (font) mirip dengan sebuah sprite. Maka, operasi gambar membutuhkan tiga petak, satu untuk setiap gambar. Secara setiap petak memiliki panjang dan lebar yang sama, maka cukup mengirim petak tujuan r dan dua titik sp (source point) dan mp (mask point): petak sumber sama dengan r yang di-"terjemahkan" sehingga r.Min pada gambar tujuan sejajaj dengan sp pada gambar sumber, dan hal yang sama berlaku untuk mp. Petak tersebut juga dipotong untuk setiap gambar yang dibatasi oleh ruang koordinat masing-masing.

go-imagedraw-package_20

Fungsi DrawMask() menerima tujuh argumen, namun argumen mask dan mask-point biasanya tidak diperlukan, sehingga fungsi Draw() menerima hanya lima:

// Draw calls DrawMask with a nil mask.
func Draw(dst Image, r image.Rectangle, src image.Image, sp image.Point, op Op)
func DrawMask(dst Image, r image.Rectangle, src image.Image, sp image.Point,
	mask image.Image, mp image.Point, op Op)

Gambar tujuan haruslah mutable (bisa diubah), sehingga paket image/draw mendefinisikan sebuah interface draw.Image yang memiliki sebuah method Set().

type Image interface {
	image.Image
	Set(x, y int, c color.Color)
}

Mengisi sebuah Petak

Untuk mengisi sebuah petak dengan warna tunggal, gunakan sumber image.Uniform. Tipe ColorImage menerjemahkan sebuah Color sebagai sebuah Image berukuran tak terbatas dari warna tersebut. Bagi yang terbiasa dengan rancangan pustaka gambar pada sistem operasi Plan 9, tidak diperlukan secara eksplisit "pengulangan bit" dalam tipe gambar Go yang berbasis slice; konsepnya dilebur ke dalam Uniform.

// image.ZP adalah titik nol -- seluruh gambar asli.
draw.Draw(dst, r, &image.Uniform{c}, image.ZP, draw.Src)

Untuk menginisiasi sebuah gambar baru yang semuanya biru:

m := image.NewRGBA(image.Rect(0, 0, 640, 480))
blue := color.RGBA{0, 0, 255, 255}
draw.Draw(m, m.Bounds(), &image.Uniform{blue}, image.ZP, draw.Src)

Untuk mereset sebuah gambar menjadi transparan (atau hitam, jika model warna pada gambar tujuan tidak dapat merepresentasikan transparansi), gunakan image.Transparent, yang mana merupakan sebuah image.Uniform:

draw.Draw(m, m.Bounds(), image.Transparent, image.ZP, draw.Src)

go-imagedraw-package_2a

Menyalin sebuah Gambar

Untuk menyalin dari sebuah petak sr dari gambar sumber ke sebuah petak yang diawali dengan sebuah titik dp pada tujuan, ubah petak sumber menjadi ruang koordinat pada gambar tujuan:

r := image.Rectangle{dp, dp.Add(sr.Size())}
draw.Draw(dst, r, src, sr.Min, draw.Src)

Atau:

r := sr.Sub(sr.Min).Add(dp)
draw.Draw(dst, r, src, sr.Min, draw.Src)

Untuk menyalin semua gambar sumber, gunakan sr = src.Bounds().

go-imagedraw-package_2b

Memindahkan sebuah Gambar

Memindahkan sebuah gambar yaitu menyalin petak gambar ke dirinya sendiri, dengan petak tujuan dan sumber yang berbeda. Gambar tujuan dan sumber yang saling menimpa adalah valid, seperti pada fungsi bawaan copy pada Go yang menangani penyalinan slice sumber dan tujuan yang saling timpa. Untuk menggeser sebuah gambar m dengan 20 piksel:

b := m.Bounds()
p := image.Pt(0, 20)
// Ingatlah bahwa walaupun argumen kedua adalah b,
// petak yang efektif lebih kecil disebabkan karena pemotongan.
draw.Draw(m, b, m, b.Min.Add(p), draw.Src)
dirtyRect := b.Intersect(image.Rect(b.Min.X, b.Max.Y-20, b.Max.X, b.Max.Y))

go-imagedraw-package_2c

Mengonversi Gambar ke RGBA

Hasil dari menerjemahkan sebuah format gambar belum tentu image.RGBA: men-decode sebuah GIF menghasilkan image.Paletted, men-decode sebuah JPEG menghasilkan ycbcr.YCbCr, dan hasil dari men-decode sebuah PNG bergantung pada data gambar. Untuk mengonversi gambar apa pun menjadi sebuah image.RGBA:

b := src.Bounds()
m := image.NewRGBA(image.Rect(0, 0, b.Dx(), b.Dy()))
draw.Draw(m, m.Bounds(), src, b.Min, draw.Src)

go-imagedraw-package_2d

Menggambar lewat sebuah Mask

Untuk menulis sebuah gambar lewat sebuah mask bundar dengan pusat p dan radius r:

type circle struct {
	p image.Point
	r int
}

func (c *circle) ColorModel() color.Model {
	return color.AlphaModel
}

func (c *circle) Bounds() image.Rectangle {
	return image.Rect(c.p.X-c.r, c.p.Y-c.r, c.p.X+c.r, c.p.Y+c.r)
}

func (c *circle) At(x, y int) color.Color {
	xx, yy, rr := float64(x-c.p.X)+0.5, float64(y-c.p.Y)+0.5, float64(c.r)
	if xx*xx+yy*yy < rr*rr {
		return color.Alpha{255}
	}
	return color.Alpha{0}
}

	draw.DrawMask(dst, dst.Bounds(), src, image.ZP, &circle{p, r}, image.ZP, draw.Over)

go-imagedraw-package_2e

Menggambar Font Glyphs

Untuk menggambar sebuah font glyph dalam warna biru dari titik p, gambarlah dengan sumber image.ColorImage dan sebuah mask image.Alpha. Demi kemudahan, kita tidak melakukan perbaikan posisi atau rendering sub-piksel apa pun, atau memperbaiki tinggi fon yang melewati garis.

src := &image.Uniform{color.RGBA{0, 0, 255, 255}}
mask := theGlyphImageForAFont()
mr := theBoundsFor(glyphIndex)
draw.DrawMask(dst, mr.Sub(mr.Min).Add(p), src, image.ZP, mask, mr.Min, draw.Over)

go-imagedraw-package_2f

Kinerja

Implementasi paket image/draw memperlihatkan bagaimana menyediakan sebuah fungsi manipulasi gambar untuk semua kebutuhan, namun tetap efisien untuk kasus-kasus umum. Fungsi DrawMask menerima argumen bertipe interface, namun langsung melakukan tipe assertion untuk memastikan argumennya adalah suatu tipe struct tertentu, yang berkorespondensi pada operasi-operasi umum seperti menulis sebuah image.RGBA ke gambar lainnya, atau menulis sebuah mask image.Alpha (seperti sebuah font glyph) menjadi gambar image.RGBA. Jika tipe assertion sukses, maka informasi tipe tersebut digunakan untuk menjalankan sebuah implementasi (operasi gambar) yang khusus. Jika assertion gagal, maka alur kode kembali menggunakan method At dan Set. Jalur-cepat yang berupa implementasi khusus adalah murni optimasi kinerja saja; gambar hasilnya akan sama saja (baik dengan jalur cepat atau biasa). Dalam praktiknya, hanya sejumlah kecil kasus-kasus spesial yang dibutuhkan untuk mendukung aplikasi-aplikasi tertentu.

Artikel Terkait