App-Music-ChordPro

 view release on metacpan or  search on metacpan

lib/ChordPro/Output/PDF/Writer.pm  view on Meta::CPAN

#! perl

package main;

our $config;

package ChordPro::Output::PDF::Writer;

use strict;
use warnings;
use Text::Layout;
use IO::String;
use Carp;
use utf8;

use ChordPro::Files;
use ChordPro::Paths;
use ChordPro::Utils qw( expand_tilde demarkup min is_corefont maybe is_true is_odd );
use ChordPro::Output::Common qw( fmt_subst prep_outlines );
use Ref::Util qw( is_arrayref is_hashref );
use feature 'state';
use Unicode::Collate;
use Unicode::Normalize;

# For regression testing, run perl with PERL_HASH_SEED set to zero.
# This eliminates the arbitrary order of font definitions and triggers
# us to pinpoint some other data that would otherwise be varying.
my $regtest = defined($ENV{PERL_HASH_SEED}) && $ENV{PERL_HASH_SEED} == 0;
my $faketime = 1465041600;

my %fontcache;			# speeds up 2 seconds per song

sub new {
    my ( $pkg, $ps, $pdfapi ) = @_;
    my $self = bless { ps => $ps }, $pkg;
    $self->{pdfapi} = $pdfapi;
    $self->{pdf} = $pdfapi->new;
    $self->{pdf}->{forcecompress} = 0 if $regtest;
    $self->{pdf}->mediabox( $ps->{papersize}->[0],
			    $ps->{papersize}->[1] );
    $self->{pdf}->page_layout( $ps->{page_layout} )
      if $ps->{page_layout};
    $self->{layout} = Text::Layout->new( $self->{pdf} );
    $self->{tmplayout} = undef;

    no strict 'refs';
    # Patches and enhancements to PDF library.
    *{$pdfapi . '::Resource::XObject::Form::width' } = \&_xo_width;
    *{$pdfapi . '::Resource::XObject::Form::height'} = \&_xo_height;

    if ( $pdfapi eq 'PDF::API2' ) {
	my $apiversion = ${$pdfapi . '::VERSION'};
	no warnings 'redefine';

	# Fix date validation.
	*{$pdfapi . '::_is_date'} = sub { 1 }
	  if $apiversion < 2.045;

	# Enhanced version that allows named destinations.
	eval "use $pdfapi" . "::Annotation";
	*{$pdfapi . '::Annotation::pdf'     } = \&pdfapi_annotation_pdf
	  if $apiversion < 999; # no milestone yet

	# Enhanced version that doesn't blow up.
	eval "use $pdfapi" . "::Basic::PDF::Array";
	*{$pdfapi . '::Basic::PDF::Array::outobjdeep' } = \&pdfapi_outobjdeep
	  if $apiversion < 999; # no milestone yet
    }
    elsif ( $pdfapi eq 'PDF::Builder' ) {
	my $apiversion = ${$pdfapi . '::VERSION'};
	no warnings 'redefine';

	# Enhanced version that allows named destinations.
	eval "use $pdfapi" . "::Annotation";
	*{$pdfapi . '::Annotation::pdf'     } = \&pdfapi_annotation_pdf
	  if $apiversion < 999; # no milestone yet
    }

lib/ChordPro/Output/PDF/Writer.pm  view on Meta::CPAN


    # PDF::API2 says $page must refer to an existing page.
    # Set to 0 to append.
    $page = 0 if $page == $self->{pdf}->pages + 1;

    $self->{pdfpage} = $self->{pdf}->page($page);
    $self->{pdfpage}->mediabox( $ps->{papersize}->[0],
				$ps->{papersize}->[1] );

    $self->{pdfgfx}  = $self->{pdfpage}->gfx;
    $self->{pdftext} = $self->{pdfpage}->text;
    unless ($ps->{theme}->{background} =~ /^white|none|#ffffff$/i ) {
	for ( $self->{pdfgfx} ) {
	    $_->save;
	    $_->fillcolor( $ps->{theme}->{background} );
	    $_->linewidth(0);
	    $_->rectxy( 0, 0, $ps->{papersize}->[0],
			$ps->{papersize}->[1] );
	    $_->fill;
	    $_->restore;
	}
    }
}

# Align.
# Ordinal page numbers start with 1.
# Assuming the next page to be written is $page, do we need
# to insert alignment pages?
# If so, insert them, and return the number of pages inserted (zero or one).
# Alignment is to an odd page, except for the back matter, whose
# final page must be even.

sub page_align {
    my ( $self, $pagectrl, $part, $page, $even ) = @_;
    my $ret = $self->_page_align( $pagectrl, $part, $page, $even );
    warn( "ALIGN( $part, page $page, ",
	  defined($even) ? "even $even, " : "",
	  ChordPro::Output::PDF::pagectrl_msg($pagectrl),
	  " ) -> $ret\n")
      if exists($::config->{debug}->{pagealign})
      && $::config->{debug}->{pagealign};
    return $ret;
}

sub _page_align {
    my ( $self, $pagectrl, $part, $page, $even ) = @_;
    $even ||= 0;

    # Only align to odd pages.
    return 0 if $even xor is_odd($page);	# already odd/even
    return 0 unless $pagectrl->{dual_pages};	# no alignment
    return 0 unless $pagectrl->{align_songs};	# no alignment

    use List::Util 'shuffle';
    my $ps = $self->{ps};
    my $bg;
    my $ffile;
    my $filler;
    if ( ($bg = $ps->{formats}->{filler}->{background})
	 &&
	 ( $ffile = expand_tilde($bg) )
	 &&
	 ( $filler = $self->{pdfapi}->open($ffile) )
       ) {
	state $file = "";
	state @pages;
	if ( $file ne $ffile || !@pages ) {
	    $file = $ffile;
	    # Try to make it reproducible.
	    local $ENV{PERL_HASH_SEED} = 0x12a02ab;
	    srand();
	    @pages = shuffle( 1..$filler->pages );
	}
	# Pick a random page.
	$self->{pdf}->import_page( $filler, shift(@pages), $page );
    }
    else {
	$self->newpage($page);
    }
    return 1;		# number of pages added
}

sub openpage {
    my ( $self, $page ) = @_;
    $self->{pdfpage} = $self->{pdf}->openpage($page);
    confess("Fatal: Page $page not found.") unless $self->{pdfpage};
    $self->{pdfgfx}  = $self->{pdfpage}->gfx;
    $self->{pdftext} = $self->{pdfpage}->text;
}

sub importpage {
    my ( $self, $fn, $pg ) = @_;
    my $bg = $self->{pdfapi}->open($fn);
    return unless $bg;		# should have been checked
    $pg = $bg->pages if $pg > $bg->pages;
    $self->{pdf}->import_page( $bg, $pg, $self->{pdfpage} );
    # Make sure the contents get on top of it.
    $self->{pdfgfx}  = $self->{pdfpage}->gfx;
    $self->{pdftext} = $self->{pdfpage}->text;
}

sub importfile {
    my ( $self, $filename ) = @_;
    my $pdf = $self->{pdfapi}->open($filename);
    return unless $pdf;		# should have been checked
    for ( my $page = 1; $page <= $pdf->pages; $page++ ) {
	$self->{pdf}->import_page( $pdf, $page );
    }
    return { pages => $pdf->pages, $pdf->info_metadata };
}

sub pagelabel {
    my ( $self, $page, $style, $prefix, $start ) = @_;
    $style //= 'arabic';
    $start //= 1;

    # PDF::API2 2.042 has some incompatible changes...
    my $c = $self->{pdf}->can("page_labels");
    if ( $c ) {			# 2.042+
	my $opts = { style => $style eq 'Roman' ? 'R' :
		              $style eq 'roman' ? 'r' :

lib/ChordPro/Output/PDF/Writer.pm  view on Meta::CPAN

    # Add bookmarks.
    my $outline = $ol_root->outline;
    $outline->title("Bookmarks");
    $outline->closed;

    my @tops =
      map  { $_->[0] }
      sort { $a->[1] cmp $b->[1] }
      map  { [ $_ => s/^song_([0-9]+)$/sprintf("song_%06d",$1)/er ] }
      grep { ! /^(?:cover|front|toc|back)$/ }
      keys %{ $self->{_nd} };

    for ( "cover", "front", "toc", @tops, "back" ) {
	next unless my $p = $self->{_nd}->{$_};
	my $ol = $outline->outline;
	$ol->title($_);
	if ( my $c = $ol->can("destination") ) {
	    $c->( $ol, $p );
	}
	else {
	    $ol->dest($p);
	}
    }

=cut

}

sub finish {
    my ( $self, $file ) = @_;

    ::dump($self->{pdf}->{pagestack})
      if $::config->{debug}->{pages} & 0x04;

    if ( $file && $file ne "-" ) {
	my $fd = fs_open( $file, '>:raw' );
	print $fd $self->{pdf}->stringify;
	close($fd);
    }
    else {
	binmode(STDOUT);
	print STDOUT ( $self->{pdf}->stringify );
	close(STDOUT);
    }
}

sub init_fonts {
    my ( $self ) = @_;
    my $ps = $self->{ps};
    my $fail;

    my $fc = Text::Layout::FontConfig->new( debug => $config->{debug}->{fonts} > 1 );

    # Add font dirs.
    my @dirs;
    my @d = ( @{$ps->{fontdir}}, @{ CP->findresdirs("fonts") }, $ENV{FONTDIR} );
    # Avoid rsc result if dummy.
    splice( @d, -2, 1 ) if $d[-2] eq "fonts/";
    for my $fontdir ( @d ) {
	next unless $fontdir;
	$fontdir = expand_tilde($fontdir);
	if ( fs_test( d => $fontdir ) ) {
	    $self->{pdfapi}->can("addFontDirs")->($fontdir);
	    $fc->add_fontdirs($fontdir);
	    push( @dirs, $fontdir );
	}
	else {
	    warn("PDF: Ignoring fontdir $fontdir [$!]\n");
	    undef $fontdir;
	}
    }

    # Make sure we have this one.
    $fc->register_font( "ChordProSymbols.ttf", "chordprosymbols", "", {} );

    # Remap corefonts if possible.
    my $remap = $ENV{CHORDPRO_COREFONTS_REMAP} // $ps->{corefonts}->{remap};
    # Packager adds the fonts.
    $remap //= "free" if CP->packager;

    unless ( defined $remap ) {

	# Not defined -- find the GNU Free Fonts.
	for my $dir ( @dirs ) {
	    my $have = 1;
	    for my $font ( qw( FreeSerif.ttf
			       FreeSerifBoldItalic.ttf
			       FreeSerifBold.ttf
			       FreeSerifItalic.ttf
			       FreeSans.ttf
			       FreeSansBoldOblique.ttf
			       FreeSansBold.ttf
			       FreeSansOblique.ttf
			       FreeMono.ttf
			       FreeMonoBoldOblique.ttf
			       FreeMonoBold.ttf
			       FreeMonoOblique.ttf
			    ) ) {
		$have = 0, last unless fs_test( fs => "$dir/$font" );
	    }
	    $remap = "free", last if $have;
	}
    }
    $fc->register_corefonts( remap => $remap ) if $remap;

    # Process the fontconfig.
    foreach my $ff ( keys( %{ $ps->{fontconfig} } ) ) {
	my @fam = split( /\s*,\s*/, $ff );
	foreach my $s ( keys( %{ $ps->{fontconfig}->{$ff} } ) ) {
	    my $v = $ps->{fontconfig}->{$ff}->{$s};
	    if ( is_hashref($v) ) {
		my $file = delete( $v->{file} );
		$fc->register_font( $file, $fam[0], $s, $v );
	    }
	    else {
		$fc->register_font( $v, $fam[0], $s );
	    }
	}
	$fc->register_aliases(@fam) if @fam > 1;
    }

    foreach my $ff ( keys( %{ $ps->{fonts} } ) ) {
	$self->init_font($ff) or $fail++;
    }

    die("Unhandled fonts detected -- aborted\n") if $fail;
}

sub init_font {
    my ( $self, $ff ) = @_;
    my $ps = $self->{ps};
    my $fd;
    if ( $ps->{fonts}->{$ff}->{file} ) {
	$fd = $self->init_filefont($ff);
    }
    elsif ( $ps->{fonts}->{$ff}->{description} ) {
	$fd = $self->init_pangofont($ff);
    }
    elsif ( $ps->{fonts}->{$ff}->{name} ) {
	$fd = $self->init_corefont($ff);
    }
    warn("No font found for \"$ff\"\n") unless $fd;
    $fd;
}

sub init_pangofont {
    my ( $self, $ff ) = @_;

    my $ps = $self->{ps};
    my $font = $ps->{fonts}->{$ff};

    my $fc = Text::Layout::FontConfig->new( debug => $config->{debug}->{fonts} > 1 );
    eval {
	$font->{fd} = $fc->from_string($font->{description});
	$font->{fd}->get_font($self->{layout}); # force load
	$font->{fd}->{font}->{Name}->{val} =~ s/~.*/~$faketime/ if $regtest;
	$font->{_ff} = $ff;
	$font->{fd}->set_shaping( $font->{fd}->get_shaping || $font->{shaping}//0);
	$font->{size} = $font->{fd}->get_size if $font->{fd}->get_size;
	1;
    } or return;
    $font->{fd};
}

sub init_filefont {
    my ( $self, $ff ) = @_;

    my $ps = $self->{ps};
    my $font = $ps->{fonts}->{$ff};

    my $fc = Text::Layout::FontConfig->new( debug => $config->{debug}->{fonts} > 1 );
    eval {
	my $t = $fc->from_filename(expand_tilde($font->{file}));
	$t->get_font($self->{layout}); # force load
	$t->{font}->{Name}->{val} =~ s/~.*/~$faketime/ if $regtest;
	$t->{_ff} = $ff;
	$font->{fd} = $t;
    };
    $font->{fd};
}

sub init_corefont {
    my ( $self, $ff ) = @_;

    my $ps = $self->{ps};
    my $font = $ps->{fonts}->{$ff};
    my $cf = is_corefont($font->{name});
    die("Config error: \"$font->{name}\" is not a built-in font\n")
      unless $cf;
    my $fc = Text::Layout::FontConfig->new( debug => $config->{debug}->{fonts} > 1 );
    eval {
	$font->{fd} = $fc->from_filename($cf);
	$font->{fd}->get_font($self->{layout}); # force load
	$font->{_ff} = $ff;
    };
    $font->{fd};
}

sub show_vpos {
    my ( $self, $y, $w ) = @_;
    $self->{pdfgfx}->move(100*$w,$y)->linewidth(0.25)->hline(100*(1+$w))->stroke;
}

sub embed {
    my ( $self, $file ) = @_;
    return unless fs_test( 'f', $file );

    # Borrow some routines from PDF Api.
    *PDFNum = \&{$self->{pdfapi} . '::Basic::PDF::Utils::PDFNum'};
    *PDFStr = \&{$self->{pdfapi} . '::Basic::PDF::Utils::PDFStr'};

    # The song.
    # Apparently the 'hidden' flag does not hide it completely,
    # so give it a rect outside the page.
    my $a = $self->{pdfpage}->annotation();
    $a->text( fs_load( $file, { fail => "soft", split => 0 } ),
	      -open => 0, -rect => [0,0,-1,-1] );
    $a->{T} = PDFStr("ChordProSong");
    $a->{F} = PDFNum(2);		# hidden

    # The config.
    $a = $self->{pdfpage}->annotation();
    $a->text( ChordPro::Config::config_final(),
	      -open => 0, -rect => [0,0,-1,-1]);
    $a->{T} = PDFStr("ChordProConfig");
    $a->{F} = PDFNum(2);		# hidden

    # Runtime info.
    $a = $self->{pdfpage}->annotation();
    $a->text( ::runtimeinfo(),
	      -open => 0, -rect => [0,0,-1,-1] );
    $a->{T} = PDFStr("ChordProRunTime");
    $a->{F} = PDFNum(2);		# hidden



( run in 0.517 second using v1.01-cache-2.11-cpan-5b529ec07f3 )