1- #Copyright (C) 2025 Alexander Vanhee
1+ # Copyright (C) 2025 Alexander Vanhee
22#
33# This program is free software: you can redistribute it and/or modify
44# it under the terms of the GNU General Public License as published by
1818from gi .repository import Gtk , Gdk , Gio , cairo , Pango , PangoCairo
1919from enum import Enum
2020import math
21- import re
2221
2322class DrawingMode (Enum ):
2423 PEN = "pen"
@@ -28,38 +27,38 @@ class DrawingMode(Enum):
2827 CIRCLE = "circle"
2928 TEXT = "text"
3029 SELECT = "select"
30+ HIGHLIGHTER = "highlighter"
3131
3232class DrawingAction :
33+ DEFAULT_PADDING = 0.02
34+
3335 def draw (self , cr : cairo .Context , image_to_widget_coords , scale : float ):
3436 raise NotImplementedError
3537
3638 def get_bounds (self ):
3739 raise NotImplementedError
3840
41+ def apply_padding (self , bounds , extra_padding = 0.0 ):
42+ min_x , min_y , max_x , max_y = bounds
43+ padding = self .DEFAULT_PADDING + extra_padding
44+ return (min_x - padding , min_y - padding , max_x + padding , max_y + padding )
45+
3946 def contains_point (self , x , y ):
4047 min_x , min_y , max_x , max_y = self .get_bounds ()
41- tolerance_x = (max_x - min_x ) * 0.1 if (max_x - min_x ) > 0 else 0.01
42- tolerance_y = (max_y - min_y ) * 0.1 if (max_y - min_y ) > 0 else 0.01
43-
4448 if isinstance (self , (LineAction , ArrowAction )):
4549 px , py = x , y
4650 x1 , y1 = self .start
4751 x2 , y2 = self .end
48-
49- line_len_sq = (x2 - x1 )** 2 + (y2 - y1 )** 2
52+ line_len_sq = (x2 - x1 ) ** 2 + (y2 - y1 ) ** 2
5053 if line_len_sq == 0 :
51- return math .hypot (px - x1 , py - y1 ) < tolerance_x
52-
54+ return math .hypot (px - x1 , py - y1 ) < self .DEFAULT_PADDING
5355 t = ((px - x1 ) * (x2 - x1 ) + (py - y1 ) * (y2 - y1 )) / line_len_sq
5456 t = max (0 , min (1 , t ))
55-
5657 closest_x = x1 + t * (x2 - x1 )
5758 closest_y = y1 + t * (y2 - y1 )
58-
5959 dist_sq = (px - closest_x )** 2 + (py - closest_y )** 2
6060 return dist_sq < (0.01 + self .width / 200.0 )** 2
61-
62- return min_x - tolerance_x <= x <= max_x + tolerance_x and min_y - tolerance_y <= y <= max_y + tolerance_y
61+ return min_x <= x <= max_x and min_y <= y <= max_y
6362
6463 def translate (self , dx , dy ):
6564 raise NotImplementedError
@@ -83,11 +82,9 @@ def draw(self, cr, image_to_widget_coords, scale):
8382
8483 def get_bounds (self ):
8584 if not self .stroke :
86- return 0 , 0 , 0 , 0
87- xs = [p [0 ] for p in self .stroke ]
88- ys = [p [1 ] for p in self .stroke ]
89- padding = self .pen_size / 200.0
90- return min (xs ) - padding , min (ys ) - padding , max (xs ) + padding , max (ys ) + padding
85+ return (0 , 0 , 0 , 0 )
86+ xs , ys = zip (* self .stroke )
87+ return self .apply_padding ((min (xs ), min (ys ), max (xs ), max (ys )))
9188
9289 def translate (self , dx , dy ):
9390 self .stroke = [(x + dx , y + dy ) for x , y in self .stroke ]
@@ -129,8 +126,7 @@ def get_bounds(self):
129126 max_x = max (self .start [0 ], self .end [0 ])
130127 min_y = min (self .start [1 ], self .end [1 ])
131128 max_y = max (self .start [1 ], self .end [1 ])
132- padding = 0.025
133- return min_x - padding , min_y - padding , max_x + padding , max_y + padding
129+ return self .apply_padding ((min_x , min_y , max_x , max_y ))
134130
135131 def translate (self , dx , dy ):
136132 self .start = (self .start [0 ] + dx , self .start [1 ] + dy )
@@ -147,72 +143,38 @@ def __init__(self, position, text, color, font_size, font_family="Sans"):
147143 def draw (self , cr , image_to_widget_coords , scale ):
148144 if not self .text .strip ():
149145 return
150-
151146 x , y = image_to_widget_coords (* self .position )
152147 cr .set_source_rgba (* self .color )
153-
154148 layout = PangoCairo .create_layout (cr )
155149 font_desc = Pango .FontDescription ()
156150 font_desc .set_family (self .font_family )
157151 font_desc .set_size (int (self .font_size * scale * Pango .SCALE ))
158152 layout .set_font_description (font_desc )
159153 layout .set_text (self .text , - 1 )
160-
161- ink_rect , logical_rect = layout .get_extents ()
154+ _ , logical_rect = layout .get_extents ()
162155 text_width = logical_rect .width / Pango .SCALE
163156 text_height = logical_rect .height / Pango .SCALE
164-
165- adjusted_x = x - (text_width / 2 )
166- adjusted_y = y - text_height
167-
168- cr .move_to (adjusted_x , adjusted_y )
157+ cr .move_to (x - text_width / 2 , y - text_height )
169158 PangoCairo .show_layout (cr , layout )
170159
171160 def get_bounds (self ):
172161 char_width_factor = 0.6
173- estimated_text_width_img_units = len (self .text ) * self .font_size * char_width_factor / 800.0
174- estimated_text_height_img_units = self .font_size / 400.0
175-
162+ width = len (self .text ) * self .font_size * char_width_factor / 800.0
163+ height = self .font_size / 400.0
176164 x , y = self .position
177-
178- min_x = x - estimated_text_width_img_units / 2
179- max_x = x + estimated_text_width_img_units / 2
180- min_y = y - estimated_text_height_img_units
181- max_y = y
182-
183- return (min_x , min_y , max_x , max_y )
165+ return self .apply_padding ((x - width / 2 , y - height , x + width / 2 , y ))
184166
185167 def translate (self , dx , dy ):
186168 self .position = (self .position [0 ] + dx , self .position [1 ] + dy )
187169
188- class LineAction (DrawingAction ):
189- def __init__ (self , start , end , color , width ):
190- self .start = start
191- self .end = end
192- self .color = color
193- self .width = width
194-
170+ class LineAction (ArrowAction ):
195171 def draw (self , cr , image_to_widget_coords , scale ):
196172 cr .set_source_rgba (* self .color )
197173 cr .set_line_width (self .width * scale )
198- start_x , start_y = image_to_widget_coords (* self .start )
199- end_x , end_y = image_to_widget_coords (* self .end )
200- cr .move_to (start_x , start_y )
201- cr .line_to (end_x , end_y )
174+ cr .move_to (* image_to_widget_coords (* self .start ))
175+ cr .line_to (* image_to_widget_coords (* self .end ))
202176 cr .stroke ()
203177
204- def get_bounds (self ):
205- min_x = min (self .start [0 ], self .end [0 ])
206- max_x = max (self .start [0 ], self .end [0 ])
207- min_y = min (self .start [1 ], self .end [1 ])
208- max_y = max (self .start [1 ], self .end [1 ])
209- padding = self .width / 200.0
210- return min_x - padding , min_y - padding , max_x + padding , max_y + padding
211-
212- def translate (self , dx , dy ):
213- self .start = (self .start [0 ] + dx , self .start [1 ] + dy )
214- self .end = (self .end [0 ] + dx , self .end [1 ] + dy )
215-
216178class RectAction (DrawingAction ):
217179 def __init__ (self , start , end , color , width , fill_color = None ):
218180 self .start = start
@@ -224,74 +186,61 @@ def __init__(self, start, end, color, width, fill_color=None):
224186 def draw (self , cr , image_to_widget_coords , scale ):
225187 x1 , y1 = image_to_widget_coords (* self .start )
226188 x2 , y2 = image_to_widget_coords (* self .end )
227- rect_x = min (x1 , x2 )
228- rect_y = min (y1 , y2 )
229- rect_w = abs (x2 - x1 )
230- rect_h = abs (y2 - y1 )
231-
189+ x , y = min (x1 , x2 ), min (y1 , y2 )
190+ w , h = abs (x2 - x1 ), abs (y2 - y1 )
232191 if self .fill_color :
233192 cr .set_source_rgba (* self .fill_color )
234- cr .rectangle (rect_x , rect_y , rect_w , rect_h )
193+ cr .rectangle (x , y , w , h )
235194 cr .fill ()
236-
237195 cr .set_source_rgba (* self .color )
238196 cr .set_line_width (self .width * scale )
239- cr .rectangle (rect_x , rect_y , rect_w , rect_h )
197+ cr .rectangle (x , y , w , h )
240198 cr .stroke ()
241199
242200 def get_bounds (self ):
243201 min_x = min (self .start [0 ], self .end [0 ])
244202 max_x = max (self .start [0 ], self .end [0 ])
245203 min_y = min (self .start [1 ], self .end [1 ])
246204 max_y = max (self .start [1 ], self .end [1 ])
247- padding = self .width / 200.0
248- return min_x - padding , min_y - padding , max_x + padding , max_y + padding
205+ return self .apply_padding ((min_x , min_y , max_x , max_y ))
249206
250207 def translate (self , dx , dy ):
251208 self .start = (self .start [0 ] + dx , self .start [1 ] + dy )
252209 self .end = (self .end [0 ] + dx , self .end [1 ] + dy )
253210
254- class CircleAction (DrawingAction ):
255- def __init__ (self , start , end , color , width , fill_color = None ):
256- self .start = start
257- self .end = end
258- self .color = color
259- self .width = width
260- self .fill_color = fill_color
261-
211+ class CircleAction (RectAction ):
262212 def draw (self , cr , image_to_widget_coords , scale ):
263213 x1 , y1 = image_to_widget_coords (* self .start )
264214 x2 , y2 = image_to_widget_coords (* self .end )
265- cx = (x1 + x2 ) / 2
266- cy = (y1 + y2 ) / 2
267- rx = abs (x2 - x1 ) / 2
268- ry = abs (y2 - y1 ) / 2
215+ cx , cy = (x1 + x2 ) / 2 , (y1 + y2 ) / 2
216+ rx , ry = abs (x2 - x1 ) / 2 , abs (y2 - y1 ) / 2
269217 if rx < 1e-3 or ry < 1e-3 :
270218 return
271-
272219 cr .save ()
273220 cr .translate (cx , cy )
274221 cr .scale (rx , ry )
275222 cr .arc (0 , 0 , 1 , 0 , 2 * math .pi )
276223 cr .restore ()
277-
278224 if self .fill_color :
279225 cr .set_source_rgba (* self .fill_color )
280226 cr .fill_preserve ()
281-
282227 cr .set_source_rgba (* self .color )
283228 cr .set_line_width (self .width * scale )
284229 cr .stroke ()
285230
286- def get_bounds (self ):
287- min_x = min (self .start [0 ], self .end [0 ])
288- max_x = max (self .start [0 ], self .end [0 ])
289- min_y = min (self .start [1 ], self .end [1 ])
290- max_y = max (self .start [1 ], self .end [1 ])
291- padding = self .width / 200.0
292- return min_x - padding , min_y - padding , max_x + padding , max_y + padding
293-
294- def translate (self , dx , dy ):
295- self .start = (self .start [0 ] + dx , self .start [1 ] + dy )
296- self .end = (self .end [0 ] + dx , self .end [1 ] + dy )
231+ class HighlighterAction (StrokeAction ):
232+ def draw (self , cr , image_to_widget_coords , scale ):
233+ if len (self .stroke ) < 2 :
234+ return
235+ coords = [image_to_widget_coords (x , y ) for x , y in self .stroke ]
236+ cr .set_operator (cairo .Operator .MULTIPLY )
237+ cr .set_source_rgba (* self .color )
238+ cr .set_line_width (self .pen_size * scale )
239+ cr .set_line_cap (cairo .LineCap .BUTT )
240+ cr .move_to (* coords [0 ])
241+ for point in coords [1 :]:
242+ cr .line_to (* point )
243+ cr .stroke ()
244+ cr .set_operator (cairo .Operator .OVER )
245+ cr .set_line_cap (cairo .LineCap .ROUND )
297246
0 commit comments