Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Android

Cách chuyển đổi ảnh thành Base64 String trên Android

Giới thiệu

Chuyển ảnh (Bitmap) thành Base64 string dùng để: gửi lên server qua JSON, lưu vào database/text, chia sẻ qua Intent, embed trong HTML/CSS. Bài viết hướng dẫn encode/decode Base64 hiệu quả, tránh OOM.

Các bước thực hiện

Bước 1: Tạo dự án mới

Android Studio → File → New Project → Empty View Activity.

Bước 2: Thiết kế giao diện (res/layout/activity_main.xml)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android"
    xmlns:tools="https://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="24dp"
    tools:context=".MainActivity">

    <ImageView
        android:id="@+id/ivSource"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:layout_gravity="center"
        android:scaleType="centerCrop"
        android:src="@drawable/sample_image"
        android:contentDescription="Ảnh gốc" />

    <Button
        android:id="@+id/btnEncode"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="24dp"
        android:text="Encode sang Base64" />

    <Button
        android:id="@+id/btnDecode"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="12dp"
        android:text="Decode về Bitmap" />

    <TextView
        android:id="@+id/tvBase64"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:layout_marginTop="16dp"
        android:padding="12dp"
        android:background="@android:color/darker_gray"
        android:textSize="12sp"
        android:scrollbars="vertical"
        tools:text="Base64 string sẽ hiển thị ở đây..." />

    <ImageView
        android:id="@+id/ivDecoded"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:layout_gravity="center"
        android:layout_marginTop="16dp"
        android:scaleType="centerCrop"
        android:visibility="gone"
        android:contentDescription="Ảnh decode" />
</LinearLayout>

Bước 3: Utility Class (Base64Utils.kt)

package com.example.base64image

import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.Base64
import java.io.ByteArrayOutputStream

object Base64Utils {

    /**
     * Encode Bitmap thành Base64 String
     * @param bitmap Bitmap nguồn
     * @param format Định dạng nén (JPEG/PNG)
     * @param quality Chất lượng 0-100 (chỉ áp dụng JPEG)
     * @return Base64 string (không có prefix "data:image/...;base64,")
     */
    @Throws(Exception::class)
    fun encodeToBase64(
        bitmap: Bitmap,
        format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG,
        quality: Int = 90
    ): String {
        val byteArrayOutputStream = ByteArrayOutputStream()
        bitmap.compress(format, quality, byteArrayOutputStream)
        val byteArray = byteArrayOutputStream.toByteArray()
        return Base64.encodeToString(byteArray, Base64.NO_WRAP) // NO_WRAP: không xuống dòng
    }

    /**
     * Decode Base64 String thành Bitmap
     * @param base64String Base64 string (có thể có hoặc không có prefix data:image/...;base64,)
     * @return Bitmap hoặc null nếu lỗi
     */
    fun decodeFromBase64(base64String: String): Bitmap? {
        var cleanBase64 = base64String.trim()
        
        // Loại bỏ prefix "data:image/...;base64," nếu có
        if (cleanBase64.contains("base64,")) {
            cleanBase64 = cleanBase64.substringAfter("base64,")
        }
        
        return try {
            val decodedBytes = Base64.decode(cleanBase64, Base64.NO_WRAP)
            BitmapFactory.decodeByteArray(decodedBytes, 0, decodedBytes.size)
        } catch (e: Exception) {
            e.printStackTrace()
            null
        }
    }

    /**
     * Encode file ảnh trực tiếp thành Base64 (không load Bitmap vào memory trước)
     * Tiết kiệm memory cho ảnh lớn
     */
    @Throws(Exception::class)
    fun encodeFileToBase64(filePath: String): String {
        val bytes = java.io.File(filePath).readBytes()
        return Base64.encodeToString(bytes, Base64.NO_WRAP)
    }
}

Bước 4: MainActivity.kt

package com.example.base64image

import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.example.base64image.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private var originalBitmap: Bitmap? = null
    private var base64String: String = ""

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // Load bitmap gốc từ drawable
        originalBitmap = BitmapFactory.decodeResource(resources, R.drawable.sample_image)

        binding.btnEncode.setOnClickListener { encodeToBase64() }
        binding.btnDecode.setOnClickListener { decodeFromBase64() }
    }

    private fun encodeToBase64() {
        originalBitmap?.let { bitmap ->
            try {
                // Encode
                base64String = Base64Utils.encodeToBase64(bitmap, Bitmap.CompressFormat.JPEG, 85)
                
                // Hiển thị (cắt ngắn nếu quá dài)
                val display = if (base64String.length > 500) {
                    "${base64String.substring(0, 500)}... [độ dài: ${base64String.length} chars]"
                } else base64String
                
                binding.tvBase64.text = display
                Toast.makeText(this, "Đã encode (${base64String.length} chars)", Toast.LENGTH_SHORT).show()
            } catch (e: Exception) {
                Toast.makeText(this, "Lỗi encode: ${e.message}", Toast.LENGTH_SHORT).show()
            }
        }
    }

    private fun decodeFromBase64() {
        if (base64String.isBlank()) {
            Toast.makeText(this, "Chưa có Base64 string", Toast.LENGTH_SHORT).show()
            return
        }

        val decodedBitmap = Base64Utils.decodeFromBase64(base64String)
        decodedBitmap?.let {
            binding.ivDecoded.setImageBitmap(it)
            binding.ivDecoded.visibility = View.VISIBLE
            Toast.makeText(this, "Decode thành công", Toast.LENGTH_SHORT).show()
        } ?: run {
            Toast.makeText(this, "Decode thất bại", Toast.LENGTH_SHORT).show()
        }
    }
}

Gửi Base64 lên Server (JSON)


// Retrofit + Gson
data class UploadRequest(
    val imageBase64: String,
    val fileName: String,
    val mimeType: String = "image/jpeg"
)

// API Interface
@POST("upload")
suspend fun uploadImage(@Body request: UploadRequest): Response

// Gọi
val request = UploadRequest(
    imageBase64 = base64String,
    fileName = "photo_${System.currentTimeMillis()}.jpg"
)
// Thêm prefix cho server dễ xử lý: "data:image/jpeg;base64,$base64String"

Tối ưu Memory cho ảnh lớn

Vấn đề: Bitmap lớn → ByteArrayOutputStream → byte[] → Base64 → **3-4x memory**.

Giải pháp 1: Resize trước khi encode


fun resizeBitmap(bitmap: Bitmap, maxDimension: Int): Bitmap {
    val scale = maxDimension.toFloat() / max(bitmap.width, bitmap.height)
    if (scale >= 1f) return bitmap
    return Bitmap.createScaledBitmap(bitmap, (bitmap.width * scale).toInt(), (bitmap.height * scale).toInt(), true)
}

Giải pháp 2: Stream-based encode (không load full bitmap)


// Chỉ dùng cho file, không cho Bitmap đã có trong memory
fun encodeFileStream(file: File): String {
    val buffer = ByteArrayOutputStream()
    file.inputStream().use { input ->
        input.copyTo(buffer)
    }
    return Base64.encodeToString(buffer.toByteArray(), Base64.NO_WRAP)
}

Giải pháp 3: Coroutine + IO Dispatcher


suspend fun encodeBitmapAsync(bitmap: Bitmap): String = withContext(Dispatchers.IO) {
    Base64Utils.encodeToBase64(bitmap)
}

Base64 Flags (android.util.Base64)

FlagMô tả
DEFAULTCó xuống dòng (line wrap 76 chars) - chuẩn RFC 2045
NO_WRAPKhuyên dùng - không xuống dòng, gọn cho JSON/network
NO_PADDINGBỏ padding '=' ở cuối
URL_SAFEThay '+' → '-', '/' → '_' (an toàn cho URL)
NO_WRAP or URL_SAFEKết hợp (dùng cho URL param)

Lưu ý quan trọng

  • Base64 tăng kích thước ~33% so với binary gốc.
  • Không dùng cho ảnh lớn (>1MB) trong JSON - dùng multipart upload (Retrofit @Part).
  • Luôn dùng Base64.NO_WRAP cho network/JSON.
  • Decode: Xử lý prefix data:image/jpeg;base64, nếu server trả về Data URI.
  • Bitmap.compress() với PNG: quality bị bỏ qua (lossless). JPEG: quality 0-100.
  • Test decode trên thiết bị thật - một số OEM modify Base64 behavior (hiếm).