Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/PIL/ImageDraw.py: 12%

474 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2023-07-17 14:22 -0600

1# 

2# The Python Imaging Library 

3# $Id$ 

4# 

5# drawing interface operations 

6# 

7# History: 

8# 1996-04-13 fl Created (experimental) 

9# 1996-08-07 fl Filled polygons, ellipses. 

10# 1996-08-13 fl Added text support 

11# 1998-06-28 fl Handle I and F images 

12# 1998-12-29 fl Added arc; use arc primitive to draw ellipses 

13# 1999-01-10 fl Added shape stuff (experimental) 

14# 1999-02-06 fl Added bitmap support 

15# 1999-02-11 fl Changed all primitives to take options 

16# 1999-02-20 fl Fixed backwards compatibility 

17# 2000-10-12 fl Copy on write, when necessary 

18# 2001-02-18 fl Use default ink for bitmap/text also in fill mode 

19# 2002-10-24 fl Added support for CSS-style color strings 

20# 2002-12-10 fl Added experimental support for RGBA-on-RGB drawing 

21# 2002-12-11 fl Refactored low-level drawing API (work in progress) 

22# 2004-08-26 fl Made Draw() a factory function, added getdraw() support 

23# 2004-09-04 fl Added width support to line primitive 

24# 2004-09-10 fl Added font mode handling 

25# 2006-06-19 fl Added font bearing support (getmask2) 

26# 

27# Copyright (c) 1997-2006 by Secret Labs AB 

28# Copyright (c) 1996-2006 by Fredrik Lundh 

29# 

30# See the README file for information on usage and redistribution. 

31# 

32 

33import math 

34import numbers 

35import warnings 

36 

37from . import Image, ImageColor 

38from ._deprecate import deprecate 

39 

40""" 

41A simple 2D drawing interface for PIL images. 

42<p> 

43Application code should use the <b>Draw</b> factory, instead of 

44directly. 

45""" 

46 

47 

48class ImageDraw: 

49 def __init__(self, im, mode=None): 

50 """ 

51 Create a drawing instance. 

52 

53 :param im: The image to draw in. 

54 :param mode: Optional mode to use for color values. For RGB 

55 images, this argument can be RGB or RGBA (to blend the 

56 drawing into the image). For all other modes, this argument 

57 must be the same as the image mode. If omitted, the mode 

58 defaults to the mode of the image. 

59 """ 

60 im.load() 

61 if im.readonly: 61 ↛ 62line 61 didn't jump to line 62, because the condition on line 61 was never true

62 im._copy() # make it writeable 

63 blend = 0 

64 if mode is None: 64 ↛ 66line 64 didn't jump to line 66, because the condition on line 64 was never false

65 mode = im.mode 

66 if mode != im.mode: 66 ↛ 67line 66 didn't jump to line 67, because the condition on line 66 was never true

67 if mode == "RGBA" and im.mode == "RGB": 

68 blend = 1 

69 else: 

70 raise ValueError("mode mismatch") 

71 if mode == "P": 71 ↛ 72line 71 didn't jump to line 72, because the condition on line 71 was never true

72 self.palette = im.palette 

73 else: 

74 self.palette = None 

75 self._image = im 

76 self.im = im.im 

77 self.draw = Image.core.draw(self.im, blend) 

78 self.mode = mode 

79 if mode in ("I", "F"): 79 ↛ 80line 79 didn't jump to line 80, because the condition on line 79 was never true

80 self.ink = self.draw.draw_ink(1) 

81 else: 

82 self.ink = self.draw.draw_ink(-1) 

83 if mode in ("1", "P", "I", "F"): 

84 # FIXME: fix Fill2 to properly support matte for I+F images 

85 self.fontmode = "1" 

86 else: 

87 self.fontmode = "L" # aliasing is okay for other modes 

88 self.fill = 0 

89 self.font = None 

90 

91 def getfont(self): 

92 """ 

93 Get the current default font. 

94 

95 :returns: An image font.""" 

96 if not self.font: 

97 # FIXME: should add a font repository 

98 from . import ImageFont 

99 

100 self.font = ImageFont.load_default() 

101 return self.font 

102 

103 def _getink(self, ink, fill=None): 

104 if ink is None and fill is None: 104 ↛ 105line 104 didn't jump to line 105, because the condition on line 104 was never true

105 if self.fill: 

106 fill = self.ink 

107 else: 

108 ink = self.ink 

109 else: 

110 if ink is not None: 110 ↛ 111line 110 didn't jump to line 111, because the condition on line 110 was never true

111 if isinstance(ink, str): 

112 ink = ImageColor.getcolor(ink, self.mode) 

113 if self.palette and not isinstance(ink, numbers.Number): 

114 ink = self.palette.getcolor(ink, self._image) 

115 ink = self.draw.draw_ink(ink) 

116 if fill is not None: 116 ↛ 122line 116 didn't jump to line 122, because the condition on line 116 was never false

117 if isinstance(fill, str): 117 ↛ 118line 117 didn't jump to line 118, because the condition on line 117 was never true

118 fill = ImageColor.getcolor(fill, self.mode) 

119 if self.palette and not isinstance(fill, numbers.Number): 119 ↛ 120line 119 didn't jump to line 120, because the condition on line 119 was never true

120 fill = self.palette.getcolor(fill, self._image) 

121 fill = self.draw.draw_ink(fill) 

122 return ink, fill 

123 

124 def arc(self, xy, start, end, fill=None, width=1): 

125 """Draw an arc.""" 

126 ink, fill = self._getink(fill) 

127 if ink is not None: 

128 self.draw.draw_arc(xy, start, end, ink, width) 

129 

130 def bitmap(self, xy, bitmap, fill=None): 

131 """Draw a bitmap.""" 

132 bitmap.load() 

133 ink, fill = self._getink(fill) 

134 if ink is None: 

135 ink = fill 

136 if ink is not None: 

137 self.draw.draw_bitmap(xy, bitmap.im, ink) 

138 

139 def chord(self, xy, start, end, fill=None, outline=None, width=1): 

140 """Draw a chord.""" 

141 ink, fill = self._getink(outline, fill) 

142 if fill is not None: 

143 self.draw.draw_chord(xy, start, end, fill, 1) 

144 if ink is not None and ink != fill and width != 0: 

145 self.draw.draw_chord(xy, start, end, ink, 0, width) 

146 

147 def ellipse(self, xy, fill=None, outline=None, width=1): 

148 """Draw an ellipse.""" 

149 ink, fill = self._getink(outline, fill) 

150 if fill is not None: 

151 self.draw.draw_ellipse(xy, fill, 1) 

152 if ink is not None and ink != fill and width != 0: 

153 self.draw.draw_ellipse(xy, ink, 0, width) 

154 

155 def line(self, xy, fill=None, width=0, joint=None): 

156 """Draw a line, or a connected sequence of line segments.""" 

157 ink = self._getink(fill)[0] 

158 if ink is not None: 

159 self.draw.draw_lines(xy, ink, width) 

160 if joint == "curve" and width > 4: 

161 if not isinstance(xy[0], (list, tuple)): 

162 xy = [tuple(xy[i : i + 2]) for i in range(0, len(xy), 2)] 

163 for i in range(1, len(xy) - 1): 

164 point = xy[i] 

165 angles = [ 

166 math.degrees(math.atan2(end[0] - start[0], start[1] - end[1])) 

167 % 360 

168 for start, end in ((xy[i - 1], point), (point, xy[i + 1])) 

169 ] 

170 if angles[0] == angles[1]: 

171 # This is a straight line, so no joint is required 

172 continue 

173 

174 def coord_at_angle(coord, angle): 

175 x, y = coord 

176 angle -= 90 

177 distance = width / 2 - 1 

178 return tuple( 

179 p + (math.floor(p_d) if p_d > 0 else math.ceil(p_d)) 

180 for p, p_d in ( 

181 (x, distance * math.cos(math.radians(angle))), 

182 (y, distance * math.sin(math.radians(angle))), 

183 ) 

184 ) 

185 

186 flipped = ( 

187 angles[1] > angles[0] and angles[1] - 180 > angles[0] 

188 ) or (angles[1] < angles[0] and angles[1] + 180 > angles[0]) 

189 coords = [ 

190 (point[0] - width / 2 + 1, point[1] - width / 2 + 1), 

191 (point[0] + width / 2 - 1, point[1] + width / 2 - 1), 

192 ] 

193 if flipped: 

194 start, end = (angles[1] + 90, angles[0] + 90) 

195 else: 

196 start, end = (angles[0] - 90, angles[1] - 90) 

197 self.pieslice(coords, start - 90, end - 90, fill) 

198 

199 if width > 8: 

200 # Cover potential gaps between the line and the joint 

201 if flipped: 

202 gap_coords = [ 

203 coord_at_angle(point, angles[0] + 90), 

204 point, 

205 coord_at_angle(point, angles[1] + 90), 

206 ] 

207 else: 

208 gap_coords = [ 

209 coord_at_angle(point, angles[0] - 90), 

210 point, 

211 coord_at_angle(point, angles[1] - 90), 

212 ] 

213 self.line(gap_coords, fill, width=3) 

214 

215 def shape(self, shape, fill=None, outline=None): 

216 """(Experimental) Draw a shape.""" 

217 shape.close() 

218 ink, fill = self._getink(outline, fill) 

219 if fill is not None: 

220 self.draw.draw_outline(shape, fill, 1) 

221 if ink is not None and ink != fill: 

222 self.draw.draw_outline(shape, ink, 0) 

223 

224 def pieslice(self, xy, start, end, fill=None, outline=None, width=1): 

225 """Draw a pieslice.""" 

226 ink, fill = self._getink(outline, fill) 

227 if fill is not None: 

228 self.draw.draw_pieslice(xy, start, end, fill, 1) 

229 if ink is not None and ink != fill and width != 0: 

230 self.draw.draw_pieslice(xy, start, end, ink, 0, width) 

231 

232 def point(self, xy, fill=None): 

233 """Draw one or more individual pixels.""" 

234 ink, fill = self._getink(fill) 

235 if ink is not None: 

236 self.draw.draw_points(xy, ink) 

237 

238 def polygon(self, xy, fill=None, outline=None, width=1): 

239 """Draw a polygon.""" 

240 ink, fill = self._getink(outline, fill) 

241 if fill is not None: 

242 self.draw.draw_polygon(xy, fill, 1) 

243 if ink is not None and ink != fill and width != 0: 

244 if width == 1: 

245 self.draw.draw_polygon(xy, ink, 0, width) 

246 else: 

247 # To avoid expanding the polygon outwards, 

248 # use the fill as a mask 

249 mask = Image.new("1", self.im.size) 

250 mask_ink = self._getink(1)[0] 

251 

252 fill_im = mask.copy() 

253 draw = Draw(fill_im) 

254 draw.draw.draw_polygon(xy, mask_ink, 1) 

255 

256 ink_im = mask.copy() 

257 draw = Draw(ink_im) 

258 width = width * 2 - 1 

259 draw.draw.draw_polygon(xy, mask_ink, 0, width) 

260 

261 mask.paste(ink_im, mask=fill_im) 

262 

263 im = Image.new(self.mode, self.im.size) 

264 draw = Draw(im) 

265 draw.draw.draw_polygon(xy, ink, 0, width) 

266 self.im.paste(im.im, (0, 0) + im.size, mask.im) 

267 

268 def regular_polygon( 

269 self, bounding_circle, n_sides, rotation=0, fill=None, outline=None 

270 ): 

271 """Draw a regular polygon.""" 

272 xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) 

273 self.polygon(xy, fill, outline) 

274 

275 def rectangle(self, xy, fill=None, outline=None, width=1): 

276 """Draw a rectangle.""" 

277 ink, fill = self._getink(outline, fill) 

278 if fill is not None: 278 ↛ 280line 278 didn't jump to line 280, because the condition on line 278 was never false

279 self.draw.draw_rectangle(xy, fill, 1) 

280 if ink is not None and ink != fill and width != 0: 280 ↛ 281line 280 didn't jump to line 281, because the condition on line 280 was never true

281 self.draw.draw_rectangle(xy, ink, 0, width) 

282 

283 def rounded_rectangle(self, xy, radius=0, fill=None, outline=None, width=1): 

284 """Draw a rounded rectangle.""" 

285 if isinstance(xy[0], (list, tuple)): 

286 (x0, y0), (x1, y1) = xy 

287 else: 

288 x0, y0, x1, y1 = xy 

289 

290 d = radius * 2 

291 

292 full_x = d >= x1 - x0 

293 if full_x: 

294 # The two left and two right corners are joined 

295 d = x1 - x0 

296 full_y = d >= y1 - y0 

297 if full_y: 

298 # The two top and two bottom corners are joined 

299 d = y1 - y0 

300 if full_x and full_y: 

301 # If all corners are joined, that is a circle 

302 return self.ellipse(xy, fill, outline, width) 

303 

304 if d == 0: 

305 # If the corners have no curve, that is a rectangle 

306 return self.rectangle(xy, fill, outline, width) 

307 

308 r = d // 2 

309 ink, fill = self._getink(outline, fill) 

310 

311 def draw_corners(pieslice): 

312 if full_x: 

313 # Draw top and bottom halves 

314 parts = ( 

315 ((x0, y0, x0 + d, y0 + d), 180, 360), 

316 ((x0, y1 - d, x0 + d, y1), 0, 180), 

317 ) 

318 elif full_y: 

319 # Draw left and right halves 

320 parts = ( 

321 ((x0, y0, x0 + d, y0 + d), 90, 270), 

322 ((x1 - d, y0, x1, y0 + d), 270, 90), 

323 ) 

324 else: 

325 # Draw four separate corners 

326 parts = ( 

327 ((x1 - d, y0, x1, y0 + d), 270, 360), 

328 ((x1 - d, y1 - d, x1, y1), 0, 90), 

329 ((x0, y1 - d, x0 + d, y1), 90, 180), 

330 ((x0, y0, x0 + d, y0 + d), 180, 270), 

331 ) 

332 for part in parts: 

333 if pieslice: 

334 self.draw.draw_pieslice(*(part + (fill, 1))) 

335 else: 

336 self.draw.draw_arc(*(part + (ink, width))) 

337 

338 if fill is not None: 

339 draw_corners(True) 

340 

341 if full_x: 

342 self.draw.draw_rectangle((x0, y0 + r + 1, x1, y1 - r - 1), fill, 1) 

343 else: 

344 self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill, 1) 

345 if not full_x and not full_y: 

346 self.draw.draw_rectangle((x0, y0 + r + 1, x0 + r, y1 - r - 1), fill, 1) 

347 self.draw.draw_rectangle((x1 - r, y0 + r + 1, x1, y1 - r - 1), fill, 1) 

348 if ink is not None and ink != fill and width != 0: 

349 draw_corners(False) 

350 

351 if not full_x: 

352 self.draw.draw_rectangle( 

353 (x0 + r + 1, y0, x1 - r - 1, y0 + width - 1), ink, 1 

354 ) 

355 self.draw.draw_rectangle( 

356 (x0 + r + 1, y1 - width + 1, x1 - r - 1, y1), ink, 1 

357 ) 

358 if not full_y: 

359 self.draw.draw_rectangle( 

360 (x0, y0 + r + 1, x0 + width - 1, y1 - r - 1), ink, 1 

361 ) 

362 self.draw.draw_rectangle( 

363 (x1 - width + 1, y0 + r + 1, x1, y1 - r - 1), ink, 1 

364 ) 

365 

366 def _multiline_check(self, text): 

367 """Draw text.""" 

368 split_character = "\n" if isinstance(text, str) else b"\n" 

369 

370 return split_character in text 

371 

372 def _multiline_split(self, text): 

373 split_character = "\n" if isinstance(text, str) else b"\n" 

374 

375 return text.split(split_character) 

376 

377 def _multiline_spacing(self, font, spacing, stroke_width): 

378 # this can be replaced with self.textbbox(...)[3] when textsize is removed 

379 with warnings.catch_warnings(): 

380 warnings.filterwarnings("ignore", category=DeprecationWarning) 

381 return ( 

382 self.textsize( 

383 "A", 

384 font=font, 

385 stroke_width=stroke_width, 

386 )[1] 

387 + spacing 

388 ) 

389 

390 def text( 

391 self, 

392 xy, 

393 text, 

394 fill=None, 

395 font=None, 

396 anchor=None, 

397 spacing=4, 

398 align="left", 

399 direction=None, 

400 features=None, 

401 language=None, 

402 stroke_width=0, 

403 stroke_fill=None, 

404 embedded_color=False, 

405 *args, 

406 **kwargs, 

407 ): 

408 if self._multiline_check(text): 

409 return self.multiline_text( 

410 xy, 

411 text, 

412 fill, 

413 font, 

414 anchor, 

415 spacing, 

416 align, 

417 direction, 

418 features, 

419 language, 

420 stroke_width, 

421 stroke_fill, 

422 embedded_color, 

423 ) 

424 

425 if embedded_color and self.mode not in ("RGB", "RGBA"): 

426 raise ValueError("Embedded color supported only in RGB and RGBA modes") 

427 

428 if font is None: 

429 font = self.getfont() 

430 

431 def getink(fill): 

432 ink, fill = self._getink(fill) 

433 if ink is None: 

434 return fill 

435 return ink 

436 

437 def draw_text(ink, stroke_width=0, stroke_offset=None): 

438 mode = self.fontmode 

439 if stroke_width == 0 and embedded_color: 

440 mode = "RGBA" 

441 coord = xy 

442 try: 

443 mask, offset = font.getmask2( 

444 text, 

445 mode, 

446 direction=direction, 

447 features=features, 

448 language=language, 

449 stroke_width=stroke_width, 

450 anchor=anchor, 

451 ink=ink, 

452 *args, 

453 **kwargs, 

454 ) 

455 coord = coord[0] + offset[0], coord[1] + offset[1] 

456 except AttributeError: 

457 try: 

458 mask = font.getmask( 

459 text, 

460 mode, 

461 direction, 

462 features, 

463 language, 

464 stroke_width, 

465 anchor, 

466 ink, 

467 *args, 

468 **kwargs, 

469 ) 

470 except TypeError: 

471 mask = font.getmask(text) 

472 if stroke_offset: 

473 coord = coord[0] + stroke_offset[0], coord[1] + stroke_offset[1] 

474 if mode == "RGBA": 

475 # font.getmask2(mode="RGBA") returns color in RGB bands and mask in A 

476 # extract mask and set text alpha 

477 color, mask = mask, mask.getband(3) 

478 color.fillband(3, (ink >> 24) & 0xFF) 

479 coord2 = coord[0] + mask.size[0], coord[1] + mask.size[1] 

480 self.im.paste(color, coord + coord2, mask) 

481 else: 

482 self.draw.draw_bitmap(coord, mask, ink) 

483 

484 ink = getink(fill) 

485 if ink is not None: 

486 stroke_ink = None 

487 if stroke_width: 

488 stroke_ink = getink(stroke_fill) if stroke_fill is not None else ink 

489 

490 if stroke_ink is not None: 

491 # Draw stroked text 

492 draw_text(stroke_ink, stroke_width) 

493 

494 # Draw normal text 

495 draw_text(ink, 0) 

496 else: 

497 # Only draw normal text 

498 draw_text(ink) 

499 

500 def multiline_text( 

501 self, 

502 xy, 

503 text, 

504 fill=None, 

505 font=None, 

506 anchor=None, 

507 spacing=4, 

508 align="left", 

509 direction=None, 

510 features=None, 

511 language=None, 

512 stroke_width=0, 

513 stroke_fill=None, 

514 embedded_color=False, 

515 ): 

516 if direction == "ttb": 

517 raise ValueError("ttb direction is unsupported for multiline text") 

518 

519 if anchor is None: 

520 anchor = "la" 

521 elif len(anchor) != 2: 

522 raise ValueError("anchor must be a 2 character string") 

523 elif anchor[1] in "tb": 

524 raise ValueError("anchor not supported for multiline text") 

525 

526 widths = [] 

527 max_width = 0 

528 lines = self._multiline_split(text) 

529 line_spacing = self._multiline_spacing(font, spacing, stroke_width) 

530 for line in lines: 

531 line_width = self.textlength( 

532 line, font, direction=direction, features=features, language=language 

533 ) 

534 widths.append(line_width) 

535 max_width = max(max_width, line_width) 

536 

537 top = xy[1] 

538 if anchor[1] == "m": 

539 top -= (len(lines) - 1) * line_spacing / 2.0 

540 elif anchor[1] == "d": 

541 top -= (len(lines) - 1) * line_spacing 

542 

543 for idx, line in enumerate(lines): 

544 left = xy[0] 

545 width_difference = max_width - widths[idx] 

546 

547 # first align left by anchor 

548 if anchor[0] == "m": 

549 left -= width_difference / 2.0 

550 elif anchor[0] == "r": 

551 left -= width_difference 

552 

553 # then align by align parameter 

554 if align == "left": 

555 pass 

556 elif align == "center": 

557 left += width_difference / 2.0 

558 elif align == "right": 

559 left += width_difference 

560 else: 

561 raise ValueError('align must be "left", "center" or "right"') 

562 

563 self.text( 

564 (left, top), 

565 line, 

566 fill, 

567 font, 

568 anchor, 

569 direction=direction, 

570 features=features, 

571 language=language, 

572 stroke_width=stroke_width, 

573 stroke_fill=stroke_fill, 

574 embedded_color=embedded_color, 

575 ) 

576 top += line_spacing 

577 

578 def textsize( 

579 self, 

580 text, 

581 font=None, 

582 spacing=4, 

583 direction=None, 

584 features=None, 

585 language=None, 

586 stroke_width=0, 

587 ): 

588 """Get the size of a given string, in pixels.""" 

589 deprecate("textsize", 10, "textbbox or textlength") 

590 if self._multiline_check(text): 

591 with warnings.catch_warnings(): 

592 warnings.filterwarnings("ignore", category=DeprecationWarning) 

593 return self.multiline_textsize( 

594 text, 

595 font, 

596 spacing, 

597 direction, 

598 features, 

599 language, 

600 stroke_width, 

601 ) 

602 

603 if font is None: 

604 font = self.getfont() 

605 with warnings.catch_warnings(): 

606 warnings.filterwarnings("ignore", category=DeprecationWarning) 

607 return font.getsize( 

608 text, 

609 direction, 

610 features, 

611 language, 

612 stroke_width, 

613 ) 

614 

615 def multiline_textsize( 

616 self, 

617 text, 

618 font=None, 

619 spacing=4, 

620 direction=None, 

621 features=None, 

622 language=None, 

623 stroke_width=0, 

624 ): 

625 deprecate("multiline_textsize", 10, "multiline_textbbox") 

626 max_width = 0 

627 lines = self._multiline_split(text) 

628 line_spacing = self._multiline_spacing(font, spacing, stroke_width) 

629 with warnings.catch_warnings(): 

630 warnings.filterwarnings("ignore", category=DeprecationWarning) 

631 for line in lines: 

632 line_width, line_height = self.textsize( 

633 line, 

634 font, 

635 spacing, 

636 direction, 

637 features, 

638 language, 

639 stroke_width, 

640 ) 

641 max_width = max(max_width, line_width) 

642 return max_width, len(lines) * line_spacing - spacing 

643 

644 def textlength( 

645 self, 

646 text, 

647 font=None, 

648 direction=None, 

649 features=None, 

650 language=None, 

651 embedded_color=False, 

652 ): 

653 """Get the length of a given string, in pixels with 1/64 precision.""" 

654 if self._multiline_check(text): 

655 raise ValueError("can't measure length of multiline text") 

656 if embedded_color and self.mode not in ("RGB", "RGBA"): 

657 raise ValueError("Embedded color supported only in RGB and RGBA modes") 

658 

659 if font is None: 

660 font = self.getfont() 

661 mode = "RGBA" if embedded_color else self.fontmode 

662 try: 

663 return font.getlength(text, mode, direction, features, language) 

664 except AttributeError: 

665 deprecate("textlength support for fonts without getlength", 10) 

666 with warnings.catch_warnings(): 

667 warnings.filterwarnings("ignore", category=DeprecationWarning) 

668 size = self.textsize( 

669 text, 

670 font, 

671 direction=direction, 

672 features=features, 

673 language=language, 

674 ) 

675 if direction == "ttb": 

676 return size[1] 

677 return size[0] 

678 

679 def textbbox( 

680 self, 

681 xy, 

682 text, 

683 font=None, 

684 anchor=None, 

685 spacing=4, 

686 align="left", 

687 direction=None, 

688 features=None, 

689 language=None, 

690 stroke_width=0, 

691 embedded_color=False, 

692 ): 

693 """Get the bounding box of a given string, in pixels.""" 

694 if embedded_color and self.mode not in ("RGB", "RGBA"): 

695 raise ValueError("Embedded color supported only in RGB and RGBA modes") 

696 

697 if self._multiline_check(text): 

698 return self.multiline_textbbox( 

699 xy, 

700 text, 

701 font, 

702 anchor, 

703 spacing, 

704 align, 

705 direction, 

706 features, 

707 language, 

708 stroke_width, 

709 embedded_color, 

710 ) 

711 

712 if font is None: 

713 font = self.getfont() 

714 mode = "RGBA" if embedded_color else self.fontmode 

715 bbox = font.getbbox( 

716 text, mode, direction, features, language, stroke_width, anchor 

717 ) 

718 return bbox[0] + xy[0], bbox[1] + xy[1], bbox[2] + xy[0], bbox[3] + xy[1] 

719 

720 def multiline_textbbox( 

721 self, 

722 xy, 

723 text, 

724 font=None, 

725 anchor=None, 

726 spacing=4, 

727 align="left", 

728 direction=None, 

729 features=None, 

730 language=None, 

731 stroke_width=0, 

732 embedded_color=False, 

733 ): 

734 if direction == "ttb": 

735 raise ValueError("ttb direction is unsupported for multiline text") 

736 

737 if anchor is None: 

738 anchor = "la" 

739 elif len(anchor) != 2: 

740 raise ValueError("anchor must be a 2 character string") 

741 elif anchor[1] in "tb": 

742 raise ValueError("anchor not supported for multiline text") 

743 

744 widths = [] 

745 max_width = 0 

746 lines = self._multiline_split(text) 

747 line_spacing = self._multiline_spacing(font, spacing, stroke_width) 

748 for line in lines: 

749 line_width = self.textlength( 

750 line, 

751 font, 

752 direction=direction, 

753 features=features, 

754 language=language, 

755 embedded_color=embedded_color, 

756 ) 

757 widths.append(line_width) 

758 max_width = max(max_width, line_width) 

759 

760 top = xy[1] 

761 if anchor[1] == "m": 

762 top -= (len(lines) - 1) * line_spacing / 2.0 

763 elif anchor[1] == "d": 

764 top -= (len(lines) - 1) * line_spacing 

765 

766 bbox = None 

767 

768 for idx, line in enumerate(lines): 

769 left = xy[0] 

770 width_difference = max_width - widths[idx] 

771 

772 # first align left by anchor 

773 if anchor[0] == "m": 

774 left -= width_difference / 2.0 

775 elif anchor[0] == "r": 

776 left -= width_difference 

777 

778 # then align by align parameter 

779 if align == "left": 

780 pass 

781 elif align == "center": 

782 left += width_difference / 2.0 

783 elif align == "right": 

784 left += width_difference 

785 else: 

786 raise ValueError('align must be "left", "center" or "right"') 

787 

788 bbox_line = self.textbbox( 

789 (left, top), 

790 line, 

791 font, 

792 anchor, 

793 direction=direction, 

794 features=features, 

795 language=language, 

796 stroke_width=stroke_width, 

797 embedded_color=embedded_color, 

798 ) 

799 if bbox is None: 

800 bbox = bbox_line 

801 else: 

802 bbox = ( 

803 min(bbox[0], bbox_line[0]), 

804 min(bbox[1], bbox_line[1]), 

805 max(bbox[2], bbox_line[2]), 

806 max(bbox[3], bbox_line[3]), 

807 ) 

808 

809 top += line_spacing 

810 

811 if bbox is None: 

812 return xy[0], xy[1], xy[0], xy[1] 

813 return bbox 

814 

815 

816def Draw(im, mode=None): 

817 """ 

818 A simple 2D drawing interface for PIL images. 

819 

820 :param im: The image to draw in. 

821 :param mode: Optional mode to use for color values. For RGB 

822 images, this argument can be RGB or RGBA (to blend the 

823 drawing into the image). For all other modes, this argument 

824 must be the same as the image mode. If omitted, the mode 

825 defaults to the mode of the image. 

826 """ 

827 try: 

828 return im.getdraw(mode) 

829 except AttributeError: 

830 return ImageDraw(im, mode) 

831 

832 

833# experimental access to the outline API 

834try: 

835 Outline = Image.core.outline 

836except AttributeError: 

837 Outline = None 

838 

839 

840def getdraw(im=None, hints=None): 

841 """ 

842 (Experimental) A more advanced 2D drawing interface for PIL images, 

843 based on the WCK interface. 

844 

845 :param im: The image to draw in. 

846 :param hints: An optional list of hints. 

847 :returns: A (drawing context, drawing resource factory) tuple. 

848 """ 

849 # FIXME: this needs more work! 

850 # FIXME: come up with a better 'hints' scheme. 

851 handler = None 

852 if not hints or "nicest" in hints: 

853 try: 

854 from . import _imagingagg as handler 

855 except ImportError: 

856 pass 

857 if handler is None: 

858 from . import ImageDraw2 as handler 

859 if im: 

860 im = handler.Draw(im) 

861 return im, handler 

862 

863 

864def floodfill(image, xy, value, border=None, thresh=0): 

865 """ 

866 (experimental) Fills a bounded region with a given color. 

867 

868 :param image: Target image. 

869 :param xy: Seed position (a 2-item coordinate tuple). See 

870 :ref:`coordinate-system`. 

871 :param value: Fill color. 

872 :param border: Optional border value. If given, the region consists of 

873 pixels with a color different from the border color. If not given, 

874 the region consists of pixels having the same color as the seed 

875 pixel. 

876 :param thresh: Optional threshold value which specifies a maximum 

877 tolerable difference of a pixel value from the 'background' in 

878 order for it to be replaced. Useful for filling regions of 

879 non-homogeneous, but similar, colors. 

880 """ 

881 # based on an implementation by Eric S. Raymond 

882 # amended by yo1995 @20180806 

883 pixel = image.load() 

884 x, y = xy 

885 try: 

886 background = pixel[x, y] 

887 if _color_diff(value, background) <= thresh: 

888 return # seed point already has fill color 

889 pixel[x, y] = value 

890 except (ValueError, IndexError): 

891 return # seed point outside image 

892 edge = {(x, y)} 

893 # use a set to keep record of current and previous edge pixels 

894 # to reduce memory consumption 

895 full_edge = set() 

896 while edge: 

897 new_edge = set() 

898 for (x, y) in edge: # 4 adjacent method 

899 for (s, t) in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)): 

900 # If already processed, or if a coordinate is negative, skip 

901 if (s, t) in full_edge or s < 0 or t < 0: 

902 continue 

903 try: 

904 p = pixel[s, t] 

905 except (ValueError, IndexError): 

906 pass 

907 else: 

908 full_edge.add((s, t)) 

909 if border is None: 

910 fill = _color_diff(p, background) <= thresh 

911 else: 

912 fill = p != value and p != border 

913 if fill: 

914 pixel[s, t] = value 

915 new_edge.add((s, t)) 

916 full_edge = edge # discard pixels processed 

917 edge = new_edge 

918 

919 

920def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation): 

921 """ 

922 Generate a list of vertices for a 2D regular polygon. 

923 

924 :param bounding_circle: The bounding circle is a tuple defined 

925 by a point and radius. The polygon is inscribed in this circle. 

926 (e.g. ``bounding_circle=(x, y, r)`` or ``((x, y), r)``) 

927 :param n_sides: Number of sides 

928 (e.g. ``n_sides=3`` for a triangle, ``6`` for a hexagon) 

929 :param rotation: Apply an arbitrary rotation to the polygon 

930 (e.g. ``rotation=90``, applies a 90 degree rotation) 

931 :return: List of regular polygon vertices 

932 (e.g. ``[(25, 50), (50, 50), (50, 25), (25, 25)]``) 

933 

934 How are the vertices computed? 

935 1. Compute the following variables 

936 - theta: Angle between the apothem & the nearest polygon vertex 

937 - side_length: Length of each polygon edge 

938 - centroid: Center of bounding circle (1st, 2nd elements of bounding_circle) 

939 - polygon_radius: Polygon radius (last element of bounding_circle) 

940 - angles: Location of each polygon vertex in polar grid 

941 (e.g. A square with 0 degree rotation => [225.0, 315.0, 45.0, 135.0]) 

942 

943 2. For each angle in angles, get the polygon vertex at that angle 

944 The vertex is computed using the equation below. 

945 X= xcos(φ) + ysin(φ) 

946 Y= −xsin(φ) + ycos(φ) 

947 

948 Note: 

949 φ = angle in degrees 

950 x = 0 

951 y = polygon_radius 

952 

953 The formula above assumes rotation around the origin. 

954 In our case, we are rotating around the centroid. 

955 To account for this, we use the formula below 

956 X = xcos(φ) + ysin(φ) + centroid_x 

957 Y = −xsin(φ) + ycos(φ) + centroid_y 

958 """ 

959 # 1. Error Handling 

960 # 1.1 Check `n_sides` has an appropriate value 

961 if not isinstance(n_sides, int): 

962 raise TypeError("n_sides should be an int") 

963 if n_sides < 3: 

964 raise ValueError("n_sides should be an int > 2") 

965 

966 # 1.2 Check `bounding_circle` has an appropriate value 

967 if not isinstance(bounding_circle, (list, tuple)): 

968 raise TypeError("bounding_circle should be a tuple") 

969 

970 if len(bounding_circle) == 3: 

971 *centroid, polygon_radius = bounding_circle 

972 elif len(bounding_circle) == 2: 

973 centroid, polygon_radius = bounding_circle 

974 else: 

975 raise ValueError( 

976 "bounding_circle should contain 2D coordinates " 

977 "and a radius (e.g. (x, y, r) or ((x, y), r) )" 

978 ) 

979 

980 if not all(isinstance(i, (int, float)) for i in (*centroid, polygon_radius)): 

981 raise ValueError("bounding_circle should only contain numeric data") 

982 

983 if not len(centroid) == 2: 

984 raise ValueError( 

985 "bounding_circle centre should contain 2D coordinates (e.g. (x, y))" 

986 ) 

987 

988 if polygon_radius <= 0: 

989 raise ValueError("bounding_circle radius should be > 0") 

990 

991 # 1.3 Check `rotation` has an appropriate value 

992 if not isinstance(rotation, (int, float)): 

993 raise ValueError("rotation should be an int or float") 

994 

995 # 2. Define Helper Functions 

996 def _apply_rotation(point, degrees, centroid): 

997 return ( 

998 round( 

999 point[0] * math.cos(math.radians(360 - degrees)) 

1000 - point[1] * math.sin(math.radians(360 - degrees)) 

1001 + centroid[0], 

1002 2, 

1003 ), 

1004 round( 

1005 point[1] * math.cos(math.radians(360 - degrees)) 

1006 + point[0] * math.sin(math.radians(360 - degrees)) 

1007 + centroid[1], 

1008 2, 

1009 ), 

1010 ) 

1011 

1012 def _compute_polygon_vertex(centroid, polygon_radius, angle): 

1013 start_point = [polygon_radius, 0] 

1014 return _apply_rotation(start_point, angle, centroid) 

1015 

1016 def _get_angles(n_sides, rotation): 

1017 angles = [] 

1018 degrees = 360 / n_sides 

1019 # Start with the bottom left polygon vertex 

1020 current_angle = (270 - 0.5 * degrees) + rotation 

1021 for _ in range(0, n_sides): 

1022 angles.append(current_angle) 

1023 current_angle += degrees 

1024 if current_angle > 360: 

1025 current_angle -= 360 

1026 return angles 

1027 

1028 # 3. Variable Declarations 

1029 angles = _get_angles(n_sides, rotation) 

1030 

1031 # 4. Compute Vertices 

1032 return [ 

1033 _compute_polygon_vertex(centroid, polygon_radius, angle) for angle in angles 

1034 ] 

1035 

1036 

1037def _color_diff(color1, color2): 

1038 """ 

1039 Uses 1-norm distance to calculate difference between two values. 

1040 """ 

1041 if isinstance(color2, tuple): 

1042 return sum(abs(color1[i] - color2[i]) for i in range(0, len(color2))) 

1043 else: 

1044 return abs(color1 - color2)