Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/IPython/core/display.py: 27%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

471 statements  

1"""Top-level display functions for displaying object in different formats.""" 

2 

3# Copyright (c) IPython Development Team. 

4# Distributed under the terms of the Modified BSD License. 

5 

6from __future__ import annotations 

7 

8from enum import Enum 

9from dataclasses import dataclass, KW_ONLY 

10from binascii import b2a_base64, hexlify 

11import os 

12import warnings 

13from copy import deepcopy 

14from os.path import splitext 

15from pathlib import Path, PurePath 

16 

17from typing import TYPE_CHECKING, Self 

18 

19from IPython.testing.skipdoctest import skip_doctest 

20from . import display_functions 

21 

22if TYPE_CHECKING: 

23 from collections.abc import Callable 

24 

25 

26__all__ = [ 

27 "display_pretty", 

28 "display_html", 

29 "display_markdown", 

30 "display_svg", 

31 "display_png", 

32 "display_jpeg", 

33 "display_webp", 

34 "display_latex", 

35 "display_json", 

36 "display_javascript", 

37 "display_pdf", 

38 "DisplayObject", 

39 "TextDisplayObject", 

40 "Pretty", 

41 "HTML", 

42 "Markdown", 

43 "Math", 

44 "Latex", 

45 "SVG", 

46 "ProgressBar", 

47 "JSON", 

48 "GeoJSON", 

49 "Javascript", 

50 "Image", 

51 "Video", 

52] 

53 

54#----------------------------------------------------------------------------- 

55# utility functions 

56#----------------------------------------------------------------------------- 

57 

58def _safe_exists(path): 

59 """Check path, but don't let exceptions raise""" 

60 try: 

61 return os.path.exists(path) 

62 except Exception: 

63 return False 

64 

65 

66def _display_mimetype(mimetype, objs, raw=False, metadata=None): 

67 """internal implementation of all display_foo methods 

68 

69 Parameters 

70 ---------- 

71 mimetype : str 

72 The mimetype to be published (e.g. 'image/png') 

73 *objs : object 

74 The Python objects to display, or if raw=True raw text data to 

75 display. 

76 raw : bool 

77 Are the data objects raw data or Python objects that need to be 

78 formatted before display? [default: False] 

79 metadata : dict (optional) 

80 Metadata to be associated with the specific mimetype output. 

81 """ 

82 if metadata: 

83 metadata = {mimetype: metadata} 

84 if raw: 

85 # turn list of pngdata into list of { 'image/png': pngdata } 

86 objs = [ {mimetype: obj} for obj in objs ] 

87 display_functions.display(*objs, raw=raw, metadata=metadata, include=[mimetype]) 

88 

89#----------------------------------------------------------------------------- 

90# Main functions 

91#----------------------------------------------------------------------------- 

92 

93 

94def display_pretty(*objs, **kwargs): 

95 """Display the pretty (default) representation of an object. 

96 

97 Parameters 

98 ---------- 

99 *objs : object 

100 The Python objects to display, or if raw=True raw text data to 

101 display. 

102 raw : bool 

103 Are the data objects raw data or Python objects that need to be 

104 formatted before display? [default: False] 

105 metadata : dict (optional) 

106 Metadata to be associated with the specific mimetype output. 

107 """ 

108 _display_mimetype('text/plain', objs, **kwargs) 

109 

110 

111def display_html(*objs, **kwargs): 

112 """Display the HTML representation of an object. 

113 

114 Note: If raw=False and the object does not have a HTML 

115 representation, no HTML will be shown. 

116 

117 Parameters 

118 ---------- 

119 *objs : object 

120 The Python objects to display, or if raw=True raw HTML data to 

121 display. 

122 raw : bool 

123 Are the data objects raw data or Python objects that need to be 

124 formatted before display? [default: False] 

125 metadata : dict (optional) 

126 Metadata to be associated with the specific mimetype output. 

127 """ 

128 _display_mimetype('text/html', objs, **kwargs) 

129 

130 

131def display_markdown(*objs, **kwargs): 

132 """Displays the Markdown representation of an object. 

133 

134 Parameters 

135 ---------- 

136 *objs : object 

137 The Python objects to display, or if raw=True raw markdown data to 

138 display. 

139 raw : bool 

140 Are the data objects raw data or Python objects that need to be 

141 formatted before display? [default: False] 

142 metadata : dict (optional) 

143 Metadata to be associated with the specific mimetype output. 

144 """ 

145 

146 _display_mimetype('text/markdown', objs, **kwargs) 

147 

148 

149def display_svg(*objs, **kwargs): 

150 """Display the SVG representation of an object. 

151 

152 Parameters 

153 ---------- 

154 *objs : object 

155 The Python objects to display, or if raw=True raw svg data to 

156 display. 

157 raw : bool 

158 Are the data objects raw data or Python objects that need to be 

159 formatted before display? [default: False] 

160 metadata : dict (optional) 

161 Metadata to be associated with the specific mimetype output. 

162 """ 

163 _display_mimetype('image/svg+xml', objs, **kwargs) 

164 

165 

166def display_png(*objs, **kwargs): 

167 """Display the PNG representation of an object. 

168 

169 Parameters 

170 ---------- 

171 *objs : object 

172 The Python objects to display, or if raw=True raw png data to 

173 display. 

174 raw : bool 

175 Are the data objects raw data or Python objects that need to be 

176 formatted before display? [default: False] 

177 metadata : dict (optional) 

178 Metadata to be associated with the specific mimetype output. 

179 """ 

180 _display_mimetype('image/png', objs, **kwargs) 

181 

182 

183def display_jpeg(*objs, **kwargs): 

184 """Display the JPEG representation of an object. 

185 

186 Parameters 

187 ---------- 

188 *objs : object 

189 The Python objects to display, or if raw=True raw JPEG data to 

190 display. 

191 raw : bool 

192 Are the data objects raw data or Python objects that need to be 

193 formatted before display? [default: False] 

194 metadata : dict (optional) 

195 Metadata to be associated with the specific mimetype output. 

196 """ 

197 _display_mimetype('image/jpeg', objs, **kwargs) 

198 

199 

200def display_webp(*objs, **kwargs): 

201 """Display the WEBP representation of an object. 

202 

203 Parameters 

204 ---------- 

205 *objs : object 

206 The Python objects to display, or if raw=True raw JPEG data to 

207 display. 

208 raw : bool 

209 Are the data objects raw data or Python objects that need to be 

210 formatted before display? [default: False] 

211 metadata : dict (optional) 

212 Metadata to be associated with the specific mimetype output. 

213 """ 

214 _display_mimetype("image/webp", objs, **kwargs) 

215 

216 

217def display_latex(*objs, **kwargs): 

218 """Display the LaTeX representation of an object. 

219 

220 Parameters 

221 ---------- 

222 *objs : object 

223 The Python objects to display, or if raw=True raw latex data to 

224 display. 

225 raw : bool 

226 Are the data objects raw data or Python objects that need to be 

227 formatted before display? [default: False] 

228 metadata : dict (optional) 

229 Metadata to be associated with the specific mimetype output. 

230 """ 

231 _display_mimetype('text/latex', objs, **kwargs) 

232 

233 

234def display_json(*objs, **kwargs): 

235 """Display the JSON representation of an object. 

236 

237 Note that not many frontends support displaying JSON. 

238 

239 Parameters 

240 ---------- 

241 *objs : object 

242 The Python objects to display, or if raw=True raw json data to 

243 display. 

244 raw : bool 

245 Are the data objects raw data or Python objects that need to be 

246 formatted before display? [default: False] 

247 metadata : dict (optional) 

248 Metadata to be associated with the specific mimetype output. 

249 """ 

250 _display_mimetype('application/json', objs, **kwargs) 

251 

252 

253def display_javascript(*objs, **kwargs): 

254 """Display the Javascript representation of an object. 

255 

256 Parameters 

257 ---------- 

258 *objs : object 

259 The Python objects to display, or if raw=True raw javascript data to 

260 display. 

261 raw : bool 

262 Are the data objects raw data or Python objects that need to be 

263 formatted before display? [default: False] 

264 metadata : dict (optional) 

265 Metadata to be associated with the specific mimetype output. 

266 """ 

267 _display_mimetype('application/javascript', objs, **kwargs) 

268 

269 

270def display_pdf(*objs, **kwargs): 

271 """Display the PDF representation of an object. 

272 

273 Parameters 

274 ---------- 

275 *objs : object 

276 The Python objects to display, or if raw=True raw javascript data to 

277 display. 

278 raw : bool 

279 Are the data objects raw data or Python objects that need to be 

280 formatted before display? [default: False] 

281 metadata : dict (optional) 

282 Metadata to be associated with the specific mimetype output. 

283 """ 

284 _display_mimetype('application/pdf', objs, **kwargs) 

285 

286 

287#----------------------------------------------------------------------------- 

288# Smart classes 

289#----------------------------------------------------------------------------- 

290 

291 

292class DisplayObject: 

293 """An object that wraps data to be displayed.""" 

294 

295 _read_flags = 'r' 

296 _show_mem_addr = False 

297 metadata = None 

298 

299 def __init__(self, data=None, url=None, filename=None, metadata=None): 

300 """Create a display object given raw data. 

301 

302 When this object is returned by an expression or passed to the 

303 display function, it will result in the data being displayed 

304 in the frontend. The MIME type of the data should match the 

305 subclasses used, so the Png subclass should be used for 'image/png' 

306 data. If the data is a URL, the data will first be downloaded 

307 and then displayed. 

308 

309 Parameters 

310 ---------- 

311 data : unicode, str or bytes 

312 The raw data or a URL or file to load the data from 

313 url : unicode 

314 A URL to download the data from. 

315 filename : unicode 

316 Path to a local file to load the data from. 

317 metadata : dict 

318 Dict of metadata associated to be the object when displayed 

319 """ 

320 if isinstance(data, (Path, PurePath)): 

321 data = str(data) 

322 

323 if data is not None and isinstance(data, str): 

324 if data.startswith('http') and url is None: 

325 url = data 

326 filename = None 

327 data = None 

328 elif _safe_exists(data) and filename is None: 

329 url = None 

330 filename = data 

331 data = None 

332 

333 self.url = url 

334 self.filename = filename 

335 # because of @data.setter methods in 

336 # subclasses ensure url and filename are set 

337 # before assigning to self.data 

338 self.data = data 

339 

340 if metadata is not None: 

341 self.metadata = metadata 

342 elif self.metadata is None: 

343 self.metadata = {} 

344 

345 self.reload() 

346 self._check_data() 

347 

348 def __repr__(self): 

349 if not self._show_mem_addr: 

350 cls = self.__class__ 

351 r = "<{}.{} object>".format(cls.__module__, cls.__name__) 

352 else: 

353 r = super().__repr__() 

354 return r 

355 

356 def _check_data(self): 

357 """Override in subclasses if there's something to check.""" 

358 pass 

359 

360 def _data_and_metadata(self): 

361 """shortcut for returning metadata with shape information, if defined""" 

362 if self.metadata: 

363 return self.data, deepcopy(self.metadata) 

364 else: 

365 return self.data 

366 

367 def reload(self): 

368 """Reload the raw data from file or URL.""" 

369 if self.filename is not None: 

370 encoding = None if "b" in self._read_flags else "utf-8" 

371 with open(self.filename, self._read_flags, encoding=encoding) as f: 

372 self.data = f.read() 

373 elif self.url is not None: 

374 # Deferred import 

375 from urllib.request import urlopen 

376 response = urlopen(self.url) 

377 data = response.read() 

378 # extract encoding from header, if there is one: 

379 encoding = None 

380 if 'content-type' in response.headers: 

381 for sub in response.headers['content-type'].split(';'): 

382 sub = sub.strip() 

383 if sub.startswith('charset'): 

384 encoding = sub.split('=')[-1].strip() 

385 break 

386 if 'content-encoding' in response.headers: 

387 if 'gzip' in response.headers['content-encoding']: 

388 import gzip 

389 from io import BytesIO 

390 

391 # assume utf-8 if encoding is not specified 

392 with gzip.open( 

393 BytesIO(data), "rt", encoding=encoding or "utf-8" 

394 ) as fp: 

395 encoding = None 

396 data = fp.read() 

397 

398 # decode data, if an encoding was specified 

399 # We only touch self.data once since 

400 # subclasses such as SVG have @data.setter methods 

401 # that transform self.data into ... well svg. 

402 if encoding: 

403 self.data = data.decode(encoding, 'replace') 

404 else: 

405 self.data = data 

406 

407 

408class TextDisplayObject(DisplayObject): 

409 """Create a text display object given raw data. 

410 

411 Parameters 

412 ---------- 

413 data : str or unicode 

414 The raw data or a URL or file to load the data from. 

415 url : unicode 

416 A URL to download the data from. 

417 filename : unicode 

418 Path to a local file to load the data from. 

419 metadata : dict 

420 Dict of metadata associated to be the object when displayed 

421 """ 

422 def _check_data(self): 

423 if self.data is not None and not isinstance(self.data, str): 

424 raise TypeError("{} expects text, not {!r}".format(self.__class__.__name__, self.data)) 

425 

426class Pretty(TextDisplayObject): 

427 

428 def _repr_pretty_(self, pp, cycle): 

429 return pp.text(self.data) 

430 

431 

432class HTML(TextDisplayObject): 

433 

434 def __init__(self, data=None, url=None, filename=None, metadata=None): 

435 def warn(): 

436 if not data: 

437 return False 

438 

439 # 

440 # Avoid calling lower() on the entire data, because it could be a 

441 # long string and we're only interested in its beginning and end. 

442 # 

443 prefix = data[:10].lower() 

444 suffix = data[-10:].lower() 

445 return prefix.startswith("<iframe ") and suffix.endswith("</iframe>") 

446 

447 if warn(): 

448 warnings.warn("Consider using IPython.display.IFrame instead") 

449 super().__init__(data=data, url=url, filename=filename, metadata=metadata) 

450 

451 def _repr_html_(self): 

452 return self._data_and_metadata() 

453 

454 def __html__(self): 

455 """ 

456 This method exists to inform other HTML-using modules (e.g. Markupsafe, 

457 htmltag, etc) that this object is HTML and does not need things like 

458 special characters (<>&) escaped. 

459 """ 

460 return self._repr_html_() 

461 

462 

463class Markdown(TextDisplayObject): 

464 

465 def _repr_markdown_(self): 

466 return self._data_and_metadata() 

467 

468 

469class Math(TextDisplayObject): 

470 

471 def _repr_latex_(self): 

472 s = r"$\displaystyle %s$" % self.data.strip('$') 

473 if self.metadata: 

474 return s, deepcopy(self.metadata) 

475 else: 

476 return s 

477 

478 

479class Latex(TextDisplayObject): 

480 

481 def _repr_latex_(self): 

482 return self._data_and_metadata() 

483 

484 

485class SVG(DisplayObject): 

486 """Embed an SVG into the display. 

487 

488 Note if you just want to view a svg image via a URL use `:class:Image` with 

489 a url=URL keyword argument. 

490 """ 

491 

492 _read_flags = 'rb' 

493 # wrap data in a property, which extracts the <svg> tag, discarding 

494 # document headers 

495 _data: str | None = None 

496 

497 @property 

498 def data(self): 

499 return self._data 

500 

501 @data.setter 

502 def data(self, svg): 

503 if svg is None: 

504 self._data = None 

505 return 

506 # parse into dom object 

507 from xml.dom import minidom 

508 x = minidom.parseString(svg) 

509 # get svg tag (should be 1) 

510 found_svg = x.getElementsByTagName('svg') 

511 if found_svg: 

512 svg = found_svg[0].toxml() 

513 else: 

514 # fallback on the input, trust the user 

515 # but this is probably an error. 

516 pass 

517 if isinstance(svg, bytes): 

518 self._data = svg.decode(errors="replace") 

519 else: 

520 self._data = svg 

521 

522 def _repr_svg_(self): 

523 return self._data_and_metadata() 

524 

525class ProgressBar(DisplayObject): 

526 """Progressbar supports displaying a progressbar like element 

527 """ 

528 def __init__(self, total): 

529 """Creates a new progressbar 

530 

531 Parameters 

532 ---------- 

533 total : int 

534 maximum size of the progressbar 

535 """ 

536 self.total = total 

537 self._progress = 0 

538 self.html_width = '60ex' 

539 self.text_width = 60 

540 self._display_id = hexlify(os.urandom(8)).decode('ascii') 

541 

542 def __repr__(self): 

543 fraction = self.progress / self.total 

544 filled = '=' * int(fraction * self.text_width) 

545 rest = ' ' * (self.text_width - len(filled)) 

546 return '[{}{}] {}/{}'.format( 

547 filled, rest, 

548 self.progress, self.total, 

549 ) 

550 

551 def _repr_html_(self): 

552 return "<progress style='width:{}' max='{}' value='{}'></progress>".format( 

553 self.html_width, self.total, self.progress) 

554 

555 def display(self): 

556 display_functions.display(self, display_id=self._display_id) 

557 

558 def update(self): 

559 display_functions.display(self, display_id=self._display_id, update=True) 

560 

561 @property 

562 def progress(self): 

563 return self._progress 

564 

565 @progress.setter 

566 def progress(self, value): 

567 self._progress = value 

568 self.update() 

569 

570 def __iter__(self): 

571 self.display() 

572 self._progress = -1 # First iteration is 0 

573 return self 

574 

575 def __next__(self): 

576 """Returns current value and increments display by one.""" 

577 self.progress += 1 

578 if self.progress < self.total: 

579 return self.progress 

580 else: 

581 raise StopIteration() 

582 

583class JSON(DisplayObject): 

584 """JSON expects a JSON-able dict or list 

585 

586 not an already-serialized JSON string. 

587 

588 Scalar types (None, number, string) are not allowed, only dict or list containers. 

589 """ 

590 # wrap data in a property, which warns about passing already-serialized JSON 

591 _data = None 

592 def __init__(self, data=None, url=None, filename=None, expanded=False, metadata=None, root='root', **kwargs): 

593 """Create a JSON display object given raw data. 

594 

595 Parameters 

596 ---------- 

597 data : dict or list 

598 JSON data to display. Not an already-serialized JSON string. 

599 Scalar types (None, number, string) are not allowed, only dict 

600 or list containers. 

601 url : unicode 

602 A URL to download the data from. 

603 filename : unicode 

604 Path to a local file to load the data from. 

605 expanded : boolean 

606 Metadata to control whether a JSON display component is expanded. 

607 metadata : dict 

608 Specify extra metadata to attach to the json display object. 

609 root : str 

610 The name of the root element of the JSON tree 

611 """ 

612 self.metadata = { 

613 'expanded': expanded, 

614 'root': root, 

615 } 

616 if metadata: 

617 self.metadata.update(metadata) 

618 if kwargs: 

619 self.metadata.update(kwargs) 

620 super().__init__(data=data, url=url, filename=filename) 

621 

622 def _check_data(self): 

623 if self.data is not None and not isinstance(self.data, (dict, list)): 

624 raise TypeError("{} expects JSONable dict or list, not {!r}".format(self.__class__.__name__, self.data)) 

625 

626 @property 

627 def data(self): 

628 return self._data 

629 

630 @data.setter 

631 def data(self, data): 

632 if isinstance(data, (Path, PurePath)): 

633 data = str(data) 

634 

635 if isinstance(data, str): 

636 if self.filename is None and self.url is None: 

637 warnings.warn("JSON expects JSONable dict or list, not JSON strings") 

638 import json 

639 data = json.loads(data) 

640 self._data = data 

641 

642 def _data_and_metadata(self): 

643 return self.data, self.metadata 

644 

645 def _repr_json_(self): 

646 return self._data_and_metadata() 

647 

648 

649_css_t = """var link = document.createElement("link"); 

650 link.rel = "stylesheet"; 

651 link.type = "text/css"; 

652 link.href = "%s"; 

653 document.head.appendChild(link); 

654""" 

655 

656_lib_t1 = """new Promise(function(resolve, reject) { 

657 var script = document.createElement("script"); 

658 script.onload = resolve; 

659 script.onerror = reject; 

660 script.src = "%s"; 

661 document.head.appendChild(script); 

662}).then(() => { 

663""" 

664 

665_lib_t2 = """ 

666});""" 

667 

668class GeoJSON(JSON): 

669 """GeoJSON expects JSON-able dict 

670 

671 not an already-serialized JSON string. 

672 

673 Scalar types (None, number, string) are not allowed, only dict containers. 

674 """ 

675 

676 def __init__(self, *args, **kwargs): 

677 """Create a GeoJSON display object given raw data. 

678 

679 Parameters 

680 ---------- 

681 data : dict or list 

682 VegaLite data. Not an already-serialized JSON string. 

683 Scalar types (None, number, string) are not allowed, only dict 

684 or list containers. 

685 url_template : string 

686 Leaflet TileLayer URL template: http://leafletjs.com/reference.html#url-template 

687 layer_options : dict 

688 Leaflet TileLayer options: http://leafletjs.com/reference.html#tilelayer-options 

689 url : unicode 

690 A URL to download the data from. 

691 filename : unicode 

692 Path to a local file to load the data from. 

693 metadata : dict 

694 Specify extra metadata to attach to the json display object. 

695 

696 Examples 

697 -------- 

698 The following will display an interactive map of Mars with a point of 

699 interest on frontend that do support GeoJSON display. 

700 

701 >>> from IPython.display import GeoJSON 

702 

703 >>> GeoJSON(data={ 

704 ... "type": "Feature", 

705 ... "geometry": { 

706 ... "type": "Point", 

707 ... "coordinates": [-81.327, 296.038] 

708 ... } 

709 ... }, 

710 ... url_template="http://s3-eu-west-1.amazonaws.com/whereonmars.cartodb.net/{basemap_id}/{z}/{x}/{y}.png", 

711 ... layer_options={ 

712 ... "basemap_id": "celestia_mars-shaded-16k_global", 

713 ... "attribution" : "Celestia/praesepe", 

714 ... "minZoom" : 0, 

715 ... "maxZoom" : 18, 

716 ... }) 

717 <IPython.core.display.GeoJSON object> 

718 

719 In the terminal IPython, you will only see the text representation of 

720 the GeoJSON object. 

721 

722 """ 

723 

724 super().__init__(*args, **kwargs) 

725 

726 

727 def _ipython_display_(self): 

728 bundle = { 

729 'application/geo+json': self.data, 

730 'text/plain': '<IPython.display.GeoJSON object>' 

731 } 

732 metadata = { 

733 'application/geo+json': self.metadata 

734 } 

735 display_functions.display(bundle, metadata=metadata, raw=True) 

736 

737class Javascript(TextDisplayObject): 

738 

739 def __init__(self, data=None, url=None, filename=None, lib=None, css=None): 

740 """Create a Javascript display object given raw data. 

741 

742 When this object is returned by an expression or passed to the 

743 display function, it will result in the data being displayed 

744 in the frontend. If the data is a URL, the data will first be 

745 downloaded and then displayed. 

746 

747 In the Notebook, the containing element will be available as `element`, 

748 and jQuery will be available. Content appended to `element` will be 

749 visible in the output area. 

750 

751 Parameters 

752 ---------- 

753 data : unicode, str or bytes 

754 The Javascript source code or a URL to download it from. 

755 url : unicode 

756 A URL to download the data from. 

757 filename : unicode 

758 Path to a local file to load the data from. 

759 lib : list or str 

760 A sequence of Javascript library URLs to load asynchronously before 

761 running the source code. The full URLs of the libraries should 

762 be given. A single Javascript library URL can also be given as a 

763 string. 

764 css : list or str 

765 A sequence of css files to load before running the source code. 

766 The full URLs of the css files should be given. A single css URL 

767 can also be given as a string. 

768 """ 

769 if isinstance(lib, str): 

770 lib = [lib] 

771 elif lib is None: 

772 lib = [] 

773 if isinstance(css, str): 

774 css = [css] 

775 elif css is None: 

776 css = [] 

777 if not isinstance(lib, (list,tuple)): 

778 raise TypeError('expected sequence, got: %r' % lib) 

779 if not isinstance(css, (list,tuple)): 

780 raise TypeError('expected sequence, got: %r' % css) 

781 self.lib = lib 

782 self.css = css 

783 super().__init__(data=data, url=url, filename=filename) 

784 

785 def _repr_javascript_(self): 

786 r = '' 

787 for c in self.css: 

788 r += _css_t % c 

789 for l in self.lib: 

790 r += _lib_t1 % l 

791 r += self.data 

792 r += _lib_t2*len(self.lib) 

793 return r 

794 

795 

796def _pngxy(data): 

797 """read the (width, height) from a PNG header""" 

798 ihdr = data.index(b'IHDR') 

799 # next 8 bytes are width/height 

800 import struct 

801 return struct.unpack('>ii', data[ihdr+4:ihdr+12]) 

802 

803 

804def _jpegxy(data): 

805 """read the (width, height) from a JPEG header""" 

806 # adapted from http://www.64lines.com/jpeg-width-height 

807 

808 import struct 

809 idx = 4 

810 while True: 

811 block_size = struct.unpack('>H', data[idx:idx+2])[0] 

812 idx = idx + block_size 

813 if data[idx:idx+2] == b'\xFF\xC0': 

814 # found Start of Frame 

815 iSOF = idx 

816 break 

817 else: 

818 # read another block 

819 idx += 2 

820 

821 h, w = struct.unpack('>HH', data[iSOF+5:iSOF+9]) 

822 return w, h 

823 

824 

825def _gifxy(data): 

826 """read the (width, height) from a GIF header""" 

827 import struct 

828 return struct.unpack('<HH', data[6:10]) 

829 

830 

831def _webpxy(data): 

832 """read the (width, height) from a WEBP header""" 

833 import struct 

834 if data[12:16] == b"VP8 ": 

835 width, height = struct.unpack("<HH", data[24:30]) 

836 width = width & 0x3FFF 

837 height = height & 0x3FFF 

838 return (width, height) 

839 elif data[12:16] == b"VP8L": 

840 size_info = struct.unpack("<I", data[21:25])[0] 

841 width = 1 + ((size_info & 0x3F) << 8) | (size_info >> 24) 

842 height = 1 + ( 

843 (((size_info >> 8) & 0xF) << 10) 

844 | (((size_info >> 14) & 0x3FC) << 2) 

845 | ((size_info >> 22) & 0x3) 

846 ) 

847 return (width, height) 

848 else: 

849 raise ValueError("Not a valid WEBP header") 

850 

851 

852@dataclass 

853class _ImageFormat: 

854 magics: tuple[bytes, ...] 

855 """Constants for identifying image data.""" 

856 

857 shape: Callable[[bytes], tuple[int, int]] 

858 """Reads (width, height) from image data.""" 

859 

860 

861class ImageFormat(_ImageFormat, Enum): 

862 png = (b"\x89PNG\r\n\x1a\n",), _pngxy 

863 jpeg = (b"\xff\xd8",), _jpegxy 

864 jpg = jpeg # alias, has `.name == "jpeg"` 

865 gif = (b"GIF87a", b"GIF89a"), _gifxy 

866 webp = (b"WEBP",), _webpxy 

867 

868 @property 

869 def mime_type(self): 

870 return f"image/{self.name}" 

871 

872 @classmethod 

873 def from_data(cls, data: bytes) -> Self | None: 

874 for fmt in cls: 

875 for magic in fmt.magics: 

876 if data.startswith(magic): 

877 return fmt 

878 return None 

879 

880 

881class Image(DisplayObject): 

882 

883 _read_flags = "rb" 

884 

885 def __init__( 

886 self, 

887 data=None, 

888 url=None, 

889 filename=None, 

890 format=None, 

891 embed=None, 

892 width=None, 

893 height=None, 

894 retina=False, 

895 unconfined=False, 

896 metadata=None, 

897 alt=None, 

898 ): 

899 """Create a PNG/JPEG/GIF/WEBP image object given raw data. 

900 

901 When this object is returned by an input cell or passed to the 

902 display function, it will result in the image being displayed 

903 in the frontend. 

904 

905 Parameters 

906 ---------- 

907 data : unicode, str or bytes 

908 The raw image data or a URL or filename to load the data from. 

909 This always results in embedded image data. 

910 

911 url : unicode 

912 A URL to download the data from. If you specify `url=`, 

913 the image data will not be embedded unless you also specify `embed=True`. 

914 

915 filename : unicode 

916 Path to a local file to load the data from. 

917 Images from a file are always embedded. 

918 

919 format : unicode 

920 The format of the image data (png/jpeg/jpg/gif/webp). If a filename or URL is given 

921 for format will be inferred from the filename extension. 

922 

923 embed : bool 

924 Should the image data be embedded using a data URI (True) or be 

925 loaded using an <img> tag. Set this to True if you want the image 

926 to be viewable later with no internet connection in the notebook. 

927 

928 Default is `True`, unless the keyword argument `url` is set, then 

929 default value is `False`. 

930 

931 Note that QtConsole is not able to display images if `embed` is set to `False` 

932 

933 width : int 

934 Width in pixels to which to constrain the image in html 

935 

936 height : int 

937 Height in pixels to which to constrain the image in html 

938 

939 retina : bool 

940 Automatically set the width and height to half of the measured 

941 width and height. 

942 This only works for embedded images because it reads the width/height 

943 from image data. 

944 For non-embedded images, you can just set the desired display width 

945 and height directly. 

946 

947 unconfined : bool 

948 Set unconfined=True to disable max-width confinement of the image. 

949 

950 metadata : dict 

951 Specify extra metadata to attach to the image. 

952 

953 alt : unicode 

954 Alternative text for the image, for use by screen readers. 

955 

956 Examples 

957 -------- 

958 embedded image data, works in qtconsole and notebook 

959 when passed positionally, the first arg can be any of raw image data, 

960 a URL, or a filename from which to load image data. 

961 The result is always embedding image data for inline images. 

962 

963 >>> Image('https://www.google.fr/images/srpr/logo3w.png') # doctest: +SKIP 

964 <IPython.core.display.Image object> 

965 

966 >>> Image('/path/to/image.jpg') 

967 <IPython.core.display.Image object> 

968 

969 >>> Image(b'RAW_PNG_DATA...') 

970 <IPython.core.display.Image object> 

971 

972 Specifying Image(url=...) does not embed the image data, 

973 it only generates ``<img>`` tag with a link to the source. 

974 This will not work in the qtconsole or offline. 

975 

976 >>> Image(url='https://www.google.fr/images/srpr/logo3w.png') 

977 <IPython.core.display.Image object> 

978 

979 """ 

980 if isinstance(data, (Path, PurePath)): 

981 data = str(data) 

982 

983 if filename is not None: 

984 ext = self._find_ext(filename) 

985 elif url is not None: 

986 ext = self._find_ext(url) 

987 elif data is None: 

988 raise ValueError("No image data found. Expecting filename, url, or data.") 

989 elif isinstance(data, str) and ( 

990 data.startswith('http') or _safe_exists(data) 

991 ): 

992 ext = self._find_ext(data) 

993 else: 

994 ext = None 

995 

996 if format is None: 

997 if ext is not None: 

998 format = ext.lower() 

999 elif isinstance(data, bytes) and ( 

1000 image_format := ImageFormat.from_data(data) 

1001 ): 

1002 format = image_format.name 

1003 else: # failed to detect format, default png 

1004 format = ImageFormat.png.name 

1005 else: 

1006 format = format.lower() 

1007 # normalize e.g. `jpg` -> `jpeg`, `UNKNOWN` → `unknown` 

1008 self.format = ( 

1009 ImageFormat[format].name if format in ImageFormat.__members__ else format 

1010 ) 

1011 

1012 self.embed = embed if embed is not None else (url is None) 

1013 if self.embed: 

1014 if self.format not in ImageFormat.__members__: 

1015 raise ValueError("Cannot embed the '%s' image format" % (self.format)) 

1016 self._mimetype = ImageFormat[self.format].mime_type 

1017 

1018 self.width = width 

1019 self.height = height 

1020 self.retina = retina 

1021 self.unconfined = unconfined 

1022 self.alt = alt 

1023 super().__init__(data=data, url=url, filename=filename, 

1024 metadata=metadata) 

1025 

1026 if self.width is None and self.metadata.get('width', {}): 

1027 self.width = metadata['width'] 

1028 

1029 if self.height is None and self.metadata.get('height', {}): 

1030 self.height = metadata['height'] 

1031 

1032 if self.alt is None and self.metadata.get("alt", {}): 

1033 self.alt = metadata["alt"] 

1034 

1035 if retina: 

1036 self._retina_shape() 

1037 

1038 

1039 def _retina_shape(self): 

1040 """load pixel-doubled width and height from image data""" 

1041 if not self.embed: 

1042 return 

1043 if self.format in ImageFormat.__members__: 

1044 w, h = ImageFormat[self.format].shape(self.data) 

1045 else: 

1046 return 

1047 self.width = w // 2 

1048 self.height = h // 2 

1049 

1050 def reload(self): 

1051 """Reload the raw data from file or URL.""" 

1052 if self.embed: 

1053 super().reload() 

1054 if self.retina: 

1055 self._retina_shape() 

1056 

1057 def _repr_html_(self): 

1058 if not self.embed: 

1059 import html 

1060 width = height = klass = alt = "" 

1061 if self.width: 

1062 width = ' width="%d"' % self.width 

1063 if self.height: 

1064 height = ' height="%d"' % self.height 

1065 if self.unconfined: 

1066 klass = ' class="unconfined"' 

1067 if self.alt: 

1068 alt = ' alt="%s"' % html.escape(self.alt) 

1069 return '<img src="{url}"{width}{height}{klass}{alt}/>'.format( 

1070 url=html.escape(self.url or ""), 

1071 width=width, 

1072 height=height, 

1073 klass=klass, 

1074 alt=alt, 

1075 ) 

1076 

1077 def _repr_mimebundle_(self, include=None, exclude=None): 

1078 """Return the image as a mimebundle 

1079 

1080 Any new mimetype support should be implemented here. 

1081 """ 

1082 if self.embed: 

1083 mimetype = self._mimetype 

1084 data, metadata = self._data_and_metadata(always_both=True) 

1085 if metadata: 

1086 metadata = {mimetype: metadata} 

1087 return {mimetype: data}, metadata 

1088 else: 

1089 return {'text/html': self._repr_html_()} 

1090 

1091 def _data_and_metadata(self, always_both=False): 

1092 """shortcut for returning metadata with shape information, if defined""" 

1093 try: 

1094 b64_data = b2a_base64(self.data, newline=False).decode("ascii") 

1095 except TypeError as e: 

1096 raise FileNotFoundError( 

1097 "No such file or directory: '%s'" % (self.data)) from e 

1098 md = {} 

1099 if self.metadata: 

1100 md.update(self.metadata) 

1101 if self.width: 

1102 md['width'] = self.width 

1103 if self.height: 

1104 md['height'] = self.height 

1105 if self.unconfined: 

1106 md['unconfined'] = self.unconfined 

1107 if self.alt: 

1108 md["alt"] = self.alt 

1109 if md or always_both: 

1110 return b64_data, md 

1111 else: 

1112 return b64_data 

1113 

1114 def _repr_png_(self): 

1115 if self.embed and self.format == ImageFormat.png.name: 

1116 return self._data_and_metadata() 

1117 

1118 def _repr_jpeg_(self): 

1119 if self.embed and self.format == ImageFormat.jpeg.name: 

1120 return self._data_and_metadata() 

1121 

1122 def _find_ext(self, s: str) -> str: 

1123 base, ext = splitext(s) 

1124 

1125 if not ext: 

1126 return base 

1127 

1128 # `splitext` includes leading period, so we skip it 

1129 return ext[1:].lower() 

1130 

1131 

1132class Video(DisplayObject): 

1133 

1134 def __init__(self, data=None, url=None, filename=None, embed=False, 

1135 mimetype=None, width=None, height=None, html_attributes="controls"): 

1136 """Create a video object given raw data or an URL. 

1137 

1138 When this object is returned by an input cell or passed to the 

1139 display function, it will result in the video being displayed 

1140 in the frontend. 

1141 

1142 Parameters 

1143 ---------- 

1144 data : unicode, str or bytes 

1145 The raw video data or a URL or filename to load the data from. 

1146 Raw data will require passing ``embed=True``. 

1147 

1148 url : unicode 

1149 A URL for the video. If you specify ``url=``, 

1150 the image data will not be embedded. 

1151 

1152 filename : unicode 

1153 Path to a local file containing the video. 

1154 Will be interpreted as a local URL unless ``embed=True``. 

1155 

1156 embed : bool 

1157 Should the video be embedded using a data URI (True) or be 

1158 loaded using a <video> tag (False). 

1159 

1160 Since videos are large, embedding them should be avoided, if possible. 

1161 You must confirm embedding as your intention by passing ``embed=True``. 

1162 

1163 Local files can be displayed with URLs without embedding the content, via:: 

1164 

1165 Video('./video.mp4') 

1166 

1167 mimetype : unicode 

1168 Specify the mimetype for embedded videos. 

1169 Default will be guessed from file extension, if available. 

1170 

1171 width : int 

1172 Width in pixels to which to constrain the video in HTML. 

1173 If not supplied, defaults to the width of the video. 

1174 

1175 height : int 

1176 Height in pixels to which to constrain the video in html. 

1177 If not supplied, defaults to the height of the video. 

1178 

1179 html_attributes : str 

1180 Attributes for the HTML ``<video>`` block. 

1181 Default: ``"controls"`` to get video controls. 

1182 Other examples: ``"controls muted"`` for muted video with controls, 

1183 ``"loop autoplay"`` for looping autoplaying video without controls. 

1184 

1185 Examples 

1186 -------- 

1187 :: 

1188 

1189 Video('https://archive.org/download/Sita_Sings_the_Blues/Sita_Sings_the_Blues_small.mp4') 

1190 Video('path/to/video.mp4') 

1191 Video('path/to/video.mp4', embed=True) 

1192 Video('path/to/video.mp4', embed=True, html_attributes="controls muted autoplay") 

1193 Video(b'raw-videodata', embed=True) 

1194 """ 

1195 if isinstance(data, (Path, PurePath)): 

1196 data = str(data) 

1197 

1198 if url is None and isinstance(data, str) and data.startswith(('http:', 'https:')): 

1199 url = data 

1200 data = None 

1201 elif data is not None and os.path.exists(data): 

1202 filename = data 

1203 data = None 

1204 

1205 if data and not embed: 

1206 msg = ''.join([ 

1207 "To embed videos, you must pass embed=True ", 

1208 "(this may make your notebook files huge)\n", 

1209 "Consider passing Video(url='...')", 

1210 ]) 

1211 raise ValueError(msg) 

1212 

1213 self.mimetype = mimetype 

1214 self.embed = embed 

1215 self.width = width 

1216 self.height = height 

1217 self.html_attributes = html_attributes 

1218 super().__init__(data=data, url=url, filename=filename) 

1219 

1220 def _repr_html_(self): 

1221 width = height = '' 

1222 if self.width: 

1223 width = ' width="%d"' % self.width 

1224 if self.height: 

1225 height = ' height="%d"' % self.height 

1226 

1227 # External URLs and potentially local files are not embedded into the 

1228 # notebook output. 

1229 if not self.embed: 

1230 import html 

1231 url = self.url if self.url is not None else self.filename 

1232 output = """<video src="{}" {} {} {}> 

1233 Your browser does not support the <code>video</code> element. 

1234 </video>""".format(html.escape(url or ""), self.html_attributes, width, height) 

1235 return output 

1236 

1237 # Embedded videos are base64-encoded. 

1238 mimetype = self.mimetype 

1239 if self.filename is not None: 

1240 if not mimetype: 

1241 import mimetypes 

1242 

1243 mimetype, _ = mimetypes.guess_type(self.filename) 

1244 

1245 with open(self.filename, 'rb') as f: 

1246 video = f.read() 

1247 else: 

1248 video = self.data 

1249 if isinstance(video, str): 

1250 # unicode input is already b64-encoded 

1251 b64_video = video 

1252 else: 

1253 b64_video = b2a_base64(video, newline=False).decode("ascii").rstrip() 

1254 

1255 output = """<video {0} {1} {2}> 

1256 <source src="data:{3};base64,{4}" type="{3}"> 

1257 Your browser does not support the video tag. 

1258 </video>""".format(self.html_attributes, width, height, mimetype, b64_video) 

1259 return output 

1260 

1261 def reload(self): 

1262 # TODO 

1263 pass