#! perl use strict; use warnings; use utf8; package Text::Layout::FontConfig; use Carp; our $VERSION = "0.039"; use Text::Layout::FontDescriptor; =head1 NAME Text::Layout::FontConfig - Pango style font description for Text::Layout =head1 SYNOPSIS Font descriptors are strings that identify the characteristics of the desired font. For example, C. The PDF context deals with physical fonts, e.g. built-in fonts like C and fonts loaded from font files like C. To map font descriptions to physical fonts, these fonts must be registered. This defines a font family, style, and weight for the font. Note that Text::Layout::FontConfig is a singleton. Creating objects with new() will always return the same object. =cut my %fonts; my @dirs; my %maps; my $loader; my $debug = 0; my $weights = [ 100 => 'thin', 100 => 'hairline', 200 => 'extra light', 200 => 'ultra light', 300 => 'light', # supported 350 => 'book', # supported 400 => 'normal', # supported 400 => 'regular', # supported 500 => 'medium', # supported 600 => 'semi bold', # supported 'semi' 600 => 'demi bold', 700 => 'bold', # supported 800 => 'extra bold', 800 => 'ultra bold', 900 => 'black', 900 => 'heavy', # supported 950 => 'extra black', 950 => 'ultra black', ]; =head2 METHODS =over =item new( [ atts... ] ) For convenience only. Text::Layout::FontConfig is a singleton. Creating objects with new() will always return the same object. Attributes: =over =item corefonts If true, a predefined set of font names (the PDF corefonts) is registered. =back =back =cut sub new { my ( $pkg, %atts ) = @_; my $self = bless {} => $pkg; $debug = $self->{debug} = $atts{debug}; if ( $atts{corefonts} ) { $self->register_corefonts; } if ( $atts{loader} ) { $loader = $atts{loader}; } return $self; } sub reset { my ( $self ) = @_; warn("FC: Reset\n") if $debug; %fonts = (); @dirs = (); %maps = (); } sub debug { shift->{debug} } =over =item register_fonts( $font, $family, $style [ , $weight ] [ , $props ] ) Registers a font fmaily, style and weight for the given font. $font can be the name of a built-in font, or the name of a TrueType or OpenType font file. $family is a font family name such as C, C, C, or C. It is possible to specify multiple family names, e.g., C. $style is the slant style, one of C, C, or C. $weight is the font weight, like C, or C. For convenience, style combinations like "bolditalic" are allowed. A final hash reference can be passed to specify additional properties for this font. Recognized properties are: =over =item * C - If set to a true value, this font will require text shaping. This is required for fonts that deal with complex glyph rendering and ligature handling like Devanagari. Text shaping requires module L. =item * C - If set overrides the font ascender. This may be necessary to improve results for some fonts. The value is expressed in 1/1000th of an em. C - If set overrides the font descender. This may be necessary to improve results for some fonts. The value is expressed in 1/1000th of an em. =item * C, C - Overrides the font specified or calculated values for underline thickness and/or position. This may improve results for some fonts. =item * C, C - Overrides the font specified or calculated values for strikeline thickness and/or position. This may improve results for some fonts. Note that strikeline thickness will default to underline thickness, if set. =item * C, C - Overrides the font specified or calculated values for overline thickness and/or position. This may improve results for some fonts. Note that overline thickness will default to underline thickness, if set. =back =back =cut sub register_font { shift if UNIVERSAL::isa( $_[0], __PACKAGE__ ); my $props; $props = pop(@_) if UNIVERSAL::isa( $_[-1], 'HASH' ); my ( $font, $family, $style, $weight ) = @_; if ( $style && !$weight && $style =~ s/^(heavy|bold|semi(?:bold)?|medium|book|light)//i ) { $weight = $1; } $style = _norm_style( $style // "normal" ); $weight = _norm_weight( $weight // "normal" ); my $ff; if ( $font =~ /\.[ot]tf$/ ) { if ( $font =~ m;^/; ) { $ff = $font if -r -s $font; } else { foreach ( @dirs ) { next unless -r -s "$_/$font"; $ff = "$_/$font"; last; } } } else { # Assume corefont. $ff = $font } croak("Cannot find font: ", $font, "\n") unless $ff; foreach ( split(/\s*,\s*/, $family) ) { $fonts{lc $_}->{$style}->{$weight}->{loader} = $loader; $fonts{lc $_}->{$style}->{$weight}->{loader_data} = $ff; warn("FC: Registered: $ff for ", lc($_), "-$style-$weight\n") if $debug; next unless $props; while ( my($k,$v) = each %$props ) { $fonts{lc $_}->{$style}->{$weight}->{$k} = $v; } } } =over =item add_fontdirs( @dirs ) Adds one or more file paths to be searched for font files. =back =cut sub add_fontdirs { shift if UNIVERSAL::isa( $_[0], __PACKAGE__ ); my ( @d ) = @_; foreach ( @d ) { unless ( -d -r -x ) { carp("Skipped font dir: $_ [$!]"); next; } push( @dirs, $_ ); } } =over =item register_aliases( $family, $aliases, ... ) Adds aliases for existing font families. Multiple aliases can be specified, e.g. $layout->register_aliases( "times", "serif, default" ); or $layout->register_aliases( "times", "serif", "default" ); =back =cut sub register_aliases { use Storable qw(dclone); shift if UNIVERSAL::isa( $_[0], __PACKAGE__ ); my ( $family, @aliases ) = @_; carp("Unknown font family: $family") unless exists $fonts{lc $family}; foreach ( @aliases ) { foreach ( split( /\s*,\s*/, $_ ) ) { $fonts{lc $_} = dclone( $fonts{lc $family} ); } } } =over =item register_corefonts( %options ) This is a convenience method that registers all built-in corefonts. Aliases for families C, C, and C are added unless $noaliases is specified. You do not need to call this method if you provide your own font registrations. Options: =over =item aliases If true, register Serif, Sans and Mono as aliases for Times, Helvetica and Courier. This is enabled by default and can be cancelled with C. =item noaliases If true, do not register Serif, Sans and Mono as aliases for Times, Helvetica and Courier. =item remap Remap the core fonts to real TrueType or OpenType font files. Supported values are C or C to use the GNU Free Fonts (http://ftp.gnu.org/gnu/freefont/) and C or C for the TeX Gyre fonts (https://www.gust.org.pl/projects/e-foundry/tex-gyre/). =back =back =cut sub register_corefonts { shift if UNIVERSAL::isa( $_[0], __PACKAGE__ ); my %options; if ( @_ == 1 ) { $options{noaliases} = shift; } else { %options = @_; } my $noaliases = defined($options{aliases}) ? !$options{aliases} : $options{noaliases}; warn("FC: Registering corefonts\n") if $debug; register_font( "Times-Roman", "Times" ); register_font( "Times-Bold", "Times", "Bold" ); register_font( "Times-Italic", "Times", "Italic" ); register_font( "Times-BoldItalic", "Times", "BoldItalic" ); register_aliases( "Times", "Serif" ) unless $noaliases; register_font( "Helvetica", "Helvetica" ); register_font( "Helvetica-Bold", "Helvetica", "Bold" ); register_font( "Helvetica-Oblique", "Helvetica", "Oblique" ); register_font( "Helvetica-BoldOblique", "Helvetica", "BoldOblique" ); register_aliases( "Helvetica", "Sans", "Arial" ) unless $noaliases; register_font( "Courier", "Courier" ); register_font( "Courier-Bold", "Courier", "Bold" ); register_font( "Courier-Oblique", "Courier", "Italic" ); register_font( "Courier-BoldOblique", "Courier", "BoldItalic" ); register_aliases( "Courier", "Mono", "Monospace", "fixed" ) unless $noaliases; register_aliases( "Courier", "Mono", "Monospace", "fixed" ) unless $noaliases; register_font( "ZapfDingbats", "Dingbats" ); if ( 0 ) { register_font( "Georgia", "Georgia" ); register_font( "Georgia,Bold", "Georgia", "Bold" ); register_font( "Georgia,Italic", "Georgia", "Italic" ); register_font( "Georgia,BoldItalic", "Georgia", "BoldItalic" ); register_font( "Verdana", "Verdana" ); register_font( "Verdana,Bold", "Verdana", "Bold" ); register_font( "Verdana,Italic", "Verdana", "Italic" ); register_font( "Verdana,BoldItalic", "Verdana", "BoldItalic" ); register_font( "WebDings", "WebDings" ); register_font( "WingDings", "WingDings" ); } # Corefont remapping to real font files. # Biggest problem is to make sure the fonts are installed, and with # the file names used here... $options{remap} //= ""; # GNU Free Fonts. # http://ftp.gnu.org/gnu/freefont/freefont-ttf-20120503.zip if ( $options{remap} =~ /^(?:gnu[-_]?)?free(?:[-_]?fonts)?$/i ) { remap( 'Times-Roman' => "FreeSerif.ttf", 'Times-BoldItalic' => "FreeSerifBoldItalic.ttf", 'Times-Bold' => "FreeSerifBold.ttf", 'Times-Italic' => "FreeSerifItalic.ttf", 'Helvetica' => "FreeSans.ttf", 'Helvetica-BoldOblique' => "FreeSansBoldOblique.ttf", 'Helvetica-Bold' => "FreeSansBold.ttf", 'Helvetica-Oblique' => "FreeSansOblique.ttf", 'Courier' => "FreeMono.ttf", 'Courier-BoldOblique' => "FreeMonoBoldOblique.ttf", 'Courier-Bold' => "FreeMonoBold.ttf", 'Courier-Oblique' => "FreeMonoOblique.ttf", ); } # TeX Gyre fonts. # https://www.gust.org.pl/projects/e-foundry/tex-gyre/whole/tg2_501otf.zip elsif ( $options{remap} =~ /^tex(?:[-_]?gyre)?$/i ) { remap( 'Times-Roman' => "texgyretermes-regular.otf", 'Times-BoldItalic' => "texgyretermes-bolditalic.otf", 'Times-Bold' => "texgyretermes-bold.otf", 'Times-Italic' => "texgyretermes-italic.otf", 'Helvetica' => "texgyreheros-regular.otf", 'Helvetica-BoldOblique' => "texgyreheros-bolditalic.otf", 'Helvetica-Bold' => "texgyreheros-bold.otf", 'Helvetica-Oblique' => "texgyreheros-italic.otf", 'Courier' => "texgyrecursor-regular.otf", 'Courier-BoldOblique' => "texgyrecursor-bolditalic.otf", 'Courier-Bold' => "texgyrecursor-bold.otf", 'Courier-Oblique' => "texgyrecursor-italic.otf", ); } elsif ( $options{remap} ) { croak("Unrecognized core remap set"); } } =over =item remap($font) =item remap( $src => $dst, ... ) Handles font remapping. The main purpose is to remap corefonts to real fonts. With a single argument, returns the remapped value, or undef if none. With a hash argument, maps each of the targets (keys) to a font file (value). This file must be present in one of the font directories. Alternatively, the key may be one of C, C and C and the value an already registered family. =back =cut sub remap { shift if UNIVERSAL::isa( $_[0], __PACKAGE__ ); return $maps{$_[0]} if @_ == 1; my %m = @_; while ( my ($k, $v ) = each %m ) { # Check for family map. if ( $k =~ /^(Courier|Times|Helvetica)$/ && defined $fonts{lc $v} ) { if ( $k eq 'Courier' ) { $maps{'Courier'} = $fonts{lc $v}{normal}{normal}{loader_data}; $maps{'Courier-Bold'} = $fonts{lc $v}{normal}{bold}{loader_data}; $maps{'Courier-Oblique'} = $fonts{lc $v}{italic}{normal}{loader_data}; $maps{'Courier-BoldOblique'} = $fonts{lc $v}{italic}{bold}{loader_data}; } elsif ( $k eq 'Helvetica' ) { $maps{'Helvetica'} = $fonts{lc $v}{normal}{normal}{loader_data}; $maps{'Helvetica-Bold'} = $fonts{lc $v}{normal}{bold}{loader_data}; $maps{'Helvetica-Oblique'} = $fonts{lc $v}{italic}{normal}{loader_data}; $maps{'Helvetica-BoldOblique'} = $fonts{lc $v}{italic}{bold}{loader_data}; } elsif ( $k eq 'Times' ) { $maps{'Times-Roman'} = $fonts{lc $v}{normal}{normal}{loader_data}; $maps{'Times-Bold'} = $fonts{lc $v}{normal}{bold}{loader_data}; $maps{'Times-Italic'} = $fonts{lc $v}{italic}{normal}{loader_data}; $maps{'Times-BoldItalic'} = $fonts{lc $v}{italic}{bold}{loader_data}; } next; } # Map font to corefont. my $ff; if ( $v =~ m;^/; ) { $ff = $v if -r -s $v; } else { foreach ( @dirs ) { next unless -r -s "$_/$v"; $ff = "$_/$v"; last; } } $maps{$k} = $ff or carp("Invalid font mapping ($v: $!)") } 1; } =over =item find_font( $family, $style, $weight ) Returns a font descriptor based on the given family, style and weight. On Linux, fallback using fontconfig. =back =cut sub find_font { shift if UNIVERSAL::isa( $_[0], __PACKAGE__ ); my $atts; $atts = pop(@_) if UNIVERSAL::isa( $_[-1], 'HASH' ); my ( $family, $style, $weight ) = @_; warn("FC: find_font( $family, $style, $weight )\n") if $debug; my $try = sub { if ( $fonts{$family} && $fonts{$family}->{$style} && $fonts{$family}->{$style}->{$weight} ) { my $ff = $fonts{$family}->{$style}->{$weight}; my %i = ( family => $family, style => $style, weight => $weight ); ; if ( $ff->{font} ) { $i{font} = $ff->{font}; } elsif ( $ff->{loader_data} ) { $i{loader_data} = $ff->{loader_data}; $i{loader} = $loader; $i{cache} = $ff; } else { return; } for ( qw( shaping ascender descender nosubset direction language underline_thickness underline_position strikeline_thickness strikeline_position overline_thickness overline_position ) ) { $i{$_} = $ff->{$_}; } if ( $debug ) { warn("FC: found( $i{family}, $i{style}, $i{weight} ) -> ", $i{loader_data}, "\n"); } return Text::Layout::FontDescriptor->new(%i); } }; $style = _norm_style( $style // "normal" ); $weight = _norm_weight( $weight // "normal" ); my $res = $try->(); return $res if $res; # TODO: Some form of font fallback. if ( _fallback( $family, $style, $weight ) ) { $res = $try->(); return $res if $res; } # Nope. croak("Cannot find font: $family $style $weight\n"); } =over =item from_string( $description ) Returns a font descriptor using a Pango-style font description, e.g. C. On Linux, fallback using fontconfig. =back =cut my $stylep = qr/^(?:heavy|bold|semi(?:bold)?|medium|book|light)? (oblique|italic)$/ix; my $weightp = qr/^(heavy|bold|semi(?:bold)?|medium|book|light) (?:oblique|italic)?$/ix; sub from_string { shift if UNIVERSAL::isa( $_[0], __PACKAGE__ ); my ( $description ) = @_; my $i = parse($description); my $res = find_font( $i->{family}, $i->{style}, $i->{weight} ); $res->set_size($i->{size}) if $res && $i->{size}; $res; } =over =item parse( $description ) Parses a Pango-style font description and returns a hash ref with keys C, C