Coverage for /home/deng/Projects/ete4/hackathon/ete4/ete4/treeview/main.py: 1%

295 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-03-21 09:19 +0100

1import re 

2import types 

3 

4from .qt import * 

5 

6from ..utils import SVG_COLORS, COLOR_SCHEMES 

7 

8import time 

9def tracktime(f): 

10 def a_wrapper_accepting_arguments(*args, **kargs): 

11 t1 = time.time() 

12 r = f(*args, **kargs) 

13 print(" -> TIME:", f.__name__, time.time() - t1) 

14 return r 

15 return a_wrapper_accepting_arguments 

16 

17 

18_LINE_TYPE_CHECKER = lambda x: x in (0, 1, 2) 

19_SIZE_CHECKER = lambda x: isinstance(x, int) 

20_COLOR_MATCH = re.compile(r"^#[A-Fa-f\d]{6}$") 

21_COLOR_CHECKER = lambda x: x.lower() in SVG_COLORS or re.match(_COLOR_MATCH, x) 

22_NODE_TYPE_CHECKER = lambda x: x in ["sphere", "circle", "square"] 

23_BOOL_CHECKER = lambda x: isinstance(x, bool) or x in (0, 1) 

24 

25FACE_POSITIONS = {"branch-right", "branch-top", "branch-bottom", "float", "float-behind", "aligned"} 

26 

27__all__ = ["NodeStyle", "TreeStyle", "FaceContainer", "_leaf", "add_face_to_node", "COLOR_SCHEMES"] 

28 

29NODE_STYLE_DEFAULT = [ 

30 ["fgcolor", "#0030c1", _COLOR_CHECKER], 

31 ["bgcolor", "#FFFFFF", _COLOR_CHECKER], 

32 #["node_bgcolor", "#FFFFFF", _COLOR_CHECKER], 

33 #["partition_bgcolor","#FFFFFF", _COLOR_CHECKER], 

34 #["faces_bgcolor", "#FFFFFF", _COLOR_CHECKER], 

35 ["vt_line_color", "#000000", _COLOR_CHECKER], 

36 ["hz_line_color", "#000000", _COLOR_CHECKER], 

37 ["hz_line_type", 0, _LINE_TYPE_CHECKER], # 0 solid, 1 dashed, 2 dotted 

38 ["vt_line_type", 0, _LINE_TYPE_CHECKER], # 0 solid, 1 dashed, 2 dotted 

39 ["size", 3, _SIZE_CHECKER], # node circle size 

40 ["shape", "circle", _NODE_TYPE_CHECKER], 

41 ["draw_descendants", True, _BOOL_CHECKER], 

42 ["hz_line_width", 0, _SIZE_CHECKER], 

43 ["vt_line_width", 0, _SIZE_CHECKER] 

44] 

45 

46TREE_STYLE_CHECKER = { 

47 "mode": lambda x: x.lower() in ["c", "r"], 

48} 

49 

50# _faces and faces are registered to allow deepcopy to work on nodes 

51VALID_NODE_STYLE_KEYS = {i[0] for i in NODE_STYLE_DEFAULT} | {"_faces"} 

52 

53class _Border: 

54 def __init__(self): 

55 self.width = None 

56 self.type = 0 

57 self.color = None 

58 

59 def apply(self, item): 

60 if self.width is not None: 

61 r = item.boundingRect() 

62 border = QGraphicsRectItem(r) 

63 border.setParentItem(item) 

64 if self.color: 

65 pen = QPen(QColor(self.color)) 

66 else: 

67 pen = QPen(Qt.PenStyle.NoPen) 

68 set_pen_style(pen, self.type) 

69 pen.setWidth(self.width) 

70 pen.setCapStyle(Qt.PenCapStyle.FlatCap) 

71 border.setPen(pen) 

72 return border 

73 else: 

74 return None 

75 

76class _Background: 

77 """ 

78 Background of the object. 

79 """ 

80 

81 def __init__(self, color=None): 

82 """ 

83 :param color: color code as RGB or from :data:`SVG_COLORS`. 

84 """ 

85 self.color = color 

86 

87 def apply(self, item): 

88 if self.color: 

89 r = item.boundingRect() 

90 bg = QGraphicsRectItem(r) 

91 bg.setParentItem(item) 

92 pen = QPen(QColor(self.color)) 

93 brush = QBrush(QColor(self.color)) 

94 bg.setPen(pen) 

95 bg.setBrush(brush) 

96 bg.setFlag(QGraphicsItem.GraphicsItemFlag.ItemStacksBehindParent) 

97 return bg 

98 else: 

99 return None 

100 

101 

102class _ActionDelegator: 

103 """ Used to associate GUI Functions to nodes and faces """ 

104 

105 def get_delegate(self): 

106 return self._delegate 

107 

108 def set_delegate(self, delegate): 

109 if hasattr(delegate, "init"): 

110 delegate.init(self) 

111 

112 for attr in dir(delegate): 

113 if not attr.startswith("_") and attr != "init" : 

114 fn = getattr(delegate, attr) 

115 setattr(self, attr, types.MethodType(fn, self)) 

116 self._delegate = delegate 

117 

118 delegate = property(get_delegate, set_delegate) 

119 

120 def __init__(self): 

121 self._delegate = None 

122 

123class NodeStyle(dict): 

124 """Dictionary with all valid node graphical attributes.""" 

125 

126 def __init__(self, *args, **kargs): 

127 """NodeStyle constructor. 

128 

129 :param #0030c1 fgcolor: RGB code or name in :data:`SVG_COLORS` 

130 :param #FFFFFF bgcolor: RGB code or name in :data:`SVG_COLORS` 

131 :param #FFFFFF node_bgcolor: RGB code or name in :data:`SVG_COLORS` 

132 :param #FFFFFF partition_bgcolor: RGB code or name in :data:`SVG_COLORS` 

133 :param #FFFFFF faces_bgcolor: RGB code or name in :data:`SVG_COLORS` 

134 :param #000000 vt_line_color: RGB code or name in :data:`SVG_COLORS` 

135 :param #000000 hz_line_color: RGB code or name in :data:`SVG_COLORS` 

136 :param 0 hz_line_type: integer number 

137 :param 0 vt_line_type: integer number 

138 :param 3 size: integer number 

139 :param "circle" shape: "circle", "square" or "sphere" 

140 :param True draw_descendants: Mark an internal node as a leaf. 

141 :param 0 hz_line_width: Integer number representing the 

142 width of the line in pixels. A line width of zero 

143 indicates a cosmetic pen. This means that the pen width is 

144 always drawn one pixel wide, independent of the 

145 transformation set on the painter. 

146 :param 0 vt_line_width: Integer number representing the 

147 width of the line in pixels. A line width of zero 

148 indicates a cosmetic pen. This means that the pen width is 

149 always drawn one pixel wide, independent of the 

150 transformation set on the painter. 

151 """ 

152 super().__init__(*args, **kargs) 

153 self.init() 

154 

155 def init(self): 

156 for key, dvalue, checker in NODE_STYLE_DEFAULT: 

157 if key not in self: 

158 self[key] = dvalue 

159 elif not checker(self[key]): 

160 raise ValueError("'%s' attribute in node style has not a valid value: %s" % 

161 (key, self[key])) 

162 

163 def __setitem__(self, i, v): 

164 if i not in VALID_NODE_STYLE_KEYS: 

165 raise ValueError("'%s' is not a valid keyword for a NodeStyle instance" % i) 

166 

167 super().__setitem__(i, v) 

168 

169 

170class TreeStyle: 

171 """Image properties used to render a tree. 

172 

173 **-- About tree design --** 

174 

175 :param None layout_fn: Layout function used to dynamically control 

176 the aspect of nodes. Valid values are: None or a pointer to a method, 

177 function, etc. 

178 

179 **-- About tree shape --** 

180 

181 :param "r" mode: Valid modes are 'c'(ircular) or 'r'(ectangular). 

182 :param 0 orientation: If 0, tree is drawn from left-to-right. If 

183 1, tree is drawn from right-to-left. This property only makes 

184 sense when "r" mode is used. 

185 :param 0 rotation: Tree figure will be rotate X degrees (clock-wise 

186 rotation). 

187 :param 1 min_leaf_separation: Min separation, in pixels, between 

188 two adjacent branches 

189 :param 0 branch_vertical_margin: Leaf branch separation margin, in 

190 pixels. This will add a separation of X pixels between adjacent 

191 leaf branches. In practice, increasing this value work as 

192 increasing Y axis scale. 

193 :param 0 arc_start: When circular trees are drawn, this defines the 

194 starting angle (in degrees) from which leaves are distributed 

195 (clock-wise) around the total arc span (0 = 3 o'clock). 

196 :param 359 arc_span: Total arc used to draw circular trees (in 

197 degrees). 

198 :param 0 margin_left: Left tree image margin, in pixels. 

199 :param 0 margin_right: Right tree image margin, in pixels. 

200 :param 0 margin_top: Top tree image margin, in pixels. 

201 :param 0 margin_bottom: Bottom tree image margin, in pixels. 

202 

203 **-- About Tree branches --** 

204 

205 :param None scale: Scale used to draw branch lengths. If None, it will 

206 be automatically calculated. 

207 :param "mid" optimal_scale_level: Two levels of automatic branch 

208 scale detection are available: :attr:`"mid"` and 

209 :attr:`"full"`. In :attr:`full` mode, branch scale will me 

210 adjusted to fully avoid dotted lines in the tree image. In other 

211 words, scale will be increased until the extra space necessary 

212 to allocated all branch-top/bottom faces and branch-right faces 

213 (in circular mode) is covered by real branches. Note, however, 

214 that the optimal scale in trees with very unbalanced branch 

215 lengths might be huge. If :attr:`"mid"` mode is selected (as it is by default), 

216 optimal scale will only satisfy the space necessary to allocate 

217 branch-right faces in circular trees. Some dotted lines 

218 (artificial branch offsets) will still appear when 

219 branch-top/bottom faces are larger than branch length. Note that 

220 both options apply only when :attr:`scale` is set to None 

221 (automatic). 

222 :param 0.25 root_opening_factor: (from 0 to 1). It defines how much the center of 

223 a circular tree could be opened when adjusting optimal scale, referred 

224 to the total tree length. By default (0.25), a blank space up to 4 

225 times smaller than the tree width could be used to calculate the 

226 optimal tree scale. A 0 value would mean that root node should 

227 always be tightly adjusted to the center of the tree. 

228 :param True complete_branch_lines_when_necessary: True or False. 

229 Draws an extra line (dotted by default) to complete branch 

230 lengths when the space to cover is larger than the branch 

231 itself. 

232 :param False pack_leaves: If True, in circular layouts pull leaf 

233 nodes closer to center while avoiding collisions. 

234 :param 2 extra_branch_line_type: 0=solid, 1=dashed, 2=dotted 

235 :param "gray" extra_branch_line_color: RGB code or name in 

236 :data:`SVG_COLORS` 

237 :param False force_topology: Convert tree branches to a fixed 

238 length, thus allowing to observe the topology of tight nodes 

239 :param False draw_guiding_lines: Draw guidelines from leaf nodes 

240 to aligned faces 

241 :param 2 guiding_lines_type: 0=solid, 1=dashed, 2=dotted. 

242 :param "gray" guiding_lines_color: RGB code or name in :data:`SVG_COLORS` 

243 

244 **-- About node faces --** 

245 

246 :param False allow_face_overlap: If True, node faces are not taken 

247 into account to scale circular tree images, just like many other 

248 visualization programs. Overlapping among branch elements (such 

249 as node labels) will be therefore ignored, and tree size 

250 will be a lot smaller. Note that in most cases, manual setting 

251 of tree scale will be also necessary. 

252 :param True draw_aligned_faces_as_table: Aligned faces will be 

253 drawn as a table, considering all columns in all node faces. 

254 :param True children_faces_on_top: When floating faces from 

255 different nodes overlap, children faces are drawn on top of 

256 parent faces. This can be reversed by setting this attribute 

257 to false. 

258 

259 **-- Addons --** 

260 

261 :param False show_border: Draw a border around the whole tree 

262 :param True show_scale: Include the scale legend in the tree 

263 image 

264 :param None scale_length: Scale length to be used as reference 

265 scale bar when visualizing tree. None = automatically adjusted. 

266 :param False show_leaf_name: Automatically adds a text Face to 

267 leaf nodes showing their names 

268 :param False show_branch_length: Automatically adds branch 

269 length information on top of branches 

270 :param False show_branch_support: Automatically adds branch 

271 support text in the bottom of tree branches 

272 

273 **-- Tree surroundings --** 

274 

275 The following options are actually Face containers, so graphical 

276 elements can be added just as it is done with nodes. In example, 

277 to add tree legend:: 

278 

279 TreeStyle.legend.add_face(CircleFace(10, "red"), column=0) 

280 TreeStyle.legend.add_face(TextFace("0.5 support"), column=1) 

281 

282 :param aligned_header: a :class:`FaceContainer` aligned to the end 

283 of the tree and placed at the top part. 

284 :param aligned_foot: a :class:`FaceContainer` aligned to the end 

285 of the tree and placed at the bottom part. 

286 :param legend: a :class:`FaceContainer` with an arbitrary number of faces 

287 representing the legend of the figure. 

288 :param 4 legend_position=4: TopLeft corner if 1, TopRight 

289 if 2, BottomLeft if 3, BottomRight if 4 

290 :param title: A Face container that can be used as tree title 

291 

292 """ 

293 

294 def set_layout_fn(self, layout): 

295 self._layout_handler = [] 

296 if type(layout) not in set([list, set, tuple, frozenset]): 

297 layout = [layout] 

298 

299 for ly in layout: 

300 # Validates layout function 

301 if callable(ly) is True or ly is None: 

302 self._layout_handler.append(ly) 

303 else: 

304 from . import layouts 

305 try: 

306 self._layout_handler.append(getattr(layouts, ly)) 

307 except Exception as e: 

308 print(e) 

309 raise ValueError ("Required layout is not a function pointer nor a valid layout name.") 

310 

311 def get_layout_fn(self): 

312 return self._layout_handler 

313 

314 layout_fn = property(get_layout_fn, set_layout_fn) 

315 

316 def __init__(self): 

317 # ::::::::::::::::::::::::: 

318 # TREE SHAPE AND SIZE 

319 # ::::::::::::::::::::::::: 

320 

321 # Valid modes are : "c" or "r" 

322 self.mode = "r" 

323 

324 # Applies only for circular mode. It prevents aligned faces to 

325 # overlap each other by increasing the radius. 

326 self.allow_face_overlap = False 

327 

328 # Layout function used to dynamically control the aspect of 

329 # nodes 

330 self._layout_handler = [] 

331 

332 # 0= tree is drawn from left-to-right 1= tree is drawn from 

333 # right-to-left. This property only has sense when "r" mode 

334 # is used. 

335 self.orientation = 0 

336 

337 # Tree rotation in degrees (clock-wise rotation) 

338 self.rotation = 0 

339 

340 # Scale used to convert branch lengths to pixels. If 'None', 

341 # the scale will be automatically calculated. 

342 self.scale = None 

343 

344 # How much the center of a circular tree can be opened, 

345 # referred to the total tree length. 

346 self.root_opening_factor = 0.25 

347 

348 # mid, or full 

349 self.optimal_scale_level = "mid" 

350 

351 # Min separation, in pixels, between to adjacent branches 

352 self.min_leaf_separation = 1 

353 

354 # Leaf branch separation margin, in pixels. This will add a 

355 # separation of X pixels between adjacent leaf branches. In 

356 # practice this produces a Y-zoom in. 

357 self.branch_vertical_margin = 0 

358 

359 # When circular trees are drawn, this defines the starting 

360 # angle (in degrees) from which leaves are distributed 

361 # (clock-wise) around the total arc. 0 = 3 o'clock 

362 self.arc_start = 0 

363 

364 # Total arc used to draw circular trees (in degrees) 

365 self.arc_span = 359 

366 

367 # Margins around tree picture 

368 self.margin_left = 1 

369 self.margin_right = 1 

370 self.margin_top = 1 

371 self.margin_bottom = 1 

372 

373 # ::::::::::::::::::::::::: 

374 # TREE BRANCHES 

375 # ::::::::::::::::::::::::: 

376 

377 # When top-branch and bottom-branch faces are larger than 

378 # branch length, branch line can be completed. Also, when 

379 # circular trees are drawn, 

380 self.complete_branch_lines_when_necessary = True 

381 self.pack_leaves = False 

382 self.extra_branch_line_type = 2 # 0 solid, 1 dashed, 2 dotted 

383 self.extra_branch_line_color = "gray" 

384 

385 # Convert tree branches to a fixed length, thus allowing to 

386 # observe the topology of tight nodes 

387 self.force_topology = False 

388 

389 # Draw guidelines from leaf nodes to aligned faces 

390 self.draw_guiding_lines = False 

391 

392 # Format and color for the guiding lines 

393 self.guiding_lines_type = 2 # 0 solid, 1 dashed, 2 dotted 

394 self.guiding_lines_color = "gray" 

395 

396 # ::::::::::::::::::::::::: 

397 # FACES 

398 # ::::::::::::::::::::::::: 

399 

400 # Aligned faces will be drawn as a table, considering all 

401 # columns in all node faces. 

402 self.draw_aligned_faces_as_table = True 

403 self.aligned_table_style = 0 # 0 = full grid (rows and 

404 # columns), 1 = semigrid ( rows 

405 # are merged ) 

406 

407 # When floating faces from different nodes overlap, children 

408 # faces are drawn on top of parent faces. This can be reversed 

409 # by setting this attribute to false. 

410 self.children_faces_on_top = True 

411 

412 # ::::::::::::::::::::::::: 

413 # Addons 

414 # ::::::::::::::::::::::::: 

415 

416 # Draw a border around the whole tree 

417 self.show_border = False 

418 

419 # Draw the scale 

420 self.show_scale = True 

421 self.scale_length = None 

422 

423 # Initialize aligned face headers 

424 self.aligned_header = FaceContainer() 

425 self.aligned_foot = FaceContainer() 

426 

427 self.show_leaf_name = True 

428 self.show_branch_length = False 

429 self.show_branch_support = False 

430 

431 self.legend = FaceContainer() 

432 self.legend_position = 2 

433 

434 

435 self.title = FaceContainer() 

436 self.tree_width = 180 

437 # PRIVATE values 

438 self._scale = None 

439 

440 self.__closed__ = 1 

441 

442 

443 def __setattr__(self, attr, val): 

444 if hasattr(self, attr) or not getattr(self, "__closed__", 0): 

445 if TREE_STYLE_CHECKER.get(attr, lambda x: True)(val): 

446 object.__setattr__(self, attr, val) 

447 else: 

448 raise ValueError("[%s] wrong type" % attr) 

449 else: 

450 raise ValueError("[%s] option is not supported" % attr) 

451 

452class _FaceAreas: 

453 def __init__(self): 

454 for a in FACE_POSITIONS: 

455 setattr(self, a, FaceContainer()) 

456 

457 def __setattr__(self, attr, val): 

458 if attr not in FACE_POSITIONS: 

459 raise AttributeError("Face area [%s] not in %s" %(attr, FACE_POSITIONS) ) 

460 return super(_FaceAreas, self).__setattr__(attr, val) 

461 

462 def __getattr__(self, attr): 

463 if attr not in FACE_POSITIONS: 

464 raise AttributeError("Face area [%s] not in %s" %(attr, FACE_POSITIONS) ) 

465 return super(_FaceAreas, self).__getattr__(attr) 

466 

467class FaceContainer(dict): 

468 """ 

469 .. versionadded:: 2.1 

470 

471 Use this object to create a grid of faces. You can add faces to different columns. 

472 """ 

473 def add_face(self, face, column): 

474 """ 

475 add the face **face** to the specified **column** 

476 """ 

477 self.setdefault(int(column), []).append(face) 

478 

479def _leaf(node): 

480 collapsed = hasattr(node, "_img_style") and not node.img_style["draw_descendants"] 

481 return collapsed or node.is_leaf 

482 

483def add_face_to_node(face, node, column, aligned=False, position="branch-right"): 

484 """ 

485 .. currentmodule:: ete3.treeview.faces 

486 

487 Adds a Face to a given node. 

488 

489 :argument face: A :class:`Face` instance 

490 

491 .. currentmodule:: ete3 

492 

493 :argument node: a tree node instance (:class:`Tree`, :class:`PhyloTree`, etc.) 

494 :argument column: An integer number starting from 0 

495 :argument "branch-right" position: Possible values are 

496 "branch-right", "branch-top", "branch-bottom", "float", "float-behind" and "aligned". 

497 """ 

498 

499 ## ADD HERE SOME TYPE CHECK FOR node and face 

500 

501 # to stay 2.0 compatible 

502 if aligned == True: 

503 position = "aligned" 

504 

505 if node.props.get("_temp_faces", None): 

506 getattr(node.props["_temp_faces"], position).add_face(face, column) 

507 else: 

508 raise Exception("This function can only be called within a layout function. Use node.add_face() instead") 

509 

510 

511def set_pen_style(pen, line_style): 

512 if line_style == 0: 

513 pen.setStyle(Qt.PenStyle.SolidLine) 

514 elif line_style == 1: 

515 pen.setStyle(Qt.PenStyle.DashLine) 

516 elif line_style == 2: 

517 pen.setStyle(Qt.PenStyle.DotLine) 

518 

519 

520def save(scene, imgName, w=None, h=None, dpi=90,\ 

521 take_region=False, units="px"): 

522 ipython_inline = False 

523 if imgName == "%%inline": 

524 ipython_inline = True 

525 ext = "PNG" 

526 elif imgName == "%%inlineSVG": 

527 ipython_inline = True 

528 ext = "SVG" 

529 elif imgName.startswith("%%return"): 

530 try: 

531 ext = imgName.split(".")[1].upper() 

532 except IndexError: 

533 ext = 'SVG' 

534 imgName = '%%return' 

535 else: 

536 ext = imgName.split(".")[-1].upper() 

537 

538 main_rect = scene.sceneRect() 

539 aspect_ratio = main_rect.height() / main_rect.width() 

540 

541 # auto adjust size 

542 if not w and not h: 

543 units = "px" 

544 w = main_rect.width() 

545 h = main_rect.height() 

546 ratio_mode = Qt.AspectRatioMode.KeepAspectRatio 

547 elif w and h: 

548 ratio_mode = Qt.AspectRatioMode.IgnoreAspectRatio 

549 elif h is None : 

550 h = w * aspect_ratio 

551 ratio_mode = Qt.AspectRatioMode.KeepAspectRatio 

552 elif w is None: 

553 w = h / aspect_ratio 

554 ratio_mode = Qt.AspectRatioMode.KeepAspectRatio 

555 

556 # Adjust to resolution 

557 if units == "mm": 

558 if w: 

559 w = w * 0.0393700787 * dpi 

560 if h: 

561 h = h * 0.0393700787 * dpi 

562 elif units == "in": 

563 if w: 

564 w = w * dpi 

565 if h: 

566 h = h * dpi 

567 elif units == "px": 

568 pass 

569 else: 

570 raise Exception("wrong unit format") 

571 

572 x_scale, y_scale = w/main_rect.width(), h/main_rect.height() 

573 

574 if ext == "SVG": 

575 svg = QSvgGenerator() 

576 targetRect = QRectF(0, 0, w, h) 

577 svg.setSize(QSize(int(w), int(h))) 

578 svg.setViewBox(targetRect) 

579 svg.setTitle("Generated with ETE http://etetoolkit.org") 

580 svg.setDescription("Generated with ETE http://etetoolkit.org") 

581 

582 if imgName == '%%return': 

583 ba = QByteArray() 

584 buf = QBuffer(ba) 

585 buf.open(QIODevice.WriteOnly) 

586 svg.setOutputDevice(buf) 

587 else: 

588 svg.setFileName(imgName) 

589 

590 pp = QPainter() 

591 pp.begin(svg) 

592 scene.render(pp, targetRect, scene.sceneRect(), ratio_mode) 

593 pp.end() 

594 if imgName == '%%return': 

595 compatible_code = str(ba) 

596 print('from memory') 

597 else: 

598 with open(imgName) as f: 

599 compatible_code = f.read() 

600 

601 # Fix a very annoying problem with Radial gradients in 

602 # inkscape and browsers... 

603 compatible_code = compatible_code.replace("xml:id=", "id=") 

604 compatible_code = re.sub(r'font-size="(\d+)"', 'font-size="\\1pt"', compatible_code) 

605 compatible_code = compatible_code.replace('\n', ' ') 

606 compatible_code = re.sub(r'<g [^>]+>\s*</g>', '', compatible_code) 

607 # End of fix 

608 if ipython_inline: 

609 from IPython.core.display import SVG 

610 return SVG(compatible_code) 

611 

612 elif imgName == '%%return': 

613 return x_scale, y_scale, compatible_code 

614 else: 

615 with open(imgName, "w") as f: 

616 f.write(compatible_code) 

617 

618 elif ext == "PDF": 

619 format = QPrinter.OutputFormat.PdfFormat 

620 

621 printer = QPrinter(QPrinter.PrinterMode.HighResolution) 

622 printer.setResolution(dpi) 

623 printer.setOutputFormat(format) 

624 printer.setPageSize(QPageSize(QPageSize.PageSizeId.A4)) 

625 

626 printer.setFullPage(True); 

627 printer.setOutputFileName(imgName); 

628 pp = QPainter(printer) 

629 targetRect = QRectF(0, 0 , w, h) 

630 scene.render(pp, targetRect, scene.sceneRect(), ratio_mode) 

631 else: 

632 targetRect = QRectF(0, 0, w, h) 

633 ii= QImage(int(w), int(h), QImage.Format.Format_ARGB32) 

634 ii.fill(QColor(Qt.GlobalColor.white).rgb()) 

635 ii.setDotsPerMeterX(int(dpi / 0.0254)) # Convert inches to meters 

636 ii.setDotsPerMeterY(int(dpi / 0.0254)) 

637 pp = QPainter(ii) 

638 pp.setRenderHint(QPainter.RenderHint.Antialiasing) 

639 pp.setRenderHint(QPainter.RenderHint.TextAntialiasing) 

640 pp.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform) 

641 

642 scene.render(pp, targetRect, scene.sceneRect(), ratio_mode) 

643 pp.end() 

644 if ipython_inline: 

645 ba = QByteArray() 

646 buf = QBuffer(ba) 

647 buf.open(QIODevice.OpenModeFlag.WriteOnly) 

648 ii.save(buf, "PNG") 

649 from IPython.core.display import Image 

650 return Image(ba.data()) 

651 elif imgName == '%%return': 

652 ba = QByteArray() 

653 buf = QBuffer(ba) 

654 buf.open(QIODevice.WriteOnly) 

655 ii.save(buf, "PNG") 

656 return x_scale, y_scale, ba.toBase64() 

657 else: 

658 ii.save(imgName) 

659 

660 return w/main_rect.width(), h/main_rect.height()