~ubuntu-branches/ubuntu/karmic/libimage-exiftool-perl/karmic

« back to all changes in this revision

Viewing changes to lib/Image/ExifTool/Flash.pm

  • Committer: Bazaar Package Importer
  • Author(s): Mari Wang
  • Date: 2008-02-04 20:32:53 UTC
  • mfrom: (1.1.5 upstream)
  • Revision ID: james.westby@ubuntu.com-20080204203253-mpbal8trlfe1fz5d
Tags: 7.00-1
* Upload of new production release (Closes: #456430)
* Added Recommends: libcompress-zlib-perl (Closes: #435589)
* Package now includes iptc2xmp.args and xmp2iptc.args, they are put
  into /usr/share/libimage-exiftool/ (Closes: #436100)
* Updated standards-version (3.7.2 -> 3.7.3). No changes needed.
* Lots of updates and bugfixes compared to last debian version
  (6.90).  See the Changes file for details
* Upload sponsored by Petter Reinholdtsen

Show diffs side-by-side

added added

removed removed

Lines of Context:
4
4
# Description:  Read Shockwave Flash meta information
5
5
#
6
6
# Revisions:    05/16/2006 - P. Harvey Created
 
7
#               06/07/2007 - PH Added support for FLV (Flash Video) files
7
8
#
8
9
# References:   1) http://www.the-labs.com/MacromediaFlash/SWF-Spec/SWFfileformat.html
9
10
#               2) http://sswf.sourceforge.net/SWFalexref.html
 
11
#               3) http://osflash.org/flv/
 
12
#
 
13
# Notes:        I'll add AMF3 support if someone sends me a FLV with AMF3 data
10
14
#------------------------------------------------------------------------------
11
15
 
12
16
package Image::ExifTool::Flash;
14
18
use strict;
15
19
use vars qw($VERSION);
16
20
use Image::ExifTool qw(:DataAccess :Utils);
17
 
 
18
 
$VERSION = '1.01';
19
 
 
 
21
use Image::ExifTool::FLAC;
 
22
 
 
23
$VERSION = '1.02';
 
24
 
 
25
sub ProcessMeta($$$;$);
 
26
 
 
27
# information extracted from SWF header
20
28
%Image::ExifTool::Flash::Main = (
21
29
    GROUPS => { 2 => 'Video' },
22
30
    NOTES => q{
24
32
        files.
25
33
    },
26
34
    FlashVersion => { },
27
 
    Compressed => { PrintConv => { 0 => 'False', 1 => 'True' } },
28
 
    ImageWidth => { },
29
 
    ImageHeight => { },
30
 
    FrameRate => { },
31
 
    FrameCount => { },
 
35
    Compressed   => { PrintConv => { 0 => 'False', 1 => 'True' } },
 
36
    ImageWidth   => { },
 
37
    ImageHeight  => { },
 
38
    FrameRate    => { },
 
39
    FrameCount   => { },
32
40
    Duration => {
33
41
        Notes => 'calculated from FrameRate and FrameCount',
34
42
        PrintConv => 'sprintf("%.2f sec",$val)',
35
43
    },
36
44
);
37
45
 
 
46
# packets in Flash Video files
 
47
%Image::ExifTool::Flash::FLV = (
 
48
    NOTES => q{
 
49
        Information is extracted from the following packets in FLV (Flash Video)
 
50
        files.
 
51
    },
 
52
    0x08 => {
 
53
        Name => 'Audio',
 
54
        BitMask => 0x04,
 
55
        SubDirectory => { TagTable => 'Image::ExifTool::Flash::Audio' },
 
56
    },
 
57
    0x09 => {
 
58
        Name => 'Video',
 
59
        BitMask => 0x01,
 
60
        SubDirectory => { TagTable => 'Image::ExifTool::Flash::Video' },
 
61
    },
 
62
    0x12 => {
 
63
        Name => 'Meta',
 
64
        SubDirectory => { TagTable => 'Image::ExifTool::Flash::Meta' },
 
65
    },
 
66
);
 
67
 
 
68
# tags in Flash Video packet header
 
69
%Image::ExifTool::Flash::Audio = (
 
70
    PROCESS_PROC => \&Image::ExifTool::FLAC::ProcessBitStream,
 
71
    GROUPS => { 2 => 'Audio' },
 
72
    NOTES => 'Information extracted from the Flash Audio header.',
 
73
    'Bit0-3' => {
 
74
        Name => 'AudioEncoding',
 
75
        PrintConv => {
 
76
            0 => 'Uncompressed',
 
77
            1 => 'ADPCM',
 
78
            2 => 'MP3',
 
79
            5 => 'Nellymoser 8kHz mono',
 
80
            6 => 'Nellymoser',
 
81
        },
 
82
    },
 
83
    'Bit4-5' => {
 
84
        Name => 'AudioSampleRate',
 
85
        ValueConv => {
 
86
            0 => 5512,
 
87
            1 => 11025,
 
88
            2 => 22050,
 
89
            3 => 44100,
 
90
        },
 
91
    },
 
92
    'Bit6' => {
 
93
        Name => 'AudioSampleBits',
 
94
        ValueConv => '8 * ($val + 1)',
 
95
    },
 
96
    'Bit7' => {
 
97
        Name => 'AudioChannels',
 
98
        ValueConv => '$val + 1',
 
99
        PrintConv => {
 
100
            1 => '1 (mono)',
 
101
            2 => '2 (stereo)',
 
102
        },
 
103
    },
 
104
);
 
105
 
 
106
# tags in Flash Video packet header
 
107
%Image::ExifTool::Flash::Video = (
 
108
    PROCESS_PROC => \&Image::ExifTool::FLAC::ProcessBitStream,
 
109
    GROUPS => { 2 => 'Video' },
 
110
    NOTES => 'Information extracted from the Flash Video header.',
 
111
    'Bit4-7' => {
 
112
        Name => 'VideoEncoding',
 
113
        PrintConv => {
 
114
            2 => 'Sorensen H.263',
 
115
            3 => 'Screen video',
 
116
            4 => 'On2 VP6',
 
117
        },
 
118
    },
 
119
);
 
120
 
 
121
# tags in Flash META packet (in ActionScript Message Format)
 
122
%Image::ExifTool::Flash::Meta = (
 
123
    PROCESS_PROC => \&ProcessMeta,
 
124
    GROUPS => { 2 => 'Video' },
 
125
    NOTES => q{
 
126
        Below are a few observed FLV Meta tags, but ExifTool will attempt to extract
 
127
        information from any tag found.
 
128
    },
 
129
    'audiocodecid'  => { Name => 'AudioCodecID',    Groups => { 2 => 'Audio' } },
 
130
    'audiodatarate' => {
 
131
        Name => 'AudioBitrate',
 
132
        Groups => { 2 => 'Audio' },
 
133
        ValueConv => '$val * 1000',
 
134
        PrintConv => 'int($val + 0.5)',
 
135
    },
 
136
    'audiodelay'    => { Name => 'AudioDelay',      Groups => { 2 => 'Audio' } },
 
137
    'audiosamplerate'=>{ Name => 'AudioSampleRate', Groups => { 2 => 'Audio' } },
 
138
    'audiosamplesize'=>{ Name => 'AudioSampleSize', Groups => { 2 => 'Audio' } },
 
139
    'audiosize'     => { Name => 'AudioSize',       Groups => { 2 => 'Audio' } },
 
140
    'canSeekToEnd'  => 'CanSeekToEnd',
 
141
    'creationdate'  => {
 
142
        # (not an AMF date type in my sample)
 
143
        Name => 'CreateDate',
 
144
        Groups => { 2 => 'Time' },
 
145
        ValueConv => '$val=~s/\s+$//; $val',    # trim trailing whitespace
 
146
    },
 
147
    'cuePoints'     => {
 
148
        Name => 'CuePoint',
 
149
        SubDirectory => { TagTable => 'Image::ExifTool::Flash::CuePoint' },
 
150
    },
 
151
    'datasize'      => 'DataSize',
 
152
    'duration' => {
 
153
        Name => 'Duration',
 
154
        PrintConv => 'sprintf("%.3fs",$val)',
 
155
    },
 
156
    'filesize'      => 'FileSizeBytes',
 
157
    'framerate'     => {
 
158
        Name => 'FrameRate',
 
159
        PrintConv => 'int($val * 1000 + 0.5) / 1000',
 
160
    },
 
161
    'hasAudio'      => { Name => 'HasAudio',        Groups => { 2 => 'Audio' } },
 
162
    'hasCuePoints'  => 'HasCuePoints',
 
163
    'hasKeyframes'  => 'HasKeyFrames',
 
164
    'hasMetadata'   => 'HasMetadata',
 
165
    'hasVideo'      => 'HasVideo',
 
166
    'height'        => 'ImageHeight',
 
167
    'keyframesTimes'=> 'KeyFramesTimes',
 
168
    'keyframesFilepositions' => 'KeyFramePositions',
 
169
    'lasttimestamp' => 'LastTimeStamp',
 
170
    'lastkeyframetimestamp' => 'LastKeyFrameTime',
 
171
    'metadatacreator'=>'MetadataCreator',
 
172
    'metadatadate'  => {
 
173
        Name => 'MetadataDate',
 
174
        Groups => { 2 => 'Time' },
 
175
        PrintConv => '$self->ConvertDateTime($val)',
 
176
    },
 
177
    'stereo'        => { Name => 'Stereo',          Groups => { 2 => 'Audio' } },
 
178
    'videocodecid'  => 'VideoCodecID',
 
179
    'videodatarate' => {
 
180
        Name => 'VideoBitrate',
 
181
        ValueConv => '$val * 1000',
 
182
        PrintConv => 'int($val + 0.5)',
 
183
    },
 
184
    'videosize'     => 'VideoSize',
 
185
    'width'         => 'ImageWidth',
 
186
);
 
187
 
 
188
# tags in Flash META CuePoint structure
 
189
%Image::ExifTool::Flash::CuePoint = (
 
190
    PROCESS_PROC => \&ProcessMeta,
 
191
    GROUPS => { 2 => 'Video' },
 
192
    NOTES => q{
 
193
        These tag names are added to the CuePoint name to generate complete tag
 
194
        names like "CuePoint0Name".
 
195
    },
 
196
    'name' => 'Name',
 
197
    'type' => 'Type',
 
198
    'time' => 'Time',
 
199
    'parameters' => {
 
200
        Name => 'Parameter',
 
201
        SubDirectory => { TagTable => 'Image::ExifTool::Flash::Parameter' },
 
202
    },
 
203
);
 
204
 
 
205
# tags in Flash META CuePoint Parameter structure
 
206
%Image::ExifTool::Flash::Parameter = (
 
207
    PROCESS_PROC => \&ProcessMeta,
 
208
    GROUPS => { 2 => 'Video' },
 
209
    NOTES => q{
 
210
        There are no pre-defined parameter tags, but ExifTool will extract any
 
211
        existing parameters, with tag names like "CuePoint0ParameterXxx".
 
212
    },
 
213
);
 
214
 
 
215
# name lookup for known AMF data types
 
216
my @amfType = qw(double boolean string object movieClip null undefined reference
 
217
                 mixedArray objectEnd array date longString unsupported recordSet
 
218
                 XML typedObject AMF3data);
 
219
 
 
220
# test for AMF structure types (object, mixed array or typed object)
 
221
my %isStruct = ( 0x03 => 1, 0x08 => 1, 0x10 => 1 );
 
222
 
 
223
#------------------------------------------------------------------------------
 
224
# Process Flash Video AMF Meta packet (ref 3)
 
225
# Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref
 
226
#         3) Set to extract single type/value only
 
227
# Returns: 1 on success, (or type/value if extracting single value)
 
228
# Notes: Updates DataPos in dirInfo if extracting single value
 
229
sub ProcessMeta($$$;$)
 
230
{
 
231
    my ($exifTool, $dirInfo, $tagTablePtr, $single) = @_;
 
232
    my $dataPt = $$dirInfo{DataPt};
 
233
    my $dataPos = $$dirInfo{DataPos};
 
234
    my $dirLen = $$dirInfo{DirLen} || length($$dataPt);
 
235
    my $pos = $$dirInfo{Pos} || 0;
 
236
    my $verbose = $exifTool->Options('Verbose');
 
237
    my ($type, $val, $rec);
 
238
 
 
239
    $exifTool->VerboseDir('Meta') unless $single;
 
240
 
 
241
Record: for ($rec=0; ; ++$rec) {
 
242
        last if $pos >= $dirLen;
 
243
        $type = ord(substr($$dataPt, $pos));
 
244
        ++$pos;
 
245
        if ($type == 0x00 or $type == 0x0b) {   # double or date
 
246
            last if $pos + 8 > $dirLen;
 
247
            $val = GetDouble($dataPt, $pos);
 
248
            $pos += 8;
 
249
            if ($type == 0x0b) {    # date
 
250
                $val /= 1000;       # convert to seconds
 
251
                my $frac = $val - int($val);    # fractional seconds
 
252
                # get time zone
 
253
                last if $pos + 2 > $dirLen;
 
254
                my $tz = Get16s($dataPt, $pos);
 
255
                $pos += 2;
 
256
                # construct date/time string
 
257
                $val = Image::ExifTool::ConvertUnixTime(int($val));
 
258
                if ($frac) {
 
259
                    $frac = sprintf('%.6f', $frac);
 
260
                    $frac =~ s/(^0|0+$)//g;
 
261
                    $val .= $frac;
 
262
                }
 
263
                # add timezone
 
264
                if ($tz < 0) {
 
265
                    $val .= '-';
 
266
                    $tz *= -1;
 
267
                } else {
 
268
                    $val .= '+';
 
269
                }
 
270
                $val .= sprintf('%.2d:%.2d', int($tz/60), $tz%60);
 
271
            }
 
272
        } elsif ($type == 0x01) {   # boolean
 
273
            last if $pos + 1 > $dirLen;
 
274
            $val = Get8u($dataPt, $pos);
 
275
            $val = { 0 => 'No', 1 => 'Yes' }->{$val} if $val < 2;
 
276
            ++$pos;
 
277
        } elsif ($type == 0x02) {   # string
 
278
            last if $pos + 2 > $dirLen;
 
279
            my $len = Get16u($dataPt, $pos);
 
280
            last if $pos + 2 + $len > $dirLen;
 
281
            $val = substr($$dataPt, $pos + 2, $len);
 
282
            $pos += 2 + $len;
 
283
        } elsif ($isStruct{$type}) {   # object, mixed array or typed object
 
284
            $exifTool->VPrint(1, "  + [$amfType[$type]]\n");
 
285
            my $getName;
 
286
            $val = '';  # dummy value
 
287
            if ($type == 0x08) {        # mixed array
 
288
                # skip last array index for mixed array
 
289
                last if $pos + 4 > $dirLen;
 
290
                $pos += 4;
 
291
            } elsif ($type == 0x10) {   # typed object
 
292
                $getName = 1;
 
293
            }
 
294
            for (;;) {
 
295
                # get tag ID (or typed object name)
 
296
                last Record if $pos + 2 > $dirLen;
 
297
                my $len = Get16u($dataPt, $pos);
 
298
                if ($pos + 2 + $len > $dirLen) {
 
299
                    $exifTool->Warn("Truncated $amfType[$type] record");
 
300
                    last Record;
 
301
                }
 
302
                my $tag = substr($$dataPt, $pos + 2, $len);
 
303
                $pos += 2 + $len;
 
304
                # first string of a typed object is the object name
 
305
                if ($getName) {
 
306
                    $exifTool->VPrint(1,"  | (object name '$tag')\n");
 
307
                    undef $getName;
 
308
                    next; # (ignore name for now)
 
309
                }
 
310
                my $subTablePtr = $tagTablePtr;
 
311
                my $tagInfo = $$subTablePtr{$tag};
 
312
                # switch to subdirectory table if necessary
 
313
                if ($tagInfo and $$tagInfo{SubDirectory}) {
 
314
                    $tag = $$tagInfo{Name}; # use our name for the tag
 
315
                    $subTablePtr = GetTagTable($tagInfo->{SubDirectory}->{TagTable});
 
316
                }
 
317
                # get object value
 
318
                my $valPos = $pos + 1;
 
319
                $$dirInfo{Pos} = $pos;
 
320
                my $structName = $$dirInfo{StructName};
 
321
                # add structure name to start of tag name
 
322
                $tag = $structName . ucfirst($tag) if defined $structName;
 
323
                $$dirInfo{StructName} = $tag;       # set new structure name
 
324
                my ($t, $v) = ProcessMeta($exifTool, $dirInfo, $subTablePtr, 1);
 
325
                $$dirInfo{StructName} = $structName;# restore original structure name
 
326
                $pos = $$dirInfo{Pos};  # update to new position in packet
 
327
                # all done if this value contained tags
 
328
                last Record unless defined $t and defined $v;
 
329
                next if $isStruct{$t};  # already handled tags in sub-structures
 
330
                next if ref($v) eq 'ARRAY' and not @$v; # ignore empty arrays
 
331
                last if $t == 0x09; # (end of object)
 
332
                if (not $$subTablePtr{$tag} and $tag =~ /^\w+$/) {
 
333
                    Image::ExifTool::AddTagToTable($subTablePtr, $tag, { Name => ucfirst($tag) });
 
334
                    $verbose > 1 and $exifTool->VPrint(1, "  | (adding $tag)\n");
 
335
                }
 
336
                $exifTool->HandleTag($subTablePtr, $tag, $v,
 
337
                    DataPt  => $dataPt,
 
338
                    DataPos => $dataPos,
 
339
                    Start   => $valPos,
 
340
                    Size    => $pos - $valPos,
 
341
                    Format  => $amfType[$t] || sprintf('0x%x',$t),
 
342
                );
 
343
            }
 
344
      # } elsif ($type == 0x04) {   # movie clip (not supported)
 
345
        } elsif ($type == 0x05 or $type == 0x06 or $type == 0x09 or $type == 0x0d) {
 
346
            # null, undefined, dirLen of object, or unsupported
 
347
            $val = '';
 
348
        } elsif ($type == 0x07) {   # reference
 
349
            last if $pos + 2 > $dirLen;
 
350
            $val = Get16u($dataPt, $pos);
 
351
            $pos += 2;
 
352
        } elsif ($type == 0x0a) {   # array
 
353
            last if $pos + 4 > $dirLen;
 
354
            my $num = Get32u($dataPt, $pos);
 
355
            $$dirInfo{Pos} = $pos + 4;
 
356
            my ($i, @vals);
 
357
            # add array index to compount tag name
 
358
            my $structName = $$dirInfo{StructName};
 
359
            for ($i=0; $i<$num; ++$i) {
 
360
                $$dirInfo{StructName} = $structName . $i if defined $structName;
 
361
                my ($t, $v) = ProcessMeta($exifTool, $dirInfo, $tagTablePtr, 1);
 
362
                last Record unless defined $v;
 
363
                # save value unless contained in a sub-structure
 
364
                push @vals, $v unless $isStruct{$t};
 
365
            }
 
366
            $$dirInfo{StructName} = $structName;
 
367
            $pos = $$dirInfo{Pos};
 
368
            $val = \@vals;
 
369
        } elsif ($type == 0x0c or $type == 0x0f) {  # long string or XML
 
370
            last if $pos + 4 > $dirLen;
 
371
            my $len = Get32u($dataPt, $pos);
 
372
            last if $pos + 4 + $len > $dirLen;
 
373
            $val = substr($$dataPt, $pos + 4, $len);
 
374
            $pos += 4 + $len;
 
375
      # } elsif ($type == 0x0e) {   # record set (not supported)
 
376
      # } elsif ($type == 0x11) {   # AMF3 data (can't add support for this without a test sample)
 
377
        } else {
 
378
            my $t = $amfType[$type] || sprintf('type 0x%x',$type);
 
379
            $exifTool->Warn("AMF $t record not yet supported");
 
380
            undef $type;    # (so we don't print another warning)
 
381
            last;           # can't continue
 
382
        }
 
383
        last if $single;        # all done if extracting single value
 
384
        unless ($isStruct{$type}) {
 
385
            # only process "onMetaData" Meta packets
 
386
            if ($type == 0x02 and not $rec) {
 
387
                my $verb = ($val eq 'onMetaData') ? 'processing' : 'ignoring';
 
388
                $exifTool->VPrint(0, "  | ($verb $val information)\n");
 
389
                last unless $val eq 'onMetaData';
 
390
            } else {
 
391
                # give verbose indication if we ignore a lone value
 
392
                my $t = $amfType[$type] || sprintf('type 0x%x',$type);
 
393
                $exifTool->VPrint(1, "  | (ignored lone $t value '$val')\n");
 
394
            }
 
395
        }
 
396
    }
 
397
    if (not defined $val and defined $type) {
 
398
        $exifTool->Warn(sprintf("Truncated AMF record 0x%x",$type));
 
399
    }
 
400
    return 1 unless $single;    # all done
 
401
    $$dirInfo{Pos} = $pos;      # update position
 
402
    return($type,$val);         # return single type/value pair
 
403
}
 
404
 
 
405
#------------------------------------------------------------------------------
 
406
# Read information frame a Flash Video file
 
407
# Inputs: 0) ExifTool object reference, 1) Directory information reference
 
408
# Returns: 1 on success, 0 if this wasn't a valid Flash Video file
 
409
sub ProcessFLV($$)
 
410
{
 
411
    my ($exifTool, $dirInfo) = @_;
 
412
    my $verbose = $exifTool->Options('Verbose');
 
413
    my $raf = $$dirInfo{RAF};
 
414
    my $buff;
 
415
 
 
416
    $raf->Read($buff, 9) == 9 or return 0;
 
417
    $buff =~ /^FLV\x01/ or return 0;
 
418
    SetByteOrder('MM');
 
419
    $exifTool->SetFileType();
 
420
    my ($flags, $offset) = unpack('x4CN', $buff);
 
421
    $raf->Seek($offset-9, 1) or return 1 if $offset > 9;
 
422
    $flags &= 0x05; # only look for audio/video
 
423
    my $found = 0;
 
424
    my $tagTablePtr = GetTagTable('Image::ExifTool::Flash::FLV');
 
425
    for (;;) {
 
426
        $raf->Read($buff, 15) == 15 or last;
 
427
        my $len = unpack('x4N', $buff);
 
428
        my $type = $len >> 24;
 
429
        $len &= 0x00ffffff;
 
430
        my $tagInfo = $exifTool->GetTagInfo($tagTablePtr, $type);
 
431
        if ($verbose > 1) {
 
432
            my $name = $tagInfo ? $$tagInfo{Name} : "type $type";
 
433
            $exifTool->VPrint(1, "FLV $name packet, len $len\n");
 
434
        }
 
435
        undef $buff;
 
436
        if ($tagInfo and $$tagInfo{SubDirectory}) {
 
437
            my $mask = $$tagInfo{BitMask};
 
438
            if ($mask) {
 
439
                # handle audio or video packet
 
440
                unless ($found & $mask) {
 
441
                    $found |= $mask;
 
442
                    $flags &= ~$mask;
 
443
                    if ($len>=1 and $raf->Read($buff, 1) == 1) {
 
444
                        $len -= 1;
 
445
                    } else {
 
446
                        $exifTool->Warn("Bad $$tagInfo{Name} packet");
 
447
                        last;
 
448
                    }
 
449
                }
 
450
            } elsif ($raf->Read($buff, $len) == $len) {
 
451
                $len = 0;
 
452
            } else {
 
453
                $exifTool->Warn('Truncated Meta packet');
 
454
                last;
 
455
            }
 
456
        }
 
457
        if (defined $buff) {
 
458
            $exifTool->HandleTag($tagTablePtr, $type, undef,
 
459
                DataPt  => \$buff,
 
460
                DataPos => $raf->Tell() - length($buff),
 
461
            );
 
462
        }
 
463
        last unless $flags;
 
464
        $raf->Seek($len, 1) or last if $len;
 
465
    }
 
466
    return 1;
 
467
}
 
468
 
38
469
#------------------------------------------------------------------------------
39
470
# Found a Flash tag
40
471
# Inputs: 0) ExifTool object ref, 1) tag name, 2) tag value
141
572
=head1 DESCRIPTION
142
573
 
143
574
This module contains definitions required by Image::ExifTool to read SWF
144
 
(Shockwave Flash) files.
 
575
(Shockwave Flash) and FLV (Flash Video) files.
 
576
 
 
577
=head1 NOTES
 
578
 
 
579
Flash Video AMF3 support has not yet been added because I haven't yet found
 
580
a FLV file containing AMF3 information.  If someone sends me a sample then I
 
581
will add AMF3 support.
145
582
 
146
583
=head1 AUTHOR
147
584
 
158
595
 
159
596
=item L<http://sswf.sourceforge.net/SWFalexref.html>
160
597
 
 
598
=item L<http://osflash.org/flv/>
 
599
 
161
600
=back
162
601
 
163
602
=head1 SEE ALSO