1 | #------------------------------------------------------------------------------
|
---|
2 | # File: Photoshop.pm
|
---|
3 | #
|
---|
4 | # Description: Read/write Photoshop IRB meta information
|
---|
5 | #
|
---|
6 | # Revisions: 02/06/2004 - P. Harvey Created
|
---|
7 | # 02/25/2004 - P. Harvey Added hack for problem with old photoshops
|
---|
8 | # 10/04/2004 - P. Harvey Added a bunch of tags (ref Image::MetaData::JPEG)
|
---|
9 | # but left most of them commented out until I have enough
|
---|
10 | # information to write PrintConv routines for them to
|
---|
11 | # display something useful
|
---|
12 | # 07/08/2005 - P. Harvey Added support for reading PSD files
|
---|
13 | # 01/07/2006 - P. Harvey Added PSD write support
|
---|
14 | # 11/04/2006 - P. Harvey Added handling of resource name
|
---|
15 | #
|
---|
16 | # References: 1) http://www.fine-view.com/jp/lab/doc/ps6ffspecsv2.pdf
|
---|
17 | # 2) http://www.ozhiker.com/electronics/pjmt/jpeg_info/irb_jpeg_qual.html
|
---|
18 | # 3) Matt Mueller private communication (tests with PS CS2)
|
---|
19 | # 4) http://www.fileformat.info/format/psd/egff.htm
|
---|
20 | # 5) http://www.telegraphics.com.au/svn/psdparse/trunk/resources.c
|
---|
21 | # 6) http://libpsd.graphest.com/files/Photoshop%20File%20Formats.pdf
|
---|
22 | # 7) http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/
|
---|
23 | #------------------------------------------------------------------------------
|
---|
24 |
|
---|
25 | package Image::ExifTool::Photoshop;
|
---|
26 |
|
---|
27 | use strict;
|
---|
28 | use vars qw($VERSION $AUTOLOAD $iptcDigestInfo);
|
---|
29 | use Image::ExifTool qw(:DataAccess :Utils);
|
---|
30 |
|
---|
31 | $VERSION = '1.65';
|
---|
32 |
|
---|
33 | sub ProcessPhotoshop($$$);
|
---|
34 | sub WritePhotoshop($$$);
|
---|
35 | sub ProcessLayers($$$);
|
---|
36 |
|
---|
37 | # map of where information is stored in PSD image
|
---|
38 | my %psdMap = (
|
---|
39 | IPTC => 'Photoshop',
|
---|
40 | XMP => 'Photoshop',
|
---|
41 | EXIFInfo => 'Photoshop',
|
---|
42 | IFD0 => 'EXIFInfo',
|
---|
43 | IFD1 => 'IFD0',
|
---|
44 | ICC_Profile => 'Photoshop',
|
---|
45 | ExifIFD => 'IFD0',
|
---|
46 | GPS => 'IFD0',
|
---|
47 | SubIFD => 'IFD0',
|
---|
48 | GlobParamIFD => 'IFD0',
|
---|
49 | PrintIM => 'IFD0',
|
---|
50 | InteropIFD => 'ExifIFD',
|
---|
51 | MakerNotes => 'ExifIFD',
|
---|
52 | );
|
---|
53 |
|
---|
54 | # tag information for PhotoshopThumbnail and PhotoshopBGRThumbnail
|
---|
55 | my %thumbnailInfo = (
|
---|
56 | Writable => 'undef',
|
---|
57 | Protected => 1,
|
---|
58 | RawConv => 'my $img=substr($val,0x1c); $self->ValidateImage(\$img,$tag)',
|
---|
59 | ValueConvInv => q{
|
---|
60 | my $et = new Image::ExifTool;
|
---|
61 | my @tags = qw{ImageWidth ImageHeight FileType};
|
---|
62 | my $info = $et->ImageInfo(\$val, @tags);
|
---|
63 | my ($w, $h, $type) = @$info{@tags};
|
---|
64 | $w and $h and $type eq 'JPEG' or warn("Not a valid JPEG image\n"), return undef;
|
---|
65 | my $wbytes = int(($w * 24 + 31) / 32) * 4;
|
---|
66 | return pack('N6n2', 1, $w, $h, $wbytes, $wbytes * $h, length($val), 24, 1) . $val;
|
---|
67 | },
|
---|
68 | );
|
---|
69 |
|
---|
70 | # tag info to decode Photoshop Unicode string
|
---|
71 | my %unicodeString = (
|
---|
72 | ValueConv => sub {
|
---|
73 | my ($val, $et) = @_;
|
---|
74 | return '<err>' if length($val) < 4;
|
---|
75 | my $len = unpack('N', $val) * 2;
|
---|
76 | return '<err>' if length($val) < 4 + $len;
|
---|
77 | return $et->Decode(substr($val, 4, $len), 'UCS2', 'MM');
|
---|
78 | },
|
---|
79 | ValueConvInv => sub {
|
---|
80 | my ($val, $et) = @_;
|
---|
81 | return pack('N', length $val) . $et->Encode($val, 'UCS2', 'MM');
|
---|
82 | },
|
---|
83 | );
|
---|
84 |
|
---|
85 | # Photoshop APP13 tag table
|
---|
86 | # (set Unknown flag for information we don't want to display normally)
|
---|
87 | %Image::ExifTool::Photoshop::Main = (
|
---|
88 | GROUPS => { 2 => 'Image' },
|
---|
89 | PROCESS_PROC => \&ProcessPhotoshop,
|
---|
90 | WRITE_PROC => \&WritePhotoshop,
|
---|
91 | 0x03e8 => { Unknown => 1, Name => 'Photoshop2Info' },
|
---|
92 | 0x03e9 => { Unknown => 1, Name => 'MacintoshPrintInfo' },
|
---|
93 | 0x03ea => { Unknown => 1, Name => 'XMLData', Binary => 1 }, #PH
|
---|
94 | 0x03eb => { Unknown => 1, Name => 'Photoshop2ColorTable' },
|
---|
95 | 0x03ed => {
|
---|
96 | Name => 'ResolutionInfo',
|
---|
97 | SubDirectory => {
|
---|
98 | TagTable => 'Image::ExifTool::Photoshop::Resolution',
|
---|
99 | },
|
---|
100 | },
|
---|
101 | 0x03ee => {
|
---|
102 | Name => 'AlphaChannelsNames',
|
---|
103 | ValueConv => 'Image::ExifTool::Photoshop::ConvertPascalString($self,$val)',
|
---|
104 | },
|
---|
105 | 0x03ef => { Unknown => 1, Name => 'DisplayInfo' },
|
---|
106 | 0x03f0 => { Unknown => 1, Name => 'PStringCaption' },
|
---|
107 | 0x03f1 => { Unknown => 1, Name => 'BorderInformation' },
|
---|
108 | 0x03f2 => { Unknown => 1, Name => 'BackgroundColor' },
|
---|
109 | 0x03f3 => { Unknown => 1, Name => 'PrintFlags', Format => 'int8u' },
|
---|
110 | 0x03f4 => { Unknown => 1, Name => 'BW_HalftoningInfo' },
|
---|
111 | 0x03f5 => { Unknown => 1, Name => 'ColorHalftoningInfo' },
|
---|
112 | 0x03f6 => { Unknown => 1, Name => 'DuotoneHalftoningInfo' },
|
---|
113 | 0x03f7 => { Unknown => 1, Name => 'BW_TransferFunc' },
|
---|
114 | 0x03f8 => { Unknown => 1, Name => 'ColorTransferFuncs' },
|
---|
115 | 0x03f9 => { Unknown => 1, Name => 'DuotoneTransferFuncs' },
|
---|
116 | 0x03fa => { Unknown => 1, Name => 'DuotoneImageInfo' },
|
---|
117 | 0x03fb => { Unknown => 1, Name => 'EffectiveBW', Format => 'int8u' },
|
---|
118 | 0x03fc => { Unknown => 1, Name => 'ObsoletePhotoshopTag1' },
|
---|
119 | 0x03fd => { Unknown => 1, Name => 'EPSOptions' },
|
---|
120 | 0x03fe => { Unknown => 1, Name => 'QuickMaskInfo' },
|
---|
121 | 0x03ff => { Unknown => 1, Name => 'ObsoletePhotoshopTag2' },
|
---|
122 | 0x0400 => { Unknown => 1, Name => 'TargetLayerID', Format => 'int16u' }, # (LayerStateInfo)
|
---|
123 | 0x0401 => { Unknown => 1, Name => 'WorkingPath' },
|
---|
124 | 0x0402 => { Unknown => 1, Name => 'LayersGroupInfo', Format => 'int16u' },
|
---|
125 | 0x0403 => { Unknown => 1, Name => 'ObsoletePhotoshopTag3' },
|
---|
126 | 0x0404 => {
|
---|
127 | Name => 'IPTCData',
|
---|
128 | SubDirectory => {
|
---|
129 | DirName => 'IPTC',
|
---|
130 | TagTable => 'Image::ExifTool::IPTC::Main',
|
---|
131 | },
|
---|
132 | },
|
---|
133 | 0x0405 => { Unknown => 1, Name => 'RawImageMode' },
|
---|
134 | 0x0406 => { #2
|
---|
135 | Name => 'JPEG_Quality',
|
---|
136 | SubDirectory => {
|
---|
137 | TagTable => 'Image::ExifTool::Photoshop::JPEG_Quality',
|
---|
138 | },
|
---|
139 | },
|
---|
140 | 0x0408 => { Unknown => 1, Name => 'GridGuidesInfo' },
|
---|
141 | 0x0409 => {
|
---|
142 | Name => 'PhotoshopBGRThumbnail',
|
---|
143 | Notes => 'this is a JPEG image, but in BGR format instead of RGB',
|
---|
144 | %thumbnailInfo,
|
---|
145 | Groups => { 2 => 'Preview' },
|
---|
146 | },
|
---|
147 | 0x040a => {
|
---|
148 | Name => 'CopyrightFlag',
|
---|
149 | Writable => 'int8u',
|
---|
150 | Groups => { 2 => 'Author' },
|
---|
151 | ValueConv => 'join(" ",unpack("C*", $val))',
|
---|
152 | ValueConvInv => 'pack("C*",split(" ",$val))',
|
---|
153 | PrintConv => { #3
|
---|
154 | 0 => 'False',
|
---|
155 | 1 => 'True',
|
---|
156 | },
|
---|
157 | },
|
---|
158 | 0x040b => {
|
---|
159 | Name => 'URL',
|
---|
160 | Writable => 'string',
|
---|
161 | Groups => { 2 => 'Author' },
|
---|
162 | },
|
---|
163 | 0x040c => {
|
---|
164 | Name => 'PhotoshopThumbnail',
|
---|
165 | %thumbnailInfo,
|
---|
166 | Groups => { 2 => 'Preview' },
|
---|
167 | },
|
---|
168 | 0x040d => {
|
---|
169 | Name => 'GlobalAngle',
|
---|
170 | Writable => 'int32u',
|
---|
171 | ValueConv => 'unpack("N",$val)',
|
---|
172 | ValueConvInv => 'pack("N",$val)',
|
---|
173 | },
|
---|
174 | 0x040e => { Unknown => 1, Name => 'ColorSamplersResource' },
|
---|
175 | 0x040f => {
|
---|
176 | Name => 'ICC_Profile',
|
---|
177 | SubDirectory => {
|
---|
178 | TagTable => 'Image::ExifTool::ICC_Profile::Main',
|
---|
179 | },
|
---|
180 | },
|
---|
181 | 0x0410 => { Unknown => 1, Name => 'Watermark', Format => 'int8u' },
|
---|
182 | 0x0411 => { Unknown => 1, Name => 'ICC_Untagged', Format => 'int8u' },
|
---|
183 | 0x0412 => { Unknown => 1, Name => 'EffectsVisible', Format => 'int8u' },
|
---|
184 | 0x0413 => { Unknown => 1, Name => 'SpotHalftone' },
|
---|
185 | 0x0414 => { Unknown => 1, Name => 'IDsBaseValue', Description => 'IDs Base Value', Format => 'int32u' },
|
---|
186 | 0x0415 => { Unknown => 1, Name => 'UnicodeAlphaNames' },
|
---|
187 | 0x0416 => { Unknown => 1, Name => 'IndexedColorTableCount', Format => 'int16u' },
|
---|
188 | 0x0417 => { Unknown => 1, Name => 'TransparentIndex', Format => 'int16u' },
|
---|
189 | 0x0419 => {
|
---|
190 | Name => 'GlobalAltitude',
|
---|
191 | Writable => 'int32u',
|
---|
192 | ValueConv => 'unpack("N",$val)',
|
---|
193 | ValueConvInv => 'pack("N",$val)',
|
---|
194 | },
|
---|
195 | 0x041a => {
|
---|
196 | Name => 'SliceInfo',
|
---|
197 | SubDirectory => { TagTable => 'Image::ExifTool::Photoshop::SliceInfo' },
|
---|
198 | },
|
---|
199 | 0x041b => { Name => 'WorkflowURL', %unicodeString },
|
---|
200 | 0x041c => { Unknown => 1, Name => 'JumpToXPEP' },
|
---|
201 | 0x041d => { Unknown => 1, Name => 'AlphaIdentifiers' },
|
---|
202 | 0x041e => {
|
---|
203 | Name => 'URL_List',
|
---|
204 | List => 1,
|
---|
205 | Writable => 1,
|
---|
206 | ValueConv => sub {
|
---|
207 | my ($val, $et) = @_;
|
---|
208 | return '<err>' if length($val) < 4;
|
---|
209 | my $num = unpack('N', $val);
|
---|
210 | my ($i, @vals);
|
---|
211 | my $pos = 4;
|
---|
212 | for ($i=0; $i<$num; ++$i) {
|
---|
213 | $pos += 8; # (skip word and ID)
|
---|
214 | last if length($val) < $pos + 4;
|
---|
215 | my $len = unpack("x${pos}N", $val) * 2;
|
---|
216 | last if length($val) < $pos + 4 + $len;
|
---|
217 | push @vals, $et->Decode(substr($val,$pos+4,$len), 'UCS2', 'MM');
|
---|
218 | $pos += 4 + $len;
|
---|
219 | }
|
---|
220 | return \@vals;
|
---|
221 | },
|
---|
222 | # (this is tricky to make writable)
|
---|
223 | },
|
---|
224 | 0x0421 => {
|
---|
225 | Name => 'VersionInfo',
|
---|
226 | SubDirectory => {
|
---|
227 | TagTable => 'Image::ExifTool::Photoshop::VersionInfo',
|
---|
228 | },
|
---|
229 | },
|
---|
230 | 0x0422 => {
|
---|
231 | Name => 'EXIFInfo', #PH (Found in EPS and PSD files)
|
---|
232 | SubDirectory => {
|
---|
233 | TagTable=> 'Image::ExifTool::Exif::Main',
|
---|
234 | ProcessProc => \&Image::ExifTool::ProcessTIFF,
|
---|
235 | WriteProc => \&Image::ExifTool::WriteTIFF,
|
---|
236 | },
|
---|
237 | },
|
---|
238 | 0x0423 => { Unknown => 1, Name => 'ExifInfo2', Binary => 1 }, #5
|
---|
239 | 0x0424 => {
|
---|
240 | Name => 'XMP',
|
---|
241 | SubDirectory => {
|
---|
242 | TagTable => 'Image::ExifTool::XMP::Main',
|
---|
243 | },
|
---|
244 | },
|
---|
245 | 0x0425 => {
|
---|
246 | Name => 'IPTCDigest',
|
---|
247 | Writable => 'string',
|
---|
248 | Protected => 1,
|
---|
249 | Notes => q{
|
---|
250 | this tag indicates provides a way for XMP-aware applications to indicate
|
---|
251 | that the XMP is synchronized with the IPTC. When writing, special values of
|
---|
252 | "new" and "old" represent the digests of the IPTC from the edited and
|
---|
253 | original files respectively, and are undefined if the IPTC does not exist in
|
---|
254 | the respective file. Set this to "new" as an indication that the XMP is
|
---|
255 | synchronized with the IPTC
|
---|
256 | },
|
---|
257 | # also note the 'new' feature requires that the IPTC comes before this tag is written
|
---|
258 | ValueConv => 'unpack("H*", $val)',
|
---|
259 | ValueConvInv => q{
|
---|
260 | if (lc($val) eq 'new' or lc($val) eq 'old') {
|
---|
261 | {
|
---|
262 | local $SIG{'__WARN__'} = sub { };
|
---|
263 | return lc($val) if eval { require Digest::MD5 };
|
---|
264 | }
|
---|
265 | warn "Digest::MD5 must be installed\n";
|
---|
266 | return undef;
|
---|
267 | }
|
---|
268 | return pack('H*', $val) if $val =~ /^[0-9a-f]{32}$/i;
|
---|
269 | warn "Value must be 'new', 'old' or 32 hexadecimal digits\n";
|
---|
270 | return undef;
|
---|
271 | }
|
---|
272 | },
|
---|
273 | 0x0426 => {
|
---|
274 | Name => 'PrintScaleInfo',
|
---|
275 | SubDirectory => { TagTable => 'Image::ExifTool::Photoshop::PrintScaleInfo' },
|
---|
276 | },
|
---|
277 | 0x0428 => {
|
---|
278 | Name => 'PixelInfo',
|
---|
279 | SubDirectory => { TagTable => 'Image::ExifTool::Photoshop::PixelInfo' },
|
---|
280 | },
|
---|
281 | 0x0429 => { Unknown => 1, Name => 'LayerComps' }, #5
|
---|
282 | 0x042a => { Unknown => 1, Name => 'AlternateDuotoneColors' }, #5
|
---|
283 | 0x042b => { Unknown => 1, Name => 'AlternateSpotColors' }, #5
|
---|
284 | 0x042d => { #7
|
---|
285 | Name => 'LayerSelectionIDs',
|
---|
286 | Description => 'Layer Selection IDs',
|
---|
287 | Unknown => 1,
|
---|
288 | ValueConv => q{
|
---|
289 | my ($n, @a) = unpack("nN*",$val);
|
---|
290 | $#a = $n - 1 if $n > @a;
|
---|
291 | return join(' ', @a);
|
---|
292 | },
|
---|
293 | },
|
---|
294 | 0x042e => { Unknown => 1, Name => 'HDRToningInfo' }, #7
|
---|
295 | 0x042f => { Unknown => 1, Name => 'PrintInfo' }, #7
|
---|
296 | 0x0430 => { Unknown => 1, Name => 'LayerGroupsEnabledID', Format => 'int8u' }, #7
|
---|
297 | 0x0431 => { Unknown => 1, Name => 'ColorSamplersResource2' }, #7
|
---|
298 | 0x0432 => { Unknown => 1, Name => 'MeasurementScale' }, #7
|
---|
299 | 0x0433 => { Unknown => 1, Name => 'TimelineInfo' }, #7
|
---|
300 | 0x0434 => { Unknown => 1, Name => 'SheetDisclosure' }, #7
|
---|
301 | 0x0435 => { Unknown => 1, Name => 'DisplayInfo' }, #7
|
---|
302 | 0x0436 => { Unknown => 1, Name => 'OnionSkins' }, #7
|
---|
303 | 0x0438 => { Unknown => 1, Name => 'CountInfo' }, #7
|
---|
304 | 0x043a => { Unknown => 1, Name => 'PrintInfo2' }, #7
|
---|
305 | 0x043b => { Unknown => 1, Name => 'PrintStyle' }, #7
|
---|
306 | 0x043c => { Unknown => 1, Name => 'MacintoshNSPrintInfo' }, #7
|
---|
307 | 0x043d => { Unknown => 1, Name => 'WindowsDEVMODE' }, #7
|
---|
308 | 0x043e => { Unknown => 1, Name => 'AutoSaveFilePath' }, #7
|
---|
309 | 0x043f => { Unknown => 1, Name => 'AutoSaveFormat' }, #7
|
---|
310 | 0x0440 => { Unknown => 1, Name => 'PathSelectionState' }, #7
|
---|
311 | # 0x07d0-0x0bb6 Path information
|
---|
312 | 0x0bb7 => {
|
---|
313 | Name => 'ClippingPathName',
|
---|
314 | # convert from a Pascal string (ignoring 6 bytes of unknown data after string)
|
---|
315 | ValueConv => q{
|
---|
316 | my $len = ord($val);
|
---|
317 | $val = substr($val, 0, $len+1) if $len < length($val);
|
---|
318 | return Image::ExifTool::Photoshop::ConvertPascalString($self,$val);
|
---|
319 | },
|
---|
320 | },
|
---|
321 | 0x0bb8 => { Unknown => 1, Name => 'OriginPathInfo' }, #7
|
---|
322 | # 0x0fa0-0x1387 - plug-in resources (ref 7)
|
---|
323 | 0x1b58 => { Unknown => 1, Name => 'ImageReadyVariables' }, #7
|
---|
324 | 0x1b59 => { Unknown => 1, Name => 'ImageReadyDataSets' }, #7
|
---|
325 | 0x1f40 => { Unknown => 1, Name => 'LightroomWorkflow' }, #7
|
---|
326 | 0x2710 => { Unknown => 1, Name => 'PrintFlagsInfo' },
|
---|
327 | );
|
---|
328 |
|
---|
329 | # Photoshop JPEG quality record (ref 2)
|
---|
330 | %Image::ExifTool::Photoshop::JPEG_Quality = (
|
---|
331 | PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
|
---|
332 | WRITE_PROC => \&Image::ExifTool::WriteBinaryData,
|
---|
333 | CHECK_PROC => \&Image::ExifTool::CheckBinaryData,
|
---|
334 | FORMAT => 'int16s',
|
---|
335 | GROUPS => { 2 => 'Image' },
|
---|
336 | 0 => {
|
---|
337 | Name => 'PhotoshopQuality',
|
---|
338 | Writable => 1,
|
---|
339 | PrintConv => '$val + 4',
|
---|
340 | PrintConvInv => '$val - 4',
|
---|
341 | },
|
---|
342 | 1 => {
|
---|
343 | Name => 'PhotoshopFormat',
|
---|
344 | PrintConv => {
|
---|
345 | 0x0000 => 'Standard',
|
---|
346 | 0x0001 => 'Optimized',
|
---|
347 | 0x0101 => 'Progressive',
|
---|
348 | },
|
---|
349 | },
|
---|
350 | 2 => {
|
---|
351 | Name => 'ProgressiveScans',
|
---|
352 | PrintConv => {
|
---|
353 | 1 => '3 Scans',
|
---|
354 | 2 => '4 Scans',
|
---|
355 | 3 => '5 Scans',
|
---|
356 | },
|
---|
357 | },
|
---|
358 | );
|
---|
359 |
|
---|
360 | # Photoshop Slices
|
---|
361 | %Image::ExifTool::Photoshop::SliceInfo = (
|
---|
362 | PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
|
---|
363 | 20 => { Name => 'SlicesGroupName', Format => 'var_ustr32' },
|
---|
364 | 24 => { Name => 'NumSlices', Format => 'int32u' },
|
---|
365 | );
|
---|
366 |
|
---|
367 | # Photoshop resolution information #PH
|
---|
368 | %Image::ExifTool::Photoshop::Resolution = (
|
---|
369 | PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
|
---|
370 | WRITE_PROC => \&Image::ExifTool::WriteBinaryData,
|
---|
371 | CHECK_PROC => \&Image::ExifTool::CheckBinaryData,
|
---|
372 | FORMAT => 'int16u',
|
---|
373 | FIRST_ENTRY => 0,
|
---|
374 | WRITABLE => 1,
|
---|
375 | GROUPS => { 2 => 'Image' },
|
---|
376 | 0 => {
|
---|
377 | Name => 'XResolution',
|
---|
378 | Format => 'int32u',
|
---|
379 | Priority => 0,
|
---|
380 | ValueConv => '$val / 0x10000',
|
---|
381 | ValueConvInv => 'int($val * 0x10000 + 0.5)',
|
---|
382 | PrintConv => 'int($val * 100 + 0.5) / 100',
|
---|
383 | PrintConvInv => '$val',
|
---|
384 | },
|
---|
385 | 2 => {
|
---|
386 | Name => 'DisplayedUnitsX',
|
---|
387 | PrintConv => {
|
---|
388 | 1 => 'inches',
|
---|
389 | 2 => 'cm',
|
---|
390 | },
|
---|
391 | },
|
---|
392 | 4 => {
|
---|
393 | Name => 'YResolution',
|
---|
394 | Format => 'int32u',
|
---|
395 | Priority => 0,
|
---|
396 | ValueConv => '$val / 0x10000',
|
---|
397 | ValueConvInv => 'int($val * 0x10000 + 0.5)',
|
---|
398 | PrintConv => 'int($val * 100 + 0.5) / 100',
|
---|
399 | PrintConvInv => '$val',
|
---|
400 | },
|
---|
401 | 6 => {
|
---|
402 | Name => 'DisplayedUnitsY',
|
---|
403 | PrintConv => {
|
---|
404 | 1 => 'inches',
|
---|
405 | 2 => 'cm',
|
---|
406 | },
|
---|
407 | },
|
---|
408 | );
|
---|
409 |
|
---|
410 | # Photoshop version information
|
---|
411 | %Image::ExifTool::Photoshop::VersionInfo = (
|
---|
412 | PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
|
---|
413 | WRITE_PROC => \&Image::ExifTool::WriteBinaryData,
|
---|
414 | CHECK_PROC => \&Image::ExifTool::CheckBinaryData,
|
---|
415 | FIRST_ENTRY => 0,
|
---|
416 | GROUPS => { 2 => 'Image' },
|
---|
417 | # (always 1) 0 => { Name => 'PhotoshopVersion', Format => 'int32u' },
|
---|
418 | 4 => { Name => 'HasRealMergedData', Format => 'int8u', PrintConv => { 0 => 'No', 1 => 'Yes' } },
|
---|
419 | 5 => { Name => 'WriterName', Format => 'var_ustr32' },
|
---|
420 | 9 => { Name => 'ReaderName', Format => 'var_ustr32' },
|
---|
421 | # (always 1) 13 => { Name => 'FileVersion', Format => 'int32u' },
|
---|
422 | );
|
---|
423 |
|
---|
424 | # Print Scale
|
---|
425 | %Image::ExifTool::Photoshop::PrintScaleInfo = (
|
---|
426 | PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
|
---|
427 | WRITE_PROC => \&Image::ExifTool::WriteBinaryData,
|
---|
428 | CHECK_PROC => \&Image::ExifTool::CheckBinaryData,
|
---|
429 | FIRST_ENTRY => 0,
|
---|
430 | GROUPS => { 2 => 'Image' },
|
---|
431 | 0 => {
|
---|
432 | Name => 'PrintStyle',
|
---|
433 | Format => 'int16u',
|
---|
434 | PrintConv => {
|
---|
435 | 0 => 'Centered',
|
---|
436 | 1 => 'Size to Fit',
|
---|
437 | 2 => 'User Defined',
|
---|
438 | },
|
---|
439 | },
|
---|
440 | 2 => { Name => 'PrintPosition', Format => 'float[2]' },
|
---|
441 | 10 => { Name => 'PrintScale', Format => 'float' },
|
---|
442 | );
|
---|
443 |
|
---|
444 | # Pixel Aspect Ratio
|
---|
445 | %Image::ExifTool::Photoshop::PixelInfo = (
|
---|
446 | PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
|
---|
447 | WRITE_PROC => \&Image::ExifTool::WriteBinaryData,
|
---|
448 | CHECK_PROC => \&Image::ExifTool::CheckBinaryData,
|
---|
449 | FIRST_ENTRY => 0,
|
---|
450 | GROUPS => { 2 => 'Image' },
|
---|
451 | # 0 - version
|
---|
452 | 4 => { Name => 'PixelAspectRatio', Format => 'double' },
|
---|
453 | );
|
---|
454 |
|
---|
455 | # Photoshop PSD file header
|
---|
456 | %Image::ExifTool::Photoshop::Header = (
|
---|
457 | PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
|
---|
458 | FORMAT => 'int16u',
|
---|
459 | GROUPS => { 2 => 'Image' },
|
---|
460 | NOTES => 'This information is found in the PSD file header.',
|
---|
461 | 6 => 'NumChannels',
|
---|
462 | 7 => { Name => 'ImageHeight', Format => 'int32u' },
|
---|
463 | 9 => { Name => 'ImageWidth', Format => 'int32u' },
|
---|
464 | 11 => 'BitDepth',
|
---|
465 | 12 => {
|
---|
466 | Name => 'ColorMode',
|
---|
467 | PrintConvColumns => 2,
|
---|
468 | PrintConv => {
|
---|
469 | 0 => 'Bitmap',
|
---|
470 | 1 => 'Grayscale',
|
---|
471 | 2 => 'Indexed',
|
---|
472 | 3 => 'RGB',
|
---|
473 | 4 => 'CMYK',
|
---|
474 | 7 => 'Multichannel',
|
---|
475 | 8 => 'Duotone',
|
---|
476 | 9 => 'Lab',
|
---|
477 | },
|
---|
478 | },
|
---|
479 | );
|
---|
480 |
|
---|
481 | # Layer information
|
---|
482 | %Image::ExifTool::Photoshop::Layers = (
|
---|
483 | PROCESS_PROC => \&ProcessLayers,
|
---|
484 | GROUPS => { 2 => 'Image' },
|
---|
485 | NOTES => 'Tags extracted from Photoshop layer information.',
|
---|
486 | # tags extracted from layer information
|
---|
487 | # (tag ID's are for convenience only)
|
---|
488 | _xcnt => { Name => 'LayerCount', Format => 'int16u' },
|
---|
489 | _xrct => {
|
---|
490 | Name => 'LayerRectangles',
|
---|
491 | Format => 'int32u',
|
---|
492 | Count => 4,
|
---|
493 | List => 1,
|
---|
494 | Notes => 'top left bottom right',
|
---|
495 | },
|
---|
496 | _xnam => { Name => 'LayerNames',
|
---|
497 | Format => 'string',
|
---|
498 | List => 1,
|
---|
499 | ValueConv => q{
|
---|
500 | my $charset = $self->Options('CharsetPhotoshop') || 'Latin';
|
---|
501 | return $self->Decode($val, $charset);
|
---|
502 | },
|
---|
503 | },
|
---|
504 | _xbnd => {
|
---|
505 | Name => 'LayerBlendModes',
|
---|
506 | Format => 'undef',
|
---|
507 | List => 1,
|
---|
508 | RawConv => 'GetByteOrder() eq "II" ? pack "N*", unpack "V*", $val : $val',
|
---|
509 | PrintConv => {
|
---|
510 | pass => 'Pass Through',
|
---|
511 | norm => 'Normal',
|
---|
512 | diss => 'Dissolve',
|
---|
513 | dark => 'Darken',
|
---|
514 | 'mul '=> 'Multiply',
|
---|
515 | idiv => 'Color Burn',
|
---|
516 | lbrn => 'Linear Burn',
|
---|
517 | dkCl => 'Darker Color',
|
---|
518 | lite => 'Lighten',
|
---|
519 | scrn => 'Screen',
|
---|
520 | 'div '=> 'Color Dodge',
|
---|
521 | lddg => 'Linear Dodge',
|
---|
522 | lgCl => 'Lighter Color',
|
---|
523 | over => 'Overlay',
|
---|
524 | sLit => 'Soft Light',
|
---|
525 | hLit => 'Hard Light',
|
---|
526 | vLit => 'Vivid Light',
|
---|
527 | lLit => 'Linear Light',
|
---|
528 | pLit => 'Pin Light',
|
---|
529 | hMix => 'Hard Mix',
|
---|
530 | diff => 'Difference',
|
---|
531 | smud => 'Exclusion',
|
---|
532 | fsub => 'Subtract',
|
---|
533 | fdiv => 'Divide',
|
---|
534 | 'hue '=> 'Hue',
|
---|
535 | 'sat '=> 'Saturation',
|
---|
536 | colr => 'Color',
|
---|
537 | 'lum '=> 'Luminosity',
|
---|
538 | },
|
---|
539 | },
|
---|
540 | _xopc => {
|
---|
541 | Name => 'LayerOpacities',
|
---|
542 | Format => 'int8u',
|
---|
543 | List => 1,
|
---|
544 | ValueConv => '100 * $val / 255',
|
---|
545 | PrintConv => 'sprintf("%d%%",$val)',
|
---|
546 | },
|
---|
547 | # tags extracted from additional layer information (tag ID's are real)
|
---|
548 | # - must be able to accommodate a blank entry to preserve the list ordering
|
---|
549 | luni => {
|
---|
550 | Name => 'LayerUnicodeNames',
|
---|
551 | List => 1,
|
---|
552 | RawConv => q{
|
---|
553 | return '' if length($val) < 4;
|
---|
554 | my $len = Get32u(\$val, 0);
|
---|
555 | return $self->Decode(substr($val, 4, $len * 2), 'UCS2');
|
---|
556 | },
|
---|
557 | },
|
---|
558 | lyid => {
|
---|
559 | Name => 'LayerIDs',
|
---|
560 | Description => 'Layer IDs',
|
---|
561 | Format => 'int32u',
|
---|
562 | List => 1,
|
---|
563 | Unknown => 1,
|
---|
564 | },
|
---|
565 | shmd => { # layer metadata (undocumented structure)
|
---|
566 | # (for now, only extract layerTime. May also contain "layerXMP" --
|
---|
567 | # it would be nice to decode this but I need a sample)
|
---|
568 | Name => 'LayerModifyDates',
|
---|
569 | Groups => { 2 => 'Time' },
|
---|
570 | List => 1,
|
---|
571 | RawConv => q{
|
---|
572 | return '' unless $val =~ /layerTime(doub|buod)(.{8})/s;
|
---|
573 | my $tmp = $2;
|
---|
574 | return GetDouble(\$tmp, 0);
|
---|
575 | },
|
---|
576 | ValueConv => 'length $val ? ConvertUnixTime($val,1) : ""',
|
---|
577 | PrintConv => 'length $val ? $self->ConvertDateTime($val) : ""',
|
---|
578 | },
|
---|
579 | );
|
---|
580 |
|
---|
581 | # tags extracted from ImageSourceData found in TIFF images (ref PH)
|
---|
582 | %Image::ExifTool::Photoshop::DocumentData = (
|
---|
583 | PROCESS_PROC => \&ProcessDocumentData,
|
---|
584 | GROUPS => { 2 => 'Image' },
|
---|
585 | Layr => {
|
---|
586 | Name => 'Layers',
|
---|
587 | SubDirectory => { TagTable => 'Image::ExifTool::Photoshop::Layers' },
|
---|
588 | },
|
---|
589 | Lr16 => { # (NC)
|
---|
590 | Name => 'Layers',
|
---|
591 | SubDirectory => { TagTable => 'Image::ExifTool::Photoshop::Layers' },
|
---|
592 | },
|
---|
593 | );
|
---|
594 |
|
---|
595 | # image data
|
---|
596 | %Image::ExifTool::Photoshop::ImageData = (
|
---|
597 | PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
|
---|
598 | GROUPS => { 2 => 'Image' },
|
---|
599 | 0 => {
|
---|
600 | Name => 'Compression',
|
---|
601 | Format => 'int16u',
|
---|
602 | PrintConv => {
|
---|
603 | 0 => 'Uncompressed',
|
---|
604 | 1 => 'RLE',
|
---|
605 | 2 => 'ZIP without prediction',
|
---|
606 | 3 => 'ZIP with prediction',
|
---|
607 | },
|
---|
608 | },
|
---|
609 | );
|
---|
610 |
|
---|
611 | # tags for unknown resource types
|
---|
612 | %Image::ExifTool::Photoshop::Unknown = (
|
---|
613 | GROUPS => { 2 => 'Unknown' },
|
---|
614 | );
|
---|
615 |
|
---|
616 | # define reference to IPTCDigest tagInfo hash for convenience
|
---|
617 | $iptcDigestInfo = $Image::ExifTool::Photoshop::Main{0x0425};
|
---|
618 |
|
---|
619 |
|
---|
620 | #------------------------------------------------------------------------------
|
---|
621 | # AutoLoad our writer routines when necessary
|
---|
622 | #
|
---|
623 | sub AUTOLOAD
|
---|
624 | {
|
---|
625 | return Image::ExifTool::DoAutoLoad($AUTOLOAD, @_);
|
---|
626 | }
|
---|
627 |
|
---|
628 | #------------------------------------------------------------------------------
|
---|
629 | # Convert pascal string(s) to something we can use
|
---|
630 | # Inputs: 1) Pascal string data
|
---|
631 | # Returns: Strings, concatenated with ', '
|
---|
632 | sub ConvertPascalString($$)
|
---|
633 | {
|
---|
634 | my ($et, $inStr) = @_;
|
---|
635 | my $outStr = '';
|
---|
636 | my $len = length($inStr);
|
---|
637 | my $i=0;
|
---|
638 | while ($i < $len) {
|
---|
639 | my $n = ord(substr($inStr, $i, 1));
|
---|
640 | last if $i + $n >= $len;
|
---|
641 | $i and $outStr .= ', ';
|
---|
642 | $outStr .= substr($inStr, $i+1, $n);
|
---|
643 | $i += $n + 1;
|
---|
644 | }
|
---|
645 | my $charset = $et->Options('CharsetPhotoshop') || 'Latin';
|
---|
646 | return $et->Decode($outStr, $charset);
|
---|
647 | }
|
---|
648 |
|
---|
649 | #------------------------------------------------------------------------------
|
---|
650 | # Process Photoshop layers and mask information section of PSD/PSB file
|
---|
651 | # Inputs: 0) ExifTool ref, 1) DirInfo ref, 2) tag table ref
|
---|
652 | # Returns: 1 on success (and seeks to the end of this section)
|
---|
653 | sub ProcessLayersAndMask($$$)
|
---|
654 | {
|
---|
655 | local $_;
|
---|
656 | my ($et, $dirInfo, $tagTablePtr) = @_;
|
---|
657 | my $raf = $$dirInfo{RAF};
|
---|
658 | my $fileType = $$et{VALUE}{FileType};
|
---|
659 | my $data;
|
---|
660 |
|
---|
661 | return 0 unless $fileType eq 'PSD' or $fileType eq 'PSB'; # (no layer section in CS1 files)
|
---|
662 |
|
---|
663 | # (some words are 4 bytes in PSD files and 8 bytes in PSB)
|
---|
664 | my ($psb, $psiz) = $fileType eq 'PSB' ? (1, 8) : (undef, 4);
|
---|
665 |
|
---|
666 | # read the layer information header
|
---|
667 | my $n = $psiz * 2 + 2;
|
---|
668 | $raf->Read($data, $n) == $n or return 0;
|
---|
669 | my $tot = $psb ? Get64u(\$data, 0) : Get32u(\$data, 0); # length of layer and mask info
|
---|
670 | return 1 if $tot == 0;
|
---|
671 | my $end = $raf->Tell() - $psiz - 2 + $tot;
|
---|
672 | $data = substr $data, $psiz;
|
---|
673 | my $len = $psb ? Get64u(\$data, 0) : Get32u(\$data, 0); # length of layer info section
|
---|
674 | my $num = Get16s(\$data, $psiz);
|
---|
675 | # check for Lr16 block if layers length is 0 (ref https://forums.adobe.com/thread/1540914)
|
---|
676 | if ($len == 0 and $num == 0) {
|
---|
677 | $raf->Read($data,10) == 10 or return 0;
|
---|
678 | if ($data =~ /^..8BIMLr16/s) {
|
---|
679 | $raf->Read($data, $psiz+2) == $psiz+2 or return 0;
|
---|
680 | $len = $psb ? Get64u(\$data, 0) : Get32u(\$data, 0);
|
---|
681 | } elsif ($data =~ /^..8BIMMt16/s) { # (have seen Mt16 before Lr16, ref PH)
|
---|
682 | $raf->Read($data, $psiz) == $psiz or return 0;
|
---|
683 | $raf->Read($data, 8) == 8 or return 0;
|
---|
684 | if ($data eq '8BIMLr16') {
|
---|
685 | $raf->Read($data, $psiz+2) == $psiz+2 or return 0;
|
---|
686 | $len = $psb ? Get64u(\$data, 0) : Get32u(\$data, 0);
|
---|
687 | } else {
|
---|
688 | $raf->Seek(-18-$psiz, 1) or return 0;
|
---|
689 | }
|
---|
690 | } else {
|
---|
691 | $raf->Seek(-10, 1) or return 0;
|
---|
692 | }
|
---|
693 | }
|
---|
694 | $len += 2; # include layer count with layer info section
|
---|
695 | $raf->Seek(-2, 1) or return 0;
|
---|
696 | my %dinfo = (
|
---|
697 | RAF => $raf,
|
---|
698 | DirLen => $len,
|
---|
699 | );
|
---|
700 | $$et{IsPSB} = $psb; # set PSB flag
|
---|
701 | ProcessLayers($et, \%dinfo, $tagTablePtr);
|
---|
702 |
|
---|
703 | # seek to the end of this section and return success flag
|
---|
704 | return $raf->Seek($end, 0) ? 1 : 0;
|
---|
705 | }
|
---|
706 |
|
---|
707 | #------------------------------------------------------------------------------
|
---|
708 | # Process Photoshop layers (beginning with layer count)
|
---|
709 | # Inputs: 0) ExifTool ref, 1) DirInfo ref, 2) tag table ref
|
---|
710 | # Returns: 1 on success
|
---|
711 | # Notes: Uses ExifTool IsPSB member to determine whether file is PSB format
|
---|
712 | sub ProcessLayers($$$)
|
---|
713 | {
|
---|
714 | local $_;
|
---|
715 | my ($et, $dirInfo, $tagTablePtr) = @_;
|
---|
716 | my ($i, $n, %count, $buff, $buf2);
|
---|
717 | my $raf = $$dirInfo{RAF};
|
---|
718 | my $dirLen = $$dirInfo{DirLen};
|
---|
719 | my $verbose = $$et{OPTIONS}{Verbose};
|
---|
720 | my %dinfo = ( DataPt => \$buff, Base => $raf->Tell() );
|
---|
721 | my $pos = 0;
|
---|
722 | return 0 if $dirLen < 2;
|
---|
723 | $raf->Read($buff, 2) == 2 or return 0;
|
---|
724 | my $num = Get16s(\$buff, 0);
|
---|
725 | $num = -$num if $num < 0; # (first channel is transparency data if negative)
|
---|
726 | $et->VerboseDir('Layers', $num, $dirLen);
|
---|
727 | $et->HandleTag($tagTablePtr, '_xcnt', $num, Start => $pos, Size => 2, %dinfo); # LayerCount
|
---|
728 | my $oldIndent = $$et{INDENT};
|
---|
729 | $$et{INDENT} .= '| ';
|
---|
730 |
|
---|
731 | $pos += 2;
|
---|
732 | my $psb = $$et{IsPSB}; # is PSB format?
|
---|
733 | my $psiz = $psb ? 8 : 4;
|
---|
734 | for ($i=0; $i<$num; ++$i) {
|
---|
735 | $et->VPrint(0, $oldIndent.'+ [Layer '.($i+1)." of $num]\n");
|
---|
736 | last if $pos + 18 > $dirLen;
|
---|
737 | $raf->Read($buff, 18) == 18 or last;
|
---|
738 | $dinfo{DataPos} = $pos;
|
---|
739 | # save the layer rectangle
|
---|
740 | $et->HandleTag($tagTablePtr, '_xrct', undef, Start => 0, Size => 16, %dinfo);
|
---|
741 | my $numChannels = Get16u(\$buff, 16);
|
---|
742 | $n = (2 + $psiz) * $numChannels; # size of channel information
|
---|
743 | $raf->Seek($n, 1) or last;
|
---|
744 | $pos += 18 + $n;
|
---|
745 | last if $pos + 20 > $dirLen;
|
---|
746 | $raf->Read($buff, 20) == 20 or last;
|
---|
747 | $dinfo{DataPos} = $pos;
|
---|
748 | my $sig = substr($buff, 0, 4);
|
---|
749 | $sig =~ /^(8BIM|MIB8)$/ or last; # verify signature
|
---|
750 | $et->HandleTag($tagTablePtr, '_xbnd', undef, Start => 4, Size => 4, %dinfo);
|
---|
751 | $et->HandleTag($tagTablePtr, '_xopc', undef, Start => 8, Size => 1, %dinfo);
|
---|
752 | my $nxt = $pos + 16 + Get32u(\$buff, 12);
|
---|
753 | $n = Get32u(\$buff, 16); # get size of layer mask data
|
---|
754 | $pos += 20 + $n; # skip layer mask data
|
---|
755 | last if $pos + 4 > $dirLen;
|
---|
756 | $raf->Seek($n, 1) and $raf->Read($buff, 4) == 4 or last;
|
---|
757 | $n = Get32u(\$buff, 0); # get size of layer blending ranges
|
---|
758 | $pos += 4 + $n; # skip layer blending ranges data
|
---|
759 | last if $pos + 1 > $dirLen;
|
---|
760 | $raf->Seek($n, 1) and $raf->Read($buff, 1) == 1 or last;
|
---|
761 | $n = Get8u(\$buff, 0); # get length of layer name
|
---|
762 | last if $pos + 1 + $n > $dirLen;
|
---|
763 | $raf->Read($buff, $n) == $n or last;
|
---|
764 | $dinfo{DataPos} = $pos + 1;
|
---|
765 | $et->HandleTag($tagTablePtr, '_xnam', undef, Start => 0, Size => $n, %dinfo);
|
---|
766 | my $frag = ($n + 1) & 0x3;
|
---|
767 | $raf->Seek(4 - $frag, 1) or last if $frag;
|
---|
768 | $n = ($n + 4) & 0xfffffffc; # +1 for length byte then pad to multiple of 4 bytes
|
---|
769 | $pos += $n;
|
---|
770 | # process additional layer info
|
---|
771 | while ($pos + 12 <= $nxt) {
|
---|
772 | $raf->Read($buff, 12) == 12 or last;
|
---|
773 | my $dat = substr($buff, 0, 8);
|
---|
774 | $dat = pack 'N*', unpack 'V*', $dat if GetByteOrder() eq 'II';
|
---|
775 | my $sig = substr($dat, 0, 4);
|
---|
776 | last unless $sig eq '8BIM' or $sig eq '8B64'; # verify signature
|
---|
777 | my $tag = substr($dat, 4, 4);
|
---|
778 | # (some structures have an 8-byte size word [augh!]
|
---|
779 | # --> it would be great if '8B64' indicated a 64-bit version, and this may well
|
---|
780 | # be the case, but it is not mentioned in the Photoshop file format specification)
|
---|
781 | if ($psb and $tag =~ /^(LMsk|Lr16|Lr32|Layr|Mt16|Mt32|Mtrn|Alph|FMsk|lnk2|FEid|FXid|PxSD)$/) {
|
---|
782 | last if $pos + 16 > $nxt;
|
---|
783 | $raf->Read($buf2, 4) == 4 or last;
|
---|
784 | $buff .= $buf2;
|
---|
785 | $n = Get64u(\$buff, 8);
|
---|
786 | $pos += 4;
|
---|
787 | } else {
|
---|
788 | $n = Get32u(\$buff, 8);
|
---|
789 | }
|
---|
790 | $pos += 12;
|
---|
791 | last if $pos + $n > $nxt;
|
---|
792 | $frag = $n & 0x3;
|
---|
793 | if ($$tagTablePtr{$tag} or $verbose) {
|
---|
794 | # pad with empty entries if necessary to keep the same index for each item in the layer
|
---|
795 | $count{$tag} = 0 unless defined $count{$tag};
|
---|
796 | $raf->Read($buff, $n) == $n or last;
|
---|
797 | $dinfo{DataPos} = $pos;
|
---|
798 | while ($count{$tag} < $i) {
|
---|
799 | $et->HandleTag($tagTablePtr, $tag, '');
|
---|
800 | ++$count{$tag};
|
---|
801 | }
|
---|
802 | $et->HandleTag($tagTablePtr, $tag, undef, Start => 0, Size => $n, %dinfo);
|
---|
803 | ++$count{$tag};
|
---|
804 | if ($frag) {
|
---|
805 | $raf->Seek(4 - $frag, 1) or last;
|
---|
806 | $n += 4 - $frag; # pad to multiple of 4 bytes (PH NC)
|
---|
807 | }
|
---|
808 | } else {
|
---|
809 | $n += 4 - $frag if $frag;
|
---|
810 | $raf->Seek($n, 1) or last;
|
---|
811 | }
|
---|
812 | $pos += $n; # step to start of next structure
|
---|
813 | }
|
---|
814 | $pos = $nxt;
|
---|
815 | }
|
---|
816 | $$et{INDENT} = $oldIndent;
|
---|
817 | return 1;
|
---|
818 | }
|
---|
819 |
|
---|
820 | #------------------------------------------------------------------------------
|
---|
821 | # Process Photoshop ImageSourceData
|
---|
822 | # Inputs: 0) ExifTool ref, 1) dirInfo ref, 2) tag table ref
|
---|
823 | # Returns: 1 on success
|
---|
824 | sub ProcessDocumentData($$$)
|
---|
825 | {
|
---|
826 | my ($et, $dirInfo, $tagTablePtr) = @_;
|
---|
827 | my $verbose = $$et{OPTIONS}{Verbose};
|
---|
828 | my $raf = $$dirInfo{RAF};
|
---|
829 | my $dirLen = $$dirInfo{DirLen};
|
---|
830 | my $pos = 36; # length of header
|
---|
831 | my ($buff, $n, $err);
|
---|
832 |
|
---|
833 | $et->VerboseDir('Photoshop Document Data', undef, $dirLen);
|
---|
834 | unless ($raf) {
|
---|
835 | my $dataPt = $$dirInfo{DataPt};
|
---|
836 | my $start = $$dirInfo{DirStart} || 0;
|
---|
837 | $raf = new File::RandomAccess($dataPt);
|
---|
838 | $raf->Seek($start, 0) if $start;
|
---|
839 | $dirLen = length $$dataPt - $start unless defined $dirLen;
|
---|
840 | $et->VerboseDump($dataPt, Start => $start, Len => $dirLen, Base => $$dirInfo{Base});
|
---|
841 | }
|
---|
842 | unless ($raf->Read($buff, $pos) == $pos and
|
---|
843 | $buff =~ /^Adobe Photoshop Document Data (Block|V0002)\0/)
|
---|
844 | {
|
---|
845 | $et->Warn('Invalid Photoshop Document Data');
|
---|
846 | return 0;
|
---|
847 | }
|
---|
848 | my $psb = ($1 eq 'V0002');
|
---|
849 | my %dinfo = ( DataPt => \$buff );
|
---|
850 | $$et{IsPSB} = $psb; # set PSB flag (needed when handling Layers directory)
|
---|
851 | while ($pos + 12 <= $dirLen) {
|
---|
852 | $raf->Read($buff, 8) == 8 or $err = 'Error reading document data', last;
|
---|
853 | # set byte order according to byte order of first signature
|
---|
854 | SetByteOrder($buff =~ /^(8BIM|8B64)/ ? 'MM' : 'II') if $pos == 36;
|
---|
855 | $buff = pack 'N*', unpack 'V*', $buff if GetByteOrder() eq 'II';
|
---|
856 | my $sig = substr($buff, 0, 4);
|
---|
857 | $sig eq '8BIM' or $sig eq '8B64' or $err = 'Bad photoshop resource', last; # verify signature
|
---|
858 | my $tag = substr($buff, 4, 4);
|
---|
859 | if ($psb and $tag =~ /^(LMsk|Lr16|Lr32|Layr|Mt16|Mt32|Mtrn|Alph|FMsk|lnk2|FEid|FXid|PxSD)$/) {
|
---|
860 | $pos + 16 > $dirLen and $err = 'Short PSB resource', last;
|
---|
861 | $raf->Read($buff, 8) == 8 or $err = 'Error reading PSB resource', last;
|
---|
862 | $n = Get64u(\$buff, 0);
|
---|
863 | $pos += 4;
|
---|
864 | } else {
|
---|
865 | $raf->Read($buff, 4) == 4 or $err = 'Error reading PSD resource', last;
|
---|
866 | $n = Get32u(\$buff, 0);
|
---|
867 | }
|
---|
868 | $pos += 12;
|
---|
869 | $pos + $n > $dirLen and $err = 'Truncated photoshop resource', last;
|
---|
870 | my $pad = (4 - ($n & 3)) & 3; # number of padding bytes
|
---|
871 | my $tagInfo = $$tagTablePtr{$tag};
|
---|
872 | if ($tagInfo or $verbose) {
|
---|
873 | if ($tagInfo and $$tagInfo{SubDirectory}) {
|
---|
874 | my $fpos = $raf->Tell() + $n + $pad;
|
---|
875 | my $subTable = GetTagTable($$tagInfo{SubDirectory}{TagTable});
|
---|
876 | $et->ProcessDirectory({ RAF => $raf, DirLen => $n }, $subTable);
|
---|
877 | $raf->Seek($fpos, 0) or $err = 'Seek error', last;
|
---|
878 | } else {
|
---|
879 | $dinfo{DataPos} = $raf->Tell();
|
---|
880 | $dinfo{Start} = 0;
|
---|
881 | $dinfo{Size} = $n;
|
---|
882 | $raf->Read($buff, $n) == $n or $err = 'Error reading photoshop resource', last;
|
---|
883 | $et->HandleTag($tagTablePtr, $tag, undef, %dinfo);
|
---|
884 | $raf->Seek($pad, 1) or $err = 'Seek error', last;
|
---|
885 | }
|
---|
886 | } else {
|
---|
887 | $raf->Seek($n + $pad, 1) or $err = 'Seek error', last;
|
---|
888 | }
|
---|
889 | $pos += $n + $pad; # step to start of next structure
|
---|
890 | }
|
---|
891 | $err and $et->Warn($err);
|
---|
892 | return 1;
|
---|
893 | }
|
---|
894 |
|
---|
895 | #------------------------------------------------------------------------------
|
---|
896 | # Process Photoshop APP13 record
|
---|
897 | # Inputs: 0) ExifTool object reference, 1) Reference to directory information
|
---|
898 | # 2) Tag table reference
|
---|
899 | # Returns: 1 on success
|
---|
900 | sub ProcessPhotoshop($$$)
|
---|
901 | {
|
---|
902 | my ($et, $dirInfo, $tagTablePtr) = @_;
|
---|
903 | my $dataPt = $$dirInfo{DataPt};
|
---|
904 | my $pos = $$dirInfo{DirStart};
|
---|
905 | my $dirEnd = $pos + $$dirInfo{DirLen};
|
---|
906 | my $verbose = $et->Options('Verbose');
|
---|
907 | my $success = 0;
|
---|
908 |
|
---|
909 | # ignore non-standard XMP while in strict MWG compatibility mode
|
---|
910 | if (($Image::ExifTool::MWG::strict or $et->Options('Validate')) and
|
---|
911 | $$et{FILE_TYPE} =~ /^(JPEG|TIFF|PSD)$/)
|
---|
912 | {
|
---|
913 | my $path = $et->MetadataPath();
|
---|
914 | unless ($path =~ /^(JPEG-APP13-Photoshop|TIFF-IFD0-Photoshop|PSD)$/) {
|
---|
915 | if ($Image::ExifTool::MWG::strict) {
|
---|
916 | $et->Warn("Ignored non-standard Photoshop at $path");
|
---|
917 | return 1;
|
---|
918 | } else {
|
---|
919 | $et->Warn("Non-standard Photoshop at $path", 1);
|
---|
920 | }
|
---|
921 | }
|
---|
922 | }
|
---|
923 | if ($$et{FILE_TYPE} eq 'JPEG' and $$dirInfo{Parent} ne 'APP13') {
|
---|
924 | $$et{LOW_PRIORITY_DIR}{'*'} = 1; # lower priority of all these tags
|
---|
925 | }
|
---|
926 | SetByteOrder('MM'); # Photoshop is always big-endian
|
---|
927 | $verbose and $et->VerboseDir('Photoshop', 0, $$dirInfo{DirLen});
|
---|
928 |
|
---|
929 | # scan through resource blocks:
|
---|
930 | # Format: 0) Type, 4 bytes - '8BIM' (or the rare 'PHUT', 'DCSR', 'AgHg' or 'MeSa')
|
---|
931 | # 1) TagID,2 bytes
|
---|
932 | # 2) Name, pascal string padded to even no. bytes
|
---|
933 | # 3) Size, 4 bytes - N
|
---|
934 | # 4) Data, N bytes
|
---|
935 | while ($pos + 8 < $dirEnd) {
|
---|
936 | my $type = substr($$dataPt, $pos, 4);
|
---|
937 | my ($ttPtr, $extra, $val, $name);
|
---|
938 | if ($type eq '8BIM') {
|
---|
939 | $ttPtr = $tagTablePtr;
|
---|
940 | } elsif ($type =~ /^(PHUT|DCSR|AgHg|MeSa)$/) { # (PHUT~ImageReady, MeSa~PhotoDeluxe)
|
---|
941 | $ttPtr = GetTagTable('Image::ExifTool::Photoshop::Unknown');
|
---|
942 | } else {
|
---|
943 | $type =~ s/([^\w])/sprintf("\\x%.2x",ord($1))/ge;
|
---|
944 | $et->Warn(qq{Bad Photoshop IRB resource "$type"});
|
---|
945 | last;
|
---|
946 | }
|
---|
947 | my $tag = Get16u($dataPt, $pos + 4);
|
---|
948 | $pos += 6; # point to start of name
|
---|
949 | my $nameLen = Get8u($dataPt, $pos);
|
---|
950 | my $namePos = ++$pos;
|
---|
951 | # skip resource block name (pascal string, padded to an even # of bytes)
|
---|
952 | $pos += $nameLen;
|
---|
953 | ++$pos unless $nameLen & 0x01;
|
---|
954 | if ($pos + 4 > $dirEnd) {
|
---|
955 | $et->Warn("Bad Photoshop resource block");
|
---|
956 | last;
|
---|
957 | }
|
---|
958 | my $size = Get32u($dataPt, $pos);
|
---|
959 | $pos += 4;
|
---|
960 | if ($size + $pos > $dirEnd) {
|
---|
961 | $et->Warn("Bad Photoshop resource data size $size");
|
---|
962 | last;
|
---|
963 | }
|
---|
964 | $success = 1;
|
---|
965 | if ($nameLen) {
|
---|
966 | $name = substr($$dataPt, $namePos, $nameLen);
|
---|
967 | $extra = qq{, Name="$name"};
|
---|
968 | } else {
|
---|
969 | $name = '';
|
---|
970 | }
|
---|
971 | my $tagInfo = $et->GetTagInfo($ttPtr, $tag);
|
---|
972 | # append resource name to value if requested (braced by "/#...#/")
|
---|
973 | if ($tagInfo and defined $$tagInfo{SetResourceName} and
|
---|
974 | $$tagInfo{SetResourceName} eq '1' and $name !~ m{/#})
|
---|
975 | {
|
---|
976 | $val = substr($$dataPt, $pos, $size) . '/#' . $name . '#/';
|
---|
977 | }
|
---|
978 | $et->HandleTag($ttPtr, $tag, $val,
|
---|
979 | TagInfo => $tagInfo,
|
---|
980 | Extra => $extra,
|
---|
981 | DataPt => $dataPt,
|
---|
982 | DataPos => $$dirInfo{DataPos},
|
---|
983 | Size => $size,
|
---|
984 | Start => $pos,
|
---|
985 | Base => $$dirInfo{Base},
|
---|
986 | Parent => $$dirInfo{DirName},
|
---|
987 | );
|
---|
988 | $size += 1 if $size & 0x01; # size is padded to an even # bytes
|
---|
989 | $pos += $size;
|
---|
990 | }
|
---|
991 | delete $$et{LOW_PRIORITY_DIR}{'*'};
|
---|
992 | return $success;
|
---|
993 | }
|
---|
994 |
|
---|
995 | #------------------------------------------------------------------------------
|
---|
996 | # extract information from Photoshop PSD file
|
---|
997 | # Inputs: 0) ExifTool object reference, 1) dirInfo reference
|
---|
998 | # Returns: 1 if this was a valid PSD file, -1 on write error
|
---|
999 | sub ProcessPSD($$)
|
---|
1000 | {
|
---|
1001 | my ($et, $dirInfo) = @_;
|
---|
1002 | my $raf = $$dirInfo{RAF};
|
---|
1003 | my $outfile = $$dirInfo{OutFile};
|
---|
1004 | my ($data, $err, $tagTablePtr);
|
---|
1005 |
|
---|
1006 | $raf->Read($data, 30) == 30 or return 0;
|
---|
1007 | $data =~ /^8BPS\0([\x01\x02])/ or return 0;
|
---|
1008 | SetByteOrder('MM');
|
---|
1009 | $et->SetFileType($1 eq "\x01" ? 'PSD' : 'PSB'); # set the FileType tag
|
---|
1010 | my %dirInfo = (
|
---|
1011 | DataPt => \$data,
|
---|
1012 | DirStart => 0,
|
---|
1013 | DirName => 'Photoshop',
|
---|
1014 | );
|
---|
1015 | my $len = Get32u(\$data, 26);
|
---|
1016 | if ($outfile) {
|
---|
1017 | Write($outfile, $data) or $err = 1;
|
---|
1018 | $raf->Read($data, $len) == $len or return -1;
|
---|
1019 | Write($outfile, $data) or $err = 1; # write color mode data
|
---|
1020 | # initialize map of where things are written
|
---|
1021 | $et->InitWriteDirs(\%psdMap);
|
---|
1022 | } else {
|
---|
1023 | # process the header
|
---|
1024 | $tagTablePtr = GetTagTable('Image::ExifTool::Photoshop::Header');
|
---|
1025 | $dirInfo{DirLen} = 30;
|
---|
1026 | $et->ProcessDirectory(\%dirInfo, $tagTablePtr);
|
---|
1027 | $raf->Seek($len, 1) or $err = 1; # skip over color mode data
|
---|
1028 | }
|
---|
1029 | # read image resource section
|
---|
1030 | $raf->Read($data, 4) == 4 or $err = 1;
|
---|
1031 | $len = Get32u(\$data, 0);
|
---|
1032 | $raf->Read($data, $len) == $len or $err = 1;
|
---|
1033 | $tagTablePtr = GetTagTable('Image::ExifTool::Photoshop::Main');
|
---|
1034 | $dirInfo{DirLen} = $len;
|
---|
1035 | my $rtnVal = 1;
|
---|
1036 | if ($outfile) {
|
---|
1037 | # rewrite IRB resources
|
---|
1038 | $data = WritePhotoshop($et, \%dirInfo, $tagTablePtr);
|
---|
1039 | if ($data) {
|
---|
1040 | $len = Set32u(length $data);
|
---|
1041 | Write($outfile, $len, $data) or $err = 1;
|
---|
1042 | # look for trailer and edit if necessary
|
---|
1043 | my $trailInfo = Image::ExifTool::IdentifyTrailer($raf);
|
---|
1044 | if ($trailInfo) {
|
---|
1045 | my $tbuf = '';
|
---|
1046 | $$trailInfo{OutFile} = \$tbuf; # rewrite trailer(s)
|
---|
1047 | # rewrite all trailers to buffer
|
---|
1048 | if ($et->ProcessTrailers($trailInfo)) {
|
---|
1049 | my $copyBytes = $$trailInfo{DataPos} - $raf->Tell();
|
---|
1050 | if ($copyBytes >= 0) {
|
---|
1051 | # copy remaining PSD file up to start of trailer
|
---|
1052 | while ($copyBytes) {
|
---|
1053 | my $n = ($copyBytes > 65536) ? 65536 : $copyBytes;
|
---|
1054 | $raf->Read($data, $n) == $n or $err = 1;
|
---|
1055 | Write($outfile, $data) or $err = 1;
|
---|
1056 | $copyBytes -= $n;
|
---|
1057 | }
|
---|
1058 | # write the trailer (or not)
|
---|
1059 | $et->WriteTrailerBuffer($trailInfo, $outfile) or $err = 1;
|
---|
1060 | } else {
|
---|
1061 | $et->Warn('Overlapping trailer');
|
---|
1062 | undef $trailInfo;
|
---|
1063 | }
|
---|
1064 | } else {
|
---|
1065 | undef $trailInfo;
|
---|
1066 | }
|
---|
1067 | }
|
---|
1068 | unless ($trailInfo) {
|
---|
1069 | # copy over the rest of the file
|
---|
1070 | while ($raf->Read($data, 65536)) {
|
---|
1071 | Write($outfile, $data) or $err = 1;
|
---|
1072 | }
|
---|
1073 | }
|
---|
1074 | } else {
|
---|
1075 | $err = 1;
|
---|
1076 | }
|
---|
1077 | $rtnVal = -1 if $err;
|
---|
1078 | } elsif ($err) {
|
---|
1079 | $et->Warn('File format error');
|
---|
1080 | } else {
|
---|
1081 | # read IRB resources
|
---|
1082 | ProcessPhotoshop($et, \%dirInfo, $tagTablePtr);
|
---|
1083 | # read layer and mask information section
|
---|
1084 | $dirInfo{RAF} = $raf;
|
---|
1085 | $tagTablePtr = GetTagTable('Image::ExifTool::Photoshop::Layers');
|
---|
1086 | my $oldIndent = $$et{INDENT};
|
---|
1087 | $$et{INDENT} .= '| ';
|
---|
1088 | if (ProcessLayersAndMask($et, \%dirInfo, $tagTablePtr) and
|
---|
1089 | # read compression mode from image data section
|
---|
1090 | $raf->Read($data,2) == 2)
|
---|
1091 | {
|
---|
1092 | my %dirInfo = (
|
---|
1093 | DataPt => \$data,
|
---|
1094 | DataPos => $raf->Tell() - 2,
|
---|
1095 | );
|
---|
1096 | $tagTablePtr = GetTagTable('Image::ExifTool::Photoshop::ImageData');
|
---|
1097 | $et->ProcessDirectory(\%dirInfo, $tagTablePtr);
|
---|
1098 | }
|
---|
1099 | $$et{INDENT} = $oldIndent;
|
---|
1100 | # process trailers if they exist
|
---|
1101 | my $trailInfo = Image::ExifTool::IdentifyTrailer($raf);
|
---|
1102 | $et->ProcessTrailers($trailInfo) if $trailInfo;
|
---|
1103 | }
|
---|
1104 | return $rtnVal;
|
---|
1105 | }
|
---|
1106 |
|
---|
1107 | 1; # end
|
---|
1108 |
|
---|
1109 |
|
---|
1110 | __END__
|
---|
1111 |
|
---|
1112 | =head1 NAME
|
---|
1113 |
|
---|
1114 | Image::ExifTool::Photoshop - Read/write Photoshop IRB meta information
|
---|
1115 |
|
---|
1116 | =head1 SYNOPSIS
|
---|
1117 |
|
---|
1118 | This module is loaded automatically by Image::ExifTool when required.
|
---|
1119 |
|
---|
1120 | =head1 DESCRIPTION
|
---|
1121 |
|
---|
1122 | Photoshop writes its own format of meta information called a Photoshop IRB
|
---|
1123 | resource which is located in the APP13 record of JPEG files. This module
|
---|
1124 | contains the definitions to read this information.
|
---|
1125 |
|
---|
1126 | =head1 NOTES
|
---|
1127 |
|
---|
1128 | Photoshop IRB blocks may have an associated resource name. These names are
|
---|
1129 | usually just an empty string, but if not empty they are displayed in the
|
---|
1130 | verbose level 2 (or greater) output. A special C<SetResourceName> flag may
|
---|
1131 | be set to '1' in the tag information hash to cause the resource name to be
|
---|
1132 | appended to the value when extracted. If this is done, the returned value
|
---|
1133 | has the form "VALUE/#NAME#/". When writing, the writer routine looks for
|
---|
1134 | this syntax (if C<SetResourceName> is defined), and and uses the embedded
|
---|
1135 | name to set the name of the new resource. This allows the resource names to
|
---|
1136 | be preserved when copying Photoshop information via user-defined tags.
|
---|
1137 |
|
---|
1138 | =head1 AUTHOR
|
---|
1139 |
|
---|
1140 | Copyright 2003-2021, Phil Harvey (philharvey66 at gmail.com)
|
---|
1141 |
|
---|
1142 | This library is free software; you can redistribute it and/or modify it
|
---|
1143 | under the same terms as Perl itself.
|
---|
1144 |
|
---|
1145 | =head1 REFERENCES
|
---|
1146 |
|
---|
1147 | =over 4
|
---|
1148 |
|
---|
1149 | =item L<http://www.fine-view.com/jp/lab/doc/ps6ffspecsv2.pdf>
|
---|
1150 |
|
---|
1151 | =item L<http://www.ozhiker.com/electronics/pjmt/jpeg_info/irb_jpeg_qual.html>
|
---|
1152 |
|
---|
1153 | =item L<http://www.fileformat.info/format/psd/egff.htm>
|
---|
1154 |
|
---|
1155 | =item L<http://libpsd.graphest.com/files/Photoshop%20File%20Formats.pdf>
|
---|
1156 |
|
---|
1157 | =item L<http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/>
|
---|
1158 |
|
---|
1159 | =back
|
---|
1160 |
|
---|
1161 | =head1 SEE ALSO
|
---|
1162 |
|
---|
1163 | L<Image::ExifTool::TagNames/Photoshop Tags>,
|
---|
1164 | L<Image::ExifTool(3pm)|Image::ExifTool>,
|
---|
1165 | L<Image::MetaData::JPEG(3pm)|Image::MetaData::JPEG>
|
---|
1166 |
|
---|
1167 | =cut
|
---|