Trong quá trình phát triển một dự án backend liên quan đến trực quan hóa dữ liệu, tôi đã phải xử lý hàng loạt biểu tượng vector do người dùng tải lên. Hầu hết các biểu tượng này ở định dạng SVG, và yêu cầu là phải trích xuất các thông tin như kích thước, tọa độ đường dẫn, và màu sắc, sau đó lưu trữ vào cơ sở dữ liệu và tạo ảnh xem trước. Ban đầu, tôi nghĩ rằng việc này khá đơn giản, chỉ là phân tích XML thôi. Tuy nhiên, tôi đã gặp phải nhiều vấn đề phức tạp: một số file SVG không có thuộc tính chiều rộng/chiều cao rõ ràng, dữ liệu đường dẫn sử dụng tọa độ tương đối, và một số file khác lại nhúng các kiểu CSS. Sau vài tuần nghiên cứu và thử nghiệm, tôi đã tìm ra cách xử lý hiệu quả. Bài viết này sẽ chia sẻ những kinh nghiệm thực tế đó, hướng dẫn bạn cách phân tích SVG bằng Java một cách chi tiết.
Nếu bạn đang phát triển các ứng dụng yêu cầu xử lý đồ họa vector, ví dụ như hệ thống quản lý biểu tượng, tích hợp công cụ thiết kế, hoặc nền tảng phân tích dữ liệu, bài viết này chắc chắn sẽ hữu ích. Tôi sẽ bắt đầu với việc lựa chọn thư viện cơ bản, sau đó đi sâu vào xử lý các tình huống phức tạp, với các ví dụ code đầy đủ và có thể chạy được. Chúng ta sẽ không chỉ thảo luận về cách "phân tích" mà còn tập trung vào cách "trích xuất" những dữ liệu quan trọng, có giá trị cho nghiệp vụ của bạn.
1. Lựa chọn Công Cụ Phân Tích: Tổng Quan Từ Cơ Bản Đến Chuyên Sâu
Khi đối mặt với việc phân tích SVG, các nhà phát triển Java có một số lựa chọn, mỗi lựa chọn phù hợp với các kịch bản và chi phí khác nhau. Việc sử dụng trực tiếp trình phân tích DOM để viết XPath nghe có vẻ hấp dẫn, nhưng bạn có thể gặp khó khăn khi đối phó với SVG có không gian tên hoặc các đoạn script nhúng.
Apache Batik là một công cụ chuyên nghiệp trong lĩnh vực này. Nó không chỉ là một trình phân tích mà còn là một bộ công cụ SVG hoàn chỉnh. Tôi chọn Batik ban đầu vì dự án yêu cầu xác thực tính hợp lệ của các file SVG – một số file do người dùng tải lên vẫn hiển thị được nhưng thực tế lại chứa các thuộc tính không chuẩn. Batik có thể giúp bạn kiểm tra tất cả những điều này.
<!-- Dependency trong Maven -->
<dependency>
<groupId>org.apache.xmlgraphics</groupId>
<artifactId>batik-svggen</artifactId>
<version>1.14</version>
</dependency>
<dependency>
<groupId>org.apache.xmlgraphics</groupId>
<artifactId>batik-transcoder</artifactId>
<version>1.14</version>
</dependency>
Tuy nhiên, Batik có một vấn đề là "nặng". Cây phụ thuộc của nó khá sâu, nếu ứng dụng của bạn nhạy cảm với kích thước gói hoặc môi trường triển khai có giới hạn, bạn cần cân nhắc. Tôi đã từng gặp tình huống Batik tiêu tốn nhiều tài nguyên khi khởi tạo trong môi trường container với bộ nhớ hạn chế.
Jsoup là một lựa chọn khác, đặc biệt nếu bạn đã quen thuộc với API phân tích HTML của nó. Đối với việc trích xuất thông tin đơn giản – ví dụ, chỉ đọc thuộc tính width và height của thẻ <svg> – Jsoup hoàn toàn đủ dùng và API của nó rất thân thiện.
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import java.io.File;
import java.io.IOException;
public class ThongTinSVGDonGian {
public static void layKichThuocCơBan(File fileSVG) throws IOException {
Document taiLieu = Jsoup.parse(fileSVG, "UTF-8");
Element phanTuSVG = taiLieu.selectFirst("svg");
if (phanTuSVG != null) {
String giaTriViewBox = phanTuSVG.attr("viewBox");
String chieuRong = phanTuSVG.attr("width");
String chieuCao = phanTuSVG.attr("height");
System.out.println("Thông tin SVG từ Jsoup:");
System.out.println(" viewBox: " + (giaTriViewBox.isEmpty() ? "Không có" : giaTriViewBox));
System.out.println(" Width: " + (chieuRong.isEmpty() ? "Không có" : chieuRong));
System.out.println(" Height: " + (chieuCao.isEmpty() ? "Không có" : chieuCao));
// Nếu width là "100%" hoặc giá trị tương đối, cần xử lý thêm tùy ngữ cảnh
} else {
System.out.println("Không tìm thấy thẻ <svg> trong file.");
}
}
public static void main(String[] args) throws IOException {
// Tạo một file SVG giả định để kiểm tra
String svgContent = "<svg width='200px' height='150px' viewBox='0 0 200 150' xmlns='http://www.w3.org/2000/svg'><rect x='0' y='0' width='100%' height='100%' fill='blue'/></svg>";
File testFile = new File("test_simple.svg");
java.nio.file.Files.write(testFile.toPath(), svgContent.getBytes());
layKichThuocCơBan(testFile);
testFile.delete(); // Xóa file test sau khi chạy
}
}
Tuy nhiên, Jsoup hỗ trợ SVG ở mức "cố gắng hết sức". Nó sẽ không xác thực cú pháp SVG, cũng không hiểu các ma trận biến đổi phức tạp. Nếu bạn cần tính toán tọa độ thực tế của một đường dẫn sau khi đã bị xoay hoặc thu phóng, Jsoup sẽ không thể giúp được.
Trình phân tích XML thuần túy (DOM/SAX có sẵn trong JDK) là giải pháp nhẹ nhất. Nó không có bất kỳ phụ thuộc bổ sung nào, nhưng bạn cần tự xử lý mọi chi tiết. Bảng dưới đây so sánh sự khác biệt chính giữa ba giải pháp:
| Tiêu chí | Apache Batik | Jsoup | Trình phân tích XML JDK |
|---|---|---|---|
| Hỗ trợ chuẩn SVG | Hỗ trợ đầy đủ SVG 1.1/1.2 | Chỉ phân tích thẻ cơ bản | Chỉ phân tích cấu trúc XML |
| Dễ sử dụng API | Chuyên nghiệp nhưng hơi phức tạp | Rất thân thiện | Ở cấp độ thấp |
| Kích thước phụ thuộc | Lớn (nhiều module) | Nhỏ (một jar) | Không có phụ thuộc bổ sung |
| Khả năng hiển thị (Render) | Có hỗ trợ | Không hỗ trợ | Không hỗ trợ |
| Kịch bản phù hợp | Xử lý, xác thực, chuyển đổi SVG chuyên nghiệp | Trích xuất thuộc tính đơn giản nhanh chóng | Cực kỳ nhẹ, phân tích tùy chỉnh |
Mẹo: Nếu ứng dụng của bạn chỉ cần xử lý SVG không thường xuyên và định dạng dữ liệu có thể kiểm soát được, bắt đầu với Jsoup là nhanh nhất. Nhưng nếu xử lý SVG là chức năng cốt lõi, đặc biệt khi cần tạo, sửa đổi hoặc xác thực SVG, việc đầu tư học Batik là rất đáng giá.
Trong dự án thực tế, tôi đã áp dụng chiến lược kết hợp: phần lớn các công việc kiểm tra và trích xuất thuộc tính đơn giản được thực hiện nhanh chóng bằng Jsoup, chỉ khi cần phân tích sâu hoặc hiển thị thì mới gọi đến Batik. Điều này đảm bảo cả hiệu quả phát triển và khả năng linh hoạt trong các tình huống phức tạp.
2. Trích Xuất Dữ Liệu Cốt Lõi: Kích Thước, Đường Dẫn và Kiểu Dáng
Phân tích SVG không chỉ là đọc file, mà còn là trích xuất những thông tin có giá trị. Phần này chúng ta sẽ đi sâu vào ba khía cạnh dữ liệu thường xuyên cần thiết: kích thước, đường dẫn và kiểu dáng.
2.1 Xác định Chính Xác Thông Tin Kích Thước
Thông tin kích thước của SVG có thể tồn tại dưới nhiều dạng; việc đọc trực tiếp các thuộc tính width/height có thể trả về giá trị rỗng hoặc giá trị tương đối (như "100%"). Phương pháp đáng tin cậy hơn là ưu tiên kiểm tra thuộc tính viewBox.
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import java.io.File;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class TrinhTrichXuatKichThuocSVG {
public static class ThongTinKichThuoc {
private double chieuRong = 0;
private double chieuCao = 0;
private boolean coViewBox = false;
private double viewBoxX = 0, viewBoxY = 0, viewBoxRong = 0, viewBoxCao = 0;
// Constructor mặc định hoặc với các tham số cụ thể
public ThongTinKichThuoc() {}
// Getters và setters cho tất cả các thuộc tính
public double getChieuRong() { return chieuRong; }
public void setChieuRong(double chieuRong) { this.chieuRong = chieuRong; }
public double getChieuCao() { return chieuCao; }
public void setChieuCao(double chieuCao) { this.chieuCao = chieuCao; }
public boolean isCoViewBox() { return coViewBox; }
public void setCoViewBox(boolean coViewBox) { this.coViewBox = coViewBox; }
public double getViewBoxX() { return viewBoxX; }
public void setViewBoxX(double viewBoxX) { this.viewBoxX = viewBoxX; }
public double getViewBoxY() { return viewBoxY; }
public void setViewBoxY(double viewBoxY) { this.viewBoxY = viewBoxY; }
public double getViewBoxRong() { return viewBoxRong; }
public void setViewBoxRong(double viewBoxRong) { this.viewBoxRong = viewBoxRong; }
public double getViewBoxCao() { return viewBoxCao; }
public void setViewBoxCao(double viewBoxCao) { this.viewBoxCao = viewBoxCao; }
@Override
public String toString() {
return "Kích thước: " + chieuRong + "x" + chieuCao +
(coViewBox ? String.format(", viewBox: (%.1f, %.1f, %.1f, %.1f)", viewBoxX, viewBoxY, viewBoxRong, viewBoxCao) : "");
}
}
public static ThongTinKichThuoc trichXuatKichThuoc(File fileSVG) throws IOException {
Document taiLieu = Jsoup.parse(fileSVG, "UTF-8");
Element phanTuSVG = taiLieu.selectFirst("svg");
ThongTinKichThuoc thongTin = new ThongTinKichThuoc();
if (phanTuSVG == null) {
System.err.println("Không tìm thấy thẻ <svg> trong file.");
return thongTin;
}
// Ưu tiên kiểm tra viewBox
String giaTriViewBox = phanTuSVG.attr("viewBox");
if (!giaTriViewBox.isEmpty()) {
String[] cacPhan = giaTriViewBox.trim().split("[\\s,]+"); // Chia theo khoảng trắng hoặc dấu phẩy
if (cacPhan.length == 4) {
try {
thongTin.setCoViewBox(true);
thongTin.setViewBoxX(Double.parseDouble(cacPhan[0]));
thongTin.setViewBoxY(Double.parseDouble(cacPhan[1]));
thongTin.setViewBoxRong(Double.parseDouble(cacPhan[2]));
thongTin.setViewBoxCao(Double.parseDouble(cacPhan[3]));
// Nếu không có width/height rõ ràng, sử dụng kích thước viewBox làm kích thước logic
thongTin.setChieuRong(thongTin.getViewBoxRong());
thongTin.setChieuCao(thongTin.getViewBoxCao());
} catch (NumberFormatException e) {
System.err.println("Lỗi phân tích viewBox: " + giaTriViewBox + ". " + e.getMessage());
}
}
}
// Sau đó kiểm tra các thuộc tính width/height
String strChieuRong = phanTuSVG.attr("width");
String strChieuCao = phanTuSVG.attr("height");
if (!strChieuRong.isEmpty() && !strChieuCao.isEmpty()) {
double parsedWidth = phanTichDonVi(strChieuRong);
double parsedHeight = phanTichDonVi(strChieuCao);
if (parsedWidth > 0 && parsedHeight > 0) { // Chỉ ghi đè nếu giá trị hợp lệ
thongTin.setChieuRong(parsedWidth);
thongTin.setChieuCao(parsedHeight);
}
}
return thongTin;
}
// Phương thức hỗ trợ để loại bỏ đơn vị và chuyển đổi sang double
private static double phanTichDonVi(String giaTri) {
Pattern pattern = Pattern.compile("([0-9.]+)([a-zA-Z%]+)?");
Matcher matcher = pattern.matcher(giaTri.trim());
if (matcher.matches()) {
try {
return Double.parseDouble(matcher.group(1));
} catch (NumberFormatException e) {
return 0; // Trả về 0 nếu không thể phân tích số
}
}
return 0; // Trả về 0 nếu không khớp mẫu
}
public static void main(String[] args) throws IOException {
// Ví dụ 1: SVG với viewBox và width/height
String svg1 = "<svg width='300px' height='200px' viewBox='0 0 100 80' xmlns='http://www.w3.org/2000/svg'><rect x='0' y='0' width='100' height='80' fill='red'/></svg>";
File file1 = new File("svg_example1.svg");
java.nio.file.Files.write(file1.toPath(), svg1.getBytes());
System.out.println("File 1: " + trichXuatKichThuoc(file1));
file1.delete();
// Ví dụ 2: SVG chỉ có viewBox
String svg2 = "<svg viewBox='0 0 500 400' xmlns='http://www.w3.org/2000/svg'><circle cx='250' cy='200' r='100' fill='green'/></svg>";
File file2 = new File("svg_example2.svg");
java.nio.file.Files.write(file2.toPath(), svg2.getBytes());
System.out.println("File 2: " + trichXuatKichThuoc(file2));
file2.delete();
// Ví dụ 3: SVG với width/height tương đối (sẽ trả về 0 cho giá trị phân tích đơn vị)
String svg3 = "<svg width='100%' height='100%' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'><path d='M10 10 H90 V90 H10 Z' fill='purple'/></svg>";
File file3 = new File("svg_example3.svg");
java.nio.file.Files.write(file3.toPath(), svg3.getBytes());
System.out.println("File 3: " + trichXuatKichThuoc(file3));
file3.delete();
}
}
Ở đây có một số điểm cần lưu ý:
- Xử lý đơn vị: SVG hỗ trợ nhiều đơn vị như px, em, ex, %, cm, mm, in, pt, pc. Nếu nghiệp vụ yêu cầu giá trị pixel chính xác, bạn có thể cần một cơ chế chuyển đổi phức tạp hơn. Trong ví dụ trên, chúng ta chỉ trích xuất giá trị số.
- Giá trị phần trăm: Khi chiều rộng/chiều cao là "100%", điều đó thường có nghĩa là SVG sẽ tự động điều chỉnh kích thước để phù hợp với vùng chứa, lúc này kích thước thực tế phụ thuộc vào môi trường hiển thị.
- Ưu tiên của viewBox: Nhiều trình chỉnh sửa SVG (như Adobe Illustrator) chủ yếu dựa vào
viewBox, trong khiwidth/heightcó thể chỉ là "100%". Chúng ta ưu tiênviewBoxđể xác định kích thước logic của nội dung.
Lưu ý quan trọng: Nếu một file SVG không có cả
width/heightrõ ràng lẫnviewBox, thì file đó có thể không hoàn chỉnh hoặc được thiết kế như một khung vẽ vô hạn. Trong trường hợp này, bạn cần quyết định kích thước mặc định dựa trên kịch bản sử dụng thực tế của mình.
2.2 Khai thác Dữ liệu Đường Dẫn Chuyên Sâu
Đường dẫn (phần tử <path>) là một trong những phần phức tạp nhưng mạnh mẽ nhất của SVG. Một biểu tượng đơn giản có thể chỉ chứa vài hình dạng cơ bản, nhưng các thiết kế phức tạp hầu như luôn sử dụng đường dẫn. Việc trích xuất dữ liệu đường dẫn không chỉ đơn thuần là lấy thuộc tính d.
Chúng ta sẽ sử dụng Batik để phân tích dữ liệu đường dẫn thành các đối tượng có thể thao tác được, giúp chúng ta hiểu rõ hơn về các lệnh vẽ (move, line, curve, close path) và tọa độ liên quan.
import org.apache.batik.parser.PathHandler;
import org.apache.batik.parser.PathParser;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
public class PhanTichDuongDanSVG {
// Định nghĩa các loại phân đoạn đường dẫn
public interface PhanDoanDuongDan {
char layMaLenh(); // M, L, H, V, C, S, Q, T, A, Z
boolean laTuongDoi(); // true nếu là lệnh tương đối (chữ thường)
String toString();
}
// Lớp cơ sở cho các phân đoạn với tọa độ
public static abstract class PhanDoanCoToaDo implements PhanDoanDuongDan {
protected boolean tuongDoi;
protected float xKetThuc;
protected float yKetThuc;
public PhanDoanCoToaDo(boolean tuongDoi, float x, float y) {
this.tuongDoi = tuongDoi;
this.xKetThuc = x;
this.yKetThuc = y;
}
@Override
public boolean laTuongDoi() { return tuongDoi; }
}
// Các lớp cụ thể cho từng lệnh đường dẫn
public static class DiChuyenDen extends PhanDoanCoToaDo {
public DiChuyenDen(boolean tuongDoi, float x, float y) { super(tuongDoi, x, y); }
@Override public char layMaLenh() { return tuongDoi ? 'm' : 'M'; }
@Override public String toString() { return String.format("%c %.1f %.1f", layMaLenh(), xKetThuc, yKetThuc); }
}
public static class VeDuongThang extends PhanDoanCoToaDo {
public VeDuongThang(boolean tuongDoi, float x, float y) { super(tuongDoi, x, y); }
@Override public char layMaLenh() { return tuongDoi ? 'l' : 'L'; }
@Override public String toString() { return String.format("%c %.1f %.1f", layMaLenh(), xKetThuc, yKetThuc); }
}
public static class VeDuongThangNgang extends PhanDoanCoToaDo {
public VeDuongThangNgang(boolean tuongDoi, float x) { super(tuongDoi, x, 0); } // y không dùng
@Override public char layMaLenh() { return tuongDoi ? 'h' : 'H'; }
@Override public String toString() { return String.format("%c %.1f", layMaLenh(), xKetThuc); }
}
public static class VeDuongThangDoc extends PhanDoanCoToaDo {
public VeDuongThangDoc(boolean tuongDoi, float y) { super(tuongDoi, 0, y); } // x không dùng
@Override public char layMaLenh() { return tuongDoi ? 'v' : 'V'; }
@Override public String toString() { return String.format("%c %.1f", layMaLenh(), yKetThuc); }
}
// ... Cần định nghĩa thêm các lớp cho C, S, Q, T, A (các loại đường cong và cung tròn)
public static class DongDuongDan implements PhanDoanDuongDan {
@Override public char layMaLenh() { return 'Z'; }
@Override public boolean laTuongDoi() { return false; } // Z luôn là tuyệt đối
@Override public String toString() { return "Z"; }
}
public static List<PhanDoanDuongDan> phanTichDuLieuDuongDan(String thuocTinhD) {
List<PhanDoanDuongDan> cacPhanDoan = new ArrayList<>();
PathParser trinhPhanTichDuongDan = new PathParser();
trinhPhanTichDuongDan.set PathHandler(new PathHandler() {
@Override
public void startPath() {
cacPhanDoan.clear();
}
@Override
public void movetoRel(float x, float y) {
cacPhanDoan.add(new DiChuyenDen(true, x, y));
}
@Override
public void movetoAbs(float x, float y) {
cacPhanDoan.add(new DiChuyenDen(false, x, y));
}
@Override
public void linetoRel(float x, float y) {
cacPhanDoan.add(new VeDuongThang(true, x, y));
}
@Override
public void linetoAbs(float x, float y) {
cacPhanDoan.add(new VeDuongThang(false, x, y));
}
@Override
public void linetoHorizontalRel(float x) {
cacPhanDoan.add(new VeDuongThangNgang(true, x));
}
@Override
public void linetoHorizontalAbs(float x) {
cacPhanDoan.add(new VeDuongThangNgang(false, x));
}
@Override
public void linetoVerticalRel(float y) {
cacPhanDoan.add(new VeDuongThangDoc(true, y));
}
@Override
public void linetoVerticalAbs(float y) {
cacPhanDoan.add(new VeDuongThangDoc(false, y));
}
// TODO: Triển khai thêm cho các lệnh đường cong (curveto, smoothCurveto, quadto, smoothQuadto, arc)
@Override
public void curvetoCubicRel(float x1, float y1, float x2, float y2, float x, float y) {
// Ví dụ: Bạn sẽ cần một lớp CurvetoCubic
// cacPhanDoan.add(new CurvetoCubic(true, x1, y1, x2, y2, x, y));
}
@Override
public void closePath() {
cacPhanDoan.add(new DongDuongDan());
}
@Override
public void endPath() {
// Kết thúc phân tích đường dẫn
}
});
try {
trinhPhanTichDuongDan.parse(new StringReader(thuocTinhD));
} catch (Exception e) {
System.err.println("Lỗi khi phân tích dữ liệu đường dẫn: " + e.getMessage());
}
return cacPhanDoan;
}
public static void main(String[] args) {
// Một ví dụ dữ liệu đường dẫn SVG phức tạp
String duongDanMau = "M10 10 L30 10 L20 30 z m 5 5 h 10 v 10 l -5 -5";
List<PhanDoanDuongDan> ketQua = phanTichDuLieuDuongDan(duongDanMau);
System.out.println("Các phân đoạn đường dẫn được phân tích:");
for (PhanDoanDuongDan phanDoan : ketQua) {
System.out.println(" " + phanDoan);
}
String duongDanDonGian = "M100,200 L150,250 V300 H50 Z";
List<PhanDoanDuongDan> ketQuaDonGian = phanTichDuLieuDuongDan(duongDanDonGian);
System.out.println("\nCác phân đoạn đường dẫn đơn giản:");
for (PhanDoanDuongDan phanDoan : ketQuaDonGian) {
System.out.println(" " + phanDoan);
}
}
}