HtmlDump.pm 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897
  1. #------------------------------------------------------------------------------
  2. # File: HtmlDump.pm
  3. #
  4. # Description: Dump information in hex to HTML page
  5. #
  6. # Revisions: 12/05/2005 - P. Harvey Created
  7. #------------------------------------------------------------------------------
  8. package Image::ExifTool::HtmlDump;
  9. use strict;
  10. use vars qw($VERSION);
  11. use Image::ExifTool; # only for FinishTiffDump()
  12. use Image::ExifTool::HTML qw(EscapeHTML);
  13. $VERSION = '1.33';
  14. sub DumpTable($$$;$$$$$);
  15. sub Open($$$;@);
  16. sub Write($@);
  17. my ($bkgStart, $bkgEnd, @bkgSpan);
  18. my $htmlHeader1 = <<_END_PART_1_;
  19. <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"
  20. "http://www.w3.org/TR/1998/REC-html40-19980424/loose.dtd">
  21. <html>
  22. <head>
  23. <title>
  24. _END_PART_1_
  25. # Note: Don't change font-weight style because it can affect line height
  26. my $htmlHeader2 = <<_END_PART_2_;
  27. </title>
  28. <style type="text/css">
  29. <!--
  30. /* character style ID's */
  31. .D { color: #000000 } /* default color */
  32. .V { color: #ff0000 } /* duplicate block 1 */
  33. .W { color: #004400 } /* normal block 1 */
  34. .X { color: #ff4488 } /* duplicate block 2 */
  35. .Y { color: #448844 } /* normal block 2 */
  36. .U { color: #cc8844 } /* unused data block */
  37. .H { color: #0000ff } /* highlighted tag name */
  38. .F { color: #aa00dd } /* actual offset differs */
  39. .M { text-decoration: underline } /* maker notes data */
  40. .tt { /* tooltip text */
  41. visibility: hidden;
  42. position: absolute;
  43. white-space: nowrap;
  44. top: 0;
  45. left: 0;
  46. font-family: Verdana, sans-serif;
  47. font-size: .7em;
  48. padding: 2px 4px;
  49. border: 1px solid gray;
  50. z-index: 3;
  51. }
  52. .tb { /* tooltip background */
  53. visibility: hidden;
  54. position: absolute;
  55. background: #ffffdd;
  56. zoom: 1;
  57. -moz-opacity: 0.8;
  58. -khtml-opacity: 0.8;
  59. -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=80)';
  60. filter: alpha(opacity=80);
  61. opacity: 0.8;
  62. z-index: 2;
  63. }
  64. /* table styles */
  65. table.dump {
  66. border-top: 1px solid gray;
  67. border-bottom: 1px solid gray;
  68. }
  69. table.dump td { padding: .2em .3em }
  70. td.c2 {
  71. border-left: 1px solid gray;
  72. border-right: 1px solid gray;
  73. }
  74. pre { margin: 0 }
  75. table { font-size: .9em }
  76. body { color: black; background: white }
  77. -->
  78. </style>
  79. <script language="JavaScript" type="text/JavaScript">
  80. <!-- Begin
  81. // tooltip positioning constants
  82. var TMAR = 4; // top/left margins
  83. var BMAR = 16; // bottom/right margins (scrollbars may overhang inner dimensions)
  84. var XOFF = 10; // x offset from cursor
  85. var YOFF = 40; // y offset
  86. var YMIN = 10; // minimum y offset
  87. var YTOP = 20; // y offset when above cursor
  88. // common variables
  89. var safari1 = navigator.userAgent.indexOf("Safari/312.6") >= 0;
  90. var ie6 = navigator.userAgent.toLowerCase().indexOf('msie 6') >= 0;
  91. var mspan = new Array;
  92. var hlist, tt, tb;
  93. function GetElementsByClass(classname, tagname) {
  94. var found = new Array();
  95. var list = document.getElementsByTagName(tagname);
  96. var len = list.length;
  97. for (var i=0, j=0; i<len; ++i) {
  98. var classes = list[i].className.split(' ');
  99. for (var k=0; k<classes.length; ++k) {
  100. if (classes[k] == classname) {
  101. found[j++] = list[i];
  102. break;
  103. }
  104. }
  105. }
  106. delete list;
  107. return found;
  108. }
  109. // move tooltip
  110. function move(e)
  111. {
  112. if (!tt) return;
  113. if (ie6 && (tt.style.top == '' || tt.style.top == 0) &&
  114. (tt.style.left == '' || tt.style.left == 0))
  115. {
  116. tt.style.width = tt.offsetWidth + 'px';
  117. tt.style.height = tt.offsetHeight + 'px';
  118. }
  119. var w, h;
  120. // browser inconsistencies make getting window size more complex than it should be,
  121. // and even then we don't know if it is smaller due to scrollbar width
  122. if (typeof(window.innerWidth) == 'number') {
  123. w = window.innerWidth;
  124. h = window.innerHeight;
  125. } else if (document.documentElement && document.documentElement.clientWidth) {
  126. w = document.documentElement.clientWidth;
  127. h = document.documentElement.clientHeight;
  128. } else {
  129. w = document.body.clientWidth;
  130. h = document.body.clientHeight;
  131. }
  132. var x = e.clientX + XOFF;
  133. var y = e.clientY + YOFF;
  134. if (safari1) { // patch for people still using OS X 10.3.9
  135. x -= document.body.scrollLeft + document.documentElement.scrollLeft;
  136. y -= document.body.scrollTop + document.documentElement.scrollTop;
  137. }
  138. var mx = w - BMAR - tt.offsetWidth;
  139. var my = h - BMAR - tt.offsetHeight;
  140. if (y > my + YOFF - YMIN) y = e.clientY - YTOP - tt.offsetHeight;
  141. if (x > mx) x = mx;
  142. if (y > my) y = my;
  143. if (x < TMAR) x = TMAR;
  144. if (y < TMAR) y = TMAR;
  145. x += document.body.scrollLeft + document.documentElement.scrollLeft;
  146. y += document.body.scrollTop + document.documentElement.scrollTop;
  147. tb.style.width = tt.offsetWidth + 'px';
  148. tb.style.height = tt.offsetHeight + 'px';
  149. tt.style.top = tb.style.top = y + 'px';
  150. tt.style.left = tb.style.left = x + 'px';
  151. tt.style.visibility = tb.style.visibility = 'visible';
  152. }
  153. // highlight/unhighlight text
  154. function high(e,on) {
  155. var targ;
  156. if (e.target) targ = e.target;
  157. else if (e.srcElement) targ = e.srcElement;
  158. if (targ.nodeType == 3) targ = targ.parentNode; // defeat Safari bug
  159. if (!targ.name) targ = targ.parentNode; // go up another level if necessary
  160. if (targ.name && document.getElementsByName) {
  161. // un-highlight current objects
  162. if (hlist) {
  163. for (var i=0; i<hlist.length; ++i) {
  164. hlist[i].style.background = 'transparent';
  165. }
  166. hlist = null;
  167. }
  168. if (tt) {
  169. // hide old tooltip
  170. tt.style.visibility = tb.style.visibility = 'hidden';
  171. tt = null;
  172. }
  173. if (on) {
  174. if (targ.name.substring(0,1) == 't') {
  175. // show our tooltip (ID is different than name to avoid confusing IE)
  176. tt = document.getElementById('p' + targ.name.substring(1));
  177. if (tt) {
  178. tb = document.getElementById('tb');
  179. move(e);
  180. }
  181. }
  182. // highlight anchor elements with the same name
  183. hlist = document.getElementsByName(targ.name);
  184. // use class name to highlight span elements if necessary
  185. for (var i=0; i<mspan.length; ++i) {
  186. if (mspan[i] != targ.name) continue;
  187. var slist = GetElementsByClass(targ.name, 'span');
  188. // add elements from hlist collection to our array
  189. for (var j=0; j<hlist.length; ++j) {
  190. slist[slist.length] = hlist[j];
  191. }
  192. hlist = slist;
  193. break;
  194. }
  195. for (var j=0; j<hlist.length; ++j) {
  196. hlist[j].style.background = '#ffcc99';
  197. }
  198. }
  199. }
  200. }
  201. _END_PART_2_
  202. my $htmlHeader3 = q[
  203. // End --->
  204. </script></head>
  205. <body><noscript><b class=V>--&gt;
  206. Enable JavaScript for active highlighting and information tool tips!
  207. </b></noscript>
  208. <table class=dump cellspacing=0 cellpadding=2>
  209. <tr><td valign='top'><pre>];
  210. my $preMouse = q(<pre onmouseover="high(event,1)" onmouseout="high(event,0)" onmousemove="move(event)">);
  211. #------------------------------------------------------------------------------
  212. # New - create new HtmlDump object
  213. # Inputs: 0) reference to HtmlDump object or HtmlDump class name
  214. sub new
  215. {
  216. local $_;
  217. my $that = shift;
  218. my $class = ref($that) || $that || 'Image::ExifTool::HtmlDump';
  219. return bless { Block => {}, TipNum => 0 }, $class;
  220. }
  221. #------------------------------------------------------------------------------
  222. # Add information to dump
  223. # Inputs: 0) HTML dump hash ref, 1) absolute offset in file, 2) data size,
  224. # 3) comment string, 4) tool tip (or SAME to use previous tip),
  225. # 5) bit flags (see below)
  226. # Bits: 0x01 - print at start of line
  227. # 0x02 - print red address
  228. # 0x04 - maker notes data ('M'-class span)
  229. # 0x08 - limit block length
  230. # 0x10 - allow double references
  231. # 0x100 - (reserved)
  232. # Notes: Block will be shown in 'unused' color if comment string begins with '['
  233. sub Add($$$$;$)
  234. {
  235. my ($self, $start, $size, $msg, $tip, $flag) = @_;
  236. my $block = $$self{Block};
  237. $$block{$start} or $$block{$start} = [ ];
  238. my $htip;
  239. if ($tip and $tip eq 'SAME') {
  240. $htip = '';
  241. } else {
  242. # use message as first line of tip, and make bold unless in brackets
  243. $htip = ($msg =~ /^[[(]/) ? $msg : "<b>$msg</b>";
  244. if (defined $tip) {
  245. ($tip = EscapeHTML($tip)) =~ s/\n/<br>/g; # HTML-ize tooltip text
  246. $htip .= '<br>' . $tip;
  247. }
  248. # add size if not already done
  249. $htip .= "<br>($size bytes)" unless $htip =~ /<br>Size:/;
  250. ++$self->{TipNum};
  251. }
  252. push @{$$block{$start}}, [ $size, $msg, $htip, $flag, $self->{TipNum} ];
  253. }
  254. #------------------------------------------------------------------------------
  255. # Print dump information to HTML page
  256. # Inputs: 0) Dump information hash reference, 1) source file RAF reference,
  257. # 2) data pointer, 3) data position, 4) output file or scalar reference,
  258. # 5) limit level (1-3), 6) title
  259. # Returns: non-zero if useful output was generated,
  260. # or -1 on error loading data and "ERROR" is set to offending data name
  261. # Note: The "Error" member may be set externally to print a specific error
  262. # message instead of doing the dump.
  263. sub Print($$;$$$$$)
  264. {
  265. local $_;
  266. my ($self, $raf, $dataPt, $dataPos, $outfile, $level, $title) = @_;
  267. my ($i, $buff, $rtnVal, $limit, $err);
  268. my $block = $$self{Block};
  269. $dataPos = 0 unless $dataPos;
  270. $outfile = \*STDOUT unless ref $outfile;
  271. $title = 'HtmlDump' unless $title;
  272. $level or $level = 0;
  273. my $tell = $raf->Tell();
  274. my $pos = 0;
  275. my $dataEnd = $dataPos + ($dataPt ? length($$dataPt) : 0);
  276. # initialize member variables
  277. $$self{Open} = [];
  278. $$self{Closed} = [];
  279. $$self{TipList} = [];
  280. $$self{MSpanList} = [];
  281. $$self{Cols} = [ '', '', '', '' ]; # text columns
  282. # set dump size limits (limits are 4x smaller if bit 0x08 set in flags)
  283. if ($level <= 1) {
  284. $limit = 1024;
  285. } elsif ($level <= 2) {
  286. $limit = 16384;
  287. } else {
  288. $limit = 256 * 1024 * 1024; # never dump bigger than 256 MB
  289. }
  290. $$self{Limit} = $limit;
  291. # pre-initialize open/closed hashes for all columns
  292. for ($i=0; $i<4; ++$i) {
  293. $self->{Open}->[$i] = { ID => [ ], Element => { } };
  294. $self->{Closed}->[$i] = { ID => [ ], Element => { } };
  295. }
  296. $bkgStart = $bkgEnd = 0;
  297. undef @bkgSpan;
  298. my $index = 0; # initialize tooltip index
  299. my (@names, $wasUnused, @starts);
  300. # only do dump if we didn't have a serious error
  301. @starts = sort { $a <=> $b } keys %$block unless $$self{Error};
  302. for ($i=0; $i<=@starts; ++$i) {
  303. my $start = $starts[$i];
  304. my $parmList;
  305. if (defined $start) {
  306. $parmList = $$block{$start};
  307. } elsif ($bkgEnd and $pos < $bkgEnd and not defined $wasUnused) {
  308. $start = $bkgEnd; # finish last bkg block
  309. } else {
  310. last;
  311. }
  312. my $len = $start - $pos;
  313. if ($len > 0 and not $wasUnused) {
  314. # we have a unused bytes before this data block
  315. --$i; # dump the data block next time around
  316. # split unused data into 2 blocks if it spans end of a bkg block
  317. my ($nextBkgEnd, $bkg);
  318. if (not defined $wasUnused and $bkgEnd) {
  319. foreach $bkg (@bkgSpan) {
  320. next if $pos >= $$bkg{End} + $dataPos or $pos + $len <= $$bkg{End} + $dataPos;
  321. $nextBkgEnd = $$bkg{End} unless $nextBkgEnd and $nextBkgEnd < $$bkg{End};
  322. }
  323. }
  324. if ($nextBkgEnd) {
  325. $start = $pos;
  326. $len = $nextBkgEnd + $dataPos - $pos;
  327. $wasUnused = 0;
  328. } else {
  329. $start = $pos; # dump the unused bytes now
  330. $wasUnused = 1; # avoid re-dumping unused bytes if we get a read error
  331. }
  332. my $str = ($len > 1) ? "unused $len bytes" : 'pad byte';
  333. $parmList = [ [ $len, "[$str]", undef, 0x108 ] ];
  334. } else {
  335. undef $wasUnused;
  336. }
  337. my $parms;
  338. foreach $parms (@$parmList) {
  339. my ($len, $msg, $tip, $flag, $tipNum) = @$parms;
  340. next unless $len > 0;
  341. $flag = 0 unless defined $flag;
  342. # generate same name for all blocks indexed by this tooltip
  343. my $name;
  344. $name = $names[$tipNum] if defined $tipNum;
  345. my $idx = $index;
  346. if ($name) {
  347. # get index from existing ID
  348. $idx = substr($name, 1);
  349. } else {
  350. $name = "t$index";
  351. $names[$tipNum] = $name if defined $tipNum;
  352. ++$index;
  353. }
  354. if ($flag & 0x14) {
  355. my %bkg = (
  356. Class => $flag & 0x04 ? "$name M" : $name,
  357. Start => $start - $dataPos,
  358. End => $start - $dataPos + $len,
  359. );
  360. push @bkgSpan, \%bkg;
  361. $bkgStart = $bkg{Start} unless $bkgStart and $bkgStart < $bkg{Start};
  362. $bkgEnd = $bkg{End} unless $bkgEnd and $bkgEnd > $bkg{End};
  363. push @{$self->{MSpanList}}, $name;
  364. next;
  365. }
  366. # loop until we read the value properly
  367. my ($end, $try);
  368. for ($try=0; $try<2; ++$try) {
  369. $end = $start + $len;
  370. # only load as much of the block as we are going to dump
  371. # (read 32 more bytes than necessary just in case there
  372. # is only one skipped line that we decide to print)
  373. my $size = ($len > $limit + 32) ? $limit / 2 + 16 : $len;
  374. if ($start >= $dataPos and $end <= $dataEnd) {
  375. $buff = substr($$dataPt, $start-$dataPos, $size);
  376. if ($len != $size) {
  377. $buff .= substr($$dataPt, $start-$dataPos+$len-$size, $size);
  378. }
  379. } else {
  380. $buff = '';
  381. if ($raf->Seek($start, 0) and $raf->Read($buff, $size) == $size) {
  382. # read end of block
  383. if ($len != $size) {
  384. my $buf2 = '';
  385. unless ($raf->Seek($start+$len-$size, 0) and
  386. $raf->Read($buf2, $size) == $size)
  387. {
  388. $err = $msg;
  389. # reset $len to the actual length of available data
  390. $raf->Seek(0, 2);
  391. $len = $raf->Tell() - $start;
  392. $tip .= "<br>Error: Only $len bytes available!" if $tip;
  393. next;
  394. }
  395. $buff .= $buf2;
  396. undef $buf2;
  397. }
  398. } else {
  399. $err = $msg;
  400. $len = length $buff;
  401. $tip .= "<br>Error: Only $len bytes available!" if $tip;
  402. }
  403. }
  404. last;
  405. }
  406. $tip and $self->{TipList}->[$idx] = $tip;
  407. next unless length $buff;
  408. # set flag to continue this line if next block is contiguous
  409. if ($i+1 < @starts and $parms eq $$parmList[-1] and
  410. ($end == $starts[$i+1] or ($end < $starts[$i+1] and $end >= $pos)))
  411. {
  412. my $nextFlag = $block->{$starts[$i+1]}->[0]->[3] || 0;
  413. $flag |= 0x100 unless $flag & 0x01 or $nextFlag & 0x01;
  414. }
  415. $self->DumpTable($start-$dataPos, \$buff, $msg, $name,
  416. $flag, $len, $pos-$dataPos);
  417. undef $buff;
  418. $pos = $end if $pos < $end;
  419. }
  420. }
  421. $self->Open('',''); # close all open elements
  422. $raf->Seek($tell,0);
  423. # write output HTML file
  424. Write($outfile, $htmlHeader1, $title);
  425. if ($self->{Cols}->[0]) {
  426. Write($outfile, $htmlHeader2);
  427. my $mspan = \@{$$self{MSpanList}};
  428. for ($i=0; $i<@$mspan; ++$i) {
  429. Write($outfile, qq(mspan[$i] = "$$mspan[$i]";\n));
  430. }
  431. Write($outfile, $htmlHeader3, $self->{Cols}->[0]);
  432. Write($outfile, '</pre></td><td valign="top">',
  433. $preMouse, $self->{Cols}->[1]);
  434. Write($outfile, '</pre></td><td class=c2 valign="top">',
  435. $preMouse, $self->{Cols}->[2]);
  436. Write($outfile, '</pre></td><td valign="top">',
  437. $preMouse, $self->{Cols}->[3]);
  438. Write($outfile, "</pre></td></tr></table>\n<div id=tb class=tb> </div>\n");
  439. my $tips = \@{$$self{TipList}};
  440. for ($i=0; $i<@$tips; ++$i) {
  441. my $tip = $$tips[$i];
  442. Write($outfile, "<div id=p$i class=tt>$tip</div>\n") if defined $tip;
  443. }
  444. delete $$self{TipList};
  445. $rtnVal = 1;
  446. } else {
  447. my $err = $$self{Error} || 'No EXIF or TIFF information found in image';
  448. Write($outfile, "$title</title></head><body>\n$err\n");
  449. $rtnVal = 0;
  450. }
  451. Write($outfile, "</body></html>\n");
  452. for ($i=0; $i<4; ++$i) {
  453. $self->{Cols}->[$i] = ''; # free memory
  454. }
  455. if ($err) {
  456. $err =~ tr/()//d;
  457. $$self{ERROR} = $err;
  458. return -1;
  459. }
  460. return $rtnVal;
  461. }
  462. #------------------------------------------------------------------------------
  463. # Open or close a specified html element
  464. # Inputs: 0) HtmlDump object ref, 1) element id, 2) element string,
  465. # 3-N) list of column numbers (empty for all columns)
  466. # - element id may be '' to close all elements
  467. # - element string may be '' to close element by ID (or 0 to close without reopening)
  468. # - element id and string may both be 1 to reopen temporarily closed elements
  469. sub Open($$$;@)
  470. {
  471. my ($self, $id, $element, @colNums) = @_;
  472. # loop through specified columns
  473. @colNums or @colNums = (0 .. $#{$self->{Open}});
  474. my $col;
  475. foreach $col (@colNums) {
  476. # get information about open elements in this column
  477. my $opHash = $self->{Open}->[$col];
  478. my $opElem = $$opHash{Element};
  479. if ($element) {
  480. # next if already open
  481. next if $$opElem{$id} and $$opElem{$id} eq $element;
  482. } elsif ($id and not $$opElem{$id}) {
  483. # next if already closed and nothing to reopen
  484. next unless $element eq '' and @{$self->{Closed}->[$col]->{ID}};
  485. }
  486. my $opID = $$opHash{ID};
  487. my $clHash = $self->{Closed}->[$col];
  488. my $clID = $$clHash{ID};
  489. my $clElem = $$clHash{Element};
  490. # get reference to output column list (use temp list if available)
  491. my $cols = $$self{TmpCols} || $$self{Cols};
  492. # close everything down to this element if necessary
  493. if ($$opElem{$id} or not $id) {
  494. while (@$opID) {
  495. my $tid = pop @$opID;
  496. my $e = $$opElem{$tid};
  497. $e =~ s/^<(\S+).*/<\/$1>/s;
  498. $$cols[$col] .= $e;
  499. if ($id eq $tid or not $id) {
  500. delete $$opElem{$tid};
  501. last if $id;
  502. next;
  503. }
  504. # add this to the temporarily closed list
  505. # (because we really didn't want to close it)
  506. push @$clID, $tid;
  507. $$clElem{$tid} = $$opElem{$tid};
  508. delete $$opElem{$tid};
  509. }
  510. unless ($id) {
  511. # forget all temporarily closed elements
  512. $clID = $$clHash{ID} = [ ];
  513. $clElem = $$clHash{Element} = { };
  514. }
  515. } elsif ($$clElem{$id}) {
  516. # delete from the list of temporarily closed elements
  517. delete $$clElem{$id};
  518. @$clID = grep !/^$id$/, @$clID;
  519. }
  520. next if $element eq '0'; # 0 = don't reopen temporarily closed elements
  521. # re-open temporarily closed elements
  522. while (@$clID) {
  523. my $tid = pop @$clID;
  524. $$cols[$col] .= $$clElem{$tid};
  525. push @$opID, $tid;
  526. $$opElem{$tid} = $$clElem{$tid};
  527. delete $$clElem{$tid};
  528. }
  529. # open specified element
  530. if ($element and $element ne '1') {
  531. $$cols[$col] .= $element;
  532. push @$opID, $id;
  533. $$opElem{$id} = $element;
  534. }
  535. }
  536. }
  537. #------------------------------------------------------------------------------
  538. # Dump a block of data in HTML table form
  539. # Inputs: 0) HtmlDump object ref, 1) data position, 2) block pointer,
  540. # 3) message, 4) object name, 5) flag, 6) full block length (actual
  541. # data may be shorter), 7) data end position
  542. sub DumpTable($$$;$$$$$)
  543. {
  544. my ($self, $pos, $blockPt, $msg, $name, $flag, $len, $endPos) = @_;
  545. $len = length $$blockPt unless defined $len;
  546. $endPos = 0 unless $endPos;
  547. my ($f0, $dblRef, $id);
  548. my $skipped = 0;
  549. if (($endPos and $pos < $endPos) or $flag & 0x02) {
  550. # display double-reference addresses in red
  551. $f0 = "<span class=V>";
  552. $dblRef = 1 if $endPos and $pos < $endPos;
  553. } else {
  554. $f0 = '';
  555. }
  556. my @c = ('','','','');
  557. $$self{TmpCols} = \@c;
  558. if ($name) {
  559. if ($msg and $msg =~ /^\[/) {
  560. $id = 'U';
  561. } else {
  562. if ($$self{A}) {
  563. $id = 'X';
  564. $$self{A} = 0;
  565. } else {
  566. $id = 'V';
  567. $$self{A} = 1;
  568. }
  569. ++$id unless $dblRef;
  570. }
  571. $name = "<a name=$name class=$id>";
  572. $msg and $msg = "$name$msg</a>";
  573. } else {
  574. $name = '';
  575. }
  576. # use base-relative offsets from now on
  577. my $cols = 0;
  578. my $p = $pos;
  579. if ($$self{Cont}) {
  580. $cols = $pos & 0x0f;
  581. $c[1] .= ($cols == 8) ? ' ' : ' ';
  582. } else {
  583. my $addr = $pos < 0 ? sprintf("-%.4x",-$pos) : sprintf("%5.4x",$pos);
  584. $self->Open('fgd', $f0, 0);
  585. $self->Open('fgd', '', 3);
  586. $c[0] .= "$addr";
  587. $p -= $pos & 0x0f unless $flag & 0x01;
  588. if ($p < $pos) {
  589. $self->Open('bkg', '', 1, 2); # don't underline white space
  590. $cols = $pos - $p;
  591. my $n = 3 * $cols;
  592. ++$n if $cols > 7;
  593. $c[1] .= ' ' x $n;
  594. $c[2] .= ' ' x $cols;
  595. $p = $pos;
  596. }
  597. }
  598. # loop through each column of hex numbers
  599. for (;;) {
  600. my (@spanClass, @spanCont, $spanClose, $bkg);
  601. if ($p >= $bkgStart and $p < $bkgEnd) {
  602. foreach $bkg (@bkgSpan) {
  603. next unless $p >= $$bkg{Start} and $p < $$bkg{End};
  604. push @spanClass, $$bkg{Class};
  605. if ($p + 1 == $$bkg{End}) {
  606. $spanClose = 1;
  607. } else {
  608. push @spanCont, $$bkg{Class}; # this span continues
  609. }
  610. }
  611. $self->Open('bkg', @spanClass ? "<span class='@spanClass'>" : '', 1, 2);
  612. } else {
  613. $self->Open('bkg', '', 1, 2);
  614. }
  615. $self->Open('a', $name, 1, 2);
  616. my $ch = substr($$blockPt,$p-$pos-$skipped,1);
  617. $c[1] .= sprintf("%.2x", ord($ch));
  618. # make the character HTML-friendly
  619. $ch =~ tr/\x00-\x1f\x7f-\xff/./;
  620. $ch =~ s/&/&amp;/g;
  621. $ch =~ s/>/&gt;/g;
  622. $ch =~ s/</&lt;/g;
  623. $c[2] .= $ch;
  624. ++$p;
  625. ++$cols;
  626. # close necessary elements
  627. if ($spanClose) {
  628. my $spanCont = @spanCont ? "<span class='@spanCont'>" : '';
  629. # close without reopening if closing anchor later
  630. my $arg = ($p - $pos >= $len) ? 0 : $spanCont;
  631. $self->Open('bkg', $arg, 1, 2);
  632. }
  633. if ($dblRef and $p >= $endPos) {
  634. $dblRef = 0;
  635. ++$id;
  636. $name =~ s/class=\w\b/class=$id/;
  637. $f0 = '';
  638. $self->Open('fgd', $f0, 0);
  639. }
  640. if ($p - $pos >= $len) {
  641. $self->Open('a', '', 1, 2); # close our anchor
  642. last;
  643. }
  644. if ($cols < 16) {
  645. $c[1] .= ($cols == 8 ? ' ' : ' ');
  646. next;
  647. } elsif ($flag & 0x01 and $cols < $len) {
  648. $c[1] .= ' ';
  649. next; # put it all on one line
  650. }
  651. unless ($$self{Msg}) {
  652. $c[3] .= $msg;
  653. $msg = '';
  654. }
  655. $_ .= "\n" foreach @c; # add CR to all lines
  656. $$self{Msg} = 0;
  657. # limit data length if specified
  658. if ($$self{Limit}) {
  659. my $div = ($flag & 0x08) ? 4 : 1;
  660. my $lim = $$self{Limit} / (2 * $div) - 16;
  661. if ($p - $pos > $lim and $len - $p + $pos > $lim) {
  662. my $n = ($len - $p + $pos - $lim) & ~0x0f;
  663. if ($n > 16) { # (no use just cutting out one line)
  664. $self->Open('bkg', '', 1, 2); # no underline
  665. my $note = sprintf "[snip %d lines]", $n / 16;
  666. $note = (' ' x (24-length($note)/2)) . $note;
  667. $c[0] .= " ...\n";
  668. $c[1] .= $note . (' ' x (48-length($note))) . "\n";
  669. $c[2] .= " [snip] \n";
  670. $c[3] .= "\n";
  671. $p += $n;
  672. $skipped += $len - length $$blockPt;
  673. }
  674. }
  675. }
  676. $c[0] .= ($p < 0 ? sprintf("-%.4x",-$p) : sprintf("%5.4x",$p));
  677. $cols = 0;
  678. }
  679. if ($msg) {
  680. $msg = " $msg" if $$self{Msg};
  681. $c[3] .= $msg;
  682. }
  683. if ($flag & 0x100 and $cols < 16) { # continue on same line?
  684. $$self{Cont} = 1;
  685. $$self{Msg} = 1 if $msg;
  686. } else {
  687. $_ .= "\n" foreach @c;
  688. $$self{Msg} = 0;
  689. $$self{Cont} = 0;
  690. }
  691. # add temporary column data to our real columns
  692. my $i;
  693. for ($i=0; $i<4; ++$i) {
  694. $self->{Cols}->[$i] .= $c[$i];
  695. }
  696. delete $$self{TmpCols};
  697. }
  698. #------------------------------------------------------------------------------
  699. # Finish dumping of TIFF image data
  700. # Inputs: 0) HtmlDump object ref, 1) ExifTool object ref, 2) length of file
  701. # (this really belongs in Image::ExifTool::Exif, but is placed here so it
  702. # is only compiled when needed)
  703. sub FinishTiffDump($$$)
  704. {
  705. my ($self, $et, $size) = @_;
  706. my ($tag, $key, $start, $blockInfo, $i);
  707. # list of all indirectly referenced TIFF data tags
  708. my %offsetPair = (
  709. StripOffsets => 'StripByteCounts',
  710. TileOffsets => 'TileByteCounts',
  711. FreeOffsets => 'FreeByteCounts',
  712. ThumbnailOffset => 'ThumbnailLength',
  713. PreviewImageStart => 'PreviewImageLength',
  714. JpgFromRawStart => 'JpgFromRawLength',
  715. OtherImageStart => 'OtherImageLength',
  716. ImageOffset => 'ImageByteCount',
  717. AlphaOffset => 'AlphaByteCount',
  718. MPImageStart => 'MPImageLength',
  719. IDCPreviewStart => 'IDCPreviewLength',
  720. );
  721. # add TIFF data to html dump
  722. foreach $tag (keys %offsetPair) {
  723. my $info = $et->GetInfo($tag);
  724. next unless %$info;
  725. # Panasonic hack: StripOffsets is not valid for Panasonic RW2 files,
  726. # and StripRowBytes is not valid for some RAW images
  727. if ($tag eq 'StripOffsets' and $$et{TAG_INFO}{$tag}{PanasonicHack}) {
  728. # use RawDataOffset instead if available since it is valid in RW2
  729. my $info2 = $et->GetInfo('RawDataOffset');
  730. $info2 = $info unless %$info2;
  731. my @keys = keys %$info2;
  732. my $offset = $$info2{$keys[0]};
  733. my $raf = $$et{RAF};
  734. # ignore StripByteCounts and assume raw data runs to the end of file
  735. if (@keys == 1 and $offset =~ /^\d+$/ and $raf) {
  736. my $pos = $raf->Tell();
  737. $raf->Seek(0, 2); # seek to end
  738. my $len = $raf->Tell() - $offset;
  739. $raf->Seek($pos, 0);
  740. if ($len > 0) {
  741. $self->Add($offset, $len, "(Panasonic raw data)", "Size: $len bytes", 0x08);
  742. next;
  743. }
  744. }
  745. }
  746. # loop through all offsets tags
  747. foreach $key (keys %$info) {
  748. my $name = Image::ExifTool::GetTagName($key);
  749. my $grp1 = $et->GetGroup($key, 1);
  750. my $info2 = $et->GetInfo($offsetPair{$tag}, { Group1 => $grp1 });
  751. my $key2 = $offsetPair{$tag};
  752. $key2 .= $1 if $key =~ /( .*)/; # use same instance number as $tag
  753. next unless $$info2{$key2};
  754. my $offsets = $$info{$key};
  755. my $byteCounts = $$info2{$key2};
  756. # ignore primary MPImage (this is the whole JPEG)
  757. next if $tag eq 'MPImageStart' and $offsets eq '0';
  758. # (long lists may be SCALAR references)
  759. my @offsets = split ' ', (ref $offsets ? $$offsets : $offsets);
  760. my @byteCounts = split ' ', (ref $byteCounts ? $$byteCounts : $byteCounts);
  761. my $num = scalar @offsets;
  762. my $li = 0;
  763. my $padBytes = 0;
  764. for ($i=0; @offsets and @byteCounts; ++$i) {
  765. my $offset = shift @offsets;
  766. my $byteCount = shift @byteCounts;
  767. my $end = $offset + $byteCount;
  768. if (@offsets and @byteCounts) {
  769. # show data as contiguous if only normal pad bytes between blocks
  770. if ($end & 0x01 and $end + 1 == $offsets[0]) {
  771. $end += 1;
  772. ++$padBytes; # count them
  773. }
  774. if ($end == $offsets[0]) {
  775. # combine these two blocks
  776. $byteCounts[0] += $offsets[0] - $offset;
  777. $offsets[0] = $offset;
  778. next;
  779. }
  780. }
  781. my $msg = $et->GetGroup($key, 1) . ':' . $tag;
  782. $msg =~ s/(Offsets?|Start)$/ /;
  783. if ($num > 1) {
  784. $msg .= "$li-" if $li != $i;
  785. $msg .= "$i ";
  786. $li = $i + 1;
  787. }
  788. $msg .= "data";
  789. my $tip = "Size: $byteCount bytes";
  790. $tip .= ", incl. $padBytes pad bytes" if $padBytes;
  791. $self->Add($offset, $byteCount, "($msg)", $tip, 0x08);
  792. }
  793. }
  794. }
  795. # find offset of last dumped information, and dump any unknown trailer
  796. my $last = 0;
  797. my $block = $$self{Block};
  798. foreach $start (keys %$block) {
  799. foreach $blockInfo (@{$$block{$start}}) {
  800. my $end = $start + $$blockInfo[0];
  801. $last = $end if $last < $end;
  802. }
  803. }
  804. my $diff = $size - $last;
  805. if ($diff > 0 and ($last or $et->Options('Unknown'))) {
  806. if ($diff > 1 or $size & 0x01) {
  807. $self->Add($last, $diff, "[unknown data]", "Size: $diff bytes", 0x08);
  808. } else {
  809. $self->Add($last, $diff, "[trailing pad byte]", undef, 0x08);
  810. }
  811. }
  812. }
  813. #------------------------------------------------------------------------------
  814. # utility routine to write to file or memory
  815. # Inputs: 0) file or scalar reference, 1-N) list of stuff to write
  816. # Returns: true on success
  817. sub Write($@)
  818. {
  819. my $outfile = shift;
  820. if (UNIVERSAL::isa($outfile,'GLOB')) {
  821. return print $outfile @_;
  822. } elsif (ref $outfile eq 'SCALAR') {
  823. $$outfile .= join('', @_);
  824. return 1;
  825. }
  826. return 0;
  827. }
  828. 1; # end
  829. __END__
  830. =head1 NAME
  831. Image::ExifTool::HtmlDump - Dump information in hex to HTML page
  832. =head1 SYNOPSIS
  833. use Image::ExifTool::HtmlDump;
  834. my $dump = new Image::ExifTool::HtmlDump;
  835. $dump->Add($start, $size, $comment);
  836. $dump->Print($dumpInfo, $raf, $dataPt, $dataPos, $outfile);
  837. =head1 DESCRIPTION
  838. This module contains code used to generate an HTML-based hex dump of
  839. information for debugging purposes. This is code is called when the
  840. ExifTool 'HtmlDump' option is used.
  841. Currently, only EXIF/TIFF and JPEG information is dumped.
  842. =head1 BUGS
  843. Due to a memory allocation bug in ActivePerl 5.8.x for Windows, this code
  844. may run extremely slowly when processing large files with this version of
  845. Perl.
  846. An HTML 4 compliant browser is needed to properly display the generated HTML
  847. page.
  848. =head1 AUTHOR
  849. Copyright 2003-2016, Phil Harvey (phil at owl.phy.queensu.ca)
  850. This library is free software; you can redistribute it and/or modify it
  851. under the same terms as Perl itself.
  852. =head1 SEE ALSO
  853. L<Image::ExifTool(3pm)|Image::ExifTool>
  854. =cut