html2pdf_api/factory/chrome.rs
1//! Chrome/Chromium browser factory implementation.
2//!
3//! This module provides [`ChromeBrowserFactory`] for creating headless Chrome
4//! browser instances with production-ready configurations.
5//!
6//! # Overview
7//!
8//! The factory handles:
9//! - Chrome binary path detection (or custom path)
10//! - Launch options configuration
11//! - Memory and stability optimizations
12//!
13//! # Example
14//!
15//! ```rust,ignore
16//! use html2pdf_api::ChromeBrowserFactory;
17//!
18//! // Auto-detect Chrome installation
19//! let factory = ChromeBrowserFactory::with_defaults();
20//!
21//! // Or specify custom path
22//! let factory = ChromeBrowserFactory::with_path("/usr/bin/google-chrome".to_string());
23//! ```
24
25use headless_chrome::{Browser, LaunchOptions};
26
27use super::BrowserFactory;
28use crate::error::{BrowserPoolError, Result};
29
30/// Factory for creating Chrome/Chromium browser instances.
31///
32/// Handles Chrome-specific launch options and path detection.
33/// Supports both auto-detection and custom Chrome binary paths.
34///
35/// # Thread Safety
36///
37/// This factory is `Send + Sync` and can be safely shared across threads.
38///
39/// # Example
40///
41/// ```rust,ignore
42/// use html2pdf_api::ChromeBrowserFactory;
43///
44/// // Auto-detect Chrome
45/// let factory = ChromeBrowserFactory::with_defaults();
46///
47/// // Or use custom path
48/// let factory = ChromeBrowserFactory::with_path("/usr/bin/google-chrome".to_string());
49/// ```
50pub struct ChromeBrowserFactory {
51 /// Function that generates launch options for each browser.
52 ///
53 /// This allows dynamic configuration per browser instance.
54 launch_options_fn: Box<dyn Fn() -> Result<LaunchOptions<'static>> + Send + Sync>,
55}
56
57impl ChromeBrowserFactory {
58 /// Create factory with custom launch options function.
59 ///
60 /// This is the most flexible constructor, allowing full control
61 /// over launch options generation.
62 ///
63 /// # Parameters
64 ///
65 /// * `launch_options_fn` - Function called for each browser creation.
66 ///
67 /// # Example
68 ///
69 /// ```rust,ignore
70 /// use html2pdf_api::{ChromeBrowserFactory, create_chrome_options, BrowserPoolError};
71 ///
72 /// let factory = ChromeBrowserFactory::new(|| {
73 /// // Custom logic here
74 /// create_chrome_options(Some("/custom/path"))
75 /// .map_err(|e| BrowserPoolError::Configuration(e.to_string()))
76 /// });
77 /// ```
78 pub fn new<F>(launch_options_fn: F) -> Self
79 where
80 F: Fn() -> Result<LaunchOptions<'static>> + Send + Sync + 'static,
81 {
82 Self {
83 launch_options_fn: Box::new(launch_options_fn),
84 }
85 }
86
87 /// Create factory with auto-detected Chrome path.
88 ///
89 /// This is the recommended default - lets headless_chrome find Chrome.
90 /// Works on Linux, macOS, and Windows.
91 ///
92 /// # Platform Detection
93 ///
94 /// The `headless_chrome` crate searches common installation paths:
95 ///
96 /// | Platform | Paths Searched |
97 /// |----------|----------------|
98 /// | Linux | `/usr/bin/google-chrome`, `/usr/bin/chromium`, etc. |
99 /// | macOS | `/Applications/Google Chrome.app/...` |
100 /// | Windows | `C:\Program Files\Google\Chrome\...` |
101 ///
102 /// # Example
103 ///
104 /// ```rust,ignore
105 /// use html2pdf_api::ChromeBrowserFactory;
106 ///
107 /// let factory = ChromeBrowserFactory::with_defaults();
108 /// ```
109 pub fn with_defaults() -> Self {
110 log::debug!(" Creating ChromeBrowserFactory with auto-detect");
111 Self::new(|| {
112 create_chrome_options(None).map_err(|e| BrowserPoolError::Configuration(e.to_string()))
113 })
114 }
115
116 /// Create factory with custom Chrome binary path.
117 ///
118 /// Use this when Chrome is installed in a non-standard location.
119 ///
120 /// # Parameters
121 ///
122 /// * `chrome_path` - Full path to Chrome/Chromium binary.
123 ///
124 /// # Example
125 ///
126 /// ```rust,ignore
127 /// use html2pdf_api::ChromeBrowserFactory;
128 ///
129 /// // Linux
130 /// let factory = ChromeBrowserFactory::with_path("/usr/bin/google-chrome".to_string());
131 ///
132 /// // macOS
133 /// let factory = ChromeBrowserFactory::with_path(
134 /// "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome".to_string()
135 /// );
136 ///
137 /// // Windows
138 /// let factory = ChromeBrowserFactory::with_path(
139 /// r"C:\Program Files\Google\Chrome\Application\chrome.exe".to_string()
140 /// );
141 /// ```
142 pub fn with_path(chrome_path: String) -> Self {
143 log::debug!(
144 " Creating ChromeBrowserFactory with custom path: {}",
145 chrome_path
146 );
147 Self::new(move || {
148 create_chrome_options(Some(&chrome_path))
149 .map_err(|e| BrowserPoolError::Configuration(e.to_string()))
150 })
151 }
152}
153
154impl BrowserFactory for ChromeBrowserFactory {
155 /// Create a new Chrome browser instance.
156 ///
157 /// Calls the launch options function and launches Chrome with those options.
158 ///
159 /// # Errors
160 ///
161 /// * Returns [`BrowserPoolError::Configuration`] if launch options generation fails.
162 /// * Returns [`BrowserPoolError::BrowserCreation`] if Chrome fails to launch.
163 fn create(&self) -> Result<Browser> {
164 log::trace!(" ChromeBrowserFactory::create() called");
165
166 // Generate launch options
167 let options = (self.launch_options_fn)()?;
168
169 // Launch browser
170 log::debug!(" Launching Chrome browser...");
171 Browser::new(options).map_err(|e| {
172 log::error!("❌ Chrome launch failed: {}", e);
173 BrowserPoolError::BrowserCreation(e.to_string())
174 })
175 }
176}
177
178/// Create Chrome launch options with optional custom path.
179///
180/// This function generates production-ready Chrome launch options with:
181/// - Memory optimization flags
182/// - GPU acceleration disabled (for headless stability)
183/// - Unnecessary features disabled
184/// - Security settings for automation
185///
186/// # Parameters
187///
188/// * `chrome_path` - Optional custom Chrome binary path. If None, auto-detects.
189///
190/// # Returns
191///
192/// LaunchOptions configured for stable headless operation.
193///
194/// # Errors
195///
196/// Returns error if options builder fails (rare, usually a bug).
197///
198/// # Chrome Flags Applied
199///
200/// ## Memory and Performance
201/// - `--disable-dev-shm-usage` - Use /tmp instead of /dev/shm (container-friendly)
202/// - `--disable-crash-reporter` - No crash reporting
203/// - `--max_old_space_size=1024` - Limit V8 heap to 1GB
204///
205/// ## GPU and Rendering
206/// - `--disable-gpu-compositing`
207/// - `--disable-software-rasterizer`
208/// - `--disable-accelerated-2d-canvas`
209/// - `--disable-gl-drawing-for-tests`
210/// - `--disable-webgl`
211/// - `--disable-webgl2`
212///
213/// ## Disabled Features
214/// - `--disable-extensions`
215/// - `--disable-plugins`
216/// - `--disable-sync`
217/// - `--disable-default-apps`
218///
219/// ## Security and Automation
220/// - `--disable-web-security` - Allow cross-origin requests (for scraping)
221/// - `--enable-automation` - Mark as automated browser
222///
223/// ## Stability
224/// - `--disable-background-timer-throttling`
225/// - `--disable-backgrounding-occluded-windows`
226/// - `--disable-hang-monitor`
227/// - `--disable-popup-blocking`
228/// - `--disable-renderer-backgrounding`
229/// - `--disable-ipc-flooding-protection`
230///
231/// # Example
232///
233/// ```rust,ignore
234/// use html2pdf_api::create_chrome_options;
235///
236/// // Auto-detect Chrome path
237/// let options = create_chrome_options(None)?;
238///
239/// // Custom Chrome path
240/// let options = create_chrome_options(Some("/usr/bin/chromium"))?;
241/// ```
242pub fn create_chrome_options(
243 chrome_path: Option<&str>,
244) -> std::result::Result<LaunchOptions<'static>, Box<dyn std::error::Error + Send + Sync>> {
245 match chrome_path {
246 Some(path) => log::debug!(" Creating Chrome options with custom path: {}", path),
247 None => log::debug!(" Creating Chrome options (auto-detect browser)"),
248 }
249
250 let mut builder = LaunchOptions::default_builder();
251
252 // Set path if provided, otherwise let headless_chrome auto-detect
253 if let Some(path) = chrome_path {
254 builder.path(Some(path.to_string().into()));
255 log::trace!(" Chrome path set to: {}", path);
256 } else {
257 log::trace!(" Chrome path: auto-detect");
258 }
259
260 // Configure launch options for stable headless operation
261 builder
262 .headless(true) // Run in headless mode
263 .sandbox(false) // Disable sandbox (required in containers)
264 .disable_default_args(true) // Use our custom args only
265 .args(vec![
266 // ===== Memory and Performance Optimization =====
267 "--disable-dev-shm-usage".as_ref(), // Use /tmp instead of /dev/shm (container-friendly)
268 "--disable-crash-reporter".as_ref(), // No crash reporting
269 "--max_old_space_size=1024".as_ref(), // Limit V8 heap to 1GB
270 // ===== GPU and Rendering Flags =====
271 // Disable GPU features for headless stability
272 "--disable-gpu-compositing".as_ref(),
273 "--disable-software-rasterizer".as_ref(),
274 "--disable-accelerated-2d-canvas".as_ref(),
275 "--disable-gl-drawing-for-tests".as_ref(),
276 "--disable-webgl".as_ref(),
277 "--disable-webgl2".as_ref(),
278 // ===== Disable Unnecessary Features =====
279 "--disable-extensions".as_ref(), // No browser extensions
280 "--disable-plugins".as_ref(), // No plugins
281 "--disable-sync".as_ref(), // No Chrome sync
282 "--disable-default-apps".as_ref(), // No default apps
283 // ===== Security and Functionality =====
284 "--disable-web-security".as_ref(), // Allow cross-origin requests (for scraping)
285 // ===== Automation and Debugging =====
286 "--enable-automation".as_ref(), // Mark as automated browser
287 // ===== Stability and Performance =====
288 "--disable-background-timer-throttling".as_ref(), // Don't throttle background tabs
289 "--disable-backgrounding-occluded-windows".as_ref(), // Don't suspend hidden windows
290 "--disable-hang-monitor".as_ref(), // Disable hang detection
291 // ===== UI Flags =====
292 "--disable-popup-blocking".as_ref(), // Allow popups
293 // ===== Better CDP (Chrome DevTools Protocol) Stability =====
294 "--disable-renderer-backgrounding".as_ref(), // Don't deprioritize renderer
295 "--disable-ipc-flooding-protection".as_ref(), // Allow rapid IPC messages
296 ])
297 .build()
298 .map_err(|e| -> Box<dyn std::error::Error + Send + Sync> {
299 let path_msg = chrome_path.unwrap_or("auto-detect");
300 log::error!(
301 "❌ Failed to build Chrome launch options (path: {}): {}",
302 path_msg,
303 e
304 );
305 e.into()
306 })
307}
308
309// ============================================================================
310// Unit Tests
311// ============================================================================
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316
317 /// Verifies that ChromeBrowserFactory can be instantiated.
318 ///
319 /// Tests that factory construction works with both auto-detect
320 /// and custom path modes. Does not actually create browsers.
321 #[test]
322 fn test_chrome_factory_creation() {
323 // Test auto-detect mode
324 let _factory = ChromeBrowserFactory::with_defaults();
325
326 // Test custom path mode
327 let _factory_with_path = ChromeBrowserFactory::with_path("/custom/chrome/path".to_string());
328
329 // If we got here without panicking, factory creation works
330 }
331
332 /// Verifies that Chrome launch options can be built.
333 ///
334 /// Tests the option builder for both auto-detect and custom path modes.
335 /// This verifies the configuration is valid, but doesn't launch Chrome.
336 #[test]
337 fn test_create_chrome_options() {
338 // Test with auto-detect (should build successfully)
339 let result = create_chrome_options(None);
340 assert!(
341 result.is_ok(),
342 "Auto-detect Chrome options should build successfully: {:?}",
343 result.err()
344 );
345
346 // Test with custom path (should build successfully)
347 let result = create_chrome_options(Some("/custom/chrome/path"));
348 assert!(
349 result.is_ok(),
350 "Custom path Chrome options should build successfully: {:?}",
351 result.err()
352 );
353 }
354}