File Coverage

blib/lib/PDF/Builder/UniWrap.pm
Criterion Covered Total %
statement 30 152 19.7
branch 0 104 0.0
condition 0 21 0.0
subroutine 10 18 55.5
pod 0 7 0.0
total 40 302 13.2


line stmt bran cond sub pod time code
1             package PDF::Builder::UniWrap;
2              
3 1     1   1481 use strict;
  1         2  
  1         28  
4 1     1   4 use warnings;
  1         2  
  1         49  
5              
6             our $VERSION = '3.024'; # VERSION
7             our $LAST_UPDATE = '3.024'; # manually update whenever code is changed
8              
9             =head1 NAME
10              
11             PDF::Builder::UniWrap - support routines for finding line breakpoints with Unicode text
12              
13             =cut
14              
15             # Implements UAX#14: Line Breaking Properties
16             # David Nesting
17              
18 0         0 BEGIN {
19              
20 1     1   5 use Encode qw(:all);
  1         2  
  1         187  
21              
22             #use 5.008; as of 3.010, 5.16 is the minimum
23 1     1   6 use base 'Exporter';
  1         2  
  1         91  
24              
25 1     1   3051 use Unicode::UCD;
  1         56383  
  1         53  
26 1     1   8 use Carp;
  1     0   2  
  1         81  
27              
28             }
29              
30             our $DEBUG = 0;
31             our $columns = 75;
32              
33             my %classified;
34             my $txt;
35              
36 1     1   6 use constant PROHIBITED => 0; ## no critic
  1         2  
  1         67  
37 1     1   5 use constant INDIRECT => 1; ## no critic
  1         2  
  1         35  
38 1     1   6 use constant DIRECT => 2; ## no critic
  1         2  
  1         33  
39 1     1   5 use constant REQUIRED => 3; ## no critic
  1         2  
  1         1729  
40              
41             my @CLASSES = qw{ OP CL QU GL NS EX SY IS PR PO NU AL ID IN HY BA BB B2 ZW CM };
42             my %BREAK_TABLE = (
43             OP => [qw[ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 ]],
44             CL => [qw[ 2 0 1 1 0 0 0 0 2 1 2 2 2 2 1 1 2 2 0 1 ]],
45             QU => [qw[ 0 0 1 1 1 0 0 0 1 1 1 1 1 1 1 1 1 1 0 1 ]],
46             GL => [qw[ 1 0 1 1 1 0 0 0 1 1 1 1 1 1 1 1 1 1 0 1 ]],
47             NS => [qw[ 2 0 1 1 1 0 0 0 2 2 2 2 2 2 1 1 2 2 0 1 ]],
48             EX => [qw[ 2 0 1 1 1 0 0 0 2 2 2 2 2 2 1 1 2 2 0 1 ]],
49             SY => [qw[ 2 0 1 1 1 0 0 0 2 2 1 2 2 2 1 1 2 2 0 1 ]],
50             IS => [qw[ 2 0 1 1 1 0 0 0 2 2 1 2 2 2 1 1 2 2 0 1 ]],
51             PR => [qw[ 1 0 1 1 1 0 0 0 2 2 1 1 1 2 1 1 2 2 0 1 ]],
52             PO => [qw[ 2 0 1 1 1 0 0 0 2 2 2 2 2 2 1 1 2 2 0 1 ]],
53             NU => [qw[ 2 0 1 1 1 0 0 0 2 1 1 1 2 1 1 1 2 2 0 1 ]],
54             AL => [qw[ 2 0 1 1 1 0 0 0 2 2 1 1 2 1 1 1 2 2 0 1 ]],
55             ID => [qw[ 2 0 1 1 1 0 0 0 2 1 2 2 2 1 1 1 2 2 0 1 ]],
56             IN => [qw[ 2 0 1 1 1 0 0 0 2 2 2 2 2 1 1 1 2 2 0 1 ]],
57             HY => [qw[ 2 0 1 1 1 0 0 0 2 2 0 2 2 2 1 1 2 2 0 1 ]],
58             BA => [qw[ 2 0 1 1 1 0 0 0 2 2 2 2 2 2 1 1 2 2 0 1 ]],
59             BB => [qw[ 1 0 1 1 1 0 0 0 1 1 1 1 1 1 1 1 1 1 0 1 ]],
60             B2 => [qw[ 2 0 1 1 1 0 0 0 2 2 2 2 2 2 1 1 2 0 0 1 ]],
61             ZW => [qw[ 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 0 1 ]],
62             CM => [qw[ 2 0 1 1 1 0 0 0 2 2 1 1 2 1 1 1 2 2 0 1 ]],
63             );
64              
65             # Convert the table above into a hash that we can use for speedier lookups
66              
67             foreach (keys %BREAK_TABLE) {
68             my @t = @CLASSES;
69             $BREAK_TABLE{$_} = { map { shift(@t) => $_ } @{$BREAK_TABLE{$_}} };
70             }
71              
72             sub new {
73 0     0 0   my $pkg = shift;
74 0           my $self = { @_ };
75 0   0       $self->{'line_length'} ||= $columns;
76 0   0       $self->{'break_table'} ||= \%BREAK_TABLE;
77              
78 0   0       $self->{'widthfunc'} ||= 1;
79              
80 0   0       bless($self, ref($pkg) || $pkg);
81 0           return $self;
82             }
83              
84             # This attempts to identify the on-screen length of a given character.
85             # For normal displays, you can generally assume the character has a
86             # length of 1, but some terminals may expand the width of certain
87             # characters, so that extra space needs to be taken into consideration
88             # here so the wrapping occurs at the proper place.
89              
90             sub char_length {
91 0 0   0 0   shift if ref($_[0]);
92 0           my ($c) = @_;
93              
94 0 0 0       if ($c eq 'CM' || $c eq 'ZW') {
95 0           return 0;
96             }
97              
98 0           return 1;
99             }
100              
101             sub lb_class {
102 0 0   0 0   my $self = ref($_[0]) ? shift() : self();
103 0           my $code = Unicode::UCD::_getcode(ord $_[0]);
104 0           my $hex;
105              
106 0 0         if (defined $code) {
107 0           $hex = sprintf("%04X", $code);
108             } else {
109 0           carp("unexpected arg \"$_[1]\" to Text::Wrap::lb_class()");
110 0           return;
111             }
112              
113 0 0         return $classified{$hex} if $classified{$hex};
114              
115 0 0         $txt = do "unicore/Lbrk.pl" unless $txt;
116              
117 0 0         if ($txt =~ m/^$hex\t\t(.+)/m) {
118 0 0         print STDERR "< found direct match for $hex = $1 >\n" if $DEBUG > 1;
119 0           return $classified{$hex} = $1;
120             } else {
121 0 0         print STDERR "< no direct match $hex >\n" if $DEBUG > 1;
122 0           pos($txt) = 0;
123              
124 0           while ($txt =~ m/^([0-9A-F]+)\t([0-9A-F]+)\t(.+)/mg) {
125 0 0         print STDERR "< examining $1 -> $2 >\n" if $DEBUG > 1;
126 0 0 0       if (hex($1) <= $code && hex($2) >= $code) {
127 0 0         print STDERR "< found range match for $hex = $3 between $1 and $2 >\n" if $DEBUG > 1;
128 0           return $classified{$hex} = $3;
129             }
130             }
131 0           return 'XX';
132             }
133             }
134              
135             # Returns a list of breaking properties for the given text
136             sub text_properties {
137 0 0   0 0   my $self = ref($_[0])? shift(): self();
138 0           my ($text) = @_;
139              
140 0           my @characters = split(//, $text);
141 0           my @classifications = map { $self->lb_class($_) } @characters;
  0            
142              
143 0           return class_properties(@classifications);
144             }
145              
146             # Returns a list of breaking properties for the provided breaking classes
147             sub class_properties {
148 0 0   0 0   my $self = ref($_[0])? shift(): self();
149              
150 0           my @breaks;
151 0           my $last_class = $_[0];
152              
153 0 0         $last_class = 'ID' if $last_class eq 'CM'; # broken combining mark
154              
155 0 0         print STDERR "find_breaks: first class=$last_class\n" if $DEBUG;
156              
157 0           for (my $i=1; $i <= $#_; $i++) {
158 0 0         print STDERR "find_breaks: i=$i class=$_[$i] prev=$last_class breaks[i-1]=$breaks[$i-1]\n" if $DEBUG;
159 0   0       $breaks[$i-1] ||= 0;
160              
161 0 0         $_[$i] = 'ID' if $_[$i] eq 'XX'; # we want as few of these as possible!
162              
163 0 0         if ($_[$i] eq 'SA') {
    0          
    0          
    0          
    0          
    0          
    0          
164             # TODO: Need a classifiation system for complex characters
165             } elsif ($_[$i] eq 'CR') {
166 0           $breaks[$i] = REQUIRED;
167             } elsif ($_[$i] eq 'LF') {
168 0 0         if ($_[$i-1] eq 'CR') {
169 0           $breaks[$i-1] = PROHIBITED;
170             }
171 0           $breaks[$i] = REQUIRED;
172             } elsif ($_[$i] eq 'BK') {
173 0           $breaks[$i] = REQUIRED;
174             } elsif ($_[$i] eq 'SP') {
175 0           $breaks[$i-1] = PROHIBITED;
176 0           next;
177             } elsif ($_[$i] eq 'CM') {
178 0 0         if ($_[$i-1] eq 'SP') {
179 0           $last_class = 'ID';
180 0 0         if ($i > 1) {
181 0 0         $breaks[$i-2] = $self->{'break_table'}->{$_[$i-2]}->{'ID'}
182             == DIRECT? DIRECT: PROHIBITED;
183             }
184             }
185             } elsif ($last_class ne 'SP') {
186 0 0         if ($breaks[$i-1] != REQUIRED) {
187 0           my $this_break = $self->{'break_table'}->{$last_class}->{$_[$i]};
188              
189 0 0         if ($this_break == INDIRECT) {
190 0 0         $breaks[$i-1] = $_[$i-1] eq 'SP'? INDIRECT: PROHIBITED;
191             } else {
192             # die "Internal error: no table mapping between '$last_class' and '$_[$i]'\n"
193             # unless defined $this_break;
194 0 0         if (defined $this_break) {
195 0           $breaks[$i-1] = $this_break;
196             } else {
197 0           $breaks[$i-1] = DIRECT;
198             }
199             }
200             }
201             }
202              
203 0           $last_class = $_[$i];
204             }
205              
206             # $breaks[$#breaks] = DIRECT;
207 0           push(@breaks, REQUIRED);
208              
209 0 0         print STDERR "find_breaks: returning " . join(":", @breaks) . "\n" if $DEBUG;
210 0           return @breaks;
211             }
212              
213             # Returns a list of break points in the provided text, based on
214             # the line length
215             sub find_breaks {
216 0 0   0 0   my $self = ref($_[0])? shift(): self();
217 0           my $text = shift;
218              
219 0           my @characters = split(//, $text);
220              
221 0           my @classifications = map { $self->lb_class($_) } @characters;
  0            
222 0           my @lengths = map { $self->char_length($_) } @characters;
  0            
223              
224 0           my @breaks = $self->class_properties(@classifications);
225 0           my @breakpoints;
226              
227 0           my $last_start = 0;
228 0           my $last_break;
229             my $last_length;
230 0           my $pos = 0;
231              
232 0           for (my $i=0; $i <= $#lengths; $i++) {
233              
234 0 0         print STDERR "[i=$i '$characters[$i]' $classifications[$i] $breaks[$i]] " if $DEBUG;
235 0 0         if ($breaks[$i] == REQUIRED) {
236 0 0         print STDERR "required breakpoint\n" if $DEBUG;
237 0           push(@breakpoints, $i+1);
238 0           $last_start = $i+1;
239 0           $pos = 0;
240 0           next;
241             }
242              
243 0           my $c = $pos + $lengths[$i];
244              
245 0 0         if ($c > $self->{'line_length'}) {
246 0 0         print STDERR "want to break " if $DEBUG;
247 0 0 0       if (defined $last_break) {
    0          
248 0 0         print STDERR "at $last_break\n" if $DEBUG;
249 0           push(@breakpoints, $last_break + 1);
250 0           $last_start = $last_break + 1;
251 0           undef $last_break;
252 0           $pos -= $last_length - 1;
253 0 0         print STDERR "[pos now $pos]\n" if $DEBUG;
254 0           next;
255             } elsif (defined $self->{'emergency_break'} && $c > $self->{'emergency_break'}) {
256 0 0         print STDERR "NOW\n" if $DEBUG;
257 0           push(@breakpoints, $i+1);
258 0           $pos = 0;
259             } else {
260 0 0         print STDERR "but can't" if $DEBUG;
261             }
262             }
263 0 0         print STDERR "\n" if $DEBUG;
264              
265 0 0         $last_break = $i if $breaks[$i];
266 0 0         $last_length = $pos if $breaks[$i];
267              
268 0           $pos += $lengths[$i];
269             }
270              
271 0 0         push(@breakpoints, $#lengths) if $breakpoints[-1] < $#lengths;
272              
273 0 0         print STDERR "find_breaks: returning breakpoints " . join(":", @breakpoints) . "\n" if $DEBUG;
274              
275 0           return @breakpoints;
276             }
277              
278             # Returns a list of lines, broken up with find_breaks
279             sub break_lines {
280 0 0   0 0   my $self = ref($_[0])? shift(): self();
281 0           my $text = shift;
282              
283 0           my @breaks = $self->find_breaks($text);
284 0           my @lines;
285              
286 0           my $last = 0;
287 0           foreach (@breaks) {
288 0           push(@lines, substr($text, $last, $_-$last));
289 0           $last = $_;
290             }
291              
292 0           return @lines;
293             }
294              
295             1;
296              
297             __END__