Vì sao mình chọn Pillow để gánh vác đống việc lặt vặt?
Python từ lâu đã là ‘cánh tay phải’ giúp mình xử lý đống task lặp đi lặp lại, từ chạy script deploy đến nhận cảnh báo server. Có lần mình phải vật lộn với kho ảnh khổng lồ hơn 50.000 tấm cho một sàn e-commerce. Việc ngồi mở từng file bằng Photoshop để resize rồi chèn logo đúng là một cực hình. Sau 6 tháng để script tự ‘cày’ trên production, mình khẳng định Pillow (nhánh fork của PIL) là thư viện ổn định nhất để giải quyết mớ hỗn độn này.
Thư viện này không chỉ đọc/ghi file siêu nhanh mà còn chạy cực kỳ êm trên server Linux. Dưới đây là những ‘chiêu’ mình đã dùng để giữ hệ thống chạy phăm phăm suốt nửa năm qua.
Bắt đầu nhanh: Xử lý ảnh cơ bản trong 1 nốt nhạc
Đầu tiên, cứ cài thư viện vào máy cái đã. Mở terminal lên và gõ:
pip install Pillow
Đây là đoạn code chạy ngay để bạn ‘thu nhỏ’ một tấm ảnh bất kỳ:
from PIL import Image
# Mở file ảnh
with Image.open("input.jpg") as img:
# Ép về kích thước 800x600
resized_img = img.resize((800, 600))
resized_img.save("output.jpg")
print("Xong! Quá nhanh quá nguy hiểm.")
Nhìn đơn giản vậy thôi, nhưng để script chạy ‘ngon’ trên server thực tế và không làm hỏng ảnh, bạn cần chú ý vài chi tiết kỹ thuật dưới đây.
Giải thích các kỹ thuật ‘xương máu’ khi làm thực tế
1. Resize ảnh không lo bị méo (Aspect Ratio)
Nếu bạn dùng hàm resize() và truyền vào một con số cứng nhắc, ảnh rất dễ bị ‘lùn đi’ hoặc ‘gầy đi’ do sai tỷ lệ. Tin mình đi, khách hàng sẽ không thích điều này đâu. Thay vào đó, mình luôn ưu tiên dùng phương thức thumbnail().
Hàm này sẽ thay đổi kích thước ảnh ngay tại chỗ và tự tính toán để ảnh nằm gọn trong khung mà vẫn giữ đúng tỷ lệ gốc:
with Image.open("landscape.jpg") as img:
max_size = (1200, 1200)
img.thumbnail(max_size, Image.Resampling.LANCZOS)
img.save("landscape_optimized.jpg")
Lưu ý nhỏ: Luôn dùng Image.Resampling.LANCZOS. Dù tốn thêm vài mili giây xử lý nhưng chất lượng ảnh đầu ra sắc nét hơn hẳn so với các bộ lọc rẻ tiền như NEAREST.
2. Chèn Watermark: Bảo vệ bản quyền hàng loạt
Việc chèn logo vào ảnh mà vẫn giữ được độ trong suốt (transparency) yêu cầu bạn phải hiểu về kênh Alpha. Đoạn code dưới đây mình đã tính toán để logo luôn nằm ở góc phải, cách lề một khoảng cố định dù ảnh gốc to hay nhỏ.
def add_watermark(main_image_path, watermark_path, output_path):
main_img = Image.open(main_image_path).convert("RGBA")
watermark = Image.open(watermark_path).convert("RGBA")
# Resize logo chiếm khoảng 20% chiều ngang ảnh chính
w_width, w_height = watermark.size
main_width, main_height = main_img.size
new_width = int(main_width * 0.2)
new_height = int(w_height * (new_width / w_width))
watermark = watermark.resize((new_width, new_height), Image.Resampling.LANCZOS)
# Vị trí góc dưới bên phải, cách lề 20px
position = (main_width - new_width - 20, main_height - new_height - 20)
overlay = Image.new("RGBA", main_img.size, (0, 0, 0, 0))
overlay.paste(watermark, position)
# Trộn layer và lưu file
combined = Image.alpha_composite(main_img, overlay)
combined.convert("RGB").save(output_path, "JPEG", quality=90)
Sử dụng alpha_composite giúp các đường viền của logo trông mịn màng, không bị răng cưa khó chịu như khi dùng hàm paste thông thường.
3. ‘Ép cân’ ảnh để tối ưu SEO
Ảnh nặng vài MB là ‘sát thủ’ diệt SEO và làm tốn băng thông. Dự án mình từng làm đã giảm được tổng dung lượng lưu trữ từ 200GB xuống còn chưa đầy 60GB nhờ áp dụng đúng 3 quy tắc này:
- Quality 85: Đây là ‘điểm ngọt’ (sweet spot). Dung lượng giảm 70% nhưng mắt thường gần như không thấy khác biệt.
- Optimize=True: Buộc Pillow quét lại bảng màu để nén thêm một lần nữa trước khi lưu.
- Chuyển sang WebP: Định dạng này giúp ảnh nhẹ hơn JPEG khoảng 30-50% mà vẫn giữ nguyên độ nét.
# Nén cực sâu với định dạng WebP
img.save("optimized.webp", "WEBP", quality=80, method=6)
Xử lý hàng loạt (Batch Processing)
Trong thực tế, chẳng ai đi chạy script cho từng file. Mình thường kết hợp với pathlib để quét sạch sành sanh một thư mục ảnh chỉ trong vài giây:
from pathlib import Path
def process_all_images(input_dir, output_dir):
path = Path(input_dir)
output_path = Path(output_dir)
output_path.mkdir(exist_ok=True)
for img_file in path.glob("*.jpg"):
try:
with Image.open(img_file) as img:
# Thực hiện các bước xử lý...
target = output_path / f"done_{img_file.name}"
img.save(target, quality=85, optimize=True)
print(f"Đã xong: {img_file.name}")
except Exception as e:
print(f"Lỗi file {img_file}: {e}")
Những ‘hố tử thần’ cần tránh
Sau nửa năm ‘nuôi’ tool này trên server, mình rút ra 3 bài học xương máu:
- Lỗi xoay ảnh (EXIF): Ảnh chụp từ iPhone thường bị xoay ngang khi mở bằng Pillow. Hãy dùng
ImageOps.exif_transposeđể sửa lỗi này ngay lập tức. - Cẩn thận với RAM: Nếu xử lý ảnh trên 10.000px, Pillow sẽ ‘ngốn’ sạch RAM của bạn. Luôn dùng câu lệnh
withđể đóng file ngay khi xong việc. - Quên convert màu: Đừng bao giờ lưu trực tiếp ảnh có độ trong suốt (RGBA) sang định dạng JPEG nếu không muốn script bị văng lỗi. Hãy luôn
convert("RGB")trước khi lưu JPEG.
Tóm lại, tự động hóa xử lý ảnh không hề khó. Chỉ với vài dòng code Python và thư viện Pillow, bạn đã có thể tự xây dựng một hệ thống chuyên nghiệp, tiết kiệm hàng chục triệu đồng chi phí thuê các dịch vụ SaaS bên ngoài.

