VCard.pm 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. #------------------------------------------------------------------------------
  2. # File: VCard.pm
  3. #
  4. # Description: Read vCard and iCalendar meta information
  5. #
  6. # Revisions: 2015/04/05 - P. Harvey Created
  7. # 2015/05/02 - PH Added iCalendar support
  8. #
  9. # References: 1) http://en.m.wikipedia.org/wiki/VCard
  10. # 2) http://tools.ietf.org/html/rfc6350
  11. # 3) http://tools.ietf.org/html/rfc5545
  12. #------------------------------------------------------------------------------
  13. package Image::ExifTool::VCard;
  14. use strict;
  15. use vars qw($VERSION);
  16. use Image::ExifTool qw(:DataAccess :Utils);
  17. $VERSION = '1.04';
  18. my %unescapeVCard = ( '\\'=>'\\', ','=>',', 'n'=>"\n", 'N'=>"\n" );
  19. # lookup for iCalendar components (used to generate family 1 group names if top level)
  20. my %isComponent = ( Event=>1, Todo=>1, Journal=>1, Freebusy=>1, Timezone=>1, Alarm=>1 );
  21. my %timeInfo = (
  22. # convert common date/time formats to EXIF style
  23. ValueConv => q{
  24. $val =~ s/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(Z?)/$1:$2:$3 $4:$5:$6$7/g;
  25. $val =~ s/(\d{4})(\d{2})(\d{2})/$1:$2:$3/g;
  26. $val =~ s/(\d{4})-(\d{2})-(\d{2})/$1:$2:$3/g;
  27. return $val;
  28. },
  29. PrintConv => '$self->ConvertDateTime($val)',
  30. );
  31. # vCard tags (ref 1/2/PH)
  32. # Note: The case of all tag ID's is normalized to lowercase with uppercase first letter
  33. %Image::ExifTool::VCard::Main = (
  34. GROUPS => { 2 => 'Document' },
  35. VARS => { NO_LOOKUP => 1 }, # omit tags from lookup
  36. NOTES => q{
  37. This table lists common vCard tags, but ExifTool will also extract any other
  38. vCard tags found. Tag names may have "Pref" added to indicate the preferred
  39. instance of a vCard property, and other "TYPE" parameters may also added to
  40. the tag name. VCF files may contain multiple vCard entries which are
  41. distinguished by the ExifTool family 3 group name (document number). See
  42. L<http://tools.ietf.org/html/rfc6350> for the vCard 4.0 specification.
  43. },
  44. Version => { Name => 'VCardVersion', Description => 'VCard Version' },
  45. Fn => { Name => 'FormattedName', Groups => { 2 => 'Author' } },
  46. N => { Name => 'Name', Groups => { 2 => 'Author' } },
  47. Bday => { Name => 'Birthday', Groups => { 2 => 'Time' }, %timeInfo },
  48. Tz => { Name => 'TimeZone', Groups => { 2 => 'Time' } },
  49. Adr => { Name => 'Address', Groups => { 2 => 'Location' } },
  50. Geo => {
  51. Name => 'Geolocation',
  52. Groups => { 2 => 'Location' },
  53. # when used as a parameter, VCard 4.0 adds a "geo:" prefix that we need to remove
  54. ValueConv => '$val =~ s/^geo://; $val',
  55. },
  56. Anniversary => { },
  57. Email => { },
  58. Gender => { },
  59. Impp => 'IMPP',
  60. Lang => 'Language',
  61. Logo => { },
  62. Nickname => { },
  63. Note => { },
  64. Org => 'Organization',
  65. Photo => { Groups => { 2 => 'Preview' } },
  66. Prodid => 'Software',
  67. Rev => 'Revision',
  68. Sound => { },
  69. Tel => 'Telephone',
  70. Title => 'JobTitle',
  71. Uid => 'UID',
  72. Url => 'URL',
  73. 'X-ablabel' => { Name => 'ABLabel', PrintConv => '$val =~ s/^_\$!<(.*)>!\$_$/$1/; $val' },
  74. 'X-abdate' => { Name => 'ABDate', Groups => { 2 => 'Time' }, %timeInfo },
  75. 'X-aim' => 'AIM',
  76. 'X-icq' => 'ICQ',
  77. 'X-abuid' => 'AB_UID',
  78. 'X-abrelatednames' => 'ABRelatedNames',
  79. 'X-socialprofile' => 'SocialProfile',
  80. );
  81. %Image::ExifTool::VCard::VCalendar = (
  82. GROUPS => { 1 => 'VCalendar', 2 => 'Document' },
  83. VARS => { NO_LOOKUP => 1 }, # omit tags from lookup
  84. NOTES => q{
  85. The VCard module is also used to process iCalendar ICS files since they use
  86. a format similar to vCard. The following table lists standard iCalendar
  87. tags, but any existing tags will be extracted. Top-level iCalendar
  88. components (eg. Event, Todo, Timezone, etc.) are used for the family 1 group
  89. names, and embedded components (eg. Alarm) are added as a prefix to the tag
  90. name. See L<http://tools.ietf.org/html/rfc5545> for the official iCalendar
  91. 2.0 specification.
  92. },
  93. Version => { Name => 'VCalendarVersion', Description => 'VCalendar Version' },
  94. Calscale => 'CalendarScale',
  95. Method => { },
  96. Prodid => 'Software',
  97. Attach => 'Attachment',
  98. Categories => { },
  99. Class => 'Classification',
  100. Comment => { },
  101. Description => { },
  102. Geo => {
  103. Name => 'Geolocation',
  104. Groups => { 2 => 'Location' },
  105. ValueConv => '$val =~ s/^geo://; $val',
  106. },
  107. Location => { Name => 'Location', Groups => { 2 => 'Location' } },
  108. 'Percent-complete' => 'PercentComplete',
  109. Priority => { },
  110. Resources => { },
  111. Status => { },
  112. Summary => { },
  113. Completed => { Name => 'DateTimeCompleted', Groups => { 2 => 'Time' }, %timeInfo },
  114. Dtend => { Name => 'DateTimeEnd', Groups => { 2 => 'Time' }, %timeInfo },
  115. Due => { Name => 'DateTimeDue', Groups => { 2 => 'Time' }, %timeInfo },
  116. Dtstart => { Name => 'DateTimeStart', Groups => { 2 => 'Time' }, %timeInfo },
  117. Duration => { },
  118. Freebusy => 'FreeBusyTime',
  119. Transp => 'TimeTransparency',
  120. Tzid => { Name => 'TimezoneID', Groups => { 2 => 'Time' } },
  121. Tzname => { Name => 'TimezoneName', Groups => { 2 => 'Time' } },
  122. Tzoffsetfrom=> { Name => 'TimezoneOffsetFrom', Groups => { 2 => 'Time' } },
  123. Tzoffsetto => { Name => 'TimezoneOffsetTo', Groups => { 2 => 'Time' } },
  124. Tzurl => { Name => 'TimeZoneURL', Groups => { 2 => 'Time' } },
  125. Attendee => { },
  126. Contact => { },
  127. Organizer => { },
  128. 'Recurrence-id' => 'RecurrenceID',
  129. 'Related-to' => 'RelatedTo',
  130. Url => 'URL',
  131. Uid => 'UID',
  132. Exdate => { Name => 'ExceptionDateTimes', Groups => { 2 => 'Time' }, %timeInfo },
  133. Rdate => { Name => 'RecurrenceDateTimes', Groups => { 2 => 'Time' }, %timeInfo },
  134. Rrule => { Name => 'RecurrenceRule', Groups => { 2 => 'Time' } },
  135. Action => { },
  136. Repeat => { },
  137. Trigger => { },
  138. Created => { Name => 'DateCreated', Groups => { 2 => 'Time' }, %timeInfo },
  139. Dtstamp => { Name => 'DateTimeStamp', Groups => { 2 => 'Time' }, %timeInfo },
  140. 'Last-modified' => { Name => 'ModifyDate', Groups => { 2 => 'Time' }, %timeInfo },
  141. Sequence => 'SequenceNumber',
  142. 'Request-status' => 'RequestStatus',
  143. Acknowledged=> { Name => 'Acknowledged', Groups => { 2 => 'Time' }, %timeInfo },
  144. );
  145. #------------------------------------------------------------------------------
  146. # Get vCard tag, creating if necessary
  147. # Inputs: 0) ExifTool ref, 1) tag table ref, 2) tag ID, 3) tag Name,
  148. # 4) source tagInfo ref, 5) lang code
  149. # Returns: tagInfo ref
  150. sub GetVCardTag($$$$;$$)
  151. {
  152. my ($et, $tagTablePtr, $tag, $name, $srcInfo, $langCode) = @_;
  153. my $tagInfo = $$tagTablePtr{$tag};
  154. unless ($tagInfo) {
  155. if ($srcInfo) {
  156. $tagInfo = { %$srcInfo };
  157. } else {
  158. $tagInfo = { };
  159. $et->VPrint(0, $$et{INDENT}, "[adding $tag]\n");
  160. }
  161. $$tagInfo{Name} = $name;
  162. delete $$tagInfo{Description}; # create new description
  163. AddTagToTable($tagTablePtr, $tag, $tagInfo);
  164. }
  165. # handle alternate languages (the "language" parameter)
  166. $tagInfo = Image::ExifTool::GetLangInfo($tagInfo, $langCode) if $langCode;
  167. return $tagInfo;
  168. }
  169. #------------------------------------------------------------------------------
  170. # Decode vCard text
  171. # Inputs: 0) ExifTool ref, 1) vCard text, 2) encoding
  172. # Returns: decoded text (or array ref for a list of values)
  173. sub DecodeVCardText($$;$)
  174. {
  175. my ($et, $val, $enc) = @_;
  176. $enc = defined($enc) ? lc $enc : '';
  177. if ($enc eq 'b' or $enc eq 'base64') {
  178. require Image::ExifTool::XMP;
  179. $val = Image::ExifTool::XMP::DecodeBase64($val);
  180. } else {
  181. if ($enc eq 'quoted-printable') {
  182. # convert "=HH" hex codes to characters
  183. $val =~ s/=([0-9a-f]{2})/chr(hex($1))/ige;
  184. }
  185. $val = $et->Decode($val, 'UTF8'); # convert from UTF-8
  186. # split into separate items if it contains an unescaped comma
  187. my $list = $val =~ s/(^|[^\\])((\\\\)*),/$1$2\0/g;
  188. # unescape necessary characters in value
  189. $val =~ s/\\(.)/$unescapeVCard{$1}||$1/sge;
  190. if ($list) {
  191. my @vals = split /\0/, $val;
  192. $val = \@vals;
  193. }
  194. }
  195. return $val;
  196. }
  197. #------------------------------------------------------------------------------
  198. # Read information in a vCard file
  199. # Inputs: 0) ExifTool ref, 1) dirInfo ref
  200. # Returns: 1 on success, 0 if this wasn't a valid vCard file
  201. sub ProcessVCard($$)
  202. {
  203. local $_;
  204. my ($et, $dirInfo) = @_;
  205. my $raf = $$dirInfo{RAF};
  206. my ($buff, $val, $ok, $component, %compNum, @count);
  207. return 0 unless $raf->Read($buff, 24) and $raf->Seek(0,0) and $buff=~/^BEGIN:(VCARD|VCALENDAR)\r\n/i;
  208. my ($type, $lbl, $tbl, $ext) = uc($1) eq 'VCARD' ? qw(VCard vCard Main VCF) : qw(ICS iCalendar VCalendar ICS);
  209. $et->SetFileType($type, undef, $ext);
  210. return 1 if $$et{OPTIONS}{FastScan} and $$et{OPTIONS}{FastScan} == 3;
  211. local $/ = "\r\n";
  212. my $tagTablePtr = GetTagTable("Image::ExifTool::VCard::$tbl");
  213. my $more = $raf->ReadLine($buff); # read first line
  214. chomp $buff if $more;
  215. while ($more) {
  216. # retrieve previous line from $buff
  217. $val = $buff if defined $buff;
  218. # read ahead to next line to see if is a continuation
  219. $more = $raf->ReadLine($buff);
  220. if ($more) {
  221. chomp $buff;
  222. # add continuation line if necessary
  223. $buff =~ s/^[ \t]// and $val .= $buff, undef($buff), next;
  224. }
  225. if ($val =~ /^(BEGIN|END):(V?)(\w+)$/i) {
  226. my ($begin, $v, $what) = ((lc($1) eq 'begin' ? 1 : 0), $2, ucfirst lc $3);
  227. if ($what eq 'Card' or $what eq 'Calendar') {
  228. if ($begin) {
  229. @count = ( { } ); # reset group counters
  230. } else {
  231. $ok = 1; # ok if we read at least on full VCARD or VCALENDAR
  232. }
  233. next;
  234. }
  235. # absorb top-level component into family 1 group name
  236. if ($isComponent{$what}) {
  237. if ($begin) {
  238. unless ($component) {
  239. # begin a new top-level component
  240. @count = ( { } );
  241. $component = $what;
  242. $compNum{$component} = ($compNum{$component} || 0) + 1;
  243. next;
  244. }
  245. } elsif ($component and $component eq $what) {
  246. # this top-level component has ended
  247. undef $component;
  248. next;
  249. }
  250. }
  251. # keep count of each component at this level
  252. if ($begin) {
  253. $count[-1]{$what} = ($count[-1]{$what} || 0) + 1 if $v;
  254. push @count, { obj => $what };
  255. } elsif (@count > 1) {
  256. pop @count;
  257. }
  258. next;
  259. } elsif ($ok) {
  260. $ok = 0;
  261. $$et{DOC_NUM} = ++$$et{DOC_COUNT}; # read next card as a new document
  262. }
  263. unless ($val =~ s/^([-A-Za-z0-9.]+)//) {
  264. $et->WarnOnce("Unrecognized line in $lbl file");
  265. next;
  266. }
  267. my $tag = $1;
  268. # set group if it exists
  269. if ($tag =~ s/^([-A-Za-z0-9]+)\.//) {
  270. $$et{SET_GROUP1} = ucfirst lc $1;
  271. } elsif ($component) {
  272. $$et{SET_GROUP1} = $component . $compNum{$component};
  273. } else {
  274. delete $$et{SET_GROUP1};
  275. }
  276. my ($name, %param, $p, @val);
  277. # vCard tag ID's are case-insensitive, so normalize to lowercase with
  278. # an uppercase first letter for use as a tag name
  279. $name = ucfirst $tag if $tag =~ /[a-z]/; # preserve mixed case in name if it exists
  280. $tag = ucfirst lc $tag;
  281. # get source tagInfo reference
  282. my $srcInfo = $et->GetTagInfo($tagTablePtr, $tag);
  283. if ($srcInfo) {
  284. $name = $$srcInfo{Name}; # use our name
  285. } else {
  286. $name or $name = $tag;
  287. # remove leading "X-" from name if it exists
  288. $name =~ s/^X-// and $name = ucfirst $name;
  289. }
  290. # add object name(s) to tag if necessary
  291. if (@count > 1) {
  292. my $i;
  293. for ($i=$#count-1; $i>=0; --$i) {
  294. my $pre = $count[$i-1]{obj}; # use containing object name as tag prefix
  295. my $c = $count[$i]{$pre}; # add index for object number
  296. $c = '' unless defined $c;
  297. $tag = $pre . $c . $tag;
  298. $name = $pre . $c . $name;
  299. }
  300. }
  301. # parse parameters
  302. while ($val =~ s/^;([-A-Za-z0-9]*)(=?)//) {
  303. $p = ucfirst lc $1;
  304. # convert old vCard 2.x parameters to the new "TYPE=" format
  305. $2 or $val = $1 . $val, $p = 'Type';
  306. # read parameter value
  307. for (;;) {
  308. last unless $val =~ s/^"([^"]*)",?// or $val =~ s/^([^";:,]+,?)//;
  309. my $v = $p eq 'Type' ? ucfirst lc $1 : $1;
  310. $param{$p} = defined($param{$p}) ? $param{$p} . $v : $v;
  311. }
  312. if (defined $param{$p}) {
  313. $param{$p} =~ s/\\(.)/$unescapeVCard{$1}||$1/sge;
  314. } else {
  315. $param{$p} = '';
  316. }
  317. }
  318. $val =~ s/^:// or $et->WarnOnce("Invalid line in $lbl file"), next;
  319. # add 'Type' parameter to id and name if it exists
  320. $param{Type} and $tag .= $param{Type}, $name .= $param{Type};
  321. # convert base64-encoded data
  322. if ($val =~ s{^data:(\w+)/(\w+);base64,}{}) {
  323. my $xtra = ucfirst(lc $1) . ucfirst(lc $2);
  324. $tag .= $xtra;
  325. $name .= $xtra;
  326. $param{Encoding} = 'base64';
  327. }
  328. $val = DecodeVCardText($et, $val, $param{Encoding});
  329. my $tagInfo = GetVCardTag($et, $tagTablePtr, $tag, $name, $srcInfo, $param{Language});
  330. $et->HandleTag($tagTablePtr, $tag, $val, TagInfo => $tagInfo);
  331. # handle some other parameters that we care about (ignore the rest for now)
  332. foreach $p (qw(Geo Label Tzid)) {
  333. next unless defined $param{$p};
  334. # use tag attributes from our table if it exists
  335. my $srcTag2 = $et->GetTagInfo($tagTablePtr, $p);
  336. my $pn = $srcTag2 ? $$srcTag2{Name} : $p;
  337. $val = DecodeVCardText($et, $param{$p});
  338. # add parameter to tag ID and name
  339. my ($tg, $nm) = ($tag . $p, $name . $pn);
  340. $tagInfo = GetVCardTag($et, $tagTablePtr, $tg, $nm, $srcTag2, $param{Language});
  341. $et->HandleTag($tagTablePtr, $tg, $val, TagInfo => $tagInfo);
  342. }
  343. }
  344. delete $$et{SET_GROUP1};
  345. delete $$et{DOC_NUM};
  346. $ok or $et->Warn("Missing $lbl end");
  347. return 1;
  348. }
  349. 1; # end
  350. __END__
  351. =head1 NAME
  352. Image::ExifTool::VCard - Read vCard and iCalendar meta information
  353. =head1 SYNOPSIS
  354. This module is used by Image::ExifTool
  355. =head1 DESCRIPTION
  356. This module contains definitions required by Image::ExifTool to read meta
  357. information from vCard VCF and iCalendar ICS files.
  358. =head1 AUTHOR
  359. Copyright 2003-2016, Phil Harvey (phil at owl.phy.queensu.ca)
  360. This library is free software; you can redistribute it and/or modify it
  361. under the same terms as Perl itself.
  362. =head1 REFERENCES
  363. =over 4
  364. =item L<http://en.m.wikipedia.org/wiki/VCard>
  365. =item L<http://tools.ietf.org/html/rfc6350>
  366. =item L<http://tools.ietf.org/html/rfc5545>
  367. =back
  368. =head1 SEE ALSO
  369. L<Image::ExifTool::TagNames/VCard Tags>,
  370. L<Image::ExifTool::TagNames/VCard VCalendar Tags>,
  371. L<Image::ExifTool(3pm)|Image::ExifTool>
  372. =cut