yu.dong | c33b307 | 2024-08-21 23:14:49 -0700 | [diff] [blame^] | 1 | package Spreadsheet::WriteExcel::Properties; |
| 2 | |
| 3 | ############################################################################### |
| 4 | # |
| 5 | # Properties - A module for creating Excel property sets. |
| 6 | # |
| 7 | # |
| 8 | # Used in conjunction with Spreadsheet::WriteExcel |
| 9 | # |
| 10 | # Copyright 2000-2010, John McNamara. |
| 11 | # |
| 12 | # Documentation after __END__ |
| 13 | # |
| 14 | |
| 15 | use Exporter; |
| 16 | use strict; |
| 17 | use Carp; |
| 18 | use POSIX 'fmod'; |
| 19 | use Time::Local 'timelocal'; |
| 20 | |
| 21 | |
| 22 | |
| 23 | |
| 24 | use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS); |
| 25 | @ISA = qw(Exporter); |
| 26 | |
| 27 | $VERSION = '2.37'; |
| 28 | |
| 29 | # Set up the exports. |
| 30 | my @all_functions = qw( |
| 31 | create_summary_property_set |
| 32 | create_doc_summary_property_set |
| 33 | _pack_property_data |
| 34 | _pack_VT_I2 |
| 35 | _pack_VT_LPSTR |
| 36 | _pack_VT_FILETIME |
| 37 | ); |
| 38 | |
| 39 | my @pps_summaries = qw( |
| 40 | create_summary_property_set |
| 41 | create_doc_summary_property_set |
| 42 | ); |
| 43 | |
| 44 | @EXPORT = (); |
| 45 | @EXPORT_OK = (@all_functions); |
| 46 | %EXPORT_TAGS = (testing => \@all_functions, |
| 47 | property_sets => \@pps_summaries, |
| 48 | ); |
| 49 | |
| 50 | |
| 51 | ############################################################################### |
| 52 | # |
| 53 | # create_summary_property_set(). |
| 54 | # |
| 55 | # Create the SummaryInformation property set. This is mainly used for the |
| 56 | # Title, Subject, Author, Keywords, Comments, Last author keywords and the |
| 57 | # creation date. |
| 58 | # |
| 59 | sub create_summary_property_set { |
| 60 | |
| 61 | my @properties = @{$_[0]}; |
| 62 | |
| 63 | my $byte_order = pack 'v', 0xFFFE; |
| 64 | my $version = pack 'v', 0x0000; |
| 65 | my $system_id = pack 'V', 0x00020105; |
| 66 | my $class_id = pack 'H*', '00000000000000000000000000000000'; |
| 67 | my $num_property_sets = pack 'V', 0x0001; |
| 68 | my $format_id = pack 'H*', 'E0859FF2F94F6810AB9108002B27B3D9'; |
| 69 | my $offset = pack 'V', 0x0030; |
| 70 | my $num_property = pack 'V', scalar @properties; |
| 71 | my $property_offsets = ''; |
| 72 | |
| 73 | # Create the property set data block and calculate the offsets into it. |
| 74 | my ($property_data, $offsets) = _pack_property_data(\@properties); |
| 75 | |
| 76 | # Create the property type and offsets based on the previous calculation. |
| 77 | for my $i (0 .. @properties -1) { |
| 78 | $property_offsets .= pack('VV', $properties[$i]->[0], $offsets->[$i]); |
| 79 | } |
| 80 | |
| 81 | # Size of $size (4 bytes) + $num_property (4 bytes) + the data structures. |
| 82 | my $size = 8 + length($property_offsets) + length($property_data); |
| 83 | $size = pack 'V', $size; |
| 84 | |
| 85 | |
| 86 | return $byte_order . |
| 87 | $version . |
| 88 | $system_id . |
| 89 | $class_id . |
| 90 | $num_property_sets . |
| 91 | $format_id . |
| 92 | $offset . |
| 93 | $size . |
| 94 | $num_property . |
| 95 | $property_offsets . |
| 96 | $property_data; |
| 97 | } |
| 98 | |
| 99 | |
| 100 | ############################################################################### |
| 101 | # |
| 102 | # Create the DocSummaryInformation property set. This is mainly used for the |
| 103 | # Manager, Company and Category keywords. |
| 104 | # |
| 105 | # The DocSummary also contains a stream for user defined properties. However |
| 106 | # this is a little arcane and probably not worth the implementation effort. |
| 107 | # |
| 108 | sub create_doc_summary_property_set { |
| 109 | |
| 110 | my @properties = @{$_[0]}; |
| 111 | |
| 112 | my $byte_order = pack 'v', 0xFFFE; |
| 113 | my $version = pack 'v', 0x0000; |
| 114 | my $system_id = pack 'V', 0x00020105; |
| 115 | my $class_id = pack 'H*', '00000000000000000000000000000000'; |
| 116 | my $num_property_sets = pack 'V', 0x0002; |
| 117 | |
| 118 | my $format_id_0 = pack 'H*', '02D5CDD59C2E1B10939708002B2CF9AE'; |
| 119 | my $format_id_1 = pack 'H*', '05D5CDD59C2E1B10939708002B2CF9AE'; |
| 120 | my $offset_0 = pack 'V', 0x0044; |
| 121 | my $num_property_0 = pack 'V', scalar @properties; |
| 122 | my $property_offsets_0 = ''; |
| 123 | |
| 124 | # Create the property set data block and calculate the offsets into it. |
| 125 | my ($property_data_0, $offsets) = _pack_property_data(\@properties); |
| 126 | |
| 127 | # Create the property type and offsets based on the previous calculation. |
| 128 | for my $i (0 .. @properties -1) { |
| 129 | $property_offsets_0 .= pack('VV', $properties[$i]->[0], $offsets->[$i]); |
| 130 | } |
| 131 | |
| 132 | # Size of $size (4 bytes) + $num_property (4 bytes) + the data structures. |
| 133 | my $data_len = 8 + length($property_offsets_0) + length($property_data_0); |
| 134 | my $size_0 = pack 'V', $data_len; |
| 135 | |
| 136 | |
| 137 | # The second property set offset is at the end of the first property set. |
| 138 | my $offset_1 = pack 'V', 0x0044 + $data_len; |
| 139 | |
| 140 | # We will use a static property set stream rather than try to generate it. |
| 141 | my $property_data_1 = pack 'H*', join '', qw ( |
| 142 | 98 00 00 00 03 00 00 00 00 00 00 00 20 00 00 00 |
| 143 | 01 00 00 00 36 00 00 00 02 00 00 00 3E 00 00 00 |
| 144 | 01 00 00 00 02 00 00 00 0A 00 00 00 5F 50 49 44 |
| 145 | 5F 47 55 49 44 00 02 00 00 00 E4 04 00 00 41 00 |
| 146 | 00 00 4E 00 00 00 7B 00 31 00 36 00 43 00 34 00 |
| 147 | 42 00 38 00 33 00 42 00 2D 00 39 00 36 00 35 00 |
| 148 | 46 00 2D 00 34 00 42 00 32 00 31 00 2D 00 39 00 |
| 149 | 30 00 33 00 44 00 2D 00 39 00 31 00 30 00 46 00 |
| 150 | 41 00 44 00 46 00 41 00 37 00 30 00 31 00 42 00 |
| 151 | 7D 00 00 00 00 00 00 00 2D 00 39 00 30 00 33 00 |
| 152 | ); |
| 153 | |
| 154 | |
| 155 | return $byte_order . |
| 156 | $version . |
| 157 | $system_id . |
| 158 | $class_id . |
| 159 | $num_property_sets . |
| 160 | $format_id_0 . |
| 161 | $offset_0 . |
| 162 | $format_id_1 . |
| 163 | $offset_1 . |
| 164 | |
| 165 | $size_0 . |
| 166 | $num_property_0 . |
| 167 | $property_offsets_0 . |
| 168 | $property_data_0 . |
| 169 | |
| 170 | $property_data_1; |
| 171 | } |
| 172 | |
| 173 | |
| 174 | ############################################################################### |
| 175 | # |
| 176 | # _pack_property_data(). |
| 177 | # |
| 178 | # Create a packed property set structure. Strings are null terminated and |
| 179 | # padded to a 4 byte boundary. We also use this function to keep track of the |
| 180 | # property offsets within the data structure. These offsets are used by the |
| 181 | # calling functions. Currently we only need to handle 4 property types: |
| 182 | # VT_I2, VT_LPSTR, VT_FILETIME. |
| 183 | # |
| 184 | sub _pack_property_data { |
| 185 | |
| 186 | my @properties = @{$_[0]}; |
| 187 | my $offset = $_[1] || 0; |
| 188 | my $packed_property = ''; |
| 189 | my $data = ''; |
| 190 | my @offsets; |
| 191 | |
| 192 | # Get the strings codepage from the first property. |
| 193 | my $codepage = $properties[0]->[2]; |
| 194 | |
| 195 | # The properties start after 8 bytes for size + num_properties + 8 bytes |
| 196 | # for each propety type/offset pair. |
| 197 | $offset += 8 * (@properties + 1); |
| 198 | |
| 199 | for my $property (@properties) { |
| 200 | push @offsets, $offset; |
| 201 | |
| 202 | my $property_type = $property->[1]; |
| 203 | |
| 204 | if ($property_type eq 'VT_I2') { |
| 205 | $packed_property = _pack_VT_I2($property->[2]); |
| 206 | } |
| 207 | elsif ($property_type eq 'VT_LPSTR') { |
| 208 | $packed_property = _pack_VT_LPSTR($property->[2], $codepage); |
| 209 | } |
| 210 | elsif ($property_type eq 'VT_FILETIME') { |
| 211 | $packed_property = _pack_VT_FILETIME($property->[2]); |
| 212 | } |
| 213 | else { |
| 214 | croak "Unknown property type: $property_type\n"; |
| 215 | } |
| 216 | |
| 217 | $offset += length $packed_property; |
| 218 | $data .= $packed_property; |
| 219 | } |
| 220 | |
| 221 | return $data, \@offsets; |
| 222 | } |
| 223 | |
| 224 | |
| 225 | ############################################################################### |
| 226 | # |
| 227 | # _pack_VT_I2(). |
| 228 | # |
| 229 | # Pack an OLE property type: VT_I2, 16-bit signed integer. |
| 230 | # |
| 231 | sub _pack_VT_I2 { |
| 232 | |
| 233 | my $type = 0x0002; |
| 234 | my $value = $_[0]; |
| 235 | |
| 236 | my $data = pack 'VV', $type, $value; |
| 237 | |
| 238 | return $data; |
| 239 | } |
| 240 | |
| 241 | |
| 242 | ############################################################################### |
| 243 | # |
| 244 | # _pack_VT_LPSTR(). |
| 245 | # |
| 246 | # Pack an OLE property type: VT_LPSTR, String in the Codepage encoding. |
| 247 | # The strings are null terminated and padded to a 4 byte boundary. |
| 248 | # |
| 249 | sub _pack_VT_LPSTR { |
| 250 | |
| 251 | my $type = 0x001E; |
| 252 | my $string = $_[0] . "\0"; |
| 253 | my $codepage = $_[1]; |
| 254 | my $length; |
| 255 | my $byte_string; |
| 256 | |
| 257 | if ($codepage == 0x04E4) { |
| 258 | # Latin1 |
| 259 | $byte_string = $string; |
| 260 | $length = length $byte_string; |
| 261 | } |
| 262 | elsif ($codepage == 0xFDE9) { |
| 263 | # UTF-8 |
| 264 | if ( $] > 5.008 ) { |
| 265 | require Encode; |
| 266 | if (Encode::is_utf8($string)) { |
| 267 | $byte_string = Encode::encode_utf8($string); |
| 268 | } |
| 269 | else { |
| 270 | $byte_string = $string; |
| 271 | } |
| 272 | } |
| 273 | else { |
| 274 | $byte_string = $string; |
| 275 | } |
| 276 | |
| 277 | $length = length $byte_string; |
| 278 | } |
| 279 | else { |
| 280 | croak "Unknown codepage: $codepage\n"; |
| 281 | } |
| 282 | |
| 283 | # Pack the data. |
| 284 | my $data = pack 'VV', $type, $length; |
| 285 | $data .= $byte_string; |
| 286 | |
| 287 | # The packed data has to null padded to a 4 byte boundary. |
| 288 | if (my $extra = $length % 4) { |
| 289 | $data .= "\0" x (4 - $extra); |
| 290 | } |
| 291 | |
| 292 | return $data; |
| 293 | } |
| 294 | |
| 295 | |
| 296 | ############################################################################### |
| 297 | # |
| 298 | # _pack_VT_FILETIME(). |
| 299 | # |
| 300 | # Pack an OLE property type: VT_FILETIME. |
| 301 | # |
| 302 | sub _pack_VT_FILETIME { |
| 303 | |
| 304 | my $type = 0x0040; |
| 305 | my $localtime = $_[0]; |
| 306 | |
| 307 | # Convert from localtime to seconds. |
| 308 | my $seconds = Time::Local::timelocal(@{$localtime}); |
| 309 | |
| 310 | # Add the number of seconds between the 1601 and 1970 epochs. |
| 311 | $seconds += 11644473600; |
| 312 | |
| 313 | # The FILETIME seconds are in units of 100 nanoseconds. |
| 314 | my $nanoseconds = $seconds * 1E7; |
| 315 | |
| 316 | # Pack the total nanoseconds into 64 bits. |
| 317 | my $time_hi = int($nanoseconds / 2**32); |
| 318 | my $time_lo = POSIX::fmod($nanoseconds, 2**32); |
| 319 | |
| 320 | my $data = pack 'VVV', $type, $time_lo, $time_hi; |
| 321 | |
| 322 | return $data; |
| 323 | } |
| 324 | |
| 325 | |
| 326 | 1; |
| 327 | |
| 328 | |
| 329 | __END__ |
| 330 | |
| 331 | |
| 332 | =head1 NAME |
| 333 | |
| 334 | Properties - A module for creating Excel property sets. |
| 335 | |
| 336 | =head1 SYNOPSIS |
| 337 | |
| 338 | See the C<set_properties()> method in the Spreadsheet::WriteExcel documentation. |
| 339 | |
| 340 | =head1 DESCRIPTION |
| 341 | |
| 342 | This module is used in conjunction with Spreadsheet::WriteExcel. |
| 343 | |
| 344 | =head1 AUTHOR |
| 345 | |
| 346 | John McNamara jmcnamara@cpan.org |
| 347 | |
| 348 | =head1 COPYRIGHT |
| 349 | |
| 350 | © MM-MMX, John McNamara. |
| 351 | |
| 352 | All Rights Reserved. This module is free software. It may be used, redistributed and/or modified under the same terms as Perl itself. |