diff --git a/README.md b/README.md index 961c469..a034e4a 100644 --- a/README.md +++ b/README.md @@ -283,31 +283,72 @@ For ease of use, some flags are set by default. However default flags are not us ``` ## Using the CLI -HTML2image comes with a Command Line Interface which you can use to generate screenshots from files and URLs on the go. - -The CLI is a work in progress and may undergo changes. -You can call it by typing `hti` or `html2image` into a terminal. - - -| argument | description | example | -| - | - | - | -| -h, --help | Shows the help message | `hti -h` | -| -U, --urls | Screenshots a list of URLs | `hti -U https://www.python.org` | -| -H, --html | Screenshots a list of HTML files | `hti -H file.html` | -| -C, --css | Attaches a CSS files to the HTML ones | `hti -H file.html -C style.css` | -| -O, --other | Screenshots a list of files of type "other" | `hti -O star.svg` | -| -S, --save-as | A list of the screenshot filename(s) | `hti -O star.svg -S star.png` | -| -s, --size | A list of the screenshot size(s) | `hti -O star.svg -s 50,50`| -| -o, --output_path| Change the output path of the screenshots (default is current working directory) | `hti star.svg -o screenshot_dir` | -| -q, --quiet| Disable all CLI's outputs | `hti --quiet` | -| -v, --verbose| More details, can help debugging | `hti --verbose` | -| --chrome_path| Specify a different chrome path || -| --custom_flags| Specify custom browser flags | -| --temp_path| Specify a different temp path (where the files are loaded)|| +HTML2image comes with a Command Line Interface which you can use to generate screenshots from files and URLs on the go. You can call it by typing `hti` or `html2image` into a terminal. + + +**Example Usage (quick start):** +Screenshot a URL with a specific output name and size: +```bash +hti --url https://example.com --save-as example_page.png --size 1280,720 +``` + +Screenshot multiple HTML files, applying a common CSS file, and saving with custom names: +```bash +hti --html-file page1.html page2.html --css-file common_styles.css --save-as shot1.jpg shot2.jpg +``` + +Screenshot an HTML string with a custom browser flags and verbose output: +```bash +hti --html-string "

Test

Content

" --custom-flags '--no-sandbox' -v +``` + +**Html2Image Instance Configuration:** + +These arguments configure the underlying `Html2Image` instance. + +| Argument | Description | Example | +|----------|-------------|---------| +| `-h, --help` | Show the help message and exit. | `hti --help` | +| `-o, --output-path PATH` | Directory to save screenshots. (Default: current working directory)| `hti --url example.com -o my_images/` | +| `--browser BROWSER`| Browser to use. Choices: `chrome`, `chromium`, `google-chrome`, `google-chrome-stable`, `googlechrome`, `edge`, `chrome-cdp`, `chromium-cdp`. (Default: `chrome`)| `hti --url example.com --browser edge` | +| `--browser-executable EXECUTABLE_PATH` | Path to the browser executable. Auto-detected if not provided. | `hti --browser-executable /usr/bin/google-chrome-stable`| +| `--cdp-port PORT` | CDP port for CDP-enabled browsers (e.g., `chrome-cdp`). (Default: library-dependent)| `hti --browser chrome-cdp --cdp-port 9222 --url example.com` | +| `--temp-path TEMP_DIR_PATH` | Directory for temporary files. (Default: system temp directory in an `html2image` subfolder) | `hti --html-file page.html --temp-path /my/tmp`| +| `--keep-temp-files`| Do not delete temporary files after screenshot generation.| `hti --html-file page.html --keep-temp-files` | +| `--custom-flags [FLAG ...]` | Custom flags to pass to the browser (e.g., `'--no-sandbox' '--disable-gpu'`). If provided, these flags will be used. | `hti --url example.com --custom-flags '--no-sandbox' '--disable-gpu'`
`hti --url example.com --custom-flags '--no-sandbox --disable-gpu'` | + +**Screenshot Sources:** + +Specify what content to screenshot. At least one source type is required. + +| Argument | Description | Example | +|----------|-------------|---------| +| `-U, --url [URL ...]` | URL(s) to screenshot. | `hti -U https://python.org https://example.com`| +| `--html-file [FILE ...]` | HTML file(s) to screenshot. | `hti --html-file mypage.html another.html` | +| `--html-string [STRING ...]`| HTML string(s) to screenshot. | `hti --html-string "

Hello

" "

World

"` | +| `--css-file [FILE ...]` | CSS file(s) to load. Used by HTML files or applied with HTML strings. | `hti --html-file page.html --css-file style1.css style2.css` | +| `--css-string [STRING ...]` | CSS string(s) to apply. Combined and used with HTML strings. | `hti --html-string "

Hi

" --css-string "body{color:red;}" "h1{font-size:40px;}"` | +| `-O, --other-file [FILE ...]` | Other file(s) to screenshot (e.g., SVG).| `hti -O star.svg`| + +**Screenshot Output Options:** + +Control how the screenshots are saved. + +| Argument | Description | Example | +|----------|-------------|---------| +| `-S, --save-as [FILENAME ...]` | Filename(s) for output images. If not provided or fewer names than items, names are auto-generated (e.g., `screenshot.png`, `screenshot_0.png`). | `hti -U python.org example.com -S py.png ex.png` | +| `-s, --size [W,H ...]`| Size(s) for screenshots as `Width,Height`. If one W,H pair is given, it applies to all screenshots. If multiple W,H pairs are given, they apply to corresponding screenshots sequentially; if fewer pairs than items, the last pair is repeated. If omitted, the library's default (1920,1080) is used. Width and height must be positive integers. | `hti -U python.org --size 800,600`
`hti -U python.org example.com -s 800,600 1024,768` | + +**General Options:** + +| Argument | Description | Example | +|----------|-------------|---------| +| `-q, --quiet`| Suppress informational output from html2image library (sets `disable_logging=True`). | `hti -U python.org -q` | +| `-v, --verbose` | Enable verbose output, including browser commands if supported by the browser handler. | `hti -U python.org -v` |
-### ... now within a Docker container ! +### Using a Docker Container You can also test the package and the CLI without having to install everything on your local machine, via a Docker container. diff --git a/html2image/cli.py b/html2image/cli.py index 80ccca9..2e3e74b 100644 --- a/html2image/cli.py +++ b/html2image/cli.py @@ -2,81 +2,247 @@ """ import argparse - +import os from html2image import Html2Image -def main(): - - def size_type(string): - try: - x, y = map(int, string.split(',')) - return x, y - except Exception: +def size_type(string): + try: + width, height = map(int, string.split(',')) + if width <= 0 or height <= 0: raise argparse.ArgumentTypeError( - f"size should be int,int, instead got {string}" + 'Width and height must be positive integers.' ) + return width, height + except ValueError: # incorrect number of values + raise argparse.ArgumentTypeError( + f"Size should be W,H (e.g., 1920,1080), instead got '{string}'" + ) + except Exception as e: # unexpected errors + raise argparse.ArgumentTypeError(f"Invalid size format '{string}': {e}") - parser = argparse.ArgumentParser() - parser.add_argument('-U', '--url', nargs='*', required=False, default=[]) - parser.add_argument('-H', '--html', nargs='*', required=False, default=[]) - parser.add_argument('-C', '--css', nargs='*', required=False, default=[]) - parser.add_argument('-O', '--other', nargs='*', required=False, default=[]) +def main(): + parser = argparse.ArgumentParser( + description='Generate images from HTML/CSS or URLs using the html2image library.', + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + # Html2Image instantiation arguments + group_hti_init = parser.add_argument_group('Html2Image Instance Configuration') + group_hti_init.add_argument( + '--output-path', '-o', + default=os.getcwd(), + help='Directory to save screenshots.' + ) - parser.add_argument( - '-S', '--save-as', nargs='*', required=False, default="screenshot.png" + # TODO : this list is duplicated from browser_map in html2image.py + browser_choices = [ + 'chrome', 'chromium', 'google-chrome', 'google-chrome-stable', + 'googlechrome', 'edge', 'chrome-cdp', 'chromium-cdp' + ] + group_hti_init.add_argument( + '--browser', + default='chrome', + choices=browser_choices, + help='Browser to use for screenshots.' ) - parser.add_argument( - '-s', '--size', nargs='*', required=False, default=[], type=size_type + group_hti_init.add_argument( + '--browser-executable', + default=None, + help='Path to the browser executable. Auto-detected if not provided.' + ) + group_hti_init.add_argument( + '--cdp-port', + type=int, + default=None, + help='CDP port for CDP-enabled browsers (e.g., chrome-cdp). Default is library-dependent.' + ) + group_hti_init.add_argument( + '--temp-path', + default=None, + help="Directory for temporary files. Defaults to system temp directory within an 'html2image' subfolder." + ) + group_hti_init.add_argument( + '--keep-temp-files', + action='store_true', + help='Do not delete temporary files after screenshot generation.' + ) + group_hti_init.add_argument( + '--custom-flags', + nargs='*', + default=[], # If not provided, defaults are used + help="Custom flags to pass to the browser (e.g., '--no-sandbox' '--disable-gpu'). If provided, these flags will be used." ) - parser.add_argument('-o', '--output_path', required=False) + # Screenshot sources arguments + group_sources = parser.add_argument_group('Screenshot Sources (at least one type is required)') + group_sources.add_argument( + '--url', '-U', + nargs='*', default=[], + metavar='URL', + help='URL(s) to screenshot.' + ) + group_sources.add_argument( + '--html-file', + nargs='*', default=[], + metavar='FILE', + help='HTML file(s) to screenshot.' + ) + group_sources.add_argument( + '--html-string', + nargs='*', default=[], + metavar='STRING', + help='HTML string(s) to screenshot.' + ) + group_sources.add_argument( + '--css-file', + nargs='*', default=[], + metavar='FILE', + help='CSS file(s) to load. Used by HTML files or applied with HTML strings.' + ) + group_sources.add_argument( + '--css-string', + nargs='*', default=[], + metavar='STRING', + help='CSS string(s) to apply. Combined and used with HTML strings.' + ) + group_sources.add_argument( + '--other-file', '-O', + nargs='*', default=[], + metavar='FILE', + help='Other file(s) to screenshot (e.g., SVG).' + ) - parser.add_argument('-q', '--quiet', required=False, action="store_true") - parser.add_argument('-v', '--verbose', required=False, action="store_true") + # Screenshot output control arguments + group_output_ctrl = parser.add_argument_group('Screenshot Output Options') + group_output_ctrl.add_argument( + '--save-as', '-S', + nargs='*', default=None, # html2image handles default naming if only one source + metavar='FILENAME', + help='Filename(s) for the output images. If not provided or fewer names than items, names are auto-generated.' + ) + group_output_ctrl.add_argument( + '--size', '-s', + nargs='*', default=[], + type=size_type, + metavar='W,H', + help="Size(s) for screenshots as W,H. If one W,H pair is given, it applies to all. If multiple, they apply to corresponding screenshots; if fewer pairs than items, the last is repeated. If omitted, (1920,1080) is used." + ) - # parser.add_argument('--browser', required=False) - parser.add_argument('--chrome_path', required=False) - # parser.add_argument('--firefox_path', required=False) - parser.add_argument('--temp_path', required=False) - parser.add_argument('--custom_flags', required=False) + # General arguments + group_general = parser.add_argument_group('General Options') + group_general.add_argument( + '--quiet', '-q', + action='store_true', + help='Suppress output from browsers (sets disable_logging=True).' + ) + group_general.add_argument( + '--verbose', '-v', + action='store_true', + help='Enable verbose output, including browser commands if supported by the browser handler.' + ) args = parser.parse_args() + # Prepare Html2Image() + hti_kwargs = { + 'output_path': args.output_path, + 'browser': args.browser, + 'browser_executable': args.browser_executable, + 'custom_flags': [cf.replace("'", '') for cf in args.custom_flags], + 'disable_logging': args.quiet, + 'temp_path': args.temp_path, + 'keep_temp_files': args.keep_temp_files, + } + + # Only pass cdp_port if a CDP browser is likely selected and port is given + if args.cdp_port and 'cdp' in args.browser.lower(): + hti_kwargs['browser_cdp_port'] = args.cdp_port + elif args.cdp_port: + print( + f"Warning: --cdp-port ({args.cdp_port}) was specified, but the selected browser ('{args.browser}') might not be a CDP browser." + ) + try: - hti = Html2Image(disable_logging=args.quiet) + # Filter out None values so defaults are used for those specific kwargs + # keep_temp_files and disable_logging are bools, always pass them. + # custom_flags should be passed even if None, so Html2Image can use its defaults or an empty list. + active_hti_kwargs = { + k: v for k, v in hti_kwargs.items() + if v is not None or k in ['keep_temp_files', 'disable_logging', 'custom_flags'] + } + hti = Html2Image(**active_hti_kwargs) + except Exception as e: - print('Could not instanciate html2image.') - print(e) + print(f'Error: Could not instantiate Html2Image: {e}') exit(1) if args.verbose: - print(f'args = {args}') - hti.browser.print_command = True - - if args.output_path: - hti.output_path = args.output_path + # The `print_command` attribute is specific to ChromiumHeadless. + # CDP browsers print logs internally. + if hasattr(hti.browser, 'print_command'): + hti.browser.print_command = True + print('Verbose mode: Browser commands will be printed for compatible handlers.') + else: + print('Verbose mode enabled. Note: Detailed browser command printing depends on the selected browser handler.') + + has_sources = any([ + args.url, args.html_file, args.html_string, args.other_file + ]) + + # Print help message if no sources were passed + if not has_sources: + print('Error: No screenshot sources (URL, HTML file/string, other file) provided.') + parser.print_usage() + exit(1) - if args.chrome_path: - hti.chrome_path = args.chrome_path + # Perform screenshot + screenshot_kwargs = { + 'url': args.url, + 'html_file': args.html_file, + 'html_str': args.html_string, + 'css_file': args.css_file, + 'css_str': args.css_string, + 'other_file': args.other_file, + 'size': args.size, # Pass the list of sizes directly from the --size CLI arg + } - if args.custom_flags: - hti.browser.flags = args.custom_flags + if args.save_as is not None: + screenshot_kwargs['save_as'] = args.save_as - if args.temp_path: - hti.temp_path = args.temp_path + try: + if args.verbose: + print('--- Html2Image Instance Configuration ---') + for k, v in active_hti_kwargs.items(): + print(f' {k}: {v}') + print('--- Screenshot Call Arguments ---') + for k, v in screenshot_kwargs.items(): + if v or k == 'size': # print if list not empty, or always for size + print(f' {k}: {v}') + + paths = hti.screenshot(**screenshot_kwargs) + + if not args.quiet: + print(f'Successfully created {len(paths)} image(s):') + for path in paths: + print(f' {path}') + + except FileNotFoundError as e: + print(f'Error: A required file was not found: {e}') + exit(1) - paths = hti.screenshot( - html_file=args.html, css_file=args.css, other_file=args.other, - url=args.url, save_as=args.save_as, size=args.size - ) + except ValueError as e: # Can be raised by browser screenshot method for bad size etc. + print(f'Error: Invalid value encountered: {e}') + exit(1) - if not args.quiet: - print(f'Created {len(paths)} file(s):') - for path in paths: - print(f'\t{path}') + except Exception as e: + print(f'An unexpected error occurred during screenshotting: {e}') + if args.verbose: + import traceback + traceback.print_exc() + exit(1) -if __name__ == "__main__": +if __name__ == '__main__': main() diff --git a/html2image/html2image.py b/html2image/html2image.py index 38b010a..7e46f30 100644 --- a/html2image/html2image.py +++ b/html2image/html2image.py @@ -389,7 +389,7 @@ def _extend_size_param(self, sizes, desired_length): return sizes @staticmethod - def _prepare_html_string(html_body, css_style_string): + def _prepare_html_string(html_body, css_style_string) -> str: """ Creates a basic HTML string from an HTML body and a css string. Parameters @@ -422,8 +422,8 @@ def _prepare_html_string(html_body, css_style_string): return dedent(prepared_html) @staticmethod - def _prepare_css_str(css_file): - """ Creates a basic string fromatted from a css file. + def _prepare_css_string(css_file: list[str])-> str: + """ Creates a basic string fromatted from a list of css files. Parameters ---------- @@ -432,10 +432,11 @@ def _prepare_css_str(css_file): Returns ------- - str - The contents of each css file. + The concatenated content of each css files. """ - css_str = "" + + css_str = '' for css in css_file: temp_css_str = '' with open(css, "r") as fd: @@ -444,6 +445,7 @@ def _prepare_css_str(css_file): return css_str + def screenshot( self, html_str=[], # html_str: Union[str, list] = [], @@ -455,7 +457,7 @@ def screenshot( save_as='screenshot.png', size=[], ): - """ Takes a screeshot using different resources. + """ Takes a screenshot using different resources. Parameters ---------- @@ -465,10 +467,14 @@ def screenshot( + Filepath(s) of HTML file(s) that will be screenshotted. - `css_str`: list of str or str + CSS string(s) that will be "associated" with the given - + HTML string(s) + + HTML string(s). - `css_file`: list of str or str - + CSS file(s) supposedly already mentionned by their filenames - + in the content of the `html_file`(s). + + Filepath(s) of CSS file(s). These files serve two purposes: + + 1. Their content is combined with `css_str` and embedded + + directly when screenshotting `html_str` (HTML strings). + + 2. They are loaded into the temporary directory, making + + them available to `html_file` (HTML files) that link to them + + (e.g., via ``). - `other_file`: list of str or str + Filepath(s) of non-HTML file(s) that will be screenshotted. - `url`: list of str or str @@ -500,47 +506,46 @@ def screenshot( # convert each parameter into list # e.g: param=value becomes param=[value] - html_str = [html_str] if isinstance(html_str, str) else html_str - html_file = [html_file] if isinstance(html_file, str) else html_file - css_str = [css_str] if isinstance(css_str, str) else css_str - css_file = [css_file] if isinstance(css_file, str) else css_file - other_file = ( - [other_file] if isinstance(other_file, str) else other_file - ) - url = [url] if isinstance(url, str) else url - save_as = [save_as] if isinstance(save_as, str) else save_as - size = [size] if isinstance(size, tuple) else size + html_strings = [html_str] if isinstance(html_str, str) else html_str + html_files = [html_file] if isinstance(html_file, str) else html_file + css_strings = [css_str] if isinstance(css_str, str) else css_str + css_files = [css_file] if isinstance(css_file, str) else css_file + other_files = [other_file] if isinstance(other_file, str) else other_file + urls = [url] if isinstance(url, str) else url + save_as_filenames = [save_as] if isinstance(save_as, str) else save_as + sizes = [size] if isinstance(size, tuple) else size planned_screenshot_count = ( - len(html_str) + len(html_file) + len(other_file) + len(url) + len(html_strings) + len(html_files) + len(other_files) + len(urls) ) - save_as = Html2Image._extend_save_as_param( - save_as, + save_as_filenames = Html2Image._extend_save_as_param( + save_as_filenames, planned_screenshot_count, ) - size = self._extend_size_param(size, planned_screenshot_count) + sizes = self._extend_size_param(sizes, planned_screenshot_count) - css_style_string = "" + css_style_string = '\n'.join(css_strings) + '\n' - for css in css_str: - css_style_string += css + '\n' + if css_files: + # add content from css_files, regardless of whether css_strings was present + css_style_string += Html2Image._prepare_css_string(css_files) - for css in css_file: + + for css in css_files: if os.path.isfile(css): self.load_file(src=css) else: raise FileNotFoundError(css) - for html in html_str: - name = save_as.pop(0) - current_size = size.pop(0) + for html in html_strings: + name = save_as_filenames.pop(0) + current_size = sizes.pop(0) base_name, _ = os.path.splitext(name) html_filename = base_name + '.html' - content = Html2Image._prepare_html_string( - html, css_style_string if css_style_string != '' else Html2Image._prepare_css_str( - css_file) - ) + + content = Html2Image._prepare_html_string(html, css_style_string) + self.load_str(content=content, as_filename=html_filename) self.screenshot_loaded_file( file=html_filename, @@ -552,10 +557,10 @@ def screenshot( screenshot_paths.append(os.path.join(self.output_path, name)) - for screenshot_target in html_file + other_file: + for screenshot_target in html_files + other_files: - name = save_as.pop(0) - current_size = size.pop(0) + name = save_as_filenames.pop(0) + current_size = sizes.pop(0) if os.path.isfile(screenshot_target): self.load_file(src=screenshot_target) @@ -564,14 +569,16 @@ def screenshot( output_file=name, size=current_size, ) + if not self.keep_temp_files: + self._remove_temp_file(os.path.basename(screenshot_target)) else: raise FileNotFoundError(screenshot_target) screenshot_paths.append(os.path.join(self.output_path, name)) - for target_url in url: - name = save_as.pop(0) - current_size = size.pop(0) + for target_url in urls: + name = save_as_filenames.pop(0) + current_size = sizes.pop(0) self.screenshot_url( url=target_url, diff --git a/pyproject.toml b/pyproject.toml index acb7f20..fb55b07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "html2image" -version = "2.0.6" # todo take a look at dynamic versionning (e.g., hatch-vcs) +version = "2.0.7" # todo take a look at dynamic versionning (e.g., hatch-vcs) description = "Package acting as a wrapper around the headless mode of existing web browsers to generate images from URLs and from HTML+CSS strings or files." authors = [ { name = "vgalin" } diff --git a/tests/test_main.py b/tests/test_main.py index 0573c84..6ebc175 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,9 +1,11 @@ from html2image import Html2Image -from PIL import Image +from PIL import Image, ImageChops import pytest +import os OUTPUT_PATH = "tests_output" +os.makedirs(OUTPUT_PATH, exist_ok=True) TEST_BROWSERS = ["edGe", "cHrOme"] @@ -96,8 +98,8 @@ def test_screenshot_url_sizes_missing_custom_names(browser): ], size=test_sizes, ) - - for wanted_size, path in zip(test_sizes, paths): + effective_wanted_sizes = test_sizes + [test_sizes[-1]] + for wanted_size, path in zip(effective_wanted_sizes, paths): img = Image.open(path) assert wanted_size == img.size @@ -118,7 +120,7 @@ def test_screenshot_string(browser): assert (1920, 1080) == img.size # default size # check colors at top left corner - assert pixels[0, 0] == (0, 0, 255) # blue + no transparency + assert pixels[0, 0][:3] == (0, 0, 255) @pytest.mark.parametrize("browser", TEST_BROWSERS) def test_screenshot_string_different_sizes(browser): @@ -155,8 +157,9 @@ def test_screenshot_other_svg(browser): assert (1920, 1080) == img.size # default size - # check colors at top left corner - assert pixels[0, 0] == (0, 0, 0, 0) # full transparency no color + # check colors at top left corner (assuming transparent background for the SVG) + # for transparent PNG, the alpha channel will be 0 + assert pixels[0, 0][3] == 0 # check alpha channel for full transparency @pytest.mark.parametrize("browser", TEST_BROWSERS) def test_screenshot_file(browser): @@ -164,7 +167,7 @@ def test_screenshot_file(browser): paths = hti.screenshot( html_file="./examples/blue_page.html", - css_file="./examples/blue_background.css", + css_file="./examples/blue_background.css", # this CSS file is linked in blue_page.html save_as="from_file.png", ) @@ -174,7 +177,7 @@ def test_screenshot_file(browser): assert (1920, 1080) == img.size # default size # check colors at top left corner - assert pixels[0, 0] == (0, 0, 255) # blue + no transparency + assert pixels[0, 0][:3] == (0, 0, 255) # blue + no transparency @pytest.mark.parametrize("browser", TEST_BROWSERS) def test_screenshot_file_different_sizes(browser): @@ -196,6 +199,119 @@ def test_screenshot_file_different_sizes(browser): img = Image.open(path) assert wanted_size == img.size + +@pytest.mark.parametrize("browser", TEST_BROWSERS) +def test_screenshot_html_str_with_css_file_only(browser): + """Test html_str styled by a css_file only.""" + hti = Html2Image(browser=browser, output_path=OUTPUT_PATH, disable_logging=True) + html_content = "

Background should be blue from file

" + css_file_path = "./examples/blue_background.css" + + paths = hti.screenshot( + html_str=html_content, + css_file=css_file_path, + save_as="html_str_blue_bg_from_file.png" + ) + + img = Image.open(paths[0]) + pixels = img.load() + assert (1920, 1080) == img.size + + # Check top-left corner for blue background + assert pixels[0, 0][:3] == (0, 0, 255) # blue + +@pytest.mark.parametrize("browser", TEST_BROWSERS) +def test_screenshot_html_str_with_css_str_and_css_file(browser): + """Test html_str styled by both css_str (for a green block) and css_file (for blue body background).""" + hti = Html2Image(browser=browser, output_path=OUTPUT_PATH, disable_logging=True) + + # this CSS describes a green block, positioned absolutely, + # using !important to ensure these styles apply + css_string_content = """ + #green-block { + background-color: rgb(0, 255, 0) !important; /* Green background */ + width: 150px !important; + height: 100px !important; + position: absolute !important; + top: 75px !important; + left: 75px !important; + } + """ + + css_file_path = "./examples/blue_background.css" + + # div that will be styled by css_string_content + html_content = '
' + + paths = hti.screenshot( + html_str=html_content, + css_str=css_string_content, # Styles #green-block + css_file=css_file_path, # Styles body background + save_as="html_str_green_block_blue_bg.png", + size=(500,400), + ) + + img = Image.open(paths[0]) + pixels = img.load() + assert (500,400) == img.size + + # check body background color (blue) + assert pixels[10, 10][:3] == (0, 0, 255) # blue + + # check #green-block color (green from css_string_content) + assert pixels[150, 125][:3] == (0, 255, 0) # green + + +@pytest.mark.parametrize("browser", TEST_BROWSERS) +def test_screenshot_html_str_with_multiple_css_files(browser): + """Test html_str styled by multiple css_files using distinct background colors.""" + hti = Html2Image(browser=browser, output_path=OUTPUT_PATH, disable_logging=True) + + # this CSS describes a red block, positioned absolutely, + # using !important to ensure these styles apply + temp_css_content = """ + #red-block { + background-color: rgb(255, 0, 0) !important; /* red background */ + width: 200px !important; + height: 150px !important; + position: absolute !important; /* for predictable positioning */ + top: 50px !important; + left: 50px !important; + } + """ + temp_css_filename = os.path.join(OUTPUT_PATH, "_temp_red_block.css") + with open(temp_css_filename, "w") as f: + f.write(temp_css_content) + + # the div that will be styled by _temp_red_block.css + html_content = '
' + + css_file_paths = [ + "./examples/blue_background.css", # sets body { background: blue; } + temp_css_filename # styles #red-block + ] + + paths = hti.screenshot( + html_str=html_content, + css_file=css_file_paths, + save_as="html_str_multi_css_blocks.png", + size=(400,300), + ) + + # clean up temporary CSS file + if os.path.exists(temp_css_filename): + os.remove(temp_css_filename) + + img = Image.open(paths[0]) + pixels = img.load() + assert (400,300) == img.size + + # check body background color (blue) + assert pixels[10, 10][:3] == (0, 0, 255) # blue + + # check #green-block color (red from _temp_red_block.css) + assert pixels[150, 125][:3] == (255, 0, 0) # red + @pytest.mark.parametrize("browser", TEST_BROWSERS) def test_extend_size_param(browser): hti = Html2Image(browser=browser, output_path=OUTPUT_PATH, disable_logging=True)