comparison src_classic/helper-geo-py.i @ 1:1d09e1dec1d9 upstream

ADD: PyMuPDF v1.26.4: the original sdist. It does not yet contain MuPDF. This normally will be downloaded when building PyMuPDF.
author Franz Glasner <fzglas.hg@dom66.de>
date Mon, 15 Sep 2025 11:37:51 +0200
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 1:1d09e1dec1d9
1 %pythoncode %{
2
3 # ------------------------------------------------------------------------
4 # Copyright 2020-2022, Harald Lieder, mailto:harald.lieder@outlook.com
5 # License: GNU AFFERO GPL 3.0, https://www.gnu.org/licenses/agpl-3.0.html
6 #
7 # Part of "PyMuPDF", a Python binding for "MuPDF" (http://mupdf.com), a
8 # lightweight PDF, XPS, and E-book viewer, renderer and toolkit which is
9 # maintained and developed by Artifex Software, Inc. https://artifex.com.
10 # ------------------------------------------------------------------------
11
12 # largest 32bit integers surviving C float conversion roundtrips
13 # used by MuPDF to define infinite rectangles
14 FZ_MIN_INF_RECT = -0x80000000
15 FZ_MAX_INF_RECT = 0x7fffff80
16
17
18 class Matrix(object):
19 """Matrix() - all zeros
20 Matrix(a, b, c, d, e, f)
21 Matrix(zoom-x, zoom-y) - zoom
22 Matrix(shear-x, shear-y, 1) - shear
23 Matrix(degree) - rotate
24 Matrix(Matrix) - new copy
25 Matrix(sequence) - from 'sequence'"""
26 def __init__(self, *args):
27 if not args:
28 self.a = self.b = self.c = self.d = self.e = self.f = 0.0
29 return None
30 if len(args) > 6:
31 raise ValueError("Matrix: bad seq len")
32 if len(args) == 6: # 6 numbers
33 self.a, self.b, self.c, self.d, self.e, self.f = map(float, args)
34 return None
35 if len(args) == 1: # either an angle or a sequ
36 if hasattr(args[0], "__float__"):
37 theta = math.radians(args[0])
38 c = round(math.cos(theta), 12)
39 s = round(math.sin(theta), 12)
40 self.a = self.d = c
41 self.b = s
42 self.c = -s
43 self.e = self.f = 0.0
44 return None
45 else:
46 self.a, self.b, self.c, self.d, self.e, self.f = map(float, args[0])
47 return None
48 if len(args) == 2 or len(args) == 3 and args[2] == 0:
49 self.a, self.b, self.c, self.d, self.e, self.f = float(args[0]), \
50 0.0, 0.0, float(args[1]), 0.0, 0.0
51 return None
52 if len(args) == 3 and args[2] == 1:
53 self.a, self.b, self.c, self.d, self.e, self.f = 1.0, \
54 float(args[1]), float(args[0]), 1.0, 0.0, 0.0
55 return None
56 raise ValueError("Matrix: bad args")
57
58 def invert(self, src=None):
59 """Calculate the inverted matrix. Return 0 if successful and replace
60 current one. Else return 1 and do nothing.
61 """
62 if src is None:
63 dst = util_invert_matrix(self)
64 else:
65 dst = util_invert_matrix(src)
66 if dst[0] == 1:
67 return 1
68 self.a, self.b, self.c, self.d, self.e, self.f = dst[1]
69 return 0
70
71 def pretranslate(self, tx, ty):
72 """Calculate pre translation and replace current matrix."""
73 tx = float(tx)
74 ty = float(ty)
75 self.e += tx * self.a + ty * self.c
76 self.f += tx * self.b + ty * self.d
77 return self
78
79 def prescale(self, sx, sy):
80 """Calculate pre scaling and replace current matrix."""
81 sx = float(sx)
82 sy = float(sy)
83 self.a *= sx
84 self.b *= sx
85 self.c *= sy
86 self.d *= sy
87 return self
88
89 def preshear(self, h, v):
90 """Calculate pre shearing and replace current matrix."""
91 h = float(h)
92 v = float(v)
93 a, b = self.a, self.b
94 self.a += v * self.c
95 self.b += v * self.d
96 self.c += h * a
97 self.d += h * b
98 return self
99
100 def prerotate(self, theta):
101 """Calculate pre rotation and replace current matrix."""
102 theta = float(theta)
103 while theta < 0: theta += 360
104 while theta >= 360: theta -= 360
105 if abs(0 - theta) < EPSILON:
106 pass
107
108 elif abs(90.0 - theta) < EPSILON:
109 a = self.a
110 b = self.b
111 self.a = self.c
112 self.b = self.d
113 self.c = -a
114 self.d = -b
115
116 elif abs(180.0 - theta) < EPSILON:
117 self.a = -self.a
118 self.b = -self.b
119 self.c = -self.c
120 self.d = -self.d
121
122 elif abs(270.0 - theta) < EPSILON:
123 a = self.a
124 b = self.b
125 self.a = -self.c
126 self.b = -self.d
127 self.c = a
128 self.d = b
129
130 else:
131 rad = math.radians(theta)
132 s = math.sin(rad)
133 c = math.cos(rad)
134 a = self.a
135 b = self.b
136 self.a = c * a + s * self.c
137 self.b = c * b + s * self.d
138 self.c =-s * a + c * self.c
139 self.d =-s * b + c * self.d
140
141 return self
142
143 def concat(self, one, two):
144 """Multiply two matrices and replace current one."""
145 if not len(one) == len(two) == 6:
146 raise ValueError("Matrix: bad seq len")
147 self.a, self.b, self.c, self.d, self.e, self.f = util_concat_matrix(one, two)
148 return self
149
150 def __getitem__(self, i):
151 return (self.a, self.b, self.c, self.d, self.e, self.f)[i]
152
153 def __setitem__(self, i, v):
154 v = float(v)
155 if i == 0: self.a = v
156 elif i == 1: self.b = v
157 elif i == 2: self.c = v
158 elif i == 3: self.d = v
159 elif i == 4: self.e = v
160 elif i == 5: self.f = v
161 else:
162 raise IndexError("index out of range")
163 return
164
165 def __len__(self):
166 return 6
167
168 def __repr__(self):
169 return "Matrix" + str(tuple(self))
170
171 def __invert__(self):
172 """Calculate inverted matrix."""
173 m1 = Matrix()
174 m1.invert(self)
175 return m1
176 __inv__ = __invert__
177
178 def __mul__(self, m):
179 if hasattr(m, "__float__"):
180 return Matrix(self.a * m, self.b * m, self.c * m,
181 self.d * m, self.e * m, self.f * m)
182 m1 = Matrix(1,1)
183 return m1.concat(self, m)
184
185 def __truediv__(self, m):
186 if hasattr(m, "__float__"):
187 return Matrix(self.a * 1./m, self.b * 1./m, self.c * 1./m,
188 self.d * 1./m, self.e * 1./m, self.f * 1./m)
189 m1 = util_invert_matrix(m)[1]
190 if not m1:
191 raise ZeroDivisionError("matrix not invertible")
192 m2 = Matrix(1,1)
193 return m2.concat(self, m1)
194 __div__ = __truediv__
195
196 def __add__(self, m):
197 if hasattr(m, "__float__"):
198 return Matrix(self.a + m, self.b + m, self.c + m,
199 self.d + m, self.e + m, self.f + m)
200 if len(m) != 6:
201 raise ValueError("Matrix: bad seq len")
202 return Matrix(self.a + m[0], self.b + m[1], self.c + m[2],
203 self.d + m[3], self.e + m[4], self.f + m[5])
204
205 def __sub__(self, m):
206 if hasattr(m, "__float__"):
207 return Matrix(self.a - m, self.b - m, self.c - m,
208 self.d - m, self.e - m, self.f - m)
209 if len(m) != 6:
210 raise ValueError("Matrix: bad seq len")
211 return Matrix(self.a - m[0], self.b - m[1], self.c - m[2],
212 self.d - m[3], self.e - m[4], self.f - m[5])
213
214 def __pos__(self):
215 return Matrix(self)
216
217 def __neg__(self):
218 return Matrix(-self.a, -self.b, -self.c, -self.d, -self.e, -self.f)
219
220 def __bool__(self):
221 return not (max(self) == min(self) == 0)
222
223 def __nonzero__(self):
224 return not (max(self) == min(self) == 0)
225
226 def __eq__(self, mat):
227 if not hasattr(mat, "__len__"):
228 return False
229 return len(mat) == 6 and bool(self - mat) is False
230
231 def __abs__(self):
232 return math.sqrt(sum([c*c for c in self]))
233
234 norm = __abs__
235
236 @property
237 def is_rectilinear(self):
238 """True if rectangles are mapped to rectangles."""
239 return (abs(self.b) < EPSILON and abs(self.c) < EPSILON) or \
240 (abs(self.a) < EPSILON and abs(self.d) < EPSILON);
241
242
243 class IdentityMatrix(Matrix):
244 """Identity matrix [1, 0, 0, 1, 0, 0]"""
245 def __init__(self):
246 Matrix.__init__(self, 1.0, 1.0)
247 def __setattr__(self, name, value):
248 if name in "ad":
249 self.__dict__[name] = 1.0
250 elif name in "bcef":
251 self.__dict__[name] = 0.0
252 else:
253 self.__dict__[name] = value
254
255 def checkargs(*args):
256 raise NotImplementedError("Identity is readonly")
257
258 prerotate = checkargs
259 preshear = checkargs
260 prescale = checkargs
261 pretranslate = checkargs
262 concat = checkargs
263 invert = checkargs
264
265 def __repr__(self):
266 return "IdentityMatrix(1.0, 0.0, 0.0, 1.0, 0.0, 0.0)"
267
268 def __hash__(self):
269 return hash((1,0,0,1,0,0))
270
271
272 Identity = IdentityMatrix()
273
274 class Point(object):
275 """Point() - all zeros\nPoint(x, y)\nPoint(Point) - new copy\nPoint(sequence) - from 'sequence'"""
276 def __init__(self, *args):
277 if not args:
278 self.x = 0.0
279 self.y = 0.0
280 return None
281
282 if len(args) > 2:
283 raise ValueError("Point: bad seq len")
284 if len(args) == 2:
285 self.x = float(args[0])
286 self.y = float(args[1])
287 return None
288 if len(args) == 1:
289 l = args[0]
290 if hasattr(l, "__getitem__") is False:
291 raise ValueError("Point: bad args")
292 if len(l) != 2:
293 raise ValueError("Point: bad seq len")
294 self.x = float(l[0])
295 self.y = float(l[1])
296 return None
297 raise ValueError("Point: bad args")
298
299 def transform(self, m):
300 """Replace point by its transformation with matrix-like m."""
301 if len(m) != 6:
302 raise ValueError("Matrix: bad seq len")
303 self.x, self.y = util_transform_point(self, m)
304 return self
305
306 @property
307 def unit(self):
308 """Unit vector of the point."""
309 s = self.x * self.x + self.y * self.y
310 if s < EPSILON:
311 return Point(0,0)
312 s = math.sqrt(s)
313 return Point(self.x / s, self.y / s)
314
315 @property
316 def abs_unit(self):
317 """Unit vector with positive coordinates."""
318 s = self.x * self.x + self.y * self.y
319 if s < EPSILON:
320 return Point(0,0)
321 s = math.sqrt(s)
322 return Point(abs(self.x) / s, abs(self.y) / s)
323
324 def distance_to(self, *args):
325 """Return distance to rectangle or another point."""
326 if not len(args) > 0:
327 raise ValueError("at least one parameter must be given")
328
329 x = args[0]
330 if len(x) == 2:
331 x = Point(x)
332 elif len(x) == 4:
333 x = Rect(x)
334 else:
335 raise ValueError("arg1 must be point-like or rect-like")
336
337 if len(args) > 1:
338 unit = args[1]
339 else:
340 unit = "px"
341 u = {"px": (1.,1.), "in": (1.,72.), "cm": (2.54, 72.),
342 "mm": (25.4, 72.)}
343 f = u[unit][0] / u[unit][1]
344
345 if type(x) is Point:
346 return abs(self - x) * f
347
348 # from here on, x is a rectangle
349 # as a safeguard, make a finite copy of it
350 r = Rect(x.top_left, x.top_left)
351 r = r | x.bottom_right
352 if self in r:
353 return 0.0
354 if self.x > r.x1:
355 if self.y >= r.y1:
356 return self.distance_to(r.bottom_right, unit)
357 elif self.y <= r.y0:
358 return self.distance_to(r.top_right, unit)
359 else:
360 return (self.x - r.x1) * f
361 elif r.x0 <= self.x <= r.x1:
362 if self.y >= r.y1:
363 return (self.y - r.y1) * f
364 else:
365 return (r.y0 - self.y) * f
366 else:
367 if self.y >= r.y1:
368 return self.distance_to(r.bottom_left, unit)
369 elif self.y <= r.y0:
370 return self.distance_to(r.top_left, unit)
371 else:
372 return (r.x0 - self.x) * f
373
374 def __getitem__(self, i):
375 return (self.x, self.y)[i]
376
377 def __len__(self):
378 return 2
379
380 def __setitem__(self, i, v):
381 v = float(v)
382 if i == 0: self.x = v
383 elif i == 1: self.y = v
384 else:
385 raise IndexError("index out of range")
386 return None
387
388 def __repr__(self):
389 return "Point" + str(tuple(self))
390
391 def __pos__(self):
392 return Point(self)
393
394 def __neg__(self):
395 return Point(-self.x, -self.y)
396
397 def __bool__(self):
398 return not (max(self) == min(self) == 0)
399
400 def __nonzero__(self):
401 return not (max(self) == min(self) == 0)
402
403 def __eq__(self, p):
404 if not hasattr(p, "__len__"):
405 return False
406 return len(p) == 2 and bool(self - p) is False
407
408 def __abs__(self):
409 return math.sqrt(self.x * self.x + self.y * self.y)
410
411 norm = __abs__
412
413 def __add__(self, p):
414 if hasattr(p, "__float__"):
415 return Point(self.x + p, self.y + p)
416 if len(p) != 2:
417 raise ValueError("Point: bad seq len")
418 return Point(self.x + p[0], self.y + p[1])
419
420 def __sub__(self, p):
421 if hasattr(p, "__float__"):
422 return Point(self.x - p, self.y - p)
423 if len(p) != 2:
424 raise ValueError("Point: bad seq len")
425 return Point(self.x - p[0], self.y - p[1])
426
427 def __mul__(self, m):
428 if hasattr(m, "__float__"):
429 return Point(self.x * m, self.y * m)
430 p = Point(self)
431 return p.transform(m)
432
433 def __truediv__(self, m):
434 if hasattr(m, "__float__"):
435 return Point(self.x * 1./m, self.y * 1./m)
436 m1 = util_invert_matrix(m)[1]
437 if not m1:
438 raise ZeroDivisionError("matrix not invertible")
439 p = Point(self)
440 return p.transform(m1)
441
442 __div__ = __truediv__
443
444 def __hash__(self):
445 return hash(tuple(self))
446
447 class Rect(object):
448 """Rect() - all zeros
449 Rect(x0, y0, x1, y1) - 4 coordinates
450 Rect(top-left, x1, y1) - point and 2 coordinates
451 Rect(x0, y0, bottom-right) - 2 coordinates and point
452 Rect(top-left, bottom-right) - 2 points
453 Rect(sequ) - new from sequence or rect-like
454 """
455 def __init__(self, *args):
456 self.x0, self.y0, self.x1, self.y1 = util_make_rect(args)
457 return None
458
459 def normalize(self):
460 """Replace rectangle with its valid version."""
461 if self.x1 < self.x0:
462 self.x0, self.x1 = self.x1, self.x0
463 if self.y1 < self.y0:
464 self.y0, self.y1 = self.y1, self.y0
465 return self
466
467 @property
468 def is_empty(self):
469 """True if rectangle area is empty."""
470 return self.x0 >= self.x1 or self.y0 >= self.y1
471
472 @property
473 def is_valid(self):
474 """True if rectangle is valid."""
475 return self.x0 <= self.x1 and self.y0 <= self.y1
476
477 @property
478 def is_infinite(self):
479 """True if this is the infinite rectangle."""
480 return self.x0 == self.y0 == FZ_MIN_INF_RECT and self.x1 == self.y1 == FZ_MAX_INF_RECT
481
482 @property
483 def top_left(self):
484 """Top-left corner."""
485 return Point(self.x0, self.y0)
486
487 @property
488 def top_right(self):
489 """Top-right corner."""
490 return Point(self.x1, self.y0)
491
492 @property
493 def bottom_left(self):
494 """Bottom-left corner."""
495 return Point(self.x0, self.y1)
496
497 @property
498 def bottom_right(self):
499 """Bottom-right corner."""
500 return Point(self.x1, self.y1)
501
502 tl = top_left
503 tr = top_right
504 bl = bottom_left
505 br = bottom_right
506
507 @property
508 def quad(self):
509 """Return Quad version of rectangle."""
510 return Quad(self.tl, self.tr, self.bl, self.br)
511
512 def torect(self, r):
513 """Return matrix that converts to target rect."""
514
515 r = Rect(r)
516 if self.is_infinite or self.is_empty or r.is_infinite or r.is_empty:
517 raise ValueError("rectangles must be finite and not empty")
518 return (
519 Matrix(1, 0, 0, 1, -self.x0, -self.y0)
520 * Matrix(r.width / self.width, r.height / self.height)
521 * Matrix(1, 0, 0, 1, r.x0, r.y0)
522 )
523
524 def morph(self, p, m):
525 """Morph with matrix-like m and point-like p.
526
527 Returns a new quad."""
528 if self.is_infinite:
529 return INFINITE_QUAD()
530 return self.quad.morph(p, m)
531
532 def round(self):
533 """Return the IRect."""
534 return IRect(util_round_rect(self))
535
536 irect = property(round)
537
538 width = property(lambda self: self.x1 - self.x0 if self.x1 > self.x0 else 0)
539 height = property(lambda self: self.y1 - self.y0 if self.y1 > self.y0 else 0)
540
541 def include_point(self, p):
542 """Extend to include point-like p."""
543 if len(p) != 2:
544 raise ValueError("Point: bad seq len")
545 self.x0, self.y0, self.x1, self.y1 = util_include_point_in_rect(self, p)
546 return self
547
548 def include_rect(self, r):
549 """Extend to include rect-like r."""
550 if len(r) != 4:
551 raise ValueError("Rect: bad seq len")
552 r = Rect(r)
553 if r.is_infinite or self.is_infinite:
554 self.x0, self.y0, self.x1, self.y1 = FZ_MIN_INF_RECT, FZ_MIN_INF_RECT, FZ_MAX_INF_RECT, FZ_MAX_INF_RECT
555 elif r.is_empty:
556 return self
557 elif self.is_empty:
558 self.x0, self.y0, self.x1, self.y1 = r.x0, r.y0, r.x1, r.y1
559 else:
560 self.x0, self.y0, self.x1, self.y1 = util_union_rect(self, r)
561 return self
562
563 def intersect(self, r):
564 """Restrict to common rect with rect-like r."""
565 if not len(r) == 4:
566 raise ValueError("Rect: bad seq len")
567 r = Rect(r)
568 if r.is_infinite:
569 return self
570 elif self.is_infinite:
571 self.x0, self.y0, self.x1, self.y1 = r.x0, r.y0, r.x1, r.y1
572 elif r.is_empty:
573 self.x0, self.y0, self.x1, self.y1 = r.x0, r.y0, r.x1, r.y1
574 elif self.is_empty:
575 return self
576 else:
577 self.x0, self.y0, self.x1, self.y1 = util_intersect_rect(self, r)
578 return self
579
580 def contains(self, x):
581 """Check if containing point-like or rect-like x."""
582 return self.__contains__(x)
583
584 def transform(self, m):
585 """Replace with the transformation by matrix-like m."""
586 if not len(m) == 6:
587 raise ValueError("Matrix: bad seq len")
588 self.x0, self.y0, self.x1, self.y1 = util_transform_rect(self, m)
589 return self
590
591 def __getitem__(self, i):
592 return (self.x0, self.y0, self.x1, self.y1)[i]
593
594 def __len__(self):
595 return 4
596
597 def __setitem__(self, i, v):
598 v = float(v)
599 if i == 0: self.x0 = v
600 elif i == 1: self.y0 = v
601 elif i == 2: self.x1 = v
602 elif i == 3: self.y1 = v
603 else:
604 raise IndexError("index out of range")
605 return None
606
607 def __repr__(self):
608 return "Rect" + str(tuple(self))
609
610 def __pos__(self):
611 return Rect(self)
612
613 def __neg__(self):
614 return Rect(-self.x0, -self.y0, -self.x1, -self.y1)
615
616 def __bool__(self):
617 return not self.x0 == self.y0 == self.x1 == self.y1 == 0
618
619 def __nonzero__(self):
620 return not self.x0 == self.y0 == self.x1 == self.y1 == 0
621
622 def __eq__(self, r):
623 if not hasattr(r, "__len__"):
624 return False
625 return len(r) == 4 and self.x0 == r[0] and self.y0 == r[1] and self.x1 == r[2] and self.y1 == r[3]
626
627 def __abs__(self):
628 if self.is_infinite or not self.is_valid:
629 return 0.0
630 return self.width * self.height
631
632 def norm(self):
633 return math.sqrt(sum([c*c for c in self]))
634
635 def __add__(self, p):
636 if hasattr(p, "__float__"):
637 return Rect(self.x0 + p, self.y0 + p, self.x1 + p, self.y1 + p)
638 if len(p) != 4:
639 raise ValueError("Rect: bad seq len")
640 return Rect(self.x0 + p[0], self.y0 + p[1], self.x1 + p[2], self.y1 + p[3])
641
642
643 def __sub__(self, p):
644 if hasattr(p, "__float__"):
645 return Rect(self.x0 - p, self.y0 - p, self.x1 - p, self.y1 - p)
646 if len(p) != 4:
647 raise ValueError("Rect: bad seq len")
648 return Rect(self.x0 - p[0], self.y0 - p[1], self.x1 - p[2], self.y1 - p[3])
649
650
651 def __mul__(self, m):
652 if hasattr(m, "__float__"):
653 return Rect(self.x0 * m, self.y0 * m, self.x1 * m, self.y1 * m)
654 r = Rect(self)
655 r = r.transform(m)
656 return r
657
658 def __truediv__(self, m):
659 if hasattr(m, "__float__"):
660 return Rect(self.x0 * 1./m, self.y0 * 1./m, self.x1 * 1./m, self.y1 * 1./m)
661 im = util_invert_matrix(m)[1]
662 if not im:
663 raise ZeroDivisionError("Matrix not invertible")
664 r = Rect(self)
665 r = r.transform(im)
666 return r
667
668 __div__ = __truediv__
669
670 def __contains__(self, x):
671 if hasattr(x, "__float__"):
672 return x in tuple(self)
673 l = len(x)
674 if l == 2:
675 return util_is_point_in_rect(x, self)
676 if l == 4:
677 r = INFINITE_RECT()
678 try:
679 r = Rect(x)
680 except:
681 r = Quad(x).rect
682 return (self.x0 <= r.x0 <= r.x1 <= self.x1 and
683 self.y0 <= r.y0 <= r.y1 <= self.y1)
684 return False
685
686
687 def __or__(self, x):
688 if not hasattr(x, "__len__"):
689 raise ValueError("bad type op 2")
690
691 r = Rect(self)
692 if len(x) == 2:
693 return r.include_point(x)
694 if len(x) == 4:
695 return r.include_rect(x)
696 raise ValueError("bad type op 2")
697
698 def __and__(self, x):
699 if not hasattr(x, "__len__") or len(x) != 4:
700 raise ValueError("bad type op 2")
701 r = Rect(self)
702 return r.intersect(x)
703
704 def intersects(self, x):
705 """Check if intersection with rectangle x is not empty."""
706 r1 = Rect(x)
707 if self.is_empty or self.is_infinite or r1.is_empty or r1.is_infinite:
708 return False
709 r = Rect(self)
710 if r.intersect(r1).is_empty:
711 return False
712 return True
713
714 def __hash__(self):
715 return hash(tuple(self))
716
717 class IRect(object):
718 """IRect() - all zeros
719 IRect(x0, y0, x1, y1) - 4 coordinates
720 IRect(top-left, x1, y1) - point and 2 coordinates
721 IRect(x0, y0, bottom-right) - 2 coordinates and point
722 IRect(top-left, bottom-right) - 2 points
723 IRect(sequ) - new from sequence or rect-like
724 """
725 def __init__(self, *args):
726 self.x0, self.y0, self.x1, self.y1 = util_make_irect(args)
727 return None
728
729 def normalize(self):
730 """Replace rectangle with its valid version."""
731 if self.x1 < self.x0:
732 self.x0, self.x1 = self.x1, self.x0
733 if self.y1 < self.y0:
734 self.y0, self.y1 = self.y1, self.y0
735 return self
736
737 @property
738 def is_empty(self):
739 """True if rectangle area is empty."""
740 return self.x0 >= self.x1 or self.y0 >= self.y1
741
742 @property
743 def is_valid(self):
744 """True if rectangle is valid."""
745 return self.x0 <= self.x1 and self.y0 <= self.y1
746
747 @property
748 def is_infinite(self):
749 """True if rectangle is infinite."""
750 return self.x0 == self.y0 == FZ_MIN_INF_RECT and self.x1 == self.y1 == FZ_MAX_INF_RECT
751
752 @property
753 def top_left(self):
754 """Top-left corner."""
755 return Point(self.x0, self.y0)
756
757 @property
758 def top_right(self):
759 """Top-right corner."""
760 return Point(self.x1, self.y0)
761
762 @property
763 def bottom_left(self):
764 """Bottom-left corner."""
765 return Point(self.x0, self.y1)
766
767 @property
768 def bottom_right(self):
769 """Bottom-right corner."""
770 return Point(self.x1, self.y1)
771
772 tl = top_left
773 tr = top_right
774 bl = bottom_left
775 br = bottom_right
776
777 @property
778 def quad(self):
779 """Return Quad version of rectangle."""
780 return Quad(self.tl, self.tr, self.bl, self.br)
781
782
783 def torect(self, r):
784 """Return matrix that converts to target rect."""
785
786 r = Rect(r)
787 if self.is_infinite or self.is_empty or r.is_infinite or r.is_empty:
788 raise ValueError("rectangles must be finite and not empty")
789 return (
790 Matrix(1, 0, 0, 1, -self.x0, -self.y0)
791 * Matrix(r.width / self.width, r.height / self.height)
792 * Matrix(1, 0, 0, 1, r.x0, r.y0)
793 )
794
795 def morph(self, p, m):
796 """Morph with matrix-like m and point-like p.
797
798 Returns a new quad."""
799 if self.is_infinite:
800 return INFINITE_QUAD()
801 return self.quad.morph(p, m)
802
803 @property
804 def rect(self):
805 return Rect(self)
806
807 width = property(lambda self: self.x1 - self.x0 if self.x1 > self.x0 else 0)
808 height = property(lambda self: self.y1 - self.y0 if self.y1 > self.y0 else 0)
809
810 def include_point(self, p):
811 """Extend rectangle to include point p."""
812 rect = self.rect.include_point(p)
813 return rect.irect
814
815 def include_rect(self, r):
816 """Extend rectangle to include rectangle r."""
817 rect = self.rect.include_rect(r)
818 return rect.irect
819
820 def intersect(self, r):
821 """Restrict rectangle to intersection with rectangle r."""
822 rect = self.rect.intersect(r)
823 return rect.irect
824
825 def __getitem__(self, i):
826 return (self.x0, self.y0, self.x1, self.y1)[i]
827
828 def __len__(self):
829 return 4
830
831 def __setitem__(self, i, v):
832 v = int(v)
833 if i == 0: self.x0 = v
834 elif i == 1: self.y0 = v
835 elif i == 2: self.x1 = v
836 elif i == 3: self.y1 = v
837 else:
838 raise IndexError("index out of range")
839 return None
840
841 def __repr__(self):
842 return "IRect" + str(tuple(self))
843
844 def __pos__(self):
845 return IRect(self)
846
847 def __neg__(self):
848 return IRect(-self.x0, -self.y0, -self.x1, -self.y1)
849
850 def __bool__(self):
851 return not self.x0 == self.y0 == self.x1 == self.y1 == 0
852
853 def __nonzero__(self):
854 return not self.x0 == self.y0 == self.x1 == self.y1 == 0
855
856 def __eq__(self, r):
857 if not hasattr(r, "__len__"):
858 return False
859 return len(r) == 4 and self.x0 == r[0] and self.y0 == r[1] and self.x1 == r[2] and self.y1 == r[3]
860
861 def __abs__(self):
862 if self.is_infinite or not self.is_valid:
863 return 0
864 return self.width * self.height
865
866 def norm(self):
867 return math.sqrt(sum([c*c for c in self]))
868
869 def __add__(self, p):
870 return Rect.__add__(self, p).round()
871
872 def __sub__(self, p):
873 return Rect.__sub__(self, p).round()
874
875 def transform(self, m):
876 return Rect.transform(self, m).round()
877
878 def __mul__(self, m):
879 return Rect.__mul__(self, m).round()
880
881 def __truediv__(self, m):
882 return Rect.__truediv__(self, m).round()
883
884 __div__ = __truediv__
885
886
887 def __contains__(self, x):
888 return Rect.__contains__(self, x)
889
890
891 def __or__(self, x):
892 return Rect.__or__(self, x).round()
893
894 def __and__(self, x):
895 return Rect.__and__(self, x).round()
896
897 def intersects(self, x):
898 return Rect.intersects(self, x)
899
900 def __hash__(self):
901 return hash(tuple(self))
902
903
904 class Quad(object):
905 """Quad() - all zero points\nQuad(ul, ur, ll, lr)\nQuad(quad) - new copy\nQuad(sequence) - from 'sequence'"""
906 def __init__(self, *args):
907 if not args:
908 self.ul = self.ur = self.ll = self.lr = Point()
909 return None
910
911 if len(args) > 4:
912 raise ValueError("Quad: bad seq len")
913 if len(args) == 4:
914 self.ul, self.ur, self.ll, self.lr = map(Point, args)
915 return None
916 if len(args) == 1:
917 l = args[0]
918 if hasattr(l, "__getitem__") is False:
919 raise ValueError("Quad: bad args")
920 if len(l) != 4:
921 raise ValueError("Quad: bad seq len")
922 self.ul, self.ur, self.ll, self.lr = map(Point, l)
923 return None
924 raise ValueError("Quad: bad args")
925
926 @property
927 def is_rectangular(self)->bool:
928 """Check if quad is rectangular.
929
930 Notes:
931 Some rotation matrix can thus transform it into a rectangle.
932 This is equivalent to three corners enclose 90 degrees.
933 Returns:
934 True or False.
935 """
936
937 sine = util_sine_between(self.ul, self.ur, self.lr)
938 if abs(sine - 1) > EPSILON: # the sine of the angle
939 return False
940
941 sine = util_sine_between(self.ur, self.lr, self.ll)
942 if abs(sine - 1) > EPSILON:
943 return False
944
945 sine = util_sine_between(self.lr, self.ll, self.ul)
946 if abs(sine - 1) > EPSILON:
947 return False
948
949 return True
950
951
952 @property
953 def is_convex(self)->bool:
954 """Check if quad is convex and not degenerate.
955
956 Notes:
957 Check that for the two diagonals, the other two corners are not
958 on the same side of the diagonal.
959 Returns:
960 True or False.
961 """
962 m = planish_line(self.ul, self.lr) # puts this diagonal on x-axis
963 p1 = self.ll * m # transform the
964 p2 = self.ur * m # other two points
965 if p1.y * p2.y > 0:
966 return False
967 m = planish_line(self.ll, self.ur) # puts other diagonal on x-axis
968 p1 = self.lr * m # tranform the
969 p2 = self.ul * m # remaining points
970 if p1.y * p2.y > 0:
971 return False
972 return True
973
974
975 width = property(lambda self: max(abs(self.ul - self.ur), abs(self.ll - self.lr)))
976 height = property(lambda self: max(abs(self.ul - self.ll), abs(self.ur - self.lr)))
977
978 @property
979 def is_empty(self):
980 """Check whether all quad corners are on the same line.
981
982 This is the case if width or height is zero.
983 """
984 return self.width < EPSILON or self.height < EPSILON
985
986 @property
987 def is_infinite(self):
988 """Check whether this is the infinite quad."""
989 return self.rect.is_infinite
990
991 @property
992 def rect(self):
993 r = Rect()
994 r.x0 = min(self.ul.x, self.ur.x, self.lr.x, self.ll.x)
995 r.y0 = min(self.ul.y, self.ur.y, self.lr.y, self.ll.y)
996 r.x1 = max(self.ul.x, self.ur.x, self.lr.x, self.ll.x)
997 r.y1 = max(self.ul.y, self.ur.y, self.lr.y, self.ll.y)
998 return r
999
1000
1001 def __contains__(self, x):
1002 try:
1003 l = x.__len__()
1004 except:
1005 return False
1006 if l == 2:
1007 return util_point_in_quad(x, self)
1008 if l != 4:
1009 return False
1010 if CheckRect(x):
1011 if Rect(x).is_empty:
1012 return True
1013 return util_point_in_quad(x[:2], self) and util_point_in_quad(x[2:], self)
1014 if CheckQuad(x):
1015 for i in range(4):
1016 if not util_point_in_quad(x[i], self):
1017 return False
1018 return True
1019 return False
1020
1021
1022 def __getitem__(self, i):
1023 return (self.ul, self.ur, self.ll, self.lr)[i]
1024
1025 def __len__(self):
1026 return 4
1027
1028 def __setitem__(self, i, v):
1029 if i == 0: self.ul = Point(v)
1030 elif i == 1: self.ur = Point(v)
1031 elif i == 2: self.ll = Point(v)
1032 elif i == 3: self.lr = Point(v)
1033 else:
1034 raise IndexError("index out of range")
1035 return None
1036
1037 def __repr__(self):
1038 return "Quad" + str(tuple(self))
1039
1040 def __pos__(self):
1041 return Quad(self)
1042
1043 def __neg__(self):
1044 return Quad(-self.ul, -self.ur, -self.ll, -self.lr)
1045
1046 def __bool__(self):
1047 return not self.is_empty
1048
1049 def __nonzero__(self):
1050 return not self.is_empty
1051
1052 def __eq__(self, quad):
1053 if not hasattr(quad, "__len__"):
1054 return False
1055 return len(quad) == 4 and (
1056 self.ul == quad[0] and
1057 self.ur == quad[1] and
1058 self.ll == quad[2] and
1059 self.lr == quad[3]
1060 )
1061
1062 def __abs__(self):
1063 if self.is_empty:
1064 return 0.0
1065 return abs(self.ul - self.ur) * abs(self.ul - self.ll)
1066
1067
1068 def morph(self, p, m):
1069 """Morph the quad with matrix-like 'm' and point-like 'p'.
1070
1071 Return a new quad."""
1072 if self.is_infinite:
1073 return INFINITE_QUAD()
1074 delta = Matrix(1, 1).pretranslate(p.x, p.y)
1075 q = self * ~delta * m * delta
1076 return q
1077
1078
1079 def transform(self, m):
1080 """Replace quad by its transformation with matrix m."""
1081 if hasattr(m, "__float__"):
1082 pass
1083 elif len(m) != 6:
1084 raise ValueError("Matrix: bad seq len")
1085 self.ul *= m
1086 self.ur *= m
1087 self.ll *= m
1088 self.lr *= m
1089 return self
1090
1091 def __mul__(self, m):
1092 q = Quad(self)
1093 q = q.transform(m)
1094 return q
1095
1096 def __add__(self, q):
1097 if hasattr(q, "__float__"):
1098 return Quad(self.ul + q, self.ur + q, self.ll + q, self.lr + q)
1099 if len(p) != 4:
1100 raise ValueError("Quad: bad seq len")
1101 return Quad(self.ul + q[0], self.ur + q[1], self.ll + q[2], self.lr + q[3])
1102
1103
1104 def __sub__(self, q):
1105 if hasattr(q, "__float__"):
1106 return Quad(self.ul - q, self.ur - q, self.ll - q, self.lr - q)
1107 if len(p) != 4:
1108 raise ValueError("Quad: bad seq len")
1109 return Quad(self.ul - q[0], self.ur - q[1], self.ll - q[2], self.lr - q[3])
1110
1111
1112 def __truediv__(self, m):
1113 if hasattr(m, "__float__"):
1114 im = 1. / m
1115 else:
1116 im = util_invert_matrix(m)[1]
1117 if not im:
1118 raise ZeroDivisionError("Matrix not invertible")
1119 q = Quad(self)
1120 q = q.transform(im)
1121 return q
1122
1123 __div__ = __truediv__
1124
1125
1126 def __hash__(self):
1127 return hash(tuple(self))
1128
1129
1130 # some special geometry objects
1131 def EMPTY_RECT():
1132 return Rect(FZ_MAX_INF_RECT, FZ_MAX_INF_RECT, FZ_MIN_INF_RECT, FZ_MIN_INF_RECT)
1133
1134
1135 def INFINITE_RECT():
1136 return Rect(FZ_MIN_INF_RECT, FZ_MIN_INF_RECT, FZ_MAX_INF_RECT, FZ_MAX_INF_RECT)
1137
1138
1139 def EMPTY_IRECT():
1140 return IRect(FZ_MAX_INF_RECT, FZ_MAX_INF_RECT, FZ_MIN_INF_RECT, FZ_MIN_INF_RECT)
1141
1142
1143 def INFINITE_IRECT():
1144 return IRect(FZ_MIN_INF_RECT, FZ_MIN_INF_RECT, FZ_MAX_INF_RECT, FZ_MAX_INF_RECT)
1145
1146
1147 def INFINITE_QUAD():
1148 return INFINITE_RECT().quad
1149
1150
1151 def EMPTY_QUAD():
1152 return EMPTY_RECT().quad
1153
1154
1155 %}