Skip to content

MESQUAL Folium Util Screenshotter

screenshotter

ScreenConfig dataclass

Configuration for the virtual browser viewport.

Attributes:

Name Type Description
width int

Viewport width in CSS pixels.

height int

Viewport height in CSS pixels.

device_pixel_ratio float

Scale factor for high-DPI rendering. A value of 2.0 produces 2x resolution output (e.g., 1920x1080 viewport -> 3840x2160 pixels).

Source code in submodules/mesqual/mesqual/utils/folium_utils/screenshotter.py
13
14
15
16
17
18
19
20
21
22
23
24
25
@dataclass
class ScreenConfig:
    """Configuration for the virtual browser viewport.

    Attributes:
        width: Viewport width in CSS pixels.
        height: Viewport height in CSS pixels.
        device_pixel_ratio: Scale factor for high-DPI rendering.
            A value of 2.0 produces 2x resolution output (e.g., 1920x1080 viewport -> 3840x2160 pixels).
    """
    width: int = 1920
    height: int = 1080
    device_pixel_ratio: float = 1.0

FrameConfig dataclass

Configuration for the screenshot crop frame.

The frame is centered on the screen by default. Use offsets to shift the crop area.

Attributes:

Name Type Description
width int

Frame width in CSS pixels.

height int

Frame height in CSS pixels.

offset_center_x int

Horizontal offset from screen center. Positive values shift right.

offset_center_y int

Vertical offset from screen center. Positive values shift down.

Source code in submodules/mesqual/mesqual/utils/folium_utils/screenshotter.py
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
@dataclass
class FrameConfig:
    """Configuration for the screenshot crop frame.

    The frame is centered on the screen by default. Use offsets to shift the crop area.

    Attributes:
        width: Frame width in CSS pixels.
        height: Frame height in CSS pixels.
        offset_center_x: Horizontal offset from screen center. Positive values shift right.
        offset_center_y: Vertical offset from screen center. Positive values shift down.
    """
    width: int
    height: int
    offset_center_x: int = 0
    offset_center_y: int = 0

    def to_crop_box(self, image_width: int, image_height: int, dpr: float) -> tuple[int, int, int, int]:
        """Convert frame config to PIL crop box coordinates.

        Args:
            image_width: Actual screenshot width in pixels.
            image_height: Actual screenshot height in pixels.
            dpr: Device pixel ratio for scaling.

        Returns:
            Tuple of (left, top, right, bottom) pixel coordinates.
        """
        center_x = image_width // 2 + int(self.offset_center_x * dpr)
        center_y = image_height // 2 + int(self.offset_center_y * dpr)
        scaled_width = int(self.width * dpr)
        scaled_height = int(self.height * dpr)
        left = center_x - scaled_width // 2
        top = center_y - scaled_height // 2
        right = left + scaled_width
        bottom = top + scaled_height
        return (left, top, right, bottom)

to_crop_box

to_crop_box(image_width: int, image_height: int, dpr: float) -> tuple[int, int, int, int]

Convert frame config to PIL crop box coordinates.

Parameters:

Name Type Description Default
image_width int

Actual screenshot width in pixels.

required
image_height int

Actual screenshot height in pixels.

required
dpr float

Device pixel ratio for scaling.

required

Returns:

Type Description
tuple[int, int, int, int]

Tuple of (left, top, right, bottom) pixel coordinates.

Source code in submodules/mesqual/mesqual/utils/folium_utils/screenshotter.py
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
def to_crop_box(self, image_width: int, image_height: int, dpr: float) -> tuple[int, int, int, int]:
    """Convert frame config to PIL crop box coordinates.

    Args:
        image_width: Actual screenshot width in pixels.
        image_height: Actual screenshot height in pixels.
        dpr: Device pixel ratio for scaling.

    Returns:
        Tuple of (left, top, right, bottom) pixel coordinates.
    """
    center_x = image_width // 2 + int(self.offset_center_x * dpr)
    center_y = image_height // 2 + int(self.offset_center_y * dpr)
    scaled_width = int(self.width * dpr)
    scaled_height = int(self.height * dpr)
    left = center_x - scaled_width // 2
    top = center_y - scaled_height // 2
    right = left + scaled_width
    bottom = top + scaled_height
    return (left, top, right, bottom)

MapViewConfig dataclass

Configuration for the Leaflet map view.

Attributes:

Name Type Description
center_lat float | None

Latitude of map center.

center_lng float | None

Longitude of map center.

zoom float | None

Map zoom level. Supports fractional values (e.g., 4.5).

Source code in submodules/mesqual/mesqual/utils/folium_utils/screenshotter.py
67
68
69
70
71
72
73
74
75
76
77
78
@dataclass
class MapViewConfig:
    """Configuration for the Leaflet map view.

    Attributes:
        center_lat: Latitude of map center.
        center_lng: Longitude of map center.
        zoom: Map zoom level. Supports fractional values (e.g., 4.5).
    """
    center_lat: float | None = None
    center_lng: float | None = None
    zoom: float | None = None

LegendInfo dataclass

Information about a detected legend element.

Attributes:

Name Type Description
element_id str

DOM element ID of the legend.

title str | None

Legend title text, if present.

width int

Legend width in CSS pixels.

height int

Legend height in CSS pixels.

Source code in submodules/mesqual/mesqual/utils/folium_utils/screenshotter.py
81
82
83
84
85
86
87
88
89
90
91
92
93
94
@dataclass
class LegendInfo:
    """Information about a detected legend element.

    Attributes:
        element_id: DOM element ID of the legend.
        title: Legend title text, if present.
        width: Legend width in CSS pixels.
        height: Legend height in CSS pixels.
    """
    element_id: str
    title: str | None
    width: int
    height: int

FoliumScreenshotter

Automated screenshot capture for Folium HTML maps.

Uses headless Chrome via Selenium to render Folium maps and capture screenshots of map layers and legends. Supports high-DPI rendering, custom crop frames, and automatic detection of base layers and legend elements.

Parameters:

Name Type Description Default
html_path str | Path

Path to the Folium-generated HTML file.

required
screen_config ScreenConfig | None

Virtual browser viewport configuration.

None
frame_config FrameConfig | None

Screenshot crop frame configuration. If None, captures full viewport.

None
map_view_config MapViewConfig | None

Leaflet map view settings (center, zoom).

None

Example:

>>> screenshotter = FoliumScreenshotter(
...     "map.html",
...     screen_config=ScreenConfig(1920, 1080, 2.0),
...     frame_config=FrameConfig(800, 600),
...     map_view_config=MapViewConfig(52.52, 13.405, 10),
... )
>>>
>>> # Capture all base layers
>>> screenshotter.capture_all_base_layers("output/layers")
>>>
>>> # Capture legends separately
>>> screenshotter.capture_legends("output/legends")
Source code in submodules/mesqual/mesqual/utils/folium_utils/screenshotter.py
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
class FoliumScreenshotter:
    """Automated screenshot capture for Folium HTML maps.

    Uses headless Chrome via Selenium to render Folium maps and capture screenshots
    of map layers and legends. Supports high-DPI rendering, custom crop frames,
    and automatic detection of base layers and legend elements.

    Args:
        html_path: Path to the Folium-generated HTML file.
        screen_config: Virtual browser viewport configuration.
        frame_config: Screenshot crop frame configuration. If None, captures full viewport.
        map_view_config: Leaflet map view settings (center, zoom).

    Example:

        >>> screenshotter = FoliumScreenshotter(
        ...     "map.html",
        ...     screen_config=ScreenConfig(1920, 1080, 2.0),
        ...     frame_config=FrameConfig(800, 600),
        ...     map_view_config=MapViewConfig(52.52, 13.405, 10),
        ... )
        >>>
        >>> # Capture all base layers
        >>> screenshotter.capture_all_base_layers("output/layers")
        >>>
        >>> # Capture legends separately
        >>> screenshotter.capture_legends("output/legends")

    """

    def __init__(
            self,
            html_path: str | Path,
            screen_config: ScreenConfig | None = None,
            frame_config: FrameConfig | None = None,
            map_view_config: MapViewConfig | None = None
    ):
        self._html_path = Path(html_path).resolve()
        self._screen_config = screen_config or ScreenConfig()
        self._frame_config = frame_config
        self._map_view_config = map_view_config or MapViewConfig()
        self._driver: webdriver.Chrome | None = None

    def _start_browser(self) -> None:
        options = Options()
        options.add_argument("--headless=new")
        options.add_argument("--disable-gpu")
        options.add_argument("--no-sandbox")
        options.add_argument(f"--window-size={self._screen_config.width},{self._screen_config.height}")
        options.add_argument(f"--force-device-scale-factor={self._screen_config.device_pixel_ratio}")
        options.add_argument("--high-dpi-support=1")
        self._driver = webdriver.Chrome(options=options)

    def _stop_browser(self) -> None:
        if self._driver:
            self._driver.quit()
            self._driver = None

    def _load_map(self) -> None:
        self._driver.get(f"file://{self._html_path}")
        WebDriverWait(self._driver, 10).until(
            EC.presence_of_element_located((By.CLASS_NAME, "leaflet-container"))
        )
        self._driver.execute_script("""
            let container = document.querySelector('.leaflet-container');
            container.style.position = 'absolute';
            container.style.top = '0';
            container.style.left = '0';
            container.style.width = '100vw';
            container.style.height = '100vh';
            container.style.margin = '0';
            container.style.padding = '0';
            document.body.style.margin = '0';
            document.body.style.padding = '0';
            document.body.style.overflow = 'hidden';
        """)
        time.sleep(1)

    def _get_map_object_name(self) -> str:
        return self._driver.execute_script("""
            for (let key in window) {
                if (window[key] instanceof L.Map) return key;
            }
            return null;
        """)

    def _set_view(self, map_name: str) -> None:
        cfg = self._map_view_config
        if cfg.center_lat is not None and cfg.center_lng is not None:
            zoom_expr = str(cfg.zoom) if cfg.zoom else f"{map_name}.getZoom()"
            self._driver.execute_script(
                f"{map_name}.setView([{cfg.center_lat}, {cfg.center_lng}], {zoom_expr});"
            )
        elif cfg.zoom is not None:
            self._driver.execute_script(f"{map_name}.setZoom({cfg.zoom});")
        self._driver.execute_script(f"{map_name}.invalidateSize();")
        time.sleep(1)

    def _get_base_layers(self) -> list[dict]:
        return self._driver.execute_script("""
            let result = [];
            document.querySelectorAll('.leaflet-control-layers-base label').forEach((label, idx) => {
                let input = label.querySelector('input');
                let name = label.textContent.trim();
                result.push({
                    name: name,
                    index: idx,
                    checked: input.checked
                });
            });
            return result;
        """)

    def _select_base_layer(self, index: int) -> None:
        self._driver.execute_script(f"""
            let inputs = document.querySelectorAll('.leaflet-control-layers-base input');
            if (inputs[{index}]) inputs[{index}].click();
        """)
        time.sleep(1)

    def _detect_legends(self) -> list[LegendInfo]:
        legends_data = self._driver.execute_script("""
            let legends = [];

            // Method 1: Find by ID pattern (*Legend_*)
            document.querySelectorAll('[id*="Legend_"]').forEach(el => {
                let titleEl = el.querySelector('.legend-title');
                let rect = el.getBoundingClientRect();
                legends.push({
                    id: el.id,
                    title: titleEl ? titleEl.textContent.trim() : null,
                    width: rect.width,
                    height: rect.height
                });
            });

            // Method 2: Find by structure (position:fixed + .legend-content)
            if (legends.length === 0) {
                document.querySelectorAll('div').forEach(el => {
                    let style = window.getComputedStyle(el);
                    if (style.position === 'fixed' && el.querySelector('.legend-content')) {
                        let titleEl = el.querySelector('.legend-title');
                        let rect = el.getBoundingClientRect();
                        legends.push({
                            id: el.id || `legend_${legends.length}`,
                            title: titleEl ? titleEl.textContent.trim() : null,
                            width: rect.width,
                            height: rect.height
                        });
                    }
                });
            }

            return legends;
        """)

        return [
            LegendInfo(
                element_id=l["id"],
                title=l["title"],
                width=int(l["width"]),
                height=int(l["height"])
            )
            for l in legends_data
        ]

    def _hide_layer_control(self) -> None:
        self._driver.execute_script("""
            let control = document.querySelector('.leaflet-control-layers');
            if (control) control.style.display = 'none';
        """)

    def _show_layer_control(self) -> None:
        self._driver.execute_script("""
            let control = document.querySelector('.leaflet-control-layers');
            if (control) control.style.display = '';
        """)

    def _hide_legends(self) -> None:
        self._driver.execute_script("""
            // By ID pattern
            document.querySelectorAll('[id*="Legend_"]').forEach(el => {
                el.style.display = 'none';
            });

            // By structure (position:fixed + .legend-content)
            document.querySelectorAll('div').forEach(el => {
                let style = window.getComputedStyle(el);
                if (style.position === 'fixed' && el.querySelector('.legend-content')) {
                    el.style.display = 'none';
                }
            });
        """)

    def _show_legends(self) -> None:
        self._driver.execute_script("""
            document.querySelectorAll('[id*="Legend_"]').forEach(el => {
                el.style.display = '';
            });

            document.querySelectorAll('div').forEach(el => {
                let style = window.getComputedStyle(el);
                if (style.position === 'fixed' && el.querySelector('.legend-content')) {
                    el.style.display = '';
                }
            });
        """)

    def _hide_ui_elements(self) -> None:
        self._hide_layer_control()
        self._hide_legends()

    def _show_ui_elements(self) -> None:
        self._show_layer_control()
        self._show_legends()

    def _take_screenshot(self, output_path: Path) -> None:
        self._hide_ui_elements()

        png_bytes = self._driver.get_screenshot_as_png()
        image = Image.open(io.BytesIO(png_bytes))

        if self._frame_config:
            crop_box = self._frame_config.to_crop_box(
                image.width,
                image.height,
                self._screen_config.device_pixel_ratio
            )
            crop_box = (
                max(0, crop_box[0]),
                max(0, crop_box[1]),
                min(image.width, crop_box[2]),
                min(image.height, crop_box[3])
            )
            image = image.crop(crop_box)

        image.save(output_path)

        self._show_ui_elements()

    def _take_element_screenshot(self, element_id: str, output_path: Path) -> None:
        self._hide_layer_control()

        self._driver.execute_script("""
            let targetId = arguments[0];
            document.querySelectorAll('[id*="Legend_"]').forEach(el => {
                if (el.id !== targetId) el.style.display = 'none';
            });
        """, element_id)

        dpr = self._screen_config.device_pixel_ratio
        rect = self._driver.execute_script("""
            let el = document.getElementById(arguments[0]);
            let rect = el.getBoundingClientRect();
            return {left: rect.left, top: rect.top, width: rect.width, height: rect.height};
        """, element_id)

        png_bytes = self._driver.get_screenshot_as_png()
        image = Image.open(io.BytesIO(png_bytes))

        crop_box = (
            int(rect["left"] * dpr),
            int(rect["top"] * dpr),
            int((rect["left"] + rect["width"]) * dpr),
            int((rect["top"] + rect["height"]) * dpr)
        )

        crop_box = (
            max(0, crop_box[0]),
            max(0, crop_box[1]),
            min(image.width, crop_box[2]),
            min(image.height, crop_box[3])
        )

        image = image.crop(crop_box)
        image.save(output_path)

        self._show_ui_elements()

    def _sanitize_filename(self, name: str) -> str:
        return "".join(c if c.isalnum() or c in "._- " else "_" for c in name).strip()

    def set_frame(
            self,
            width: int,
            height: int,
            offset_center_x: int = 0,
            offset_center_y: int = 0
    ) -> "FoliumScreenshotter":
        """Set the screenshot crop frame.

        Args:
            width: Frame width in CSS pixels.
            height: Frame height in CSS pixels.
            offset_center_x: Horizontal offset from screen center.
            offset_center_y: Vertical offset from screen center.

        Returns:
            Self for method chaining.
        """
        self._frame_config = FrameConfig(width, height, offset_center_x, offset_center_y)
        return self

    def set_map_view(self, lat: float, lng: float, zoom: float | None = None) -> "FoliumScreenshotter":
        """Set the map view center and zoom level.

        Args:
            lat: Center latitude.
            lng: Center longitude.
            zoom: Zoom level. Supports fractional values.

        Returns:
            Self for method chaining.
        """
        self._map_view_config = MapViewConfig(lat, lng, zoom)
        return self

    def capture_all_base_layers(self, output_dir: str | Path) -> list[Path]:
        """Capture screenshots of all base layers (overlay=False feature groups).

        Iterates through each base layer in the Leaflet layer control,
        selects it, and takes a screenshot. UI elements (layer control, legends)
        are hidden during capture.

        Args:
            output_dir: Directory to save screenshots. Created if it doesn't exist.

        Returns:
            List of paths to saved screenshot files.

        Raises:
            RuntimeError: If no Leaflet map object is found in the HTML.
        """
        output_dir = Path(output_dir)
        output_dir.mkdir(parents=True, exist_ok=True)
        saved_files = []

        try:
            self._start_browser()
            self._load_map()

            map_name = self._get_map_object_name()
            if not map_name:
                raise RuntimeError("Could not find Leaflet map object")

            self._set_view(map_name)
            base_layers = self._get_base_layers()

            if not base_layers:
                print("No base layers (overlay=False) found")
                return saved_files

            for layer in base_layers:
                self._select_base_layer(layer["index"])
                filename = f"{self._sanitize_filename(layer['name'])}.png"
                output_path = output_dir / filename
                self._take_screenshot(output_path)
                saved_files.append(output_path)
                print(f"Saved: {output_path}")

            return saved_files
        finally:
            self._stop_browser()

    def capture_single_view(self, output_path: str | Path) -> Path:
        """Capture a single screenshot of the current map view.

        Takes a screenshot with the currently active base layer.
        UI elements (layer control, legends) are hidden during capture.

        Args:
            output_path: Path for the output PNG file.

        Returns:
            Path to the saved screenshot file.
        """
        output_path = Path(output_path)
        output_path.parent.mkdir(parents=True, exist_ok=True)

        try:
            self._start_browser()
            self._load_map()

            map_name = self._get_map_object_name()
            if map_name:
                self._set_view(map_name)

            self._take_screenshot(output_path)
            return output_path
        finally:
            self._stop_browser()

    def capture_legends(self, output_dir: str | Path) -> dict[str, Path]:
        """Capture screenshots of all detected legend elements.

        Detects legends by ID pattern (`*Legend_*`) or by structure
        (position:fixed elements containing `.legend-content`).
        Each legend is captured individually with other legends hidden.

        Args:
            output_dir: Directory to save legend screenshots.

        Returns:
            Dictionary mapping legend names (title or element ID) to file paths.
        """
        output_dir = Path(output_dir)
        output_dir.mkdir(parents=True, exist_ok=True)
        saved_files = {}

        try:
            self._start_browser()
            self._load_map()

            legends = self._detect_legends()

            if not legends:
                print("No legends detected")
                return saved_files

            for legend in legends:
                name = legend.title or legend.element_id
                filename = f"{self._sanitize_filename(name)}.png"
                output_path = output_dir / filename
                self._take_element_screenshot(legend.element_id, output_path)
                saved_files[name] = output_path
                print(f"Saved legend: {output_path} ({legend.width}x{legend.height}px)")

            return saved_files
        finally:
            self._stop_browser()

    def capture_all(self, output_dir: str | Path) -> dict[str, list[Path] | dict[str, Path]]:
        """Capture screenshots of all base layers and legends.

        Convenience method that captures everything in a single browser session.
        Outputs are organized into subdirectories: `layers/` for base layer
        screenshots and `legends/` for legend screenshots.

        Args:
            output_dir: Root directory for output. Subdirectories are created automatically.

        Returns:
            Dictionary with keys:

            - `base_layers`: List of paths to layer screenshots.
            - `legends`: Dictionary mapping legend names to file paths.

        Raises:
            RuntimeError: If no Leaflet map object is found in the HTML.
        """
        output_dir = Path(output_dir)
        output_dir.mkdir(parents=True, exist_ok=True)

        results = {
            "base_layers": [],
            "legends": {}
        }

        try:
            self._start_browser()
            self._load_map()

            map_name = self._get_map_object_name()
            if not map_name:
                raise RuntimeError("Could not find Leaflet map object")

            self._set_view(map_name)

            legends = self._detect_legends()
            legends_dir = output_dir / "legends"
            legends_dir.mkdir(exist_ok=True)

            for legend in legends:
                name = legend.title or legend.element_id
                filename = f"{self._sanitize_filename(name)}.png"
                output_path = legends_dir / filename
                self._take_element_screenshot(legend.element_id, output_path)
                results["legends"][name] = output_path
                print(f"Saved legend: {output_path}")

            base_layers = self._get_base_layers()
            layers_dir = output_dir / "layers"
            layers_dir.mkdir(exist_ok=True)

            for layer in base_layers:
                self._select_base_layer(layer["index"])
                filename = f"{self._sanitize_filename(layer['name'])}.png"
                output_path = layers_dir / filename
                self._take_screenshot(output_path)
                results["base_layers"].append(output_path)
                print(f"Saved layer: {output_path}")

            return results
        finally:
            self._stop_browser()

set_frame

set_frame(width: int, height: int, offset_center_x: int = 0, offset_center_y: int = 0) -> FoliumScreenshotter

Set the screenshot crop frame.

Parameters:

Name Type Description Default
width int

Frame width in CSS pixels.

required
height int

Frame height in CSS pixels.

required
offset_center_x int

Horizontal offset from screen center.

0
offset_center_y int

Vertical offset from screen center.

0

Returns:

Type Description
FoliumScreenshotter

Self for method chaining.

Source code in submodules/mesqual/mesqual/utils/folium_utils/screenshotter.py
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
def set_frame(
        self,
        width: int,
        height: int,
        offset_center_x: int = 0,
        offset_center_y: int = 0
) -> "FoliumScreenshotter":
    """Set the screenshot crop frame.

    Args:
        width: Frame width in CSS pixels.
        height: Frame height in CSS pixels.
        offset_center_x: Horizontal offset from screen center.
        offset_center_y: Vertical offset from screen center.

    Returns:
        Self for method chaining.
    """
    self._frame_config = FrameConfig(width, height, offset_center_x, offset_center_y)
    return self

set_map_view

set_map_view(lat: float, lng: float, zoom: float | None = None) -> FoliumScreenshotter

Set the map view center and zoom level.

Parameters:

Name Type Description Default
lat float

Center latitude.

required
lng float

Center longitude.

required
zoom float | None

Zoom level. Supports fractional values.

None

Returns:

Type Description
FoliumScreenshotter

Self for method chaining.

Source code in submodules/mesqual/mesqual/utils/folium_utils/screenshotter.py
400
401
402
403
404
405
406
407
408
409
410
411
412
def set_map_view(self, lat: float, lng: float, zoom: float | None = None) -> "FoliumScreenshotter":
    """Set the map view center and zoom level.

    Args:
        lat: Center latitude.
        lng: Center longitude.
        zoom: Zoom level. Supports fractional values.

    Returns:
        Self for method chaining.
    """
    self._map_view_config = MapViewConfig(lat, lng, zoom)
    return self

capture_all_base_layers

capture_all_base_layers(output_dir: str | Path) -> list[Path]

Capture screenshots of all base layers (overlay=False feature groups).

Iterates through each base layer in the Leaflet layer control, selects it, and takes a screenshot. UI elements (layer control, legends) are hidden during capture.

Parameters:

Name Type Description Default
output_dir str | Path

Directory to save screenshots. Created if it doesn't exist.

required

Returns:

Type Description
list[Path]

List of paths to saved screenshot files.

Raises:

Type Description
RuntimeError

If no Leaflet map object is found in the HTML.

Source code in submodules/mesqual/mesqual/utils/folium_utils/screenshotter.py
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
def capture_all_base_layers(self, output_dir: str | Path) -> list[Path]:
    """Capture screenshots of all base layers (overlay=False feature groups).

    Iterates through each base layer in the Leaflet layer control,
    selects it, and takes a screenshot. UI elements (layer control, legends)
    are hidden during capture.

    Args:
        output_dir: Directory to save screenshots. Created if it doesn't exist.

    Returns:
        List of paths to saved screenshot files.

    Raises:
        RuntimeError: If no Leaflet map object is found in the HTML.
    """
    output_dir = Path(output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)
    saved_files = []

    try:
        self._start_browser()
        self._load_map()

        map_name = self._get_map_object_name()
        if not map_name:
            raise RuntimeError("Could not find Leaflet map object")

        self._set_view(map_name)
        base_layers = self._get_base_layers()

        if not base_layers:
            print("No base layers (overlay=False) found")
            return saved_files

        for layer in base_layers:
            self._select_base_layer(layer["index"])
            filename = f"{self._sanitize_filename(layer['name'])}.png"
            output_path = output_dir / filename
            self._take_screenshot(output_path)
            saved_files.append(output_path)
            print(f"Saved: {output_path}")

        return saved_files
    finally:
        self._stop_browser()

capture_single_view

capture_single_view(output_path: str | Path) -> Path

Capture a single screenshot of the current map view.

Takes a screenshot with the currently active base layer. UI elements (layer control, legends) are hidden during capture.

Parameters:

Name Type Description Default
output_path str | Path

Path for the output PNG file.

required

Returns:

Type Description
Path

Path to the saved screenshot file.

Source code in submodules/mesqual/mesqual/utils/folium_utils/screenshotter.py
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
def capture_single_view(self, output_path: str | Path) -> Path:
    """Capture a single screenshot of the current map view.

    Takes a screenshot with the currently active base layer.
    UI elements (layer control, legends) are hidden during capture.

    Args:
        output_path: Path for the output PNG file.

    Returns:
        Path to the saved screenshot file.
    """
    output_path = Path(output_path)
    output_path.parent.mkdir(parents=True, exist_ok=True)

    try:
        self._start_browser()
        self._load_map()

        map_name = self._get_map_object_name()
        if map_name:
            self._set_view(map_name)

        self._take_screenshot(output_path)
        return output_path
    finally:
        self._stop_browser()

capture_legends

capture_legends(output_dir: str | Path) -> dict[str, Path]

Capture screenshots of all detected legend elements.

Detects legends by ID pattern (*Legend_*) or by structure (position:fixed elements containing .legend-content). Each legend is captured individually with other legends hidden.

Parameters:

Name Type Description Default
output_dir str | Path

Directory to save legend screenshots.

required

Returns:

Type Description
dict[str, Path]

Dictionary mapping legend names (title or element ID) to file paths.

Source code in submodules/mesqual/mesqual/utils/folium_utils/screenshotter.py
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
def capture_legends(self, output_dir: str | Path) -> dict[str, Path]:
    """Capture screenshots of all detected legend elements.

    Detects legends by ID pattern (`*Legend_*`) or by structure
    (position:fixed elements containing `.legend-content`).
    Each legend is captured individually with other legends hidden.

    Args:
        output_dir: Directory to save legend screenshots.

    Returns:
        Dictionary mapping legend names (title or element ID) to file paths.
    """
    output_dir = Path(output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)
    saved_files = {}

    try:
        self._start_browser()
        self._load_map()

        legends = self._detect_legends()

        if not legends:
            print("No legends detected")
            return saved_files

        for legend in legends:
            name = legend.title or legend.element_id
            filename = f"{self._sanitize_filename(name)}.png"
            output_path = output_dir / filename
            self._take_element_screenshot(legend.element_id, output_path)
            saved_files[name] = output_path
            print(f"Saved legend: {output_path} ({legend.width}x{legend.height}px)")

        return saved_files
    finally:
        self._stop_browser()

capture_all

capture_all(output_dir: str | Path) -> dict[str, list[Path] | dict[str, Path]]

Capture screenshots of all base layers and legends.

Convenience method that captures everything in a single browser session. Outputs are organized into subdirectories: layers/ for base layer screenshots and legends/ for legend screenshots.

Parameters:

Name Type Description Default
output_dir str | Path

Root directory for output. Subdirectories are created automatically.

required

Returns:

Type Description
dict[str, list[Path] | dict[str, Path]]

Dictionary with keys:

dict[str, list[Path] | dict[str, Path]]
  • base_layers: List of paths to layer screenshots.
dict[str, list[Path] | dict[str, Path]]
  • legends: Dictionary mapping legend names to file paths.

Raises:

Type Description
RuntimeError

If no Leaflet map object is found in the HTML.

Source code in submodules/mesqual/mesqual/utils/folium_utils/screenshotter.py
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
def capture_all(self, output_dir: str | Path) -> dict[str, list[Path] | dict[str, Path]]:
    """Capture screenshots of all base layers and legends.

    Convenience method that captures everything in a single browser session.
    Outputs are organized into subdirectories: `layers/` for base layer
    screenshots and `legends/` for legend screenshots.

    Args:
        output_dir: Root directory for output. Subdirectories are created automatically.

    Returns:
        Dictionary with keys:

        - `base_layers`: List of paths to layer screenshots.
        - `legends`: Dictionary mapping legend names to file paths.

    Raises:
        RuntimeError: If no Leaflet map object is found in the HTML.
    """
    output_dir = Path(output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)

    results = {
        "base_layers": [],
        "legends": {}
    }

    try:
        self._start_browser()
        self._load_map()

        map_name = self._get_map_object_name()
        if not map_name:
            raise RuntimeError("Could not find Leaflet map object")

        self._set_view(map_name)

        legends = self._detect_legends()
        legends_dir = output_dir / "legends"
        legends_dir.mkdir(exist_ok=True)

        for legend in legends:
            name = legend.title or legend.element_id
            filename = f"{self._sanitize_filename(name)}.png"
            output_path = legends_dir / filename
            self._take_element_screenshot(legend.element_id, output_path)
            results["legends"][name] = output_path
            print(f"Saved legend: {output_path}")

        base_layers = self._get_base_layers()
        layers_dir = output_dir / "layers"
        layers_dir.mkdir(exist_ok=True)

        for layer in base_layers:
            self._select_base_layer(layer["index"])
            filename = f"{self._sanitize_filename(layer['name'])}.png"
            output_path = layers_dir / filename
            self._take_screenshot(output_path)
            results["base_layers"].append(output_path)
            print(f"Saved layer: {output_path}")

        return results
    finally:
        self._stop_browser()