#!/usr/bin/env perl use warnings; ############################################################# ############################################################# ######################################################################## # hyperxmp-add-bytecount # # Adds/updates byteCount specification in XMP packet in pdf file(s) # # made by hyperxmp, with byteCount = file size. # # Copyright (C) 2020 John Collins # # and Scott Pakin, # # # # This program may be distributed and/or modified under the conditions # # of the LaTeX Project Public License, either version 1.3c of this # # license or (at your option) any later version. # # # # The latest version of this license is in: # # # # http://www.latex-project.org/lppl.txt # # # # and version 1.3c or later is part of all distributions of LaTeX # # version 2008/05/04 or later. # ######################################################################## $name = 'hyperref-add-bytecount'; $version = '1.1-2020-11-20'; $maintainer = 'John Collins, jcc8@psu.edu; Scott Pakin, scott+hyxmp@pakin.org'; my $exit_code = 0; if ( (! @ARGV) || ($ARGV[0] =~ /^(-|--)(h|help)$/) ) { print "$name $version.\n", "Usage: $name [options] pdf_filename(s)\n", " Adds/updates byteCount specification in XMP packet in pdf file(s) from\n", " hyperxmp, with byteCount = file size.\n", " No change if there's no XMP packet of the form produced by hyperxmp.\n", "Options:\n", " -help or -h Output usage information.\n", " -version or -v Output version information.\n", "Bug reports to:\n $maintainer.\n"; exit; } elsif ( $ARGV[0] =~ /^(-|--)(v|version)$/ ) { print "$name $version.\n", "Bug reports to:\n $maintainer.\n"; exit; } foreach (@ARGV) { if ( ! fix_pdf($_) ) { $exit_code = 1; } } exit $exit_code; #====================================================== sub fix_pdf { # Change/insert byteCount field with correct file length, while preserving # the file size and the length of the stream containing xmp metadata. # Return 1 on success, else 0. local $pdf_name = shift; local $tmp_name = "$pdf_name.new.pdf"; local $pdf_size = (stat($pdf_name))[7]; warn "Inserting/correcting byteCount field in '$pdf_name' ...\n"; # Strings surrounding (and identifying) the byteCount field, and other # parts of the xmp packet: local $xmp_start = ''; local $decl_bC = 'byteCount'; local $pre_bC = ''; local $post_bC = ''; local $pC = ''; local $rd_end = ''; local $xmp_end = ''; local *PDF; local *TMP; if (! open PDF, "<", $pdf_name ) { warn " Cannot read '$pdf_name'\n"; return 0; } if ( ! open TMP, ">", $tmp_name ) { warn " Cannot write temporary file '$tmp_name'\n"; close PDF; return 0; } local $status = 0; # 0 = no XMP packet, 1 = success, >= errors while ( ) { # Only examine first XMP packet: if ( ($status == 0) && /^\s*\Q$xmp_start\E/ ) { local @xmp = $_; local $len_padding = 0; local $xmp_after_line = ''; &xmp_get_mod; print TMP @xmp; # Insert correct padding to leave file size unchanged: while ( $len_padding > 0 ) { my $len_line = 64; if ( $len_line > $len_padding ) { $len_line = $len_padding; } $len_padding -= $len_line; print TMP (' ' x ($len_line - 1) ), "\n"; } print TMP $xmp_after_line; $xmp_after_line = ''; } else { print TMP "$_"; } } close PDF; close TMP; if ($status == 0) { warn " Could not insert/modify byteCount, since no XMP packet was found.\n"; warn " So '$pdf_name' is unchanged,\n", " and I will delete temporary file '$tmp_name'.\n"; unlink $tmp_name; } elsif ($status == 1) { rename $tmp_name, $pdf_name or die " Cannot move temporary file '$tmp_name' to '$pdf_name'.\n", " Error is '$!'\n"; } else { warn " Could not insert correct byteCount. See above for reason.\n"; warn " So '$pdf_name' is unchanged,\n", " and I will delete temporary file '$tmp_name'.\n"; unlink $tmp_name; } return ($status == 1); } #====================================================== sub xmp_get_mod { # Get xmp packet, given that @xmp contains its first line. # Get amount of trailing padding, and line after that. # If possible, insert a byteCount field: # Either replace existing specification, if it exists, # or insert one in expected place for hyperxmp, if the XMP packet # matches what hyperxmp would produce. # Return xmp packet in @xmp, amount of padding needed in $len_padding, # line after that in $xmp_after_line, and error code in $error. # Set $status appropriately: 1 for success; >=1 for failure. $len_padding = 0; $xmp_after_line = ''; my $bC_index = -1; my $xmp_end_found = 0; my $decl_bC_found = 0; while ( ) { push @xmp, $_; if ( /^\s*\Q$xmp_end\E/ ) { $xmp_end_found = 1; # Get amount of padding; while () { if ( /^\s*$/ ) { $len_padding += length($_); } else { $xmp_after_line = $_; last; } } last; } elsif ( $bC_index >= 0 ){ next; } # Rest of conditions only apply if no place yet found for byteCount # specification. elsif ( /^(\s*)\Q$pre_bC\E.*?\Q$post_bC\E\s*$/ ) { $bC_index = $#xmp; } elsif ( /^\s*\Q$decl_bC\E/ ) { $decl_bC_found = 1; } elsif ( /^(\s*)\Q$rd_end\E/ ){ # End of rdf:Description block. # So having previous declaration of byteCount is irrelevant. $decl_bC_found = 0; } elsif ( $decl_bC_found && /^(\s*)\Q$pC\E/ ){ $bC_index = $#xmp; pop @xmp; push @xmp, '', $_; } } # End reading of XMP if ($bC_index < 0) { if ( ! $xmp_end_found ) { warn " End of XMP packet not found.\n"; $status = 2; } elsif ( ! $decl_bC_found ) { warn " XMP packet not in appropriate hyperxmp-compatible format.\n"; $status = 3; } return; } my $new_line = ' ' . $pre_bC . $pdf_size . $post_bC . "\n"; my $old_line = $xmp[$bC_index]; my $delta_len = length($new_line) - length($old_line); if ($delta_len > $len_padding) { warn " Cannot get padding correct for '$pdf_name'.\n", " Length change of bC line = $delta_len; ", " Padding bytes available = $len_padding.\n"; $status = 4; return; } else { $len_padding -= $delta_len; $xmp[$bC_index] = $new_line; $status = 1; } } #======================================================