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}