source: main/trunk/greenstone2/perllib/cpan/Image/ExifTool/H264.pm@ 34921

Last change on this file since 34921 was 34921, checked in by anupama, 3 years ago

Committing the improvements to EmbeddedMetaPlugin's processing of Keywords vs other metadata fields. Keywords were literally stored as arrays of words rather than phrases in PDFs (at least in Diego's sample PDF), whereas other meta fields like Subjects and Creators stored them as arrays of phrases. To get both to work, Kathy updated EXIF to a newer version, to retrieve the actual EXIF values stored in the PDF. And Kathy and Dr Bainbridge came up with a new option that I added called apply_join_before_split_to_metafields that's a regex which can list the metadata fields to apply the join_before_split to and whcih previously always got applied to all metadata fields. Now it's applied to any *Keywords metafields by default, as that's the metafield we have experience of that behaves differently to the others, as it stores by word instead of phrases. Tested on Diego's sample PDF. Diego has double-checked it to works on his sample PDF too, setting the split char to ; and turning on the join_before_split and leaving apply_join_before_split_to_metafields at its default of .*Keywords. File changes are strings.properties for the tooltip, the plugin introducing the option and working with it and Kathy's EXIF updates affecting cpan/File and cpan/Image.

  • Property svn:executable set to *
File size: 39.3 KB
Line 
1#------------------------------------------------------------------------------
2# File: H264.pm
3#
4# Description: Read meta information from H.264 video
5#
6# Revisions: 2010/01/31 - P. Harvey Created
7#
8# References: 1) http://www.itu.int/rec/T-REC-H.264/e (T-REC-H.264-200305-S!!PDF-E.pdf)
9# 2) http://miffteevee.co.uk/documentation/development/H264Parser_8cpp-source.html
10# 3) http://ffmpeg.org/
11# 4) US Patent 2009/0052875 A1
12# 5) European Patent (EP2 051 528A1) application no. 07792522.0 filed 08.08.2007
13# 6) Dave Nicholson private communication
14# 7) http://www.freepatentsonline.com/20050076039.pdf
15# 8) Michael Reitinger private communication (RX100)
16#
17# Glossary: RBSP = Raw Byte Sequence Payload
18#------------------------------------------------------------------------------
19
20package Image::ExifTool::H264;
21
22use strict;
23use vars qw($VERSION %convMake);
24use Image::ExifTool qw(:DataAccess :Utils);
25use Image::ExifTool::Exif;
26use Image::ExifTool::GPS;
27
28$VERSION = '1.17';
29
30sub ProcessSEI($$);
31
32my $parsePictureTiming; # flag to enable parsing of picture timing information (test only)
33
34# lookup for camera manufacturer name
35%convMake = (
36 0x0103 => 'Panasonic',
37 0x0108 => 'Sony',
38 0x1011 => 'Canon',
39 0x1104 => 'JVC', #Rob Lewis
40);
41
42# information extracted from H.264 video streams
43%Image::ExifTool::H264::Main = (
44 GROUPS => { 2 => 'Video' },
45 VARS => { NO_ID => 1 },
46 NOTES => q{
47 Tags extracted from H.264 video streams. The metadata for AVCHD videos is
48 stored in this stream.
49 },
50 ImageWidth => { },
51 ImageHeight => { },
52 MDPM => { SubDirectory => { TagTable => 'Image::ExifTool::H264::MDPM' } },
53);
54
55# H.264 Supplemental Enhancement Information User Data (ref PH/4)
56%Image::ExifTool::H264::MDPM = (
57 GROUPS => { 2 => 'Camera' },
58 PROCESS_PROC => \&ProcessSEI,
59 TAG_PREFIX => 'MDPM',
60 NOTES => q{
61 The following tags are decoded from the Modified Digital Video Pack Metadata
62 (MDPM) of the unregistered user data with UUID
63 17ee8c60f84d11d98cd60800200c9a66 in the H.264 Supplemental Enhancement
64 Information (SEI). I<[Yes, this description is confusing, but nothing
65 compared to the challenge of actually decoding the data!]> This information
66 may exist at regular intervals through the entire video, but only the first
67 occurrence is extracted unless the L<ExtractEmbedded|../ExifTool.html#ExtractEmbedded> (-ee) option is used (in
68 which case subsequent occurrences are extracted as sub-documents).
69 },
70 # (Note: all these are explained in IEC 61834-4, but it costs money so it is useless to me)
71 # 0x00 - ControlCassetteID (ref 7)
72 # 0x01 - ControlTapeLength (ref 7)
73 # 0x02 - ControlTimerActDate (ref 7)
74 # 0x03 - ControlTimerACS_S_S (ref 7)
75 # 0x04-0x05 - ControlPR_StartPoint (ref 7)
76 # 0x06 - ControlTagIDNoGenre (ref 7)
77 # 0x07 - ControlTopicPageHeader (ref 7)
78 # 0x08 - ControlTextHeader (ref 7)
79 # 0x09 - ControlText (ref 7)
80 # 0x0a-0x0b - ControlTag (ref 7)
81 # 0x0c - ControlTeletextInfo (ref 7)
82 # 0x0d - ControlKey (ref 7)
83 # 0x0e-0x0f - ControlZoneEnd (ref 7)
84 # 0x10 - TitleTotalTime (ref 7)
85 # 0x11 - TitleRemainTime (ref 7)
86 # 0x12 - TitleChapterTotalNo (ref 7)
87 0x13 => {
88 Name => 'TimeCode',
89 Notes => 'hours:minutes:seconds:frames',
90 ValueConv => 'sprintf("%.2x:%.2x:%.2x:%.2x",reverse unpack("C*",$val))',
91 },
92 # 0x14 - TitleBinaryGroup - val: 0x00000000,0x14200130
93 # 0x15 - TitleCassetteNo (ref 7)
94 # 0x16-0x17 - TitleSoftID (ref 7)
95 # (0x18,0x19 listed as TitleTextHeader/TitleText by ref 7)
96 0x18 => {
97 Name => 'DateTimeOriginal',
98 Description => 'Date/Time Original',
99 Groups => { 2 => 'Time' },
100 Notes => 'combined with tag 0x19',
101 Combine => 1, # the next tag (0x19) contains the rest of the date
102 # first byte is timezone information:
103 # 0x80 - unused
104 # 0x40 - DST flag
105 # 0x20 - TimeZoneSign
106 # 0x1e - TimeZoneValue
107 # 0x01 - half-hour flag
108 ValueConv => q{
109 my ($tz, @a) = unpack('C*',$val);
110 return sprintf('%.2x%.2x:%.2x:%.2x %.2x:%.2x:%.2x%s%.2d:%s%s', @a,
111 $tz & 0x20 ? '-' : '+', ($tz >> 1) & 0x0f,
112 $tz & 0x01 ? '30' : '00',
113 $tz & 0x40 ? ' DST' : '');
114 },
115 PrintConv => '$self->ConvertDateTime($val)',
116 },
117 # 0x1a-0x1b - TitleStart (ref 7)
118 # 0x1c-0x1d - TitleReelID (ref 7)
119 # 0x1e-0x1f - TitleEnd (ref 7)
120 # 0x20 - ChapterTotalTime (ref 7)
121 # 0x42 - ProgramRecDTime (ref 7)
122 # 0x50/0x60 - (AAUX/VAUX)Source (ref 7)
123 # 0x51/0x61 - (AAUX/VAUX)SourceControl (ref 7)
124 # 0x52/0x62 - (AAUX/VAUX)RecDate (ref 7)
125 # 0x53/0x63 - (AAUX/VAUX)RecTime (ref 7)
126 # 0x54/0x64 - (AAUX/VAUX)BinaryGroup (ref 7)
127 # 0x55/0x65 - (AAUX/VAUX)ClosedCaption (ref 7)
128 # 0x56/0x66 - (AAUX/VAUX)TR (ref 7)
129 0x70 => { # ConsumerCamera1
130 Name => 'Camera1',
131 SubDirectory => { TagTable => 'Image::ExifTool::H264::Camera1' },
132 },
133 0x71 => { # ConsumerCamera2
134 Name => 'Camera2',
135 SubDirectory => { TagTable => 'Image::ExifTool::H264::Camera2' },
136 },
137 # 0x73 Lens - val: 0x04ffffd3,0x0effffd3,0x15ffffd3,0x41ffffd3,0x52ffffd3,0x59ffffd3,0x65ffffd3,0x71ffffd3,0x75ffffd3,0x79ffffd3,0x7fffffd3,0xffffffd3...
138 # 0x74 Gain - val: 0xb8ffff0f
139 # 0x75 Pedestal
140 # 0x76 Gamma
141 # 0x77 Detail
142 # 0x7b CameraPreset
143 # 0x7c Flare
144 # 0x7d Shading
145 # 0x7e Knee
146 0x7f => { # Shutter
147 Name => 'Shutter',
148 SubDirectory => {
149 TagTable => 'Image::ExifTool::H264::Shutter',
150 ByteOrder => 'LittleEndian', # weird
151 },
152 },
153 0xa0 => {
154 Name => 'ExposureTime',
155 Format => 'rational32u',
156 Groups => { 2 => 'Image' },
157 PrintConv => 'Image::ExifTool::Exif::PrintExposureTime($val)',
158 },
159 0xa1 => {
160 Name => 'FNumber',
161 Format => 'rational32u',
162 Groups => { 2 => 'Image' },
163 },
164 0xa2 => {
165 Name => 'ExposureProgram',
166 Format => 'int32u', # (guess)
167 PrintConv => {
168 0 => 'Not Defined',
169 1 => 'Manual',
170 2 => 'Program AE',
171 3 => 'Aperture-priority AE',
172 4 => 'Shutter speed priority AE',
173 5 => 'Creative (Slow speed)',
174 6 => 'Action (High speed)',
175 7 => 'Portrait',
176 8 => 'Landscape',
177 },
178 },
179 0xa3 => {
180 Name => 'BrightnessValue',
181 Format => 'rational32s',
182 Groups => { 2 => 'Image' },
183 },
184 0xa4 => {
185 Name => 'ExposureCompensation',
186 Format => 'rational32s',
187 Groups => { 2 => 'Image' },
188 PrintConv => 'Image::ExifTool::Exif::PrintFraction($val)',
189 },
190 0xa5 => {
191 Name => 'MaxApertureValue',
192 Format => 'rational32u',
193 ValueConv => '2 ** ($val / 2)',
194 PrintConv => 'sprintf("%.1f",$val)',
195 },
196 0xa6 => {
197 Name => 'Flash',
198 Format => 'int32u', # (guess)
199 Flags => 'PrintHex',
200 SeparateTable => 'EXIF Flash',
201 PrintConv => \%Image::ExifTool::Exif::flash,
202 },
203 0xa7 => {
204 Name => 'CustomRendered',
205 Format => 'int32u', # (guess)
206 Groups => { 2 => 'Image' },
207 PrintConv => {
208 0 => 'Normal',
209 1 => 'Custom',
210 },
211 },
212 0xa8 => {
213 Name => 'WhiteBalance',
214 Format => 'int32u', # (guess)
215 Priority => 0,
216 PrintConv => {
217 0 => 'Auto',
218 1 => 'Manual',
219 },
220 },
221 0xa9 => {
222 Name => 'FocalLengthIn35mmFormat',
223 Format => 'rational32u',
224 PrintConv => '"$val mm"',
225 },
226 0xaa => {
227 Name => 'SceneCaptureType',
228 Format => 'int32u', # (guess)
229 PrintConv => {
230 0 => 'Standard',
231 1 => 'Landscape',
232 2 => 'Portrait',
233 3 => 'Night',
234 },
235 },
236 # 0xab-0xaf ExifOption
237 0xb0 => {
238 Name => 'GPSVersionID',
239 Format => 'int8u',
240 Count => 4,
241 Groups => { 1 => 'GPS', 2 => 'Location' },
242 PrintConv => '$val =~ tr/ /./; $val',
243 },
244 0xb1 => {
245 Name => 'GPSLatitudeRef',
246 Format => 'string',
247 Groups => { 1 => 'GPS', 2 => 'Location' },
248 PrintConv => {
249 N => 'North',
250 S => 'South',
251 },
252 },
253 0xb2 => {
254 Name => 'GPSLatitude',
255 Format => 'rational32u',
256 Groups => { 1 => 'GPS', 2 => 'Location' },
257 Notes => 'combined with tags 0xb3 and 0xb4',
258 Combine => 2, # combine the next 2 tags (0xb2=deg, 0xb3=min, 0xb4=sec)
259 ValueConv => 'Image::ExifTool::GPS::ToDegrees($val)',
260 PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1)',
261 },
262 0xb5 => {
263 Name => 'GPSLongitudeRef',
264 Format => 'string',
265 Groups => { 1 => 'GPS', 2 => 'Location' },
266 PrintConv => {
267 E => 'East',
268 W => 'West',
269 },
270 },
271 0xb6 => {
272 Name => 'GPSLongitude',
273 Format => 'rational32u',
274 Groups => { 1 => 'GPS', 2 => 'Location' },
275 Combine => 2, # combine the next 2 tags (0xb6=deg, 0xb7=min, 0xb8=sec)
276 Notes => 'combined with tags 0xb7 and 0xb8',
277 ValueConv => 'Image::ExifTool::GPS::ToDegrees($val)',
278 PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1)',
279 },
280 0xb9 => {
281 Name => 'GPSAltitudeRef',
282 Format => 'int32u', # (guess)
283 Groups => { 1 => 'GPS', 2 => 'Location' },
284 ValueConv => '$val ? 1 : 0', # because I'm not sure about the Format
285 PrintConv => {
286 0 => 'Above Sea Level',
287 1 => 'Below Sea Level',
288 },
289 },
290 0xba => {
291 Name => 'GPSAltitude',
292 Format => 'rational32u',
293 Groups => { 1 => 'GPS', 2 => 'Location' },
294 },
295 0xbb => {
296 Name => 'GPSTimeStamp',
297 Format => 'rational32u',
298 Groups => { 1 => 'GPS', 2 => 'Time' },
299 Combine => 2, # the next tags (0xbc/0xbd) contain the minutes/seconds
300 Notes => 'combined with tags 0xbc and 0xbd',
301 ValueConv => 'Image::ExifTool::GPS::ConvertTimeStamp($val)',
302 PrintConv => 'Image::ExifTool::GPS::PrintTimeStamp($val)',
303 },
304 0xbe => {
305 Name => 'GPSStatus',
306 Format => 'string',
307 Groups => { 1 => 'GPS', 2 => 'Location' },
308 PrintConv => {
309 A => 'Measurement Active',
310 V => 'Measurement Void',
311 },
312 },
313 0xbf => {
314 Name => 'GPSMeasureMode',
315 Format => 'string',
316 Groups => { 1 => 'GPS', 2 => 'Location' },
317 PrintConv => {
318 2 => '2-Dimensional Measurement',
319 3 => '3-Dimensional Measurement',
320 },
321 },
322 0xc0 => {
323 Name => 'GPSDOP',
324 Description => 'GPS Dilution Of Precision',
325 Format => 'rational32u',
326 Groups => { 1 => 'GPS', 2 => 'Location' },
327 },
328 0xc1 => {
329 Name => 'GPSSpeedRef',
330 Format => 'string',
331 Groups => { 1 => 'GPS', 2 => 'Location' },
332 PrintConv => {
333 K => 'km/h',
334 M => 'mph',
335 N => 'knots',
336 },
337 },
338 0xc2 => {
339 Name => 'GPSSpeed',
340 Format => 'rational32u',
341 Groups => { 1 => 'GPS', 2 => 'Location' },
342 },
343 0xc3 => {
344 Name => 'GPSTrackRef',
345 Format => 'string',
346 Groups => { 1 => 'GPS', 2 => 'Location' },
347 PrintConv => {
348 M => 'Magnetic North',
349 T => 'True North',
350 },
351 },
352 0xc4 => {
353 Name => 'GPSTrack',
354 Format => 'rational32u',
355 Groups => { 1 => 'GPS', 2 => 'Location' },
356 },
357 0xc5 => {
358 Name => 'GPSImgDirectionRef',
359 Format => 'string',
360 Groups => { 1 => 'GPS', 2 => 'Location' },
361 PrintConv => {
362 M => 'Magnetic North',
363 T => 'True North',
364 },
365 },
366 0xc6 => {
367 Name => 'GPSImgDirection',
368 Format => 'rational32u',
369 Groups => { 1 => 'GPS', 2 => 'Location' },
370 },
371 0xc7 => {
372 Name => 'GPSMapDatum',
373 Format => 'string',
374 Groups => { 1 => 'GPS', 2 => 'Location' },
375 Combine => 1, # the next tag (0xc8) contains the rest of the string
376 Notes => 'combined with tag 0xc8',
377 },
378 # 0xc9-0xcf - GPSOption
379 # 0xc9 - val: 0x001d0203
380 0xca => { #PH (Sony DSC-HX7V)
381 Name => 'GPSDateStamp',
382 Format => 'string',
383 Groups => { 1 => 'GPS', 2 => 'Time' },
384 Combine => 2, # the next 2 tags contain the rest of the string
385 Notes => 'combined with tags 0xcb and 0xcc',
386 ValueConv => 'Image::ExifTool::Exif::ExifDate($val)',
387 },
388 0xe0 => {
389 Name => 'MakeModel',
390 SubDirectory => { TagTable => 'Image::ExifTool::H264::MakeModel' },
391 },
392 # 0xe1-0xef - MakerOption
393 # 0xe1 - val: 0x01000670,0x01000678,0x06ffffff,0x01ffffff,0x01000020,0x01000400...
394 0xe1 => { #6
395 Name => 'RecInfo',
396 Condition => '$$self{Make} eq "Canon"',
397 Notes => 'Canon only',
398 SubDirectory => { TagTable => 'Image::ExifTool::H264::RecInfo' },
399 },
400 # 0xe2-0xe8 - val: 0x00000000 in many samples
401 # 0xe2 - val: 0x00000000,0x01000000,0x01010000,0x8080900c,0x8080a074
402 # 0xe3 - val: 0x00801f89,0x00801f8b,0x00c01f89,0xc9c01f80
403 0xe4 => { #PH
404 Name => 'Model',
405 Condition => '$$self{Make} eq "Sony"', # (possibly also Canon models?)
406 Description => 'Camera Model Name',
407 Notes => 'Sony only, combined with tags 0xe5 and 0xe6',
408 Format => 'string',
409 Combine => 2, # (not sure about 0xe6, but include it just in case)
410 RawConv => '$val eq "" ? undef : $val',
411 },
412 # 0xeb - val: 0x008a0a00,0x0a300000,0x508a0a00,0x52880a00,0x528a0a00
413 # 0xec - val: 0x0b700000
414 # 0xed - val: 0x0ce0f819
415 0xee => { #6 (HFS200)
416 Name => 'FrameInfo',
417 Condition => '$$self{Make} eq "Canon"',
418 Notes => 'Canon only',
419 SubDirectory => { TagTable => 'Image::ExifTool::H264::FrameInfo' },
420 },
421 # 0xef - val: 0x01c00000,0x0e00000c
422);
423
424# ConsumerCamera1 information (ref PH)
425%Image::ExifTool::H264::Camera1 = (
426 PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
427 GROUPS => { 2 => 'Camera' },
428 TAG_PREFIX => 'Camera1',
429 PRINT_CONV => 'sprintf("0x%.2x",$val)',
430 FIRST_ENTRY => 0,
431 0 => {
432 Name => 'ApertureSetting',
433 PrintHex => 1,
434 PrintConv => {
435 0xff => 'Auto',
436 0xfe => 'Closed',
437 OTHER => sub { sprintf('%.1f', 2 ** (($_[0] & 0x3f) / 8)) },
438 },
439 },
440 1 => {
441 Name => 'Gain',
442 Mask => 0x0f,
443 # (0x0f would translate to 42 dB, but this value is used by the Sony
444 # HXR-NX5U for any out-of-range value such as -6 dB or "hyper gain" - PH)
445 ValueConv => '($val - 1) * 3',
446 PrintConv => '$val==42 ? "Out of range" : "$val dB"',
447 },
448 1.1 => {
449 Name => 'ExposureProgram',
450 Mask => 0xf0,
451 ValueConv => '$val == 15 ? undef : $val',
452 PrintConv => {
453 0 => 'Program AE',
454 1 => 'Gain', #?
455 2 => 'Shutter speed priority AE',
456 3 => 'Aperture-priority AE',
457 4 => 'Manual',
458 },
459 },
460 2.1 => {
461 Name => 'WhiteBalance',
462 Mask => 0xe0,
463 ValueConv => '$val == 7 ? undef : $val',
464 PrintConv => {
465 0 => 'Auto',
466 1 => 'Hold',
467 2 => '1-Push',
468 3 => 'Daylight',
469 },
470 },
471 3 => {
472 Name => 'Focus',
473 ValueConv => '$val == 0xff ? undef : $val',
474 PrintConv => q{
475 my $foc = ($val & 0x7e) / (($val & 0x01) ? 40 : 400);
476 return(($val & 0x80 ? 'Manual' : 'Auto') . " ($foc)");
477 },
478 },
479);
480
481# ConsumerCamera2 information (ref PH)
482%Image::ExifTool::H264::Camera2 = (
483 PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
484 GROUPS => { 2 => 'Camera' },
485 TAG_PREFIX => 'Camera2',
486 PRINT_CONV => 'sprintf("0x%.2x",$val)',
487 FIRST_ENTRY => 0,
488 1 => {
489 Name => 'ImageStabilization',
490 PrintHex => 1,
491 PrintConv => {
492 0 => 'Off',
493 0x3f => 'On (0x3f)', #8
494 0xbf => 'Off (0xbf)', #8
495 0xff => 'n/a',
496 OTHER => sub {
497 my $val = shift;
498 sprintf("%s (0x%.2x)", $val & 0x10 ? "On" : "Off", $val);
499 },
500 },
501 },
502);
503
504# camera info 0x7f (ref PH)
505%Image::ExifTool::H264::Shutter = (
506 PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
507 GROUPS => { 2 => 'Image' },
508 TAG_PREFIX => 'Shutter',
509 PRINT_CONV => 'sprintf("0x%.2x",$val)',
510 FIRST_ENTRY => 0,
511 FORMAT => 'int16u',
512 1.1 => { #6
513 Name => 'ExposureTime',
514 Mask => 0x7fff, # (what is bit 0x8000 for?)
515 RawConv => '$val == 0x7fff ? undef : $val', #7
516 ValueConv => '$val / 28125', #PH (Vixia HF G30, ref forum5588) (was $val/33640 until 9.49)
517 PrintConv => 'Image::ExifTool::Exif::PrintExposureTime($val)',
518 },
519);
520
521# camera info 0xe0 (ref PH)
522%Image::ExifTool::H264::MakeModel = (
523 PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
524 GROUPS => { 2 => 'Camera' },
525 FORMAT => 'int16u',
526 FIRST_ENTRY => 0,
527 0 => {
528 Name => 'Make',
529 PrintHex => 1,
530 RawConv => '$$self{Make} = ($Image::ExifTool::H264::convMake{$val} || "Unknown"); $val',
531 PrintConv => \%convMake,
532 },
533 # 1 => ModelIDCode according to ref 4/5 (I think not - PH)
534 # 1 => { Name => 'ModelIDCode', PrintConv => 'sprintf("%.4x",$val)' },
535 # vals: 0x0313 - various Pansonic HDC models
536 # 0x0345 - Panasonic HC-V7272
537 # 0x0414 - Panasonic AG-AF100
538 # 0x0591 - various Panasonic DMC models
539 # 0x0802 - Panasonic DMC-TZ60 with GPS information off
540 # 0x0803 - Panasonic DMC-TZ60 with GPS information on
541 # 0x3001 - various Sony DSC, HDR, NEX and SLT models
542 # 0x3003 - various Sony DSC models
543 # 0x3100 - various Sony DSC, ILCE, NEX and SLT models
544 # 0x1000 - Sony HDR-UX1
545 # 0x2000 - Canon HF100 (60i)
546 # 0x3000 - Canon HF100 (30p)
547 # 0x3101 - Canon HFM300 (PH, all qualities and frame rates)
548 # 0x3102 - Canon HFS200
549 # 0x4300 - Canon HFG30
550);
551
552# camera info 0xe1 (ref 6)
553%Image::ExifTool::H264::RecInfo = (
554 PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
555 GROUPS => { 2 => 'Camera' },
556 FORMAT => 'int8u',
557 NOTES => 'Recording information stored by some Canon video cameras.',
558 FIRST_ENTRY => 0,
559 0 => {
560 Name => 'RecordingMode',
561 PrintConv => {
562 0x02 => 'XP+', # High Quality 12 Mbps
563 0x04 => 'SP', # Standard Play 7 Mbps
564 0x05 => 'LP', # Long Play 5 Mbps
565 0x06 => 'FXP', # High Quality 17 Mbps
566 0x07 => 'MXP', # High Quality 24 Mbps
567 },
568 },
569);
570
571# camera info 0xee (ref 6)
572%Image::ExifTool::H264::FrameInfo = (
573 PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
574 GROUPS => { 2 => 'Video' },
575 FORMAT => 'int8u',
576 NOTES => 'Frame rate information stored by some Canon video cameras.',
577 FIRST_ENTRY => 0,
578 0 => 'CaptureFrameRate',
579 1 => 'VideoFrameRate',
580 # 2 - 8=60i, 10=PF30, 74=PF24 (PH, HFM300)
581);
582
583#==============================================================================
584# Bitstream functions (used for H264 video)
585#
586# Member variables:
587# Mask = mask for next bit to read (0 when all data has been read)
588# Pos = byte offset of next word to read
589# Word = current data word
590# Len = total data length in bytes
591# DataPt = data pointer
592#..............................................................................
593
594#------------------------------------------------------------------------------
595# Read next word from bitstream
596# Inputs: 0) BitStream ref
597# Returns: true if there is more data (and updates
598# Mask, Pos and Word for first bit in next word)
599sub ReadNextWord($)
600{
601 my $bstr = shift;
602 my $pos = $$bstr{Pos};
603 if ($pos + 4 <= $$bstr{Len}) {
604 $$bstr{Word} = unpack("x$pos N", ${$$bstr{DataPt}});
605 $$bstr{Mask} = 0x80000000;
606 $$bstr{Pos} += 4;
607 } elsif ($pos < $$bstr{Len}) {
608 my @bytes = unpack("x$pos C*", ${$$bstr{DataPt}});
609 my ($word, $mask) = (shift(@bytes), 0x80);
610 while (@bytes) {
611 $word = ($word << 8) | shift(@bytes);
612 $mask <<= 8;
613 }
614 $$bstr{Word} = $word;
615 $$bstr{Mask} = $mask;
616 $$bstr{Pos} = $$bstr{Len};
617 } else {
618 return 0;
619 }
620 return 1;
621}
622
623#------------------------------------------------------------------------------
624# Create a new BitStream object
625# Inputs: 0) data ref
626# Returns: BitStream ref, or null if data is empty
627sub NewBitStream($)
628{
629 my $dataPt = shift;
630 my $bstr = {
631 DataPt => $dataPt,
632 Len => length($$dataPt),
633 Pos => 0,
634 Mask => 0,
635 };
636 ReadNextWord($bstr) or undef $bstr;
637 return $bstr;
638}
639
640#------------------------------------------------------------------------------
641# Get number of bits remaining in bit stream
642# Inputs: 0) BitStream ref
643# Returns: number of bits remaining
644sub BitsLeft($)
645{
646 my $bstr = shift;
647 my $bits = 0;
648 my $mask = $$bstr{Mask};
649 while ($mask) {
650 ++$bits;
651 $mask >>= 1;
652 }
653 return $bits + 8 * ($$bstr{Len} - $$bstr{Pos});
654}
655
656#------------------------------------------------------------------------------
657# Get integer from bitstream
658# Inputs: 0) BitStream ref, 1) number of bits
659# Returns: integer (and increments position in bitstream)
660sub GetIntN($$)
661{
662 my ($bstr, $bits) = @_;
663 my $val = 0;
664 while ($bits--) {
665 $val <<= 1;
666 ++$val if $$bstr{Mask} & $$bstr{Word};
667 $$bstr{Mask} >>= 1 and next;
668 ReadNextWord($bstr) or last;
669 }
670 return $val;
671}
672
673#------------------------------------------------------------------------------
674# Get Exp-Golomb integer from bitstream
675# Inputs: 0) BitStream ref
676# Returns: integer (and increments position in bitstream)
677sub GetGolomb($)
678{
679 my $bstr = shift;
680 # first, count the number of zero bits to get the integer bit width
681 my $count = 0;
682 until ($$bstr{Mask} & $$bstr{Word}) {
683 ++$count;
684 $$bstr{Mask} >>= 1 and next;
685 ReadNextWord($bstr) or last;
686 }
687 # then return the adjusted integer
688 return GetIntN($bstr, $count + 1) - 1;
689}
690
691#------------------------------------------------------------------------------
692# Get signed Exp-Golomb integer from bitstream
693# Inputs: 0) BitStream ref
694# Returns: integer (and increments position in bitstream)
695sub GetGolombS($)
696{
697 my $bstr = shift;
698 my $val = GetGolomb($bstr) + 1;
699 return ($val & 1) ? -($val >> 1) : ($val >> 1);
700}
701
702# end bitstream functions
703#==============================================================================
704
705#------------------------------------------------------------------------------
706# Decode H.264 scaling matrices
707# Inputs: 0) BitStream ref
708# Reference: http://ffmpeg.org/
709sub DecodeScalingMatrices($)
710{
711 my $bstr = shift;
712 if (GetIntN($bstr, 1)) {
713 my ($i, $j);
714 for ($i=0; $i<8; ++$i) {
715 my $size = $i<6 ? 16 : 64;
716 next unless GetIntN($bstr, 1);
717 my ($last, $next) = (8, 8);
718 for ($j=0; $j<$size; ++$j) {
719 $next = ($last + GetGolombS($bstr)) & 0xff if $next;
720 last unless $j or $next;
721 }
722 }
723 }
724}
725
726#------------------------------------------------------------------------------
727# Parse H.264 sequence parameter set RBSP (ref 1)
728# Inputs: 0) ExifTool ref, 1) tag table ref, 2) data ref
729# Notes: All this just to get the image size!
730sub ParseSeqParamSet($$$)
731{
732 my ($et, $tagTablePtr, $dataPt) = @_;
733 # initialize our bitstream object
734 my $bstr = NewBitStream($dataPt) or return;
735 my ($t, $i, $j, $n);
736 # the messy nature of H.264 encoding makes it difficult to use
737 # data-driven structure parsing, so I code it explicitly (yuck!)
738 $t = GetIntN($bstr, 8); # profile_idc
739 GetIntN($bstr, 16); # constraints and level_idc
740 GetGolomb($bstr); # seq_parameter_set_id
741 if ($t >= 100) { # (ref b)
742 $t = GetGolomb($bstr); # chroma_format_idc
743 if ($t == 3) {
744 GetIntN($bstr, 1); # separate_colour_plane_flag
745 $n = 12;
746 } else {
747 $n = 8;
748 }
749 GetGolomb($bstr); # bit_depth_luma_minus8
750 GetGolomb($bstr); # bit_depth_chroma_minus8
751 GetIntN($bstr, 1); # qpprime_y_zero_transform_bypass_flag
752 DecodeScalingMatrices($bstr);
753 }
754 GetGolomb($bstr); # log2_max_frame_num_minus4
755 $t = GetGolomb($bstr); # pic_order_cnt_type
756 if ($t == 0) {
757 GetGolomb($bstr); # log2_max_pic_order_cnt_lsb_minus4
758 } elsif ($t == 1) {
759 GetIntN($bstr, 1); # delta_pic_order_always_zero_flag
760 GetGolomb($bstr); # offset_for_non_ref_pic
761 GetGolomb($bstr); # offset_for_top_to_bottom_field
762 $n = GetGolomb($bstr); # num_ref_frames_in_pic_order_cnt_cycle
763 for ($i=0; $i<$n; ++$i) {
764 GetGolomb($bstr); # offset_for_ref_frame[i]
765 }
766 }
767 GetGolomb($bstr); # num_ref_frames
768 GetIntN($bstr, 1); # gaps_in_frame_num_value_allowed_flag
769 my $w = GetGolomb($bstr); # pic_width_in_mbs_minus1
770 my $h = GetGolomb($bstr); # pic_height_in_map_units_minus1
771 my $f = GetIntN($bstr, 1); # frame_mbs_only_flag
772 $f or GetIntN($bstr, 1); # mb_adaptive_frame_field_flag
773 GetIntN($bstr, 1); # direct_8x8_inference_flag
774 # convert image size to pixels
775 $w = ($w + 1) * 16;
776 $h = (2 - $f) * ($h + 1) * 16;
777 # account for cropping (if any)
778 $t = GetIntN($bstr, 1); # frame_cropping_flag
779 if ($t) {
780 my $m = 4 - $f * 2;
781 $w -= 4 * GetGolomb($bstr);# frame_crop_left_offset
782 $w -= 4 * GetGolomb($bstr);# frame_crop_right_offset
783 $h -= $m * GetGolomb($bstr);# frame_crop_top_offset
784 $h -= $m * GetGolomb($bstr);# frame_crop_bottom_offset
785 }
786 # quick validity checks (just in case)
787 return unless $$bstr{Mask};
788 if ($w>=160 and $w<=4096 and $h>=120 and $h<=3072) {
789 $et->HandleTag($tagTablePtr, ImageWidth => $w);
790 $et->HandleTag($tagTablePtr, ImageHeight => $h);
791 # (whew! -- so much work just to get ImageSize!!)
792 }
793 # return now unless interested in picture timing information
794 return unless $parsePictureTiming;
795
796 # parse vui parameters if they exist
797 GetIntN($bstr, 1) or return; # vui_parameters_present_flag
798 $t = GetIntN($bstr, 1); # aspect_ratio_info_present_flag
799 if ($t) {
800 $t = GetIntN($bstr, 8); # aspect_ratio_idc
801 if ($t == 255) { # Extended_SAR ?
802 GetIntN($bstr, 32); # sar_width/sar_height
803 }
804 }
805 $t = GetIntN($bstr, 1); # overscan_info_present_flag
806 GetIntN($bstr, 1) if $t; # overscan_appropriate_flag
807 $t = GetIntN($bstr, 1); # video_signal_type_present_flag
808 if ($t) {
809 GetIntN($bstr, 4); # video_format/video_full_range_flag
810 $t = GetIntN($bstr, 1); # colour_description_present_flag
811 GetIntN($bstr, 24) if $t; # colour_primaries/transfer_characteristics/matrix_coefficients
812 }
813 $t = GetIntN($bstr, 1); # chroma_loc_info_present_flag
814 if ($t) {
815 GetGolomb($bstr); # chroma_sample_loc_type_top_field
816 GetGolomb($bstr); # chroma_sample_loc_type_bottom_field
817 }
818 $t = GetIntN($bstr, 1); # timing_info_present_flag
819 if ($t) {
820 return if BitsLeft($bstr) < 65;
821 $$et{VUI_units} = GetIntN($bstr, 32); # num_units_in_tick
822 $$et{VUI_scale} = GetIntN($bstr, 32); # time_scale
823 GetIntN($bstr, 1); # fixed_frame_rate_flag
824 }
825 my $hard;
826 for ($j=0; $j<2; ++$j) {
827 $t = GetIntN($bstr, 1); # nal_/vcl_hrd_parameters_present_flag
828 if ($t) {
829 $$et{VUI_hard} = 1;
830 $hard = 1;
831 $n = GetGolomb($bstr); # cpb_cnt_minus1
832 GetIntN($bstr, 8); # bit_rate_scale/cpb_size_scale
833 for ($i=0; $i<=$n; ++$i) {
834 GetGolomb($bstr); # bit_rate_value_minus1[SchedSelIdx]
835 GetGolomb($bstr); # cpb_size_value_minus1[SchedSelIdx]
836 GetIntN($bstr, 1); # cbr_flag[SchedSelIdx]
837 }
838 GetIntN($bstr, 5); # initial_cpb_removal_delay_length_minus1
839 $$et{VUI_clen} = GetIntN($bstr, 5); # cpb_removal_delay_length_minus1
840 $$et{VUI_dlen} = GetIntN($bstr, 5); # dpb_output_delay_length_minus1
841 $$et{VUI_toff} = GetIntN($bstr, 5); # time_offset_length
842 }
843 }
844 GetIntN($bstr, 1) if $hard; # low_delay_hrd_flag
845 $$et{VUI_pic} = GetIntN($bstr, 1); # pic_struct_present_flag
846 # (don't yet decode the rest of the vui data)
847}
848
849#------------------------------------------------------------------------------
850# Parse H.264 picture timing SEI message (payload type 1) (ref 1)
851# Inputs: 0) ExifTool ref, 1) data ref
852# Notes: this routine is for test purposes only, and not called unless the
853# $parsePictureTiming flag is set
854sub ParsePictureTiming($$)
855{
856 my ($et, $dataPt) = @_;
857 my $bstr = NewBitStream($dataPt) or return;
858 my ($i, $t, $n);
859 # the specification is very odd on this point: the following delays
860 # exist if the VUI hardware parameters are present, or if
861 # "determined by the application, by some means not specified" -- WTF??
862 if ($$et{VUI_hard}) {
863 GetIntN($bstr, $$et{VUI_clen} + 1); # cpb_removal_delay
864 GetIntN($bstr, $$et{VUI_dlen} + 1); # dpb_output_delay
865 }
866 if ($$et{VUI_pic}) {
867 $t = GetIntN($bstr, 4); # pic_struct
868 # determine NumClockTS ($n)
869 $n = { 0=>1, 1=>1, 2=>1, 3=>2, 4=>2, 5=>3, 6=>3, 7=>2, 8=>3 }->{$t};
870 $n or return;
871 for ($i=0; $i<$n; ++$i) {
872 $t = GetIntN($bstr, 1); # clock_timestamp_flag[i]
873 next unless $t;
874 my ($nu, $s, $m, $h, $o);
875 GetIntN($bstr, 2); # ct_type
876 $nu = GetIntN($bstr, 1);# nuit_field_based_flag
877 GetIntN($bstr, 5); # counting_type
878 $t = GetIntN($bstr, 1); # full_timestamp_flag
879 GetIntN($bstr, 1); # discontinuity_flag
880 GetIntN($bstr, 1); # cnt_dropped_flag
881 GetIntN($bstr, 8); # n_frames
882 if ($t) {
883 $s = GetIntN($bstr, 6); # seconds_value
884 $m = GetIntN($bstr, 6); # minutes_value
885 $h = GetIntN($bstr, 5); # hours_value
886 } else {
887 $t = GetIntN($bstr, 1); # seconds_flag
888 if ($t) {
889 $s = GetIntN($bstr, 6); # seconds_value
890 $t = GetIntN($bstr, 1); # minutes_flag
891 if ($t) {
892 $m = GetIntN($bstr, 6); # minutes_value
893 $t = GetIntN($bstr, 1); # hours_flag
894 $h = GetIntN($bstr, 5) if $t; # hours_value
895 }
896 }
897 }
898 if ($$et{VUI_toff}) {
899 $o = GetIntN($bstr, $$et{VUI_toff}); # time_offset
900 }
901 last; # only parse the first clock timestamp found
902 }
903 }
904}
905
906#------------------------------------------------------------------------------
907# Process H.264 Supplementary Enhancement Information (ref 1/PH)
908# Inputs: 0) ExifTool ref, 1) dirInfo ref, 2) tag table ref
909# Returns: 1 if we processed payload type 5
910# Payload types:
911# 0 - buffer period
912# 1 - pic timing
913# 2 - pan scan rect
914# 3 - filler payload
915# 4 - user data registered itu t t35
916# 5 - user data unregistered
917# 6 - recovery point
918# 7 - dec ref pic marking repetition
919# 8 - spare pic
920# 9 - sene info
921# 10 - sub seq info
922# 11 - sub seq layer characteristics
923# 12 - sub seq characteristics
924# 13 - full frame freeze
925# 14 - full frame freeze release
926# 15 - full frame snapshot
927# 16 - progressive refinement segment start
928# 17 - progressive refinement segment end
929# 18 - motion constrained slice group set
930sub ProcessSEI($$)
931{
932 my ($et, $dirInfo) = @_;
933 my $dataPt = $$dirInfo{DataPt};
934 my $end = length($$dataPt);
935 my $pos = 0;
936 my ($type, $size, $index, $t);
937
938 # scan through SEI payload for type 5 (the unregistered user data)
939 for (;;) {
940 $type = 0;
941 for (;;) {
942 return 0 if $pos >= $end;
943 $t = Get8u($dataPt, $pos++); # payload type
944 $type += $t;
945 last unless $t == 255;
946 }
947 return 0 if $type == 0x80; # terminator (ref PH - maybe byte alignment bits?)
948 $size = 0;
949 for (;;) {
950 return 0 if $pos >= $end;
951 $t = Get8u($dataPt, $pos++); # payload data length
952 $size += $t;
953 last unless $t == 255;
954 }
955 return 0 if $pos + $size > $end;
956 $et->VPrint(1," (SEI type $type)\n");
957 if ($type == 1) { # picture timing information
958 if ($parsePictureTiming) {
959 my $buff = substr($$dataPt, $pos, $size);
960 ParsePictureTiming($et, $dataPt);
961 }
962 } elsif ($type == 5) { # unregistered user data
963 last; # exit loop to process user data now
964 }
965 $pos += $size;
966 }
967
968 # look for our 16-byte UUID
969 # - plus "MDPM" for "ModifiedDVPackMeta"
970 # - plus "GA94" for closed-caption data (currently not decoded)
971 return 0 unless $size > 20 and substr($$dataPt, $pos, 20) eq
972 "\x17\xee\x8c\x60\xf8\x4d\x11\xd9\x8c\xd6\x08\0\x20\x0c\x9a\x66MDPM";
973#
974# parse the MDPM records in the UUID 17ee8c60f84d11d98cd60800200c9a66
975# unregistered user data payload (ref PH)
976#
977 my $tagTablePtr = GetTagTable('Image::ExifTool::H264::MDPM');
978 my $oldIndent = $$et{INDENT};
979 $$et{INDENT} .= '| ';
980 $end = $pos + $size; # end of payload
981 $pos += 20; # skip UUID + "MDPM"
982 my $num = Get8u($dataPt, $pos++); # get entry count
983 my $lastTag = 0;
984 $et->VerboseDir('MDPM', $num) if $et->Options('Verbose');
985 # walk through entries in the MDPM payload
986 for ($index=0; $index<$num and $pos<$end; ++$index) {
987 my $tag = Get8u($dataPt, $pos);
988 if ($tag <= $lastTag) { # should be in numerical order (PH)
989 $et->Warn('Entries in MDPM directory are out of sequence');
990 last;
991 }
992 $lastTag = $tag;
993 my $buff = substr($$dataPt, $pos + 1, 4);
994 my $from;
995 my $tagInfo = $et->GetTagInfo($tagTablePtr, $tag);
996 if ($tagInfo) {
997 # use our own print conversion for Unknown tags
998 if ($$tagInfo{Unknown} and not $$tagInfo{SetPrintConv}) {
999 $$tagInfo{PrintConv} = 'sprintf("0x%.8x", unpack("N", $val))';
1000 $$tagInfo{SetPrintConv} = 1;
1001 }
1002 # combine with next value(s) if necessary
1003 my $combine = $$tagTablePtr{$tag}{Combine};
1004 while ($combine) {
1005 last if $pos + 5 >= $end;
1006 my $t = Get8u($dataPt, $pos + 5);
1007 last if $t != $lastTag + 1; # must be consecutive tag ID's
1008 $pos += 5;
1009 $buff .= substr($$dataPt, $pos + 1, 4);
1010 $from = $index unless defined $from;
1011 ++$index;
1012 ++$lastTag;
1013 --$combine;
1014 }
1015 $et->HandleTag($tagTablePtr, $tag, undef,
1016 TagInfo => $tagInfo,
1017 DataPt => \$buff,
1018 Size => length($buff),
1019 Index => defined $from ? "$from-$index" : $index,
1020 );
1021 }
1022 $pos += 5;
1023 }
1024 $$et{INDENT} = $oldIndent;
1025 return 1;
1026}
1027
1028#------------------------------------------------------------------------------
1029# Extract information from H.264 video stream
1030# Inputs: 0) ExifTool ref, 1) data ref
1031# Returns: 0 = done parsing, 1 = we want to parse more of these
1032sub ParseH264Video($$)
1033{
1034 my ($et, $dataPt) = @_;
1035 my $verbose = $et->Options('Verbose');
1036 my $out = $et->Options('TextOut');
1037 my $tagTablePtr = GetTagTable('Image::ExifTool::H264::Main');
1038 my %parseNalUnit = ( 0x06 => 1, 0x07 => 1 ); # NAL unit types to parse
1039 my $foundUserData;
1040 my $len = length $$dataPt;
1041 my $pos = 0;
1042 while ($pos < $len) {
1043 my ($nextPos, $end);
1044 # find start of next NAL unit
1045 if ($$dataPt =~ /(\0{2,3}\x01)/g) {
1046 $nextPos = pos $$dataPt;
1047 $end = $nextPos - length $1;
1048 $pos or $pos = $nextPos, next;
1049 } else {
1050 last unless $pos;
1051 $nextPos = $end = $len;
1052 }
1053 last if $pos >= $len;
1054 # parse NAL unit from $pos to $end
1055 my $nal_unit_type = Get8u($dataPt, $pos);
1056 ++$pos;
1057 # check forbidden_zero_bit
1058 $nal_unit_type & 0x80 and $et->Warn('H264 forbidden bit error'), last;
1059 $nal_unit_type &= 0x1f;
1060 # ignore this NAL unit unless we will parse it
1061 $parseNalUnit{$nal_unit_type} or $verbose or $pos = $nextPos, next;
1062 # read NAL unit (and convert all 0x000003's to 0x0000 as per spec.)
1063 my $buff = '';
1064 pos($$dataPt) = $pos + 1;
1065 while ($$dataPt =~ /\0\0\x03/g) {
1066 last if pos $$dataPt > $end;
1067 $buff .= substr($$dataPt, $pos, pos($$dataPt)-1-$pos);
1068 $pos = pos $$dataPt;
1069 }
1070 $buff .= substr($$dataPt, $pos, $end - $pos);
1071 if ($verbose > 1) {
1072 printf $out " NAL Unit Type: 0x%x (%d bytes)\n",$nal_unit_type, length $buff;
1073 $et->VerboseDump(\$buff);
1074 }
1075 pos($$dataPt) = $pos = $nextPos;
1076
1077 if ($nal_unit_type == 0x06) { # sei_rbsp (supplemental enhancement info)
1078
1079 if ($$et{GotNAL06}) {
1080 # process only the first SEI unless ExtractEmbedded is set
1081 next unless $et->Options('ExtractEmbedded');
1082 $$et{DOC_NUM} = $$et{GotNAL06};
1083 }
1084 $foundUserData = ProcessSEI($et, { DataPt => \$buff } );
1085 delete $$et{DOC_NUM};
1086 # keep parsing SEI's until we find the user data
1087 next unless $foundUserData;
1088 $$et{GotNAL06} = ($$et{GotNAL06} || 0) + 1;
1089
1090 } elsif ($nal_unit_type == 0x07) { # sequence_parameter_set_rbsp
1091
1092 # process this NAL unit type only once
1093 next if $$et{GotNAL07};
1094 $$et{GotNAL07} = 1;
1095 ParseSeqParamSet($et, $tagTablePtr, \$buff);
1096 }
1097 # we were successful, so don't parse this NAL unit type again
1098 delete $parseNalUnit{$nal_unit_type};
1099 }
1100 # parse one extra H264 frame if we didn't find the user data in this one
1101 # (Panasonic cameras don't put the SEI in the first frame)
1102 return 0 if $foundUserData or $$et{ParsedH264};
1103 $$et{ParsedH264} = 1;
1104 return 1;
1105}
1106
11071; # end
1108
1109__END__
1110
1111=head1 NAME
1112
1113Image::ExifTool::H264 - Read meta information from H.264 video
1114
1115=head1 SYNOPSIS
1116
1117This module is used by Image::ExifTool
1118
1119=head1 DESCRIPTION
1120
1121This module contains routines required by Image::ExifTool to extract
1122information from H.264 video streams.
1123
1124=head1 AUTHOR
1125
1126Copyright 2003-2021, Phil Harvey (philharvey66 at gmail.com)
1127
1128This library is free software; you can redistribute it and/or modify it
1129under the same terms as Perl itself.
1130
1131=head1 REFERENCES
1132
1133=over 4
1134
1135=item L<http://www.itu.int/rec/T-REC-H.264/e>
1136
1137=item L<http://miffteevee.co.uk/documentation/development/H264Parser_8cpp-source.html>
1138
1139=item L<http://ffmpeg.org/>
1140
1141=back
1142
1143=head1 SEE ALSO
1144
1145L<Image::ExifTool::TagNames/H264 Tags>,
1146L<Image::ExifTool(3pm)|Image::ExifTool>
1147
1148=cut
1149
Note: See TracBrowser for help on using the repository browser.