-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrenderer.py
More file actions
133 lines (107 loc) · 4.67 KB
/
renderer.py
File metadata and controls
133 lines (107 loc) · 4.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
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
import io
import datetime
from playwright.sync_api import sync_playwright
from PIL import Image, ImageOps
from config import Config
def capture_dashboard(url):
"""
Captures a screenshot of the dashboard page using Playwright.
Returns:
bytes: The screenshot image data in PNG format.
"""
# Define the design resolution that matches the CSS/HTML layout
DESIGN_WIDTH = 1680
DESIGN_HEIGHT = 1264
# Calculate scale factor based on target screen width versus design width
# We want 2x SSAA (Super Sampling Anti-Aliasing).
# Render at 2x the target resolution, then downsample later.
# scale_factor = (Config.SCREEN_WIDTH / DESIGN_WIDTH) * 2
# No SSAA
scale_factor = Config.SCREEN_WIDTH / DESIGN_WIDTH
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
# We set the viewport to the DESIGN resolution so the CSS layout remains identical.
# We set device_scale_factor so the browser renders at a higher/lower resolution
# effectively scaling the output image to (DESIGN_WIDTH * scale) x (DESIGN_HEIGHT * scale).
page = browser.new_page(
viewport={"width": DESIGN_WIDTH, "height": DESIGN_HEIGHT},
device_scale_factor=scale_factor
)
try:
page.goto(url, wait_until="networkidle", timeout=Config.RENDER_TIMEOUT)
# Extra wait to ensure all JS rendering (charts, etc) is done if networkidle isn't enough
# page.wait_for_timeout(2000)
screenshot_bytes = page.screenshot(type="png")
return screenshot_bytes
except Exception as e:
print(f"Error capturing screenshot: {e}")
raise
finally:
browser.close()
def process_image_for_kindle(input_bytes):
"""
Processes the raw screenshot for Kindle Oasis 2 display.
Apply resizing (if needed), rotation (if needed), grayscale, and dithering.
The user's request says: "1680*1264" and the CSS is landscape.
Kindle typically renders portrait.
If the user holds the Kindle sideways, we just need to ensure the image is 1680x1264 or 1264x1680.
"""
try:
img = Image.open(io.BytesIO(input_bytes))
# 1. Force RGB
if img.mode != 'RGB':
img = img.convert('RGB')
# 2. Resize/Fit (Downsampling for SSAA)
# We use the configured screen resolution as the target size.
target_size = (Config.SCREEN_WIDTH, Config.SCREEN_HEIGHT)
# Optimization: If the image is already at the target size (or very close), skip heavy resizing
# With SSAA enabled, the input image will be larger (2x) than target_size, so this will naturally fall through to fit()
if img.size == target_size:
img_fitted = img
else:
# Fallback to Resize/Fit if dimensions don't match exactly or for SSAA downsampling
# This handles cases where scale factor rounding or other issues might cause slight off-by-one.
# Using LANCZOS for high-quality downsampling.
img_fitted = ImageOps.fit(
img,
target_size,
method=Image.Resampling.LANCZOS,
centering=(0.5, 0.5)
)
# 3. 16-color Grayscale Palette
palette_img = Image.new('P', (1, 1))
palette_data = []
for i in range(16):
val = int(i * 255 / 15)
palette_data.extend((val, val, val))
palette_data.extend([0] * (768 - len(palette_data)))
palette_img.putpalette(palette_data)
# 4. Quantize + Dither
# User requested disabling Floyd-Steinberg dithering for cleaner image.
img_dithered_p = img_fitted.quantize(
palette=palette_img,
dither=Image.Dither.NONE
)
# 5. Convert back to 'L'
final_img = img_dithered_p.convert('L')
# 6. Save to bytes
output = io.BytesIO()
final_img.save(output, format="PNG", optimize=True)
output.seek(0)
return output
except Exception as e:
print(f"Error processing image: {e}")
import traceback
traceback.print_exc()
raise
def render_dashboard_to_bytes(url):
"""
Full pipeline: Capture -> Process -> Return Bytes
"""
start_time = datetime.datetime.now()
print(f"[{start_time}] Starting Render Job for {url}")
raw_png = capture_dashboard(url)
processed_png_io = process_image_for_kindle(raw_png)
end_time = datetime.datetime.now()
print(f"[{end_time}] Render finished in {(end_time - start_time).total_seconds()}s")
return processed_png_io