| 方案 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| 外部工具 | 渲染完美、支持複雜 JS/CSS | 依賴系統環境、部署麻煩、進程開銷大 | 對渲染精度要求極高、可控制服務器環境 |
| Selenium/WebDriver | 真實瀏覽器、支持動態頁面 | 資源消耗大、啟動慢、需維護驅動 | 自動化測試、動態內容截圖 |
| 純 Java (JFX/无头) | 無外部依賴、輕量、跨平台 | 渲染引擎較弱、不支持現代 JS | 靜態頁面、簡單報表、內網環境 |
使用 wkhtmltoimage 或 Puppeteer(Node.js)作為後端服務,Java 通過 HTTP/CLI 調用:
@Component
public class ScreenshotService {
public byte[] captureWithWkhtmltoimage(String url) throws IOException, InterruptedException {
ProcessBuilder pb = new ProcessBuilder(
"wkhtmltoimage",
"--format", "png",
"--width", "1920",
"--height", "1080",
url, "-"
);
Process process = pb.start();
byte[] output = process.getInputStream().readAllBytes();
int exitCode = process.waitFor();
if (exitCode != 0) {
String err = new String(process.getErrorStream().readAllBytes());
throw new RuntimeException("截圖失敗: " + err);
}
return output;
}
// Puppeteer 微服務調用示例
public byte[] captureViaPuppeteerService(String url) {
// HTTP POST 到內部 Puppeteer 服務
// return restClient.post("/screenshot", Map.of("url", url), byte[].class);
}
}
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.15.0</version>
</dependency>
<dependency>
<groupId>io.github.bonigarcia</groupId>
<artifactId>webdrivermanager</artifactId>
<version>5.6.2</version>
</dependency>
public class SeleniumScreenshot {
public byte[] capture(String url, int width, int height) {
WebDriverManager.chromedriver().setup();
ChromeOptions options = new ChromeOptions();
options.addArguments("--headless=new"); // Chrome 109+ 新無頭模式
options.addArguments("--disable-gpu", "--no-sandbox", "--disable-dev-shm-usage");
options.addArguments("--window-size=" + width + "," + height);
options.addArguments("--hide-scrollbars");
try (WebDriver driver = new ChromeDriver(options)) {
driver.get(url);
// 等待頁面加載完成(可自定義等待條件)
new WebDriverWait(driver, Duration.ofSeconds(10))
.until(webDriver -> ((JavascriptExecutor) webDriver)
.executeScript("return document.readyState").equals("complete"));
// 滾動截長圖(可選)
// ((JavascriptExecutor) driver).executeScript("window.scrollTo(0, document.body.scrollHeight)");
return ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES);
}
}
}
需 JDK 包含 JavaFX(JDK 11+ 需單獨引入):
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-web</artifactId>
<version>21</version>
</dependency>
public class JavaFXScreenshot {
public byte[] capture(String url, int width, int height) throws Exception {
CountDownLatch latch = new CountDownLatch(1);
ByteArrayOutputStream output = new ByteArrayOutputStream();
AtomicReference<Throwable> errorRef = new AtomicReference<>();
// JavaFX 必須在 FX Application Thread 運行
Platform.startup(() -> {});
Platform.runLater(() -> {
try {
WebView webView = new WebView();
webView.setPrefSize(width, height);
webView.getEngine().getLoadWorker().stateProperty().addListener((obs, old, state) -> {
if (state == Worker.State.SUCCEEDED) {
// 延遲確保渲染完成
PauseTransition delay = new PauseTransition(Duration.millis(500));
delay.setOnFinished(e -> {
WritableImage snapshot = webView.snapshot(new SnapshotParameters(), null);
ImageIO.write(SwingFXUtils.fromFXImage(snapshot, null), "png", output);
latch.countDown();
});
delay.play();
} else if (state == Worker.State.FAILED) {
errorRef.set(webView.getEngine().getLoadWorker().getException());
latch.countDown();
}
});
webView.getEngine().load(url);
} catch (Exception ex) {
errorRef.set(ex);
latch.countDown();
}
});
latch.await(30, TimeUnit.SECONDS);
if (errorRef.get() != null) throw new RuntimeException(errorRef.get());
return output.toByteArray();
}
}
<dependency>
<groupId>com.machinepublishers</groupId>
<artifactId>jbrowserdriver</artifactId>
<version>1.2.0</version>
</dependency>
public class JBrowserScreenshot {
public byte[] capture(String url) {
JBrowserDriver driver = new JBrowserDriver(Settings.builder()
.headless(true)
.viewportSize(1920, 1080)
.build());
try {
driver.get(url);
return driver.getScreenshotAs(OutputType.BYTES);
} finally {
driver.quit();
}
}
}
apt-get install fonts-noto-cjk)--ignore-certificate-errorsloading=eager* { animation: none !important; transition: none !important; }