File Coverage

blib/lib/Image/ExifTool/Google.pm
Criterion Covered Total %
statement 72 111 64.8
branch 14 42 33.3
condition 2 14 14.2
subroutine 5 6 83.3
pod 0 2 0.0
total 93 175 53.1


line stmt bran cond sub pod time code
1             #------------------------------------------------------------------------------
2             # File: Google.pm
3             #
4             # Description: Google maker notes and XMP tags
5             #
6             # Revisions: 2025-09-17 - P. Harvey Created
7             #
8             # References: 1) https://github.com/jakiki6/ruminant/blob/master/ruminant/modules/images.py
9             #------------------------------------------------------------------------------
10              
11             package Image::ExifTool::Google;
12              
13 14     14   7036 use strict;
  14         35  
  14         705  
14 14     14   90 use vars qw($VERSION);
  14         33  
  14         964  
15 14     14   85 use Image::ExifTool qw(:DataAccess :Utils);
  14         53  
  14         4509  
16 14     14   1376 use Image::ExifTool::XMP;
  14         42  
  14         50698  
17              
18             $VERSION = '1.01';
19              
20             sub ProcessHDRP($$$);
21              
22             # default formats based on Google format size
23             my @formatName = ( undef, 'string', 'int16s', undef, 'int32s' );
24              
25             my %sPose = (
26             STRUCT_NAME => 'Google Pose',
27             NAMESPACE => { Pose => 'http://ns.google.com/photos/dd/1.0/pose/' },
28             PositionX => { Writable => 'real', Groups => { 2 => 'Location' } },
29             PositionY => { Writable => 'real', Groups => { 2 => 'Location' } },
30             PositionZ => { Writable => 'real', Groups => { 2 => 'Location' } },
31             RotationX => { Writable => 'real', Groups => { 2 => 'Location' } },
32             RotationY => { Writable => 'real', Groups => { 2 => 'Location' } },
33             RotationZ => { Writable => 'real', Groups => { 2 => 'Location' } },
34             RotationW => { Writable => 'real', Groups => { 2 => 'Location' } },
35             Timestamp => {
36             Writable => 'integer',
37             Shift => 'Time',
38             Groups => { 2 => 'Time' },
39             ValueConv => 'ConvertUnixTime($val / 1000, 1, 3)',
40             ValueConvInv => 'int(GetUnixTime($val, 1) * 1000)',
41             PrintConv => '$self->ConvertDateTime($val)',
42             PrintConvInv => '$self->InverseDateTime($val,undef,1)',
43             },
44             );
45             my %sEarthPose = (
46             STRUCT_NAME => 'Google EarthPose',
47             NAMESPACE => { EarthPose => 'http://ns.google.com/photos/dd/1.0/earthpose/' },
48             Latitude => {
49             Writable => 'real',
50             Groups => { 2 => 'Location' },
51             ValueConv => 'Image::ExifTool::GPS::ToDegrees($val, 1)',
52             ValueConvInv => '$val',
53             PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "N")',
54             PrintConvInv => 'Image::ExifTool::GPS::ToDegrees($val, 1, "lat")',
55             },
56             Longitude => {
57             Writable => 'real',
58             Groups => { 2 => 'Location' },
59             ValueConv => 'Image::ExifTool::GPS::ToDegrees($val, 1)',
60             ValueConvInv => '$val',
61             PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "E")',
62             PrintConvInv => 'Image::ExifTool::GPS::ToDegrees($val, 1, "lon")',
63             },
64             Altitude => {
65             Writable => 'real',
66             Groups => { 2 => 'Location' },
67             PrintConv => '"$val m"',
68             PrintConvInv => '$val=~s/\s*m$//;$val',
69             },
70             RotationX => { Writable => 'real', Groups => { 2 => 'Location' } },
71             RotationY => { Writable => 'real', Groups => { 2 => 'Location' } },
72             RotationZ => { Writable => 'real', Groups => { 2 => 'Location' } },
73             RotationW => { Writable => 'real', Groups => { 2 => 'Location' } },
74             Timestamp => {
75             Writable => 'integer',
76             Shift => 'Time',
77             Groups => { 2 => 'Time' },
78             ValueConv => 'ConvertUnixTime($val / 1000, 1, 3)',
79             ValueConvInv => 'int(GetUnixTime($val, 1) * 1000)',
80             PrintConv => '$self->ConvertDateTime($val)',
81             PrintConvInv => '$self->InverseDateTime($val,undef,1)',
82             },
83             );
84             my %sVendorInfo = (
85             STRUCT_NAME => 'Google VendorInfo',
86             NAMESPACE => { VendorInfo => 'http://ns.google.com/photos/dd/1.0/vendorinfo/' },
87             Model => { },
88             Manufacturer => { },
89             Notes => { },
90             );
91             my %sAppInfo = (
92             STRUCT_NAME => 'Google AppInfo',
93             NAMESPACE => { AppInfo => 'http://ns.google.com/photos/dd/1.0/appinfo/' },
94             Application => { },
95             Version => { },
96             ItemURI => { },
97             );
98              
99             # Google audio namespace
100             %Image::ExifTool::Google::GAudio = (
101             %Image::ExifTool::XMP::xmpTableDefaults,
102             GROUPS => { 0 => 'XMP', 1 => 'XMP-GAudio', 2 => 'Audio' },
103             NAMESPACE => 'GAudio',
104             Data => {
105             Name => 'AudioData',
106             ValueConv => 'Image::ExifTool::XMP::DecodeBase64($val)',
107             ValueConvInv => 'Image::ExifTool::XMP::EncodeBase64($val)',
108             },
109             Mime => { Name => 'AudioMimeType' },
110             );
111              
112             # Google image namespace
113             %Image::ExifTool::Google::GImage = (
114             %Image::ExifTool::XMP::xmpTableDefaults,
115             GROUPS => { 0 => 'XMP', 1 => 'XMP-GImage', 2 => 'Image' },
116             NAMESPACE => 'GImage',
117             Data => {
118             Name => 'ImageData',
119             ValueConv => 'Image::ExifTool::XMP::DecodeBase64($val)',
120             ValueConvInv => 'Image::ExifTool::XMP::EncodeBase64($val)',
121             },
122             Mime => { Name => 'ImageMimeType' },
123             );
124              
125             # Google panorama namespace properties
126             # (ref https://exiftool.org/forum/index.php/topic,4569.0.html)
127             %Image::ExifTool::Google::GPano = (
128             %Image::ExifTool::XMP::xmpTableDefaults,
129             GROUPS => { 0 => 'XMP', 1 => 'XMP-GPano', 2 => 'Image' },
130             NAMESPACE => 'GPano',
131             NOTES => q{
132             Panorama tags written by Google Photosphere. See
133             L for the
134             specification.
135             },
136             UsePanoramaViewer => { Writable => 'boolean' },
137             CaptureSoftware => { },
138             StitchingSoftware => { },
139             ProjectionType => { },
140             PoseHeadingDegrees => { Writable => 'real' },
141             PosePitchDegrees => { Writable => 'real' },
142             PoseRollDegrees => { Writable => 'real' },
143             InitialViewHeadingDegrees => { Writable => 'real' },
144             InitialViewPitchDegrees => { Writable => 'real' },
145             InitialViewRollDegrees => { Writable => 'real' },
146             InitialHorizontalFOVDegrees => { Writable => 'real' },
147             InitialVerticalFOVDegrees => { Writable => 'real' },
148             FirstPhotoDate => { %Image::ExifTool::XMP::dateTimeInfo, Groups => { 2 => 'Time' } },
149             LastPhotoDate => { %Image::ExifTool::XMP::dateTimeInfo, Groups => { 2 => 'Time' } },
150             SourcePhotosCount => { Writable => 'integer' },
151             ExposureLockUsed => { Writable => 'boolean' },
152             CroppedAreaImageWidthPixels => { Writable => 'real' },
153             CroppedAreaImageHeightPixels => { Writable => 'real' },
154             FullPanoWidthPixels => { Writable => 'real' },
155             FullPanoHeightPixels => { Writable => 'real' },
156             CroppedAreaLeftPixels => { Writable => 'real' },
157             CroppedAreaTopPixels => { Writable => 'real' },
158             InitialCameraDolly => { Writable => 'real' },
159             # (the following have been observed, but are not in the specification)
160             LargestValidInteriorRectLeft => { Writable => 'real' },
161             LargestValidInteriorRectTop => { Writable => 'real' },
162             LargestValidInteriorRectWidth => { Writable => 'real' },
163             LargestValidInteriorRectHeight => { Writable => 'real' },
164             );
165              
166             # Google Spherical Images namespace (ref https://github.com/google/spatial-media/blob/master/docs/spherical-video-rfc.md)
167             %Image::ExifTool::Google::GSpherical = (
168             %Image::ExifTool::XMP::xmpTableDefaults,
169             GROUPS => { 0 => 'XMP', 1 => 'XMP-GSpherical', 2 => 'Image' },
170             WRITE_GROUP => 'GSpherical', # write in special location for video files
171             NAMESPACE => 'GSpherical',
172             AVOID => 1,
173             NOTES => q{
174             Not actually XMP. These RDF/XML tags are used in Google spherical MP4
175             videos. These tags are written into the video track of MOV/MP4 files, and
176             not at the top level like other XMP tags. See
177             L
178             for the specification.
179             },
180             # (avoid due to conflicts with XMP-GPano tags)
181             Spherical => { Writable => 'boolean' },
182             Stitched => { Writable => 'boolean' },
183             StitchingSoftware => { },
184             ProjectionType => { },
185             StereoMode => { },
186             SourceCount => { Writable => 'integer' },
187             InitialViewHeadingDegrees => { Writable => 'real' },
188             InitialViewPitchDegrees => { Writable => 'real' },
189             InitialViewRollDegrees => { Writable => 'real' },
190             Timestamp => {
191             Name => 'TimeStamp',
192             Groups => { 2 => 'Time' },
193             Writable => 'integer',
194             Shift => 'Time',
195             ValueConv => 'ConvertUnixTime($val)', #(NC)
196             ValueConvInv => 'GetUnixTime($val)',
197             PrintConv => '$self->ConvertDateTime($val)',
198             PrintConvInv => '$self->InverseDateTime($val)',
199             },
200             FullPanoWidthPixels => { Writable => 'integer' },
201             FullPanoHeightPixels => { Writable => 'integer' },
202             CroppedAreaImageWidthPixels => { Writable => 'integer' },
203             CroppedAreaImageHeightPixels=> { Writable => 'integer' },
204             CroppedAreaLeftPixels => { Writable => 'integer' },
205             CroppedAreaTopPixels => { Writable => 'integer' },
206             );
207              
208             # Google depthmap information (ref https://developers.google.com/depthmap-metadata/reference)
209             %Image::ExifTool::Google::GDepth = (
210             GROUPS => { 0 => 'XMP', 1 => 'XMP-GDepth', 2 => 'Image' },
211             NAMESPACE => 'GDepth',
212             AVOID => 1, # (too many potential tag name conflicts)
213             NOTES => q{
214             Google depthmap information. See
215             L for the specification.
216             },
217             WRITABLE => 'string', # (default to string-type tags)
218             PRIORITY => 0,
219             Format => {
220             PrintConv => {
221             RangeInverse => 'RangeInverse',
222             RangeLinear => 'RangeLinear',
223             },
224             },
225             Near => { Writable => 'real' },
226             Far => { Writable => 'real' },
227             Mime => { },
228             Data => {
229             Name => 'DepthImage',
230             ValueConv => 'Image::ExifTool::XMP::DecodeBase64($val)',
231             ValueConvInv => 'Image::ExifTool::XMP::EncodeBase64($val)',
232             },
233             Units => { },
234             MeasureType => {
235             PrintConv => {
236             OpticalAxis => 'OpticalAxis',
237             OpticalRay => 'OpticalRay',
238             },
239             },
240             ConfidenceMime => { },
241             Confidence => {
242             ValueConv => 'Image::ExifTool::XMP::DecodeBase64($val)',
243             ValueConvInv => 'Image::ExifTool::XMP::EncodeBase64($val)',
244             },
245             Manufacturer=> { },
246             Model => { },
247             Software => { },
248             ImageWidth => { Writable => 'real' },
249             ImageHeight => { Writable => 'real' },
250             );
251              
252             # Google focus namespace
253             %Image::ExifTool::Google::GFocus = (
254             %Image::ExifTool::XMP::xmpTableDefaults,
255             GROUPS => { 0 => 'XMP', 1 => 'XMP-GFocus', 2 => 'Image' },
256             NAMESPACE => 'GFocus',
257             NOTES => 'Focus information found in Google depthmap images.',
258             BlurAtInfinity => { Writable => 'real' },
259             FocalDistance => { Writable => 'real' },
260             FocalPointX => { Writable => 'real' },
261             FocalPointY => { Writable => 'real' },
262             );
263              
264             # Google camera namespace (ref PH)
265             %Image::ExifTool::Google::GCamera = (
266             %Image::ExifTool::XMP::xmpTableDefaults,
267             GROUPS => { 0 => 'XMP', 1 => 'XMP-GCamera', 2 => 'Camera' },
268             NAMESPACE => 'GCamera',
269             NOTES => 'Camera information found in Google panorama images.',
270             BurstID => { },
271             BurstPrimary => { },
272             PortraitNote => { },
273             PortraitRequest => {
274             Writable => 'string', # (writable in encoded format)
275             Binary => 1,
276             SubDirectory => { TagTable => 'Image::ExifTool::Google::HDRPMakerNote' },
277             },
278             PortraitVersion => { },
279             SpecialTypeID => { List => 'Bag' },
280             PortraitNote => { },
281             DisableAutoCreation => { List => 'Bag' },
282             DisableSuggestedAction => { List => 'Bag' }, #forum16147
283             hdrp_makernote => {
284             Name => 'HDRPMakerNote',
285             Writable => 'string', # (writable in encoded format)
286             Binary => 1,
287             SubDirectory => { TagTable => 'Image::ExifTool::Google::HDRPMakerNote' },
288             },
289             MicroVideo => { Writable => 'integer' },
290             MicroVideoVersion => { Writable => 'integer' },
291             MicroVideoOffset => { Writable => 'integer' },
292             MicroVideoPresentationTimestampUs => { Writable => 'integer' },
293             shot_log_data => { #forum14108
294             Name => 'ShotLogData',
295             Writable => 'string', # (writable in encoded format)
296             IsProtobuf => 1,
297             Binary => 1,
298             SubDirectory => { TagTable => 'Image::ExifTool::Google::ShotLogData' },
299             },
300             HdrPlusMakernote => {
301             Name => 'HDRPlusMakerNote',
302             Writable => 'string', # (writable in encoded format)
303             Binary => 1,
304             SubDirectory => { TagTable => 'Image::ExifTool::Google::HDRPlusMakerNote' },
305             },
306             MotionPhoto => { Writable => 'integer' },
307             MotionPhotoVersion => { Writable => 'integer' },
308             MotionPhotoPresentationTimestampUs => { Writable => 'integer' },
309             );
310              
311             # Google creations namespace (ref PH)
312             %Image::ExifTool::Google::GCreations = (
313             %Image::ExifTool::XMP::xmpTableDefaults,
314             GROUPS => { 0 => 'XMP', 1 => 'XMP-GCreations', 2 => 'Camera' },
315             NAMESPACE => 'GCreations',
316             NOTES => 'Google creations tags.',
317             CameraBurstID => { },
318             Type => { Avoid => 1 },
319             );
320              
321             # Google depth-map Device namespace (ref 13)
322             %Image::ExifTool::Google::Device = (
323             %Image::ExifTool::XMP::xmpTableDefaults,
324             GROUPS => { 0 => 'XMP', 1 => 'XMP-Device', 2 => 'Camera' },
325             NAMESPACE => { Device => 'http://ns.google.com/photos/dd/1.0/device/' },
326             NOTES => q{
327             Google depth-map Device tags. See
328             L for
329             the specification.
330             },
331             Container => {
332             Struct => {
333             STRUCT_NAME => 'Google DeviceContainer',
334             NAMESPACE => { Container => 'http://ns.google.com/photos/dd/1.0/container/' },
335             Directory => {
336             List => 'Seq',
337             Struct => {
338             STRUCT_NAME => 'Google DeviceDirectory',
339             NAMESPACE => { Container => 'http://ns.google.com/photos/dd/1.0/container/' },
340             Item => {
341             Struct => {
342             STRUCT_NAME => 'Google DeviceItem',
343             NAMESPACE => { Item => 'http://ns.google.com/photos/dd/1.0/item/' },
344             # use this as a key to process Google trailer
345             Mime => { RawConv => '$$self{ProcessGoogleTrailer} = $val' },
346             Length => { Writable => 'integer' },
347             Padding => { Writable => 'integer' },
348             DataURI => { },
349             },
350             },
351             },
352             }
353             },
354             },
355             Profiles => {
356             List => 'Seq',
357             FlatName => '',
358             Struct => {
359             STRUCT_NAME => 'Google DeviceProfiles',
360             NAMESPACE => { Device => 'http://ns.google.com/photos/dd/1.0/device/' },
361             Profile => {
362             Struct => {
363             STRUCT_NAME => 'Google DeviceProfile',
364             NAMESPACE => { Profile => 'http://ns.google.com/photos/dd/1.0/profile/' },
365             CameraIndices => { List => 'Seq', Writable => 'integer' },
366             Type => { },
367             },
368             },
369             },
370             },
371             Cameras => {
372             List => 'Seq',
373             FlatName => '',
374             Struct => {
375             STRUCT_NAME => 'Google DeviceCameras',
376             NAMESPACE => { Device => 'http://ns.google.com/photos/dd/1.0/device/' },
377             Camera => {
378             Struct => {
379             STRUCT_NAME => 'Google DeviceCamera',
380             NAMESPACE => { Camera => 'http://ns.google.com/photos/dd/1.0/camera/' },
381             DepthMap => {
382             Struct => {
383             STRUCT_NAME => 'Google DeviceDepthMap',
384             NAMESPACE => { DepthMap => 'http://ns.google.com/photos/dd/1.0/depthmap/' },
385             ConfidenceURI => { },
386             DepthURI => { },
387             Far => { Writable => 'real' },
388             Format => { },
389             ItemSemantic=> { },
390             MeasureType => { },
391             Near => { Writable => 'real' },
392             Units => { },
393             Software => { },
394             FocalTableEntryCount => { Writable => 'integer' },
395             FocalTable => { }, # (base64)
396             },
397             },
398             Image => {
399             Struct => {
400             STRUCT_NAME => 'Google DeviceImage',
401             NAMESPACE => { Image => 'http://ns.google.com/photos/dd/1.0/image/' },
402             ItemSemantic=> { },
403             ItemURI => { },
404             },
405             },
406             ImagingModel => {
407             Struct => {
408             STRUCT_NAME => 'Google DeviceImagingModel',
409             NAMESPACE => { ImagingModel => 'http://ns.google.com/photos/dd/1.0/imagingmodel/' },
410             Distortion => { }, # (base64)
411             DistortionCount => { Writable => 'integer' },
412             FocalLengthX => { Writable => 'real' },
413             FocalLengthY => { Writable => 'real' },
414             ImageHeight => { Writable => 'integer' },
415             ImageWidth => { Writable => 'integer' },
416             PixelAspectRatio=> { Writable => 'real' },
417             PrincipalPointX => { Writable => 'real' },
418             PrincipalPointY => { Writable => 'real' },
419             Skew => { Writable => 'real' },
420             },
421             },
422             PointCloud => {
423             Struct => {
424             STRUCT_NAME => 'Google DevicePointCloud',
425             NAMESPACE => { PointCloud => 'http://ns.google.com/photos/dd/1.0/pointcloud/' },
426             PointCloud => { Writable => 'integer' },
427             Points => { },
428             Metric => { Writable => 'boolean' },
429             },
430             },
431             Pose => { Struct => \%sPose },
432             LightEstimate => {
433             Struct => {
434             STRUCT_NAME => 'Google DeviceLightEstimate',
435             NAMESPACE => { LightEstimate => 'http://ns.google.com/photos/dd/1.0/lightestimate/' },
436             ColorCorrectionR => { Writable => 'real' },
437             ColorCorrectionG => { Writable => 'real' },
438             ColorCorrectionB => { Writable => 'real' },
439             PixelIntensity => { Writable => 'real' },
440             },
441             },
442             VendorInfo => { Struct => \%sVendorInfo },
443             AppInfo => { Struct => \%sAppInfo },
444             Trait => { },
445             },
446             },
447             },
448             },
449             VendorInfo => { Struct => \%sVendorInfo },
450             AppInfo => { Struct => \%sAppInfo },
451             EarthPos => { Struct => \%sEarthPose },
452             Pose => { Struct => \%sPose },
453             Planes => {
454             List => 'Seq',
455             FlatName => '',
456             Struct => {
457             STRUCT_NAME => 'Google DevicePlanes',
458             NAMESPACE => { Device => 'http://ns.google.com/photos/dd/1.0/device/' },
459             Plane => {
460             Struct => {
461             STRUCT_NAME => 'Google DevicePlane',
462             NAMESPACE => { Plane => 'http://ns.google.com/photos/dd/1.0/plane/' },
463             Pose => { Struct => \%sPose },
464             ExtentX => { Writable => 'real' },
465             ExtentZ => { Writable => 'real' },
466             BoundaryVertexCount => { Writable => 'integer' },
467             Boundary => { },
468             },
469             },
470             },
471             },
472             );
473              
474             # Google container tags (ref https://developer.android.com/guide/topics/media/platform/hdr-image-format)
475             # NOTE: The namespace prefix used by ExifTool is 'GContainer' instead of 'Container'
476             # dueo to a conflict with Google's depth-map Device 'Container' namespace!
477             # (see ../pics/GooglePixel8Pro.jpg sample image)
478             %Image::ExifTool::Google::GContainer = (
479             %Image::ExifTool::XMP::xmpTableDefaults,
480             GROUPS => { 0 => 'XMP', 1 => 'XMP-GContainer', 2 => 'Image' },
481             NAMESPACE => 'GContainer',
482             NOTES => q{
483             Google Container namespace. ExifTool uses the prefix 'GContainer' instead
484             of 'Container' to avoid a conflict with the Google Device Container
485             namespace.
486             },
487             Directory => {
488             Name => 'ContainerDirectory',
489             FlatName => 'Directory',
490             List => 'Seq',
491             Struct => {
492             STRUCT_NAME => 'Google Directory',
493             Item => {
494             Namespace => 'GContainer',
495             Struct => {
496             STRUCT_NAME => 'Google Item',
497             # (use 'GItem' to avoid conflict with Google Device Container Item)
498             NAMESPACE => { GItem => 'http://ns.google.com/photos/1.0/container/item/'},
499             Mime => { RawConv => '$$self{ProcessGoogleTrailer} = $val' },
500             Semantic => { },
501             Length => { Writable => 'integer' },
502             Label => { },
503             Padding => { Writable => 'integer' },
504             URI => { },
505             },
506             },
507             },
508             },
509             );
510              
511             # DHRP maker notes (ref PH)
512             %Image::ExifTool::Google::HDRPlusMakerNote = (
513             GROUPS => { 0 => 'MakerNotes', 2 => 'Image' },
514             TAG_PREFIX => 'HDRPlusMakerNote',
515             PROCESS_PROC => \&ProcessHDRP,
516             VARS => {
517             ID_FMT => 'str',
518             SORT_PROC => sub {
519             my ($a,$b) = @_;
520             $a =~ s/(\d+)/sprintf("%.3d",$1)/eg;
521             $b =~ s/(\d+)/sprintf("%.3d",$1)/eg;
522             return $a cmp $b;
523             },
524             },
525             NOTES => q{
526             Google protobuf-format HDR-Plus maker notes. Tag ID's are hierarchical
527             protobuf field numbers. Stored as base64-encoded, encrypted and gzipped
528             Protobuf data. Much of this metadata is still unknown, but is extracted
529             using the Unknown option.
530             },
531             '1-1' => 'ImageName',
532             '1-2' => { Name => 'ImageData', Format => 'undef', Binary => 1 },
533             '2' => { Name => 'TimeLogText', Binary => 1 },
534             '3' => { Name => 'SummaryText', Binary => 1 },
535             '9-3' => { Name => 'FrameCount', Format => 'unsigned' },
536             # 9-4 - smaller for larger focal lengths
537             '9-36-1' => {
538             Name => 'CreateDate',
539             Groups => { 2 => 'Time' },
540             Format => 'unsigned',
541             Priority => 0, # (to give EXIF priority)
542             ValueConv => 'ConvertUnixTime($val, 1)',
543             PrintConv => '$self->ConvertDateTime($val)',
544             },
545             '12-1' => { Name => 'DeviceMake', Groups => { 2 => 'Device' } },
546             '12-2' => { Name => 'DeviceModel', Groups => { 2 => 'Device' } },
547             '12-3' => { Name => 'DeviceCodename', Groups => { 2 => 'Device' } },
548             '12-4' => { Name => 'DeviceHardwareRevision', Groups => { 2 => 'Device' } },
549             '12-6' => { Name => 'HDRPSoftware', Groups => { 2 => 'Device' } },
550             '12-7' => { Name => 'AndroidRelease', Groups => { 2 => 'Device' } },
551             '12-8' => {
552             Name => 'SoftwareDate',
553             Groups => { 2 => 'Time' },
554             Format => 'unsigned',
555             ValueConv => 'ConvertUnixTime($val / 1000, 1, 3)',
556             PrintConv => '$self->ConvertDateTime($val)',
557             },
558             '12-9' => { Name => 'Application', Groups => { 2 => 'Device' } },
559             '12-10' => { Name => 'AppVersion', Groups => { 2 => 'Device' } },
560             '12-12-1' => {
561             Name => 'ExposureTimeMin',
562             Groups => { 2 => 'Camera' },
563             Format => 'float',
564             ValueConv => '$val / 1000',
565             },
566             '12-12-2' => {
567             Name => 'ExposureTimeMax',
568             Groups => { 2 => 'Camera' },
569             Format => 'float',
570             ValueConv => '$val / 1000',
571             },
572             '12-13-1' => { Name => 'ISOMin', Format => 'float', Groups => { 2 => 'Camera' } }, # (NC)
573             '12-13-2' => { Name => 'ISOMax', Format => 'float', Groups => { 2 => 'Camera' } }, # (NC)
574             '12-14' => { Name => 'MaxAnalogISO', Format => 'float', Groups => { 2 => 'Camera' } }, # (NC)
575             );
576              
577             %Image::ExifTool::Google::ShotLogData = (
578             GROUPS => { 0 => 'MakerNotes', 2 => 'Image' },
579             TAG_PREFIX => 'ShotLogData',
580             PROCESS_PROC => \&ProcessHDRP,
581             VARS => { ID_FMT => 'str' },
582             NOTES => 'Stored as base64-encoded, encrypted and gzipped Protobuf data.',
583             2 => { Name => 'MeteringFrameCount', Format => 'unsigned' }, # (NC)
584             3 => { Name => 'OriginalPayloadFrameCount', Format => 'unsigned' }, # (NC)
585             # 1-6 - pure_fraction_of_pixels_from_long_exposure?
586             # 1-7 - weighted_fraction_of_pixels_from_long_exposure?
587             );
588              
589             %Image::ExifTool::Google::HDRPMakerNote = (
590             GROUPS => { 0 => 'MakerNotes', 2 => 'Image' },
591             TAG_PREFIX => '',
592             PROCESS_PROC => \&ProcessHDRP,
593             VARS => { LONG_TAGS => 1 },
594             NOTES => q{
595             Google text-based HDRP maker note tags. Stored as base64-encoded,
596             encrypted and gzipped text.
597             },
598             'InitParams' => { Name => 'InitParamsText', Binary => 1 },
599             'Logging metadata' => { Name => 'LoggingMetadataText', Binary => 1 },
600             'Merged image' => { Name => 'MergedImage', Binary => 1 },
601             'Finished image' => { Name => 'FinishedImage', Binary => 1 },
602             'Payload frame' => { Name => 'PayloadFrame', Binary => 1 },
603             'Payload metadata' => { Name => 'PayloadMetadataText', Binary => 1 },
604             'ShotLogData' => { Name => 'ShotLogDataText', Binary => 1 },
605             'ShotParams' => { Name => 'ShotParamsText', Binary => 1 },
606             'StaticMetadata' => { Name => 'StaticMetadataText', Binary => 1 },
607             'Summary' => { Name => 'SummaryText', Binary => 1 },
608             'Time log' => { Name => 'TimeLogText', Binary => 1 },
609             'Unused logging metadata' => { Name => 'UnusedLoggingMetadata', Binary => 1 },
610             'Rectiface' => { Name => 'RectifaceText', Binary => 1 },
611             'GoudaRequest' => { Name => 'GoudaRequestText', Binary => 1 },
612             ProcessingNotes => { },
613             );
614              
615             %Image::ExifTool::Google::PortraitReq = (
616             GROUPS => { 0 => 'MakerNotes', 2 => 'Image' },
617             TAG_PREFIX => '',
618             PROCESS_PROC => \&ProcessHDRP,
619             NOTES => q{
620             Google text-based PortraitRequest information. Stored as base64-encoded,
621             encrypted and gzipped text.
622             },
623             );
624              
625             #------------------------------------------------------------------------------
626             # Read HDRP text-format maker note version 2
627             # Inputs: 0) ExifTool ref, 1) data ref
628             sub ProcessHDRPMakerNote($$)
629             {
630 0     0 0 0 my ($et, $dataPt) = @_;
631 0         0 my ($tag, $dat, $pos);
632 0         0 my $tagTbl = GetTagTable('Image::ExifTool::Google::HDRPMakerNote');
633 0         0 for (;;) {
634 0         0 my ($end, $last);
635 0 0       0 if ($$dataPt =~ /^ ?([A-Z].*)$/mg) {
636 0         0 $end = pos($$dataPt) - length($1);
637             } else {
638 0         0 $end = length($$dataPt);
639 0         0 $last = 1;
640             }
641 0 0       0 if ($tag) {
642 0         0 my $len = $end - ($pos + 1);
643 0 0       0 last if $len <= 0; # (just to be safe)
644 0         0 $et->HandleTag($tagTbl, $tag, substr($$dataPt, $pos + 1, $len), MakeTagInfo => 1);
645             }
646 0 0       0 last if $last;
647 0         0 $tag = $1;
648 0 0 0     0 unless ($tag =~ /:/ or $tag =~ /^\w+$/) {
649 0         0 $et->HandleTag($tagTbl, 'ProcessingNotes', $tag);
650 0         0 undef $tag;
651 0         0 next;
652             }
653 0         0 $pos = pos $$dataPt;
654 0 0 0     0 if ($tag =~ s/( \(base64\))?: ?(.*)// and $2) {
655 0         0 my $dat = $2;
656 0 0       0 $dat = Image::ExifTool::XMP::DecodeBase64($dat) if $1;
657 0         0 $et->HandleTag($tagTbl, $tag, $dat, MakeTagInfo => 1);
658 0         0 undef $tag;
659             }
660             }
661             }
662              
663             #------------------------------------------------------------------------------
664             # Decode HDRP maker notes (ref https://github.com/jakiki6/ruminant/blob/master/ruminant/modules/images.py)
665             # Inputs: 0) ExifTool ref, 1) base64-encoded string, 2) tagInfo ref
666             # Returns: reference to decoded+decrypted+gunzipped data
667             # - also extracts protobuf info as separate tags when Unknown used if $$tagInfo{IsProtobuf} is set
668             sub ProcessHDRP($$$)
669             {
670 1     1 0 5 my ($et, $dirInfo, $tagTbl) = @_;
671 1         4 my $dataPt = $$dirInfo{DataPt};
672 1         4 my $tagInfo = $$dirInfo{TagInfo};
673 1 50       6 my $tagName = $tagInfo ? $$tagInfo{Name} : '';
674 1         6 my $verbose = $et->Options('Verbose');
675 1   50     5 my $fast = $et->Options('FastScan') || 0;
676 1         3 my ($ver, $valPt);
677              
678 1 50       5 return undef if $fast > 1;
679              
680 1 50       6 if ($$dirInfo{DirStart}) {
681 0         0 my $dat = substr($$dataPt, $$dirInfo{DirStart}, $$dirInfo{DirLen});
682 0         0 $dataPt = \$dat;
683             }
684 1 50       7 if ($$dataPt =~ /^HDRP[\x02\x03]/) {
685 0         0 $valPt = $dataPt;
686             } else {
687 1         8 $et->VerboseDir($tagName, undef, length($$dataPt));
688 1 50       4 $et->VerboseDump($dataPt) if $verbose > 2;
689 1         5 $valPt = Image::ExifTool::XMP::DecodeBase64($$dataPt);
690 1 50       4 if ($verbose > 2) {
691 0         0 $et->VerboseDir("Base64-decoded $tagName", undef, length($$valPt));
692 0         0 $et->VerboseDump($valPt);
693             }
694             }
695 1 50       32 if ($$valPt =~ s/^HDRP([\x02\x03])//) {
696 1         3 $ver = ord($1);
697             } else {
698 0         0 $et->Warn('Unrecognized HDRP format');
699 0         0 return undef;
700             }
701 1         4 my $pad = (8 - (length($$valPt) % 8)) & 0x07;
702 1 50       3 $$valPt .= "\0" x $pad if $pad; # pad to an even 8 bytes
703 1         725 my @words = unpack('V*', $$valPt);
704             # my $key = 0x2515606b4a7791cd;
705 1         32 my ($hi, $lo) = ( 0x2515606b, 0x4a7791cd );
706 1         3 my $i = 0;
707 1         7 while ($i < @words) {
708             # (messy, but handle all 64-bit arithmetic with 32-bit backward
709             # compatibility, so no bit operations on any number > 0xffffffff)
710             # rotate the key for each new 64-bit word
711             # $key ^= $key >> 12;
712 3595         6979 $lo ^= $lo >> 12 | ($hi & 0xfff) << 20;
713 3595         5744 $hi ^= $hi >> 12;
714             # $key ^= ($key << 25) & 0xffffffffffffffff;
715 3595         6594 $hi ^= ($hi & 0x7f) << 25 | $lo >> 7;
716 3595         5755 $lo ^= ($lo & 0x7f) << 25;
717             # $key ^= ($key >> 27) & 0xffffffffffffffff;
718 3595         6151 $lo ^= $lo >> 27 | ($hi & 0x7ffffff) << 5;
719 3595         5766 $hi ^= $hi >> 27;
720             # $key = ($key * 0x2545f4914f6cdd1d) & 0xffffffffffffffff;
721             # (multiply using 16-bit math to avoid overflowing 32-bit integers)
722 3595         11444 my @a = unpack('n*', pack('N*', $hi, $lo));
723 3595         7192 my @b = (0x2545, 0xf491, 0x4f6c, 0xdd1d);
724 3595         7744 my @c = (0) x 7;
725 3595         6170 my ($j, $k);
726 3595         8081 for ($j=0; $j<4; ++$j) {
727 14380         28426 for ($k=0; $k<4; ++$k) {
728 57520         131543 $c[$j+$k] += $a[$j] * $b[$k];
729             }
730             }
731             # (we will only retain the low 64-bits of the key, so
732             # don't bother finishing the calculation of the upper bits)
733 3595         7920 for ($j=6; $j>=3; --$j) {
734 14380         29863 while ($c[$j] > 0xffffffff) {
735 4557         7038 ++$c[$j-2];
736 4557         9687 $c[$j] -= 4294967296;
737             }
738 14380         24630 $c[$j-1] += $c[$j] >> 16;
739 14380         30299 $c[$j] &= 0xffff;
740             }
741 3595         6112 $hi = ($c[3] << 16) + $c[4];
742 3595         5781 $lo = ($c[5] << 16) + $c[6];
743             # apply the key to this 64-bit word
744 3595         6079 $words[$i++] ^= $lo;
745 3595         11807 $words[$i++] ^= $hi;
746             }
747 1         5 my $result;
748 1         312 my $val = pack('V*', @words);
749 1 50       10 $val = substr($val,0,-$pad) if $pad; # remove padding
750 1 50       6 if ($verbose > 2) {
751 0         0 $et->VerboseDir("Decrypted $tagName", undef, length($val));
752 0         0 $et->VerboseDump(\$val);
753             }
754 1 50       4 if (eval { require IO::Uncompress::Gunzip }) {
  1         23  
755 1         3 my $buff;
756 1 50       11 if (IO::Uncompress::Gunzip::gunzip(\$val, \$buff)) {
757 1 50       7031 if ($verbose > 2) {
758 0         0 $et->VerboseDir("Gunzipped $tagName", undef, length($buff));
759 0         0 $et->VerboseDump(\$buff);
760             }
761 1 50 0     7 if ($ver == 3 or ($tagInfo and $$tagInfo{IsProtobuf})) {
      33        
762 1         6 my %dirInfo = (
763             DataPt => \$buff,
764             DirName => $tagName,
765             );
766 1         736 require Image::ExifTool::Protobuf;
767 1         8 Image::ExifTool::Protobuf::ProcessProtobuf($et, \%dirInfo, $tagTbl);
768             } else {
769 0         0 ProcessHDRPMakerNote($et, \$buff);
770             }
771 1         5 $result = \$buff;
772             } else {
773 0         0 $et->Warn("Error inflating stream: $IO::Uncompress::Gunzip::GunzipError");
774             }
775             } else {
776 0         0 $et->Warn('Install IO::Uncompress::Gunzip to decode HDRP makernote');
777             }
778 1         107 return $result;
779             }
780              
781             1; # end
782              
783             __END__