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

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

478 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 if self.total else 0 

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 # lossy: 14 bits each, after the frame tag and the start code 

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

837 width = width & 0x3FFF 

838 height = height & 0x3FFF 

839 return (width, height) 

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

841 # lossless: 14 bits each of (width - 1, height - 1), after the signature 

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

843 width = 1 + (size_info & 0x3FFF) 

844 height = 1 + ((size_info >> 14) & 0x3FFF) 

845 return (width, height) 

846 elif data[12:16] == b"VP8X": 

847 # extended: 24 bits each of (canvas width - 1, canvas height - 1) 

848 width = 1 + int.from_bytes(data[24:27], "little") 

849 height = 1 + int.from_bytes(data[27:30], "little") 

850 return (width, height) 

851 else: 

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

853 

854 

855@dataclass 

856class _ImageFormat: 

857 magics: tuple[bytes, ...] 

858 """Constants for identifying image data.""" 

859 

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

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

862 

863 

864class ImageFormat(_ImageFormat, Enum): 

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

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

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

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

869 webp = (b"RIFF",), _webpxy 

870 

871 @property 

872 def mime_type(self): 

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

874 

875 @classmethod 

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

877 for fmt in cls: 

878 for magic in fmt.magics: 

879 if data.startswith(magic): 

880 # RIFF is a generic container (WAV, AVI, ...), WEBP files 

881 # are identified by the form type after the size field. 

882 if fmt is cls.webp and data[8:12] != b"WEBP": 

883 continue 

884 return fmt 

885 return None 

886 

887 

888class Image(DisplayObject): 

889 

890 _read_flags = "rb" 

891 

892 def __init__( 

893 self, 

894 data=None, 

895 url=None, 

896 filename=None, 

897 format=None, 

898 embed=None, 

899 width=None, 

900 height=None, 

901 retina=False, 

902 unconfined=False, 

903 metadata=None, 

904 alt=None, 

905 ): 

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

907 

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

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

910 in the frontend. 

911 

912 Parameters 

913 ---------- 

914 data : unicode, str or bytes 

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

916 This always results in embedded image data. 

917 

918 url : unicode 

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

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

921 

922 filename : unicode 

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

924 Images from a file are embedded unless you specify `embed=False`, 

925 in which case the path is used as the image source. 

926 

927 format : unicode 

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

929 for format will be inferred from the filename extension. 

930 

931 embed : bool 

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

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

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

935 

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

937 default value is `False`. 

938 

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

940 

941 width : int 

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

943 

944 height : int 

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

946 

947 retina : bool 

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

949 width and height. 

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

951 from image data. 

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

953 and height directly. 

954 

955 unconfined : bool 

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

957 

958 metadata : dict 

959 Specify extra metadata to attach to the image. 

960 

961 alt : unicode 

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

963 

964 Examples 

965 -------- 

966 embedded image data, works in qtconsole and notebook 

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

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

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

970 

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

972 <IPython.core.display.Image object> 

973 

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

975 <IPython.core.display.Image object> 

976 

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

978 <IPython.core.display.Image object> 

979 

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

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

982 This will not work in the qtconsole or offline. 

983 

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

985 <IPython.core.display.Image object> 

986 

987 """ 

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

989 data = str(data) 

990 

991 if filename is not None: 

992 ext = self._find_ext(filename) 

993 elif url is not None: 

994 ext = self._find_ext(url) 

995 elif data is None: 

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

997 elif isinstance(data, str) and ( 

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

999 ): 

1000 ext = self._find_ext(data) 

1001 else: 

1002 ext = None 

1003 

1004 if format is None: 

1005 if ext is not None: 

1006 format = ext.lower() 

1007 elif isinstance(data, bytes) and ( 

1008 image_format := ImageFormat.from_data(data) 

1009 ): 

1010 format = image_format.name 

1011 else: # failed to detect format, default png 

1012 format = ImageFormat.png.name 

1013 else: 

1014 format = format.lower() 

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

1016 self.format = ( 

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

1018 ) 

1019 

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

1021 if self.embed: 

1022 if self.format not in ImageFormat.__members__: 

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

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

1025 

1026 self.width = width 

1027 self.height = height 

1028 self.retina = retina 

1029 self.unconfined = unconfined 

1030 self.alt = alt 

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

1032 metadata=metadata) 

1033 

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

1035 self.width = metadata['width'] 

1036 

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

1038 self.height = metadata['height'] 

1039 

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

1041 self.alt = metadata["alt"] 

1042 

1043 if retina: 

1044 self._retina_shape() 

1045 

1046 

1047 def _retina_shape(self): 

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

1049 if not self.embed: 

1050 return 

1051 if self.format in ImageFormat.__members__: 

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

1053 else: 

1054 return 

1055 self.width = w // 2 

1056 self.height = h // 2 

1057 

1058 def reload(self): 

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

1060 if self.embed: 

1061 super().reload() 

1062 if self.retina: 

1063 self._retina_shape() 

1064 

1065 def _repr_html_(self): 

1066 if not self.embed: 

1067 import html 

1068 width = height = klass = alt = "" 

1069 if self.width: 

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

1071 if self.height: 

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

1073 if self.unconfined: 

1074 klass = ' class="unconfined"' 

1075 if self.alt: 

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

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

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

1079 url=html.escape(url or ""), 

1080 width=width, 

1081 height=height, 

1082 klass=klass, 

1083 alt=alt, 

1084 ) 

1085 

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

1087 """Return the image as a mimebundle 

1088 

1089 Any new mimetype support should be implemented here. 

1090 """ 

1091 if self.embed: 

1092 mimetype = self._mimetype 

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

1094 if metadata: 

1095 metadata = {mimetype: metadata} 

1096 return {mimetype: data}, metadata 

1097 else: 

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

1099 

1100 def _data_and_metadata(self, always_both=False): 

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

1102 try: 

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

1104 except TypeError as e: 

1105 raise FileNotFoundError( 

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

1107 md = {} 

1108 if self.metadata: 

1109 md.update(self.metadata) 

1110 if self.width: 

1111 md['width'] = self.width 

1112 if self.height: 

1113 md['height'] = self.height 

1114 if self.unconfined: 

1115 md['unconfined'] = self.unconfined 

1116 if self.alt: 

1117 md["alt"] = self.alt 

1118 if md or always_both: 

1119 return b64_data, md 

1120 else: 

1121 return b64_data 

1122 

1123 def _repr_png_(self): 

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

1125 return self._data_and_metadata() 

1126 

1127 def _repr_jpeg_(self): 

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

1129 return self._data_and_metadata() 

1130 

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

1132 base, ext = splitext(s) 

1133 

1134 if not ext: 

1135 return base 

1136 

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

1138 return ext[1:].lower() 

1139 

1140 

1141class Video(DisplayObject): 

1142 

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

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

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

1146 

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

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

1149 in the frontend. 

1150 

1151 Parameters 

1152 ---------- 

1153 data : unicode, str or bytes 

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

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

1156 

1157 url : unicode 

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

1159 the image data will not be embedded. 

1160 

1161 filename : unicode 

1162 Path to a local file containing the video. 

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

1164 

1165 embed : bool 

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

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

1168 

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

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

1171 

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

1173 

1174 Video('./video.mp4') 

1175 

1176 mimetype : unicode 

1177 Specify the mimetype for embedded videos. 

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

1179 

1180 width : int 

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

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

1183 

1184 height : int 

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

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

1187 

1188 html_attributes : str 

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

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

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

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

1193 

1194 Examples 

1195 -------- 

1196 :: 

1197 

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

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

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

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

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

1203 """ 

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

1205 data = str(data) 

1206 

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

1208 url = data 

1209 data = None 

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

1211 filename = data 

1212 data = None 

1213 

1214 if data and not embed: 

1215 msg = ''.join([ 

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

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

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

1219 ]) 

1220 raise ValueError(msg) 

1221 

1222 self.mimetype = mimetype 

1223 self.embed = embed 

1224 self.width = width 

1225 self.height = height 

1226 self.html_attributes = html_attributes 

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

1228 

1229 def _repr_html_(self): 

1230 width = height = '' 

1231 if self.width: 

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

1233 if self.height: 

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

1235 

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

1237 # notebook output. 

1238 if not self.embed: 

1239 import html 

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

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

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

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

1244 return output 

1245 

1246 # Embedded videos are base64-encoded. 

1247 mimetype = self.mimetype 

1248 if self.filename is not None: 

1249 if not mimetype: 

1250 import mimetypes 

1251 

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

1253 

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

1255 video = f.read() 

1256 else: 

1257 video = self.data 

1258 if isinstance(video, str): 

1259 # unicode input is already b64-encoded 

1260 b64_video = video 

1261 else: 

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

1263 

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

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

1266 Your browser does not support the video tag. 

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

1268 return output 

1269 

1270 def reload(self): 

1271 # TODO 

1272 pass