@@ -83,26 +83,24 @@ def add_watermark(
8383 font_size : int | None = None ,
8484 padding : int | None = None ,
8585) -> None :
86- """Add pyplots.ai branded watermark to an image.
86+ """Add pyplots.ai branded watermark footer to an image.
8787
88- Adds pyplots.ai (in brand colors) to bottom-right and spec_id to bottom- left.
89- Uses JetBrains Mono Bold font with gray shadow for readability .
88+ Adds a white footer strip below the image with spec_id ( left) and pyplots.ai (right) .
89+ Uses JetBrains Mono Bold font and brand colors .
9090 Font size and padding scale automatically based on image width.
9191
9292 Args:
9393 input_path: Path to the source image.
9494 output_path: Path where the watermarked image will be saved.
9595 spec_id: Spec ID for bottom-left corner (e.g., "scatter-basic").
96- font_size: Size of the watermark font in pixels. If None, auto-scales (~1% of width).
97- padding: Padding from the image edge in pixels. If None, auto-scales (~0.5 % of width).
96+ font_size: Size of the watermark font in pixels. If None, auto-scales (~1.35 % of width).
97+ padding: Padding from the image edge in pixels. If None, auto-scales (~0.8 % of width).
9898
9999 Raises:
100100 FileNotFoundError: If input_path does not exist.
101101 PIL.UnidentifiedImageError: If input is not a valid image.
102102 """
103- img = Image .open (input_path ).convert ("RGBA" )
104- overlay = Image .new ("RGBA" , img .size , (0 , 0 , 0 , 0 ))
105- draw = ImageDraw .Draw (overlay )
103+ img = Image .open (input_path ).convert ("RGB" )
106104
107105 # Auto-scale font size and padding based on image width
108106 if font_size is None :
@@ -111,44 +109,49 @@ def add_watermark(
111109 padding = max (15 , int (img .width * 0.008 )) # ~0.8% of width, min 15px
112110
113111 font = _get_font (font_size )
114- alpha = int (255 * 0.95 )
115112
116- # Brand colors
117- py_color = _hex_to_rgba (PYPLOTS_BLUE , alpha )
118- plots_color = _hex_to_rgba (PYPLOTS_YELLOW , alpha )
119- ai_color = _hex_to_rgba (PYPLOTS_DARK , alpha )
120- shadow_color = (50 , 50 , 50 , 150 ) # Gray shadow
113+ # Measure text height for footer
114+ temp_draw = ImageDraw .Draw (img )
115+ text_h = temp_draw .textbbox ((0 , 0 ), "py" , font = font )[3 ]
121116
122- # Measure text dimensions
117+ # Footer height: text + minimal padding (~half font height total padding)
118+ footer_padding_v = max (4 , text_h // 4 ) # Small vertical padding
119+ footer_height = text_h + footer_padding_v * 2
120+
121+ # Create new image with footer
122+ new_height = img .height + footer_height
123+ result = Image .new ("RGB" , (img .width , new_height ), (255 , 255 , 255 ))
124+ result .paste (img , (0 , 0 ))
125+
126+ # Draw on footer
127+ draw = ImageDraw .Draw (result )
128+
129+ # Brand colors (RGB, no alpha needed on white background)
130+ py_color = tuple (int (PYPLOTS_BLUE .lstrip ("#" )[i : i + 2 ], 16 ) for i in (0 , 2 , 4 ))
131+ plots_color = tuple (int (PYPLOTS_YELLOW .lstrip ("#" )[i : i + 2 ], 16 ) for i in (0 , 2 , 4 ))
132+ ai_color = tuple (int (PYPLOTS_DARK .lstrip ("#" )[i : i + 2 ], 16 ) for i in (0 , 2 , 4 ))
133+
134+ # Measure text widths
123135 py_w = draw .textbbox ((0 , 0 ), "py" , font = font )[2 ]
124136 plots_w = draw .textbbox ((0 , 0 ), "plots" , font = font )[2 ]
125137 ai_w = draw .textbbox ((0 , 0 ), ".ai" , font = font )[2 ]
126138 url_w = py_w + plots_w + ai_w
127- text_h = draw .textbbox ((0 , 0 ), "py" , font = font )[3 ]
128139
129- # Position: bottom with padding
130- y = img .height - text_h - padding
140+ # Position: centered vertically in footer
141+ y = img .height + footer_padding_v
131142 url_x = img .width - url_w - padding
132143 spec_x = padding
133- shadow_offset = 2
134-
135- # Draw pyplots.ai with shadow (right side)
136- # Shadow first
137- draw .text ((url_x + shadow_offset , y + shadow_offset ), "py" , font = font , fill = shadow_color )
138- draw .text ((url_x + py_w + shadow_offset , y + shadow_offset ), "plots" , font = font , fill = shadow_color )
139- draw .text ((url_x + py_w + plots_w + shadow_offset , y + shadow_offset ), ".ai" , font = font , fill = shadow_color )
140- # Colored text
144+
145+ # Draw pyplots.ai (right side)
141146 draw .text ((url_x , y ), "py" , font = font , fill = py_color )
142147 draw .text ((url_x + py_w , y ), "plots" , font = font , fill = plots_color )
143148 draw .text ((url_x + py_w + plots_w , y ), ".ai" , font = font , fill = ai_color )
144149
145- # Draw spec_id with shadow (left side)
150+ # Draw spec_id (left side)
146151 if spec_id :
147- draw .text ((spec_x + shadow_offset , y + shadow_offset ), spec_id , font = font , fill = shadow_color )
148152 draw .text ((spec_x , y ), spec_id , font = font , fill = py_color )
149153
150- result = Image .alpha_composite (img , overlay )
151- result .convert ("RGB" ).save (output_path , optimize = True )
154+ result .save (output_path , optimize = True )
152155
153156
154157def optimize_png (input_path : str | Path , output_path : str | Path | None = None , quality : int = 80 ) -> int :
0 commit comments