##// END OF EJS Templates
Fix style and typo
Pablo de Oliveira -
Show More
@@ -1,708 +1,708 b''
1 1 # -*- coding: utf-8 -*-
2 2 """Top-level display functions for displaying object in different formats.
3 3
4 4 Authors:
5 5
6 6 * Brian Granger
7 7 """
8 8
9 9 #-----------------------------------------------------------------------------
10 10 # Copyright (C) 2013 The IPython Development Team
11 11 #
12 12 # Distributed under the terms of the BSD License. The full license is in
13 13 # the file COPYING, distributed as part of this software.
14 14 #-----------------------------------------------------------------------------
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Imports
18 18 #-----------------------------------------------------------------------------
19 19
20 20 from __future__ import print_function
21 21
22 22 import os
23 23 import struct
24 24
25 25 from IPython.utils.py3compat import (string_types, cast_bytes_py2, cast_unicode,
26 26 unicode_type)
27 27
28 28 from .displaypub import publish_display_data
29 29
30 30 #-----------------------------------------------------------------------------
31 31 # utility functions
32 32 #-----------------------------------------------------------------------------
33 33
34 34 def _safe_exists(path):
35 35 """Check path, but don't let exceptions raise"""
36 36 try:
37 37 return os.path.exists(path)
38 38 except Exception:
39 39 return False
40 40
41 41 def _merge(d1, d2):
42 42 """Like update, but merges sub-dicts instead of clobbering at the top level.
43 43
44 44 Updates d1 in-place
45 45 """
46 46
47 47 if not isinstance(d2, dict) or not isinstance(d1, dict):
48 48 return d2
49 49 for key, value in d2.items():
50 50 d1[key] = _merge(d1.get(key), value)
51 51 return d1
52 52
53 53 def _display_mimetype(mimetype, objs, raw=False, metadata=None):
54 54 """internal implementation of all display_foo methods
55 55
56 56 Parameters
57 57 ----------
58 58 mimetype : str
59 59 The mimetype to be published (e.g. 'image/png')
60 60 objs : tuple of objects
61 61 The Python objects to display, or if raw=True raw text data to
62 62 display.
63 63 raw : bool
64 64 Are the data objects raw data or Python objects that need to be
65 65 formatted before display? [default: False]
66 66 metadata : dict (optional)
67 67 Metadata to be associated with the specific mimetype output.
68 68 """
69 69 if metadata:
70 70 metadata = {mimetype: metadata}
71 71 if raw:
72 72 # turn list of pngdata into list of { 'image/png': pngdata }
73 73 objs = [ {mimetype: obj} for obj in objs ]
74 74 display(*objs, raw=raw, metadata=metadata, include=[mimetype])
75 75
76 76 #-----------------------------------------------------------------------------
77 77 # Main functions
78 78 #-----------------------------------------------------------------------------
79 79
80 80 def display(*objs, **kwargs):
81 81 """Display a Python object in all frontends.
82 82
83 83 By default all representations will be computed and sent to the frontends.
84 84 Frontends can decide which representation is used and how.
85 85
86 86 Parameters
87 87 ----------
88 88 objs : tuple of objects
89 89 The Python objects to display.
90 90 raw : bool, optional
91 91 Are the objects to be displayed already mimetype-keyed dicts of raw display data,
92 92 or Python objects that need to be formatted before display? [default: False]
93 93 include : list or tuple, optional
94 94 A list of format type strings (MIME types) to include in the
95 95 format data dict. If this is set *only* the format types included
96 96 in this list will be computed.
97 97 exclude : list or tuple, optional
98 98 A list of format type strings (MIME types) to exclude in the format
99 99 data dict. If this is set all format types will be computed,
100 100 except for those included in this argument.
101 101 metadata : dict, optional
102 102 A dictionary of metadata to associate with the output.
103 103 mime-type keys in this dictionary will be associated with the individual
104 104 representation formats, if they exist.
105 105 """
106 106 raw = kwargs.get('raw', False)
107 107 include = kwargs.get('include')
108 108 exclude = kwargs.get('exclude')
109 109 metadata = kwargs.get('metadata')
110 110
111 111 from IPython.core.interactiveshell import InteractiveShell
112 112
113 113 if raw:
114 114 for obj in objs:
115 115 publish_display_data('display', obj, metadata)
116 116 else:
117 117 format = InteractiveShell.instance().display_formatter.format
118 118 for obj in objs:
119 119 format_dict, md_dict = format(obj, include=include, exclude=exclude)
120 120 if metadata:
121 121 # kwarg-specified metadata gets precedence
122 122 _merge(md_dict, metadata)
123 123 publish_display_data('display', format_dict, md_dict)
124 124
125 125
126 126 def display_pretty(*objs, **kwargs):
127 127 """Display the pretty (default) representation of an object.
128 128
129 129 Parameters
130 130 ----------
131 131 objs : tuple of objects
132 132 The Python objects to display, or if raw=True raw text data to
133 133 display.
134 134 raw : bool
135 135 Are the data objects raw data or Python objects that need to be
136 136 formatted before display? [default: False]
137 137 metadata : dict (optional)
138 138 Metadata to be associated with the specific mimetype output.
139 139 """
140 140 _display_mimetype('text/plain', objs, **kwargs)
141 141
142 142
143 143 def display_html(*objs, **kwargs):
144 144 """Display the HTML representation of an object.
145 145
146 146 Parameters
147 147 ----------
148 148 objs : tuple of objects
149 149 The Python objects to display, or if raw=True raw HTML data to
150 150 display.
151 151 raw : bool
152 152 Are the data objects raw data or Python objects that need to be
153 153 formatted before display? [default: False]
154 154 metadata : dict (optional)
155 155 Metadata to be associated with the specific mimetype output.
156 156 """
157 157 _display_mimetype('text/html', objs, **kwargs)
158 158
159 159
160 160 def display_svg(*objs, **kwargs):
161 161 """Display the SVG representation of an object.
162 162
163 163 Parameters
164 164 ----------
165 165 objs : tuple of objects
166 166 The Python objects to display, or if raw=True raw svg data to
167 167 display.
168 168 raw : bool
169 169 Are the data objects raw data or Python objects that need to be
170 170 formatted before display? [default: False]
171 171 metadata : dict (optional)
172 172 Metadata to be associated with the specific mimetype output.
173 173 """
174 174 _display_mimetype('image/svg+xml', objs, **kwargs)
175 175
176 176
177 177 def display_png(*objs, **kwargs):
178 178 """Display the PNG representation of an object.
179 179
180 180 Parameters
181 181 ----------
182 182 objs : tuple of objects
183 183 The Python objects to display, or if raw=True raw png data to
184 184 display.
185 185 raw : bool
186 186 Are the data objects raw data or Python objects that need to be
187 187 formatted before display? [default: False]
188 188 metadata : dict (optional)
189 189 Metadata to be associated with the specific mimetype output.
190 190 """
191 191 _display_mimetype('image/png', objs, **kwargs)
192 192
193 193
194 194 def display_jpeg(*objs, **kwargs):
195 195 """Display the JPEG representation of an object.
196 196
197 197 Parameters
198 198 ----------
199 199 objs : tuple of objects
200 200 The Python objects to display, or if raw=True raw JPEG data to
201 201 display.
202 202 raw : bool
203 203 Are the data objects raw data or Python objects that need to be
204 204 formatted before display? [default: False]
205 205 metadata : dict (optional)
206 206 Metadata to be associated with the specific mimetype output.
207 207 """
208 208 _display_mimetype('image/jpeg', objs, **kwargs)
209 209
210 210
211 211 def display_latex(*objs, **kwargs):
212 212 """Display the LaTeX representation of an object.
213 213
214 214 Parameters
215 215 ----------
216 216 objs : tuple of objects
217 217 The Python objects to display, or if raw=True raw latex data to
218 218 display.
219 219 raw : bool
220 220 Are the data objects raw data or Python objects that need to be
221 221 formatted before display? [default: False]
222 222 metadata : dict (optional)
223 223 Metadata to be associated with the specific mimetype output.
224 224 """
225 225 _display_mimetype('text/latex', objs, **kwargs)
226 226
227 227
228 228 def display_json(*objs, **kwargs):
229 229 """Display the JSON representation of an object.
230 230
231 231 Note that not many frontends support displaying JSON.
232 232
233 233 Parameters
234 234 ----------
235 235 objs : tuple of objects
236 236 The Python objects to display, or if raw=True raw json data to
237 237 display.
238 238 raw : bool
239 239 Are the data objects raw data or Python objects that need to be
240 240 formatted before display? [default: False]
241 241 metadata : dict (optional)
242 242 Metadata to be associated with the specific mimetype output.
243 243 """
244 244 _display_mimetype('application/json', objs, **kwargs)
245 245
246 246
247 247 def display_javascript(*objs, **kwargs):
248 248 """Display the Javascript representation of an object.
249 249
250 250 Parameters
251 251 ----------
252 252 objs : tuple of objects
253 253 The Python objects to display, or if raw=True raw javascript data to
254 254 display.
255 255 raw : bool
256 256 Are the data objects raw data or Python objects that need to be
257 257 formatted before display? [default: False]
258 258 metadata : dict (optional)
259 259 Metadata to be associated with the specific mimetype output.
260 260 """
261 261 _display_mimetype('application/javascript', objs, **kwargs)
262 262
263 263 #-----------------------------------------------------------------------------
264 264 # Smart classes
265 265 #-----------------------------------------------------------------------------
266 266
267 267
268 268 class DisplayObject(object):
269 269 """An object that wraps data to be displayed."""
270 270
271 271 _read_flags = 'r'
272 272
273 273 def __init__(self, data=None, url=None, filename=None):
274 274 """Create a display object given raw data.
275 275
276 276 When this object is returned by an expression or passed to the
277 277 display function, it will result in the data being displayed
278 278 in the frontend. The MIME type of the data should match the
279 279 subclasses used, so the Png subclass should be used for 'image/png'
280 280 data. If the data is a URL, the data will first be downloaded
281 281 and then displayed. If
282 282
283 283 Parameters
284 284 ----------
285 285 data : unicode, str or bytes
286 286 The raw data or a URL or file to load the data from
287 287 url : unicode
288 288 A URL to download the data from.
289 289 filename : unicode
290 290 Path to a local file to load the data from.
291 291 """
292 292 if data is not None and isinstance(data, string_types):
293 293 if data.startswith('http') and url is None:
294 294 url = data
295 295 filename = None
296 296 data = None
297 297 elif _safe_exists(data) and filename is None:
298 298 url = None
299 299 filename = data
300 300 data = None
301 301
302 302 self.data = data
303 303 self.url = url
304 304 self.filename = None if filename is None else unicode_type(filename)
305 305
306 306 self.reload()
307 307
308 308 def reload(self):
309 309 """Reload the raw data from file or URL."""
310 310 if self.filename is not None:
311 311 with open(self.filename, self._read_flags) as f:
312 312 self.data = f.read()
313 313 elif self.url is not None:
314 314 try:
315 315 import urllib2
316 316 response = urllib2.urlopen(self.url)
317 317 self.data = response.read()
318 318 # extract encoding from header, if there is one:
319 319 encoding = None
320 320 for sub in response.headers['content-type'].split(';'):
321 321 sub = sub.strip()
322 322 if sub.startswith('charset'):
323 323 encoding = sub.split('=')[-1].strip()
324 324 break
325 325 # decode data, if an encoding was specified
326 326 if encoding:
327 327 self.data = self.data.decode(encoding, 'replace')
328 328 except:
329 329 self.data = None
330 330
331 331 class Pretty(DisplayObject):
332 332
333 333 def _repr_pretty_(self):
334 334 return self.data
335 335
336 336
337 337 class HTML(DisplayObject):
338 338
339 339 def _repr_html_(self):
340 340 return self.data
341 341
342 342 def __html__(self):
343 343 """
344 344 This method exists to inform other HTML-using modules (e.g. Markupsafe,
345 345 htmltag, etc) that this object is HTML and does not need things like
346 346 special characters (<>&) escaped.
347 347 """
348 348 return self._repr_html_()
349 349
350 350
351 351 class Math(DisplayObject):
352 352
353 353 def _repr_latex_(self):
354 354 s = self.data.strip('$')
355 355 return "$$%s$$" % s
356 356
357 357
358 358 class Latex(DisplayObject):
359 359
360 360 def _repr_latex_(self):
361 361 return self.data
362 362
363 363
364 364 class SVG(DisplayObject):
365 365
366 366 def __init__(self, data=None, url=None, filename=None, scoped=False):
367 367 """Create a SVG display object given raw data.
368 368
369 369 When this object is returned by an expression or passed to the
370 370 display function, it will result in the data being displayed
371 371 in the frontend. If the data is a URL, the data will first be
372 372 downloaded and then displayed.
373 373
374 374 Parameters
375 375 ----------
376 376 data : unicode, str or bytes
377 377 The Javascript source code or a URL to download it from.
378 378 url : unicode
379 379 A URL to download the data from.
380 380 filename : unicode
381 381 Path to a local file to load the data from.
382 382 scoped : bool
383 383 Should the SVG declarations be scoped.
384 384 """
385 385 if not isinstance(scoped, (bool)):
386 386 raise TypeError('expected bool, got: %r' % scoped)
387 387 self.scoped = scoped
388 388 super(SVG, self).__init__(data=data, url=url, filename=filename)
389 389
390 390 # wrap data in a property, which extracts the <svg> tag, discarding
391 391 # document headers
392 392 _data = None
393 393
394 394 @property
395 395 def data(self):
396 396 return self._data
397 397
398 398 _scoped_class = "ipython-scoped"
399 399
400 400 @data.setter
401 401 def data(self, svg):
402 402 if svg is None:
403 403 self._data = None
404 404 return
405 405 # parse into dom object
406 406 from xml.dom import minidom
407 407 svg = cast_bytes_py2(svg)
408 408 x = minidom.parseString(svg)
409 409 # get svg tag (should be 1)
410 410 found_svg = x.getElementsByTagName('svg')
411 411 if found_svg:
412 # If the user request scoping, tag the svg with the
412 # If the user requests scoping, tag the svg with the
413 413 # ipython-scoped class
414 414 if self.scoped:
415 415 classes = (found_svg[0].getAttribute('class') +
416 416 " " + self._scoped_class)
417 417 found_svg[0].setAttribute('class', classes)
418 418 svg = found_svg[0].toxml()
419 419 else:
420 420 # fallback on the input, trust the user
421 421 # but this is probably an error.
422 422 pass
423 423 svg = cast_unicode(svg)
424 424 self._data = svg
425 425
426 426 def _repr_svg_(self):
427 427 return self.data
428 428
429 429
430 430 class JSON(DisplayObject):
431 431
432 432 def _repr_json_(self):
433 433 return self.data
434 434
435 435 css_t = """$("head").append($("<link/>").attr({
436 436 rel: "stylesheet",
437 437 type: "text/css",
438 438 href: "%s"
439 439 }));
440 440 """
441 441
442 442 lib_t1 = """$.getScript("%s", function () {
443 443 """
444 444 lib_t2 = """});
445 445 """
446 446
447 447 class Javascript(DisplayObject):
448 448
449 449 def __init__(self, data=None, url=None, filename=None, lib=None, css=None):
450 450 """Create a Javascript display object given raw data.
451 451
452 452 When this object is returned by an expression or passed to the
453 453 display function, it will result in the data being displayed
454 454 in the frontend. If the data is a URL, the data will first be
455 455 downloaded and then displayed.
456 456
457 457 In the Notebook, the containing element will be available as `element`,
458 458 and jQuery will be available. The output area starts hidden, so if
459 459 the js appends content to `element` that should be visible, then
460 460 it must call `container.show()` to unhide the area.
461 461
462 462 Parameters
463 463 ----------
464 464 data : unicode, str or bytes
465 465 The Javascript source code or a URL to download it from.
466 466 url : unicode
467 467 A URL to download the data from.
468 468 filename : unicode
469 469 Path to a local file to load the data from.
470 470 lib : list or str
471 471 A sequence of Javascript library URLs to load asynchronously before
472 472 running the source code. The full URLs of the libraries should
473 473 be given. A single Javascript library URL can also be given as a
474 474 string.
475 475 css: : list or str
476 476 A sequence of css files to load before running the source code.
477 477 The full URLs of the css files should be given. A single css URL
478 478 can also be given as a string.
479 479 """
480 480 if isinstance(lib, string_types):
481 481 lib = [lib]
482 482 elif lib is None:
483 483 lib = []
484 484 if isinstance(css, string_types):
485 485 css = [css]
486 486 elif css is None:
487 487 css = []
488 488 if not isinstance(lib, (list,tuple)):
489 489 raise TypeError('expected sequence, got: %r' % lib)
490 490 if not isinstance(css, (list,tuple)):
491 491 raise TypeError('expected sequence, got: %r' % css)
492 492 self.lib = lib
493 493 self.css = css
494 494 super(Javascript, self).__init__(data=data, url=url, filename=filename)
495 495
496 496 def _repr_javascript_(self):
497 497 r = ''
498 498 for c in self.css:
499 499 r += css_t % c
500 500 for l in self.lib:
501 501 r += lib_t1 % l
502 502 r += self.data
503 503 r += lib_t2*len(self.lib)
504 504 return r
505 505
506 506 # constants for identifying png/jpeg data
507 507 _PNG = b'\x89PNG\r\n\x1a\n'
508 508 _JPEG = b'\xff\xd8'
509 509
510 510 def _pngxy(data):
511 511 """read the (width, height) from a PNG header"""
512 512 ihdr = data.index(b'IHDR')
513 513 # next 8 bytes are width/height
514 514 w4h4 = data[ihdr+4:ihdr+12]
515 515 return struct.unpack('>ii', w4h4)
516 516
517 517 def _jpegxy(data):
518 518 """read the (width, height) from a JPEG header"""
519 519 # adapted from http://www.64lines.com/jpeg-width-height
520 520
521 521 idx = 4
522 522 while True:
523 523 block_size = struct.unpack('>H', data[idx:idx+2])[0]
524 524 idx = idx + block_size
525 525 if data[idx:idx+2] == b'\xFF\xC0':
526 526 # found Start of Frame
527 527 iSOF = idx
528 528 break
529 529 else:
530 530 # read another block
531 531 idx += 2
532 532
533 533 h, w = struct.unpack('>HH', data[iSOF+5:iSOF+9])
534 534 return w, h
535 535
536 536 class Image(DisplayObject):
537 537
538 538 _read_flags = 'rb'
539 539 _FMT_JPEG = u'jpeg'
540 540 _FMT_PNG = u'png'
541 541 _ACCEPTABLE_EMBEDDINGS = [_FMT_JPEG, _FMT_PNG]
542 542
543 543 def __init__(self, data=None, url=None, filename=None, format=u'png', embed=None, width=None, height=None, retina=False):
544 544 """Create a PNG/JPEG image object given raw data.
545 545
546 546 When this object is returned by an input cell or passed to the
547 547 display function, it will result in the image being displayed
548 548 in the frontend.
549 549
550 550 Parameters
551 551 ----------
552 552 data : unicode, str or bytes
553 553 The raw image data or a URL or filename to load the data from.
554 554 This always results in embedded image data.
555 555 url : unicode
556 556 A URL to download the data from. If you specify `url=`,
557 557 the image data will not be embedded unless you also specify `embed=True`.
558 558 filename : unicode
559 559 Path to a local file to load the data from.
560 560 Images from a file are always embedded.
561 561 format : unicode
562 562 The format of the image data (png/jpeg/jpg). If a filename or URL is given
563 563 for format will be inferred from the filename extension.
564 564 embed : bool
565 565 Should the image data be embedded using a data URI (True) or be
566 566 loaded using an <img> tag. Set this to True if you want the image
567 567 to be viewable later with no internet connection in the notebook.
568 568
569 569 Default is `True`, unless the keyword argument `url` is set, then
570 570 default value is `False`.
571 571
572 572 Note that QtConsole is not able to display images if `embed` is set to `False`
573 573 width : int
574 574 Width to which to constrain the image in html
575 575 height : int
576 576 Height to which to constrain the image in html
577 577 retina : bool
578 578 Automatically set the width and height to half of the measured
579 579 width and height.
580 580 This only works for embedded images because it reads the width/height
581 581 from image data.
582 582 For non-embedded images, you can just set the desired display width
583 583 and height directly.
584 584
585 585 Examples
586 586 --------
587 587 # embedded image data, works in qtconsole and notebook
588 588 # when passed positionally, the first arg can be any of raw image data,
589 589 # a URL, or a filename from which to load image data.
590 590 # The result is always embedding image data for inline images.
591 591 Image('http://www.google.fr/images/srpr/logo3w.png')
592 592 Image('/path/to/image.jpg')
593 593 Image(b'RAW_PNG_DATA...')
594 594
595 595 # Specifying Image(url=...) does not embed the image data,
596 596 # it only generates `<img>` tag with a link to the source.
597 597 # This will not work in the qtconsole or offline.
598 598 Image(url='http://www.google.fr/images/srpr/logo3w.png')
599 599
600 600 """
601 601 if filename is not None:
602 602 ext = self._find_ext(filename)
603 603 elif url is not None:
604 604 ext = self._find_ext(url)
605 605 elif data is None:
606 606 raise ValueError("No image data found. Expecting filename, url, or data.")
607 607 elif isinstance(data, string_types) and (
608 608 data.startswith('http') or _safe_exists(data)
609 609 ):
610 610 ext = self._find_ext(data)
611 611 else:
612 612 ext = None
613 613
614 614 if ext is not None:
615 615 format = ext.lower()
616 616 if ext == u'jpg' or ext == u'jpeg':
617 617 format = self._FMT_JPEG
618 618 if ext == u'png':
619 619 format = self._FMT_PNG
620 620 elif isinstance(data, bytes) and format == 'png':
621 621 # infer image type from image data header,
622 622 # only if format might not have been specified.
623 623 if data[:2] == _JPEG:
624 624 format = 'jpeg'
625 625
626 626 self.format = unicode_type(format).lower()
627 627 self.embed = embed if embed is not None else (url is None)
628 628
629 629 if self.embed and self.format not in self._ACCEPTABLE_EMBEDDINGS:
630 630 raise ValueError("Cannot embed the '%s' image format" % (self.format))
631 631 self.width = width
632 632 self.height = height
633 633 self.retina = retina
634 634 super(Image, self).__init__(data=data, url=url, filename=filename)
635 635
636 636 if retina:
637 637 self._retina_shape()
638 638
639 639 def _retina_shape(self):
640 640 """load pixel-doubled width and height from image data"""
641 641 if not self.embed:
642 642 return
643 643 if self.format == 'png':
644 644 w, h = _pngxy(self.data)
645 645 elif self.format == 'jpeg':
646 646 w, h = _jpegxy(self.data)
647 647 else:
648 648 # retina only supports png
649 649 return
650 650 self.width = w // 2
651 651 self.height = h // 2
652 652
653 653 def reload(self):
654 654 """Reload the raw data from file or URL."""
655 655 if self.embed:
656 656 super(Image,self).reload()
657 657 if self.retina:
658 658 self._retina_shape()
659 659
660 660 def _repr_html_(self):
661 661 if not self.embed:
662 662 width = height = ''
663 663 if self.width:
664 664 width = ' width="%d"' % self.width
665 665 if self.height:
666 666 height = ' height="%d"' % self.height
667 667 return u'<img src="%s"%s%s/>' % (self.url, width, height)
668 668
669 669 def _data_and_metadata(self):
670 670 """shortcut for returning metadata with shape information, if defined"""
671 671 md = {}
672 672 if self.width:
673 673 md['width'] = self.width
674 674 if self.height:
675 675 md['height'] = self.height
676 676 if md:
677 677 return self.data, md
678 678 else:
679 679 return self.data
680 680
681 681 def _repr_png_(self):
682 682 if self.embed and self.format == u'png':
683 683 return self._data_and_metadata()
684 684
685 685 def _repr_jpeg_(self):
686 686 if self.embed and (self.format == u'jpeg' or self.format == u'jpg'):
687 687 return self._data_and_metadata()
688 688
689 689 def _find_ext(self, s):
690 690 return unicode_type(s.split('.')[-1].lower())
691 691
692 692
693 693 def clear_output(wait=False):
694 694 """Clear the output of the current cell receiving output.
695 695
696 696 Parameters
697 697 ----------
698 698 wait : bool [default: false]
699 699 Wait to clear the output until new output is available to replace it."""
700 700 from IPython.core.interactiveshell import InteractiveShell
701 701 if InteractiveShell.initialized():
702 702 InteractiveShell.instance().display_pub.clear_output(wait)
703 703 else:
704 704 from IPython.utils import io
705 705 print('\033[2K\r', file=io.stdout, end='')
706 706 io.stdout.flush()
707 707 print('\033[2K\r', file=io.stderr, end='')
708 708 io.stderr.flush()
1 NO CONTENT: modified file
General Comments 0
You need to be logged in to leave comments. Login now