1 package C4::Labels::Label;
3 # Copyright 2006 Katipo Communications.
4 # Some parts Copyright 2009 Foundations Bible College.
6 # This file is part of Koha.
8 # Koha is free software; you can redistribute it and/or modify it under the
9 # terms of the GNU General Public License as published by the Free Software
10 # Foundation; either version 2 of the License, or (at your option) any later
13 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
14 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
15 # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License along with
18 # Koha; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
19 # Suite 330, Boston, MA 02111-1307 USA
24 use Sys::Syslog qw(syslog);
26 use Algorithm::CheckDigits;
35 use version; our $VERSION = qv('1.0.0_1');
38 my $possible_decimal = qr/\d{3,}(?:\.\d+)?/; # at least three digits for a DDCN
41 my ( $llx, $lly, $width, $height ) = @_;
42 my $obj_stream = "q\n"; # save the graphic state
43 $obj_stream .= "0.5 w\n"; # border line width
44 $obj_stream .= "1.0 0.0 0.0 RG\n"; # border color red
45 $obj_stream .= "1.0 1.0 1.0 rg\n"; # fill color white
46 $obj_stream .= "$llx $lly $width $height re\n"; # a rectangle
47 $obj_stream .= "B\n"; # fill (and a little more)
48 $obj_stream .= "Q\n"; # restore the graphic state
53 my $item_number = shift;
54 my $barcode_only = shift || 0;
55 my $dbh = C4::Context->dbh;
57 # FIXME This makes for a very bulky data structure; data from tables w/duplicate col names also gets overwritten.
58 # Something like this, perhaps, but this also causes problems because we need more fields sometimes.
59 # SELECT i.barcode, i.itemcallnumber, i.itype, bi.isbn, bi.issn, b.title, b.author
60 "SELECT bi.*, i.*, b.*
61 FROM items AS i, biblioitems AS bi ,biblio AS b
62 WHERE itemnumber=? AND i.biblioitemnumber=bi.biblioitemnumber AND bi.biblionumber=b.biblionumber";
63 my $sth = $dbh->prepare($query);
64 $sth->execute($item_number);
66 syslog("LOG_ERR", "C4::Labels::Label::_get_label_item : Database returned the following error: %s", $sth->errstr);
68 my $data = $sth->fetchrow_hashref;
69 # Replaced item's itemtype with the more user-friendly description...
70 my $sth1 = $dbh->prepare("SELECT itemtype,description FROM itemtypes WHERE itemtype = ?");
71 $sth1->execute($data->{'itemtype'});
73 syslog("LOG_ERR", "C4::Labels::Label::_get_label_item : Database returned the following error: %s", $sth1->errstr);
75 my $data1 = $sth->fetchrow_hashref;
76 $data->{'itemtype'} = $data1->{'description'};
77 $data->{'itype'} = $data1->{'description'};
78 $barcode_only ? return $data->{'barcode'} : return $data;
81 sub _get_text_fields {
82 my $format_string = shift;
83 my $csv = Text::CSV_XS->new({allow_whitespace => 1});
84 my $status = $csv->parse($format_string);
85 my @sorted_fields = map {{ 'code' => $_, desc => $_ }} $csv->fields();
86 my $error = $csv->error_input();
87 syslog("LOG_ERR", "C4::Labels::Label::_get_text_fields : Text field sort failed with this error: %s", $error) if $error;
88 return \@sorted_fields;
95 # lccn examples: 'HE8700.7 .P6T44 1983', 'BS2545.E8 H39 1996';
97 ^([a-zA-Z]+) # HE # BS
98 (\d+(?:\.\d)*) # 8700.7 # 2545
100 (\.*\D+\d*) # .P6 # .E8
102 (.*) # T44 1983 # H39 1996 # everything else (except any bracketing spaces)
105 unless (scalar @parts) {
106 syslog("LOG_ERR", "C4::Labels::Label::_split_lccn : regexp failed to match string: %s", $_);
107 push @parts, $_; # if no match, just push the whole string.
109 push @parts, split /\s+/, pop @parts; # split the last piece into an arbitrary number of pieces at spaces
110 $debug and print STDERR "split_lccn array: ", join(" | ", @parts), "\n";
117 s/\///g; # in theory we should be able to simply remove all segmentation markers and arrive at the correct call number...
118 # ddcn examples: 'R220.3 H2793Z H32 c.2', 'BIO JP2 R5c.1'
121 ^([a-zA-Z-]+(?:$possible_decimal)?) # R220.3 # BIO # first example will require extra splitting
123 (.+) # H2793Z H32 c.2 # R5c.1 # everything else (except bracketing spaces)
126 unless (scalar @parts) {
127 syslog("LOG_ERR", "C4::Labels::Label::_split_ddcn : regexp failed to match string: %s", $_);
128 push @parts, $_; # if no match, just push the whole string.
131 if ($parts[ 0] =~ /^([a-zA-Z]+)($possible_decimal)$/) {
132 shift @parts; # pull off the mathching first element, like example 1
133 unshift @parts, $1, $2; # replace it with the two pieces
136 push @parts, split /\s+/, pop @parts; # split the last piece into an arbitrary number of pieces at spaces
138 if ($parts[-1] !~ /^.*\d-\d.*$/ && $parts[-1] =~ /^(.*\d+)(\D.*)$/) {
139 pop @parts; # pull off the mathching last element, like example 2
140 push @parts, $1, $2; # replace it with the two pieces
143 $debug and print STDERR "split_ddcn array: ", join(" | ", @parts), "\n";
150 # Split fiction call numbers based on spaces
153 if ($fcn =~ m/([A-Za-z0-9]+\.?[0-9]?)(\W?).*?/x) {
154 push (@fcn_split, $1);
158 last SPLIT_FCN; # No match, break out of the loop
161 unless (scalar @fcn_split) {
162 syslog("LOG_ERR", "C4::Labels::Label::_split_fcn : regexp failed to match string: %s", $_);
163 push (@fcn_split, $_);
169 my ( $layout_id, $sorttype ) = @_;
171 my $sortorder = get_layout($layout_id);
173 return $sortorder->{'formatstring'};
176 my $csv = Text::CSV_XS->new( { allow_whitespace => 1 } );
177 my $line = $sortorder->{'formatstring'};
178 my $status = $csv->parse($line);
180 map { { 'code' => $_, desc => $_ } } $csv->fields();
181 if (my $error = $csv->error_input()) {
182 syslog("LOG_ERR", "C4::Labels::Label::_get_fields : Text::CSV_XS returned the following error: %s", $error);
187 sub _get_item_fields {
197 sub _get_barcode_data {
198 my ( $f, $item, $record ) = @_;
199 my $kohatables = _desc_koha_tables();
201 my $match_kohatable = join(
204 @{ $kohatables->{'biblio'} },
205 @{ $kohatables->{'biblioitems'} },
206 @{ $kohatables->{'items'} }
213 if ( $f =~ /^'(.*)'.*/ ) {
214 # single quotes indicate a static text string.
219 elsif ( $f =~ /^($match_kohatable).*/ ) {
221 $datastring .= $item->{$f};
224 syslog("LOG_ERR", "C4::Labels::Label::_get_barcode_data : The '%s' field contains no data.", $f);
229 elsif ( $f =~ /^([0-9a-z]{3})(\w)(\W?).*?/ ) {
230 my ($field,$subf,$ws) = ($1,$2,$3);
232 my ($itemtag, $itemsubfieldcode) = &GetMarcFromKohaField("items.itemnumber",'');
233 my @marcfield = $record->field($field);
235 if($field eq $itemtag) { # item-level data, we need to get the right item.
237 foreach my $itemfield (@marcfield) {
238 if ( $itemfield->subfield($itemsubfieldcode) eq $item->{'itemnumber'} ) {
239 if ($itemfield->subfield($subf)) {
240 $datastring .= $itemfield->subfield($subf) . $ws;
243 syslog("LOG_ERR", "C4::Labels::Label::_get_barcode_data : The '%s' field contains no data.", $f);
248 } else { # bib-level data, we'll take the first matching tag/subfield.
249 if ($marcfield[0]->subfield($subf)) {
250 $datastring .= $marcfield[0]->subfield($subf) . $ws;
253 syslog("LOG_ERR", "C4::Labels::Label::_get_barcode_data : The '%s' field contains no data.", $f);
261 syslog("LOG_ERR", "C4::Labels::Label::_get_barcode_data : Failed to parse label format string: %s", $f);
262 last FIELD_LIST; # Failed to match
268 sub _desc_koha_tables {
269 my $dbh = C4::Context->dbh();
271 for my $table ( 'biblio','biblioitems','items' ) {
272 my $sth = $dbh->column_info(undef,undef,$table,'%');
273 while (my $info = $sth->fetchrow_hashref()){
274 push @{$kohatables->{$table}} , $info->{'COLUMN_NAME'} ;
282 my ($invocant, %params) = @_;
283 my $type = ref($invocant) || $invocant;
285 batch_id => $params{'batch_id'},
286 item_number => $params{'item_number'},
287 height => $params{'height'},
288 width => $params{'width'},
289 top_text_margin => $params{'top_text_margin'},
290 left_text_margin => $params{'left_text_margin'},
291 barcode_type => $params{'barcode_type'},
292 printing_type => $params{'printing_type'},
293 guidebox => $params{'guidebox'},
294 font => $params{'font'},
295 font_size => $params{'font_size'},
296 callnum_split => $params{'callnum_split'},
297 justify => $params{'justify'},
298 format_string => $params{'format_string'},
299 text_wrap_cols => $params{'text_wrap_cols'},
302 if ($self->{'guidebox'}) {
303 $self->{'guidebox'} = _guide_box($self->{'llx'}, $self->{'lly'}, $self->{'width'}, $self->{'height'});
305 bless ($self, $type);
311 return $self->{'printing_type'};
314 =head2 $label->get_attr("attr")
316 Invoking the I<get_attr> method will return the value of the requested attribute or 1 on errors.
319 my $value = $label->get_attr("attr");
325 # if (_check_params(@_) eq 1) {
329 if (exists($self->{$attr})) {
330 return $self->{$attr};
338 =head2 $label->draw_label_text()
340 Invoking the I<draw_label_text> method generates the label text for the label object.
342 $label->draw_label_text(
345 top_text_margin => $label_top_text_margin,
346 line_spacer => $text_leading,
348 font_size => $text_font_size,
349 justify => $text_justification,
353 sub draw_label_text {
354 my ($self, %params) = @_;
357 my $text_lly = $params{'lly'};
358 my $font = $self->{'font'};
359 my $item = _get_label_item($self->{'item_number'});
360 my $label_fields = _get_text_fields($self->{'format_string'});
361 my $record = GetMarcBiblio($item->{'biblionumber'});
362 # FIXME - returns all items, so you can't get data from an embedded holdings field.
363 # TODO - add a GetMarcBiblio1item(bibnum,itemnum) or a GetMarcItem(itemnum).
364 my $cn_source = ($item->{'cn_source'} ? $item->{'cn_source'} : C4::Context->preference('DefaultClassificationSource'));
365 LABEL_FIELDS: # process data for requested fields on current label
366 for my $field (@$label_fields) {
367 if ($field->{'code'} eq 'itemtype') {
368 $field->{'data'} = C4::Context->preference('item-level_itypes') ? $item->{'itype'} : $item->{'itemtype'};
371 $field->{'data'} = _get_barcode_data($field->{'code'},$item,$record);
373 ($field->{'code'} eq 'title') ? (($font =~ /T/) ? ($font = 'TI') : ($font = ($font . 'O'))) : ($font = $font);
374 my $field_data = $field->{'data'};
376 $field_data =~ s/\n//g;
377 $field_data =~ s/\r//g;
380 my @callnumber_list = ('itemcallnumber', '050a', '050b', '082a', '952o'); # Fields which hold call number data FIXME: ( 060? 090? 092? 099? )
381 if ((grep {$field->{'code'} =~ m/$_/} @callnumber_list) and ($self->{'printing_type'} eq 'BIB') and ($self->{'callnum_split'})) { # If the field contains the call number, we do some sp
382 if ($cn_source eq 'lcc') {
383 @label_lines = _split_lccn($field_data);
384 @label_lines = _split_fcn($field_data) if !@label_lines; # If it was not a true lccn, try it as a fiction call number
385 push (@label_lines, $field_data) if !@label_lines; # If it was not that, send it on unsplit
386 } elsif ($cn_source eq 'ddc') {
387 @label_lines = _split_ddcn($field_data);
388 @label_lines = _split_fcn($field_data) if !@label_lines;
389 push (@label_lines, $field_data) if !@label_lines;
391 syslog("LOG_ERR", "C4::Labels::Label->draw_label_text : Call number splitting failed for: %s. Please add this call number to bug #2500 at bugs.koha.org", $field_data);
392 push @label_lines, $field_data;
397 $field_data =~ s/\/$//g; # Here we will strip out all trailing '/' in fields other than the call number...
398 $field_data =~ s/\(/\\\(/g; # Escape '(' and ')' for the pdf object stream...
399 $field_data =~ s/\)/\\\)/g;
401 eval{$Text::Wrap::columns = $self->{'text_wrap_cols'};};
402 my @line = split(/\n/ ,wrap('', '', $field_data));
403 # If this is a title field, limit to two lines; all others limit to one... FIXME: this is rather arbitrary
404 if ($field->{'code'} eq 'title' && scalar(@line) >= 2) {
405 while (scalar(@line) > 2) {
409 while (scalar(@line) > 1) {
413 push(@label_lines, @line);
415 LABEL_LINES: # generate lines of label text for current field
416 foreach my $line (@label_lines) {
417 next LABEL_LINES if $line eq '';
418 my $string_width = C4::Labels::PDF->StrWidth($line, $font, $self->{'font_size'});
419 if ($self->{'justify'} eq 'R') {
420 $text_llx = $params{'llx'} + $self->{'width'} - ($self->{'left_text_margin'} + $string_width);
422 elsif($self->{'justify'} eq 'C') {
423 # some code to try and center each line on the label based on font size and string point width...
424 my $whitespace = ($self->{'width'} - ($string_width + (2 * $self->{'left_text_margin'})));
425 $text_llx = (($whitespace / 2) + $params{'llx'} + $self->{'left_text_margin'});
428 $text_llx = ($params{'llx'} + $self->{'left_text_margin'});
431 text_llx => $text_llx,
432 text_lly => $text_lly,
434 font_size => $self->{'font_size'},
437 $text_lly = $text_lly - $params{'line_spacer'};
439 $font = $self->{'font'}; # reset font for next field
444 =head2 $label->barcode()
446 Invoking the I<barcode> method generates a barcode for the label object and inserts it into the current pdf stream. C<barcode_data> is optional
447 and omitting it will cause the barcode from the current item to be used. C<barcode_type> is also optional. Omission results in the barcode
448 type of the current template being used.
454 width => $barcode_width,
455 y_scale_factor => $barcode_y_scale_factor,
456 barcode_data => $barcode,
457 barcode_type => $barcodetype,
464 $params{'barcode'} = _get_label_item($self->{'item_number'}, 1) if !$params{'barcode'};
465 $params{'barcode_type'} = $self->{'barcode_type'} if !$params{'barcode_type'};
466 my $x_scale_factor = 1;
467 my $num_of_bars = length($params{'barcode'});
468 my $tot_bar_length = 0;
470 my $guard_length = 10;
471 my $hide_text = 'yes';
472 if ($params{'barcode_type'} =~ m/CODE39/) {
473 $bar_length = '17.5';
474 $tot_bar_length = ($bar_length * $num_of_bars) + ($guard_length * 2);
475 $x_scale_factor = ($params{'width'} / $tot_bar_length);
476 if ($params{'barcode_type'} eq 'CODE39MOD') {
477 my $c39 = CheckDigits('visa'); # get modulo43 checksum
478 $params{'barcode'} = $c39->complete($params{'barcode'});
480 elsif ($params{'barcode_type'} eq 'CODE39MOD10') {
481 my $c39_10 = CheckDigits('visa'); # get modulo43 checksum
482 $params{'barcode'} = $c39_10->complete($params{'barcode'});
486 PDF::Reuse::Barcode::Code39(
489 value => "*$params{barcode}*",
490 xSize => $x_scale_factor,
491 ySize => $params{'y_scale_factor'},
498 syslog("LOG_ERR", "Barcode generation failed for item %s with this error: %s", $self->{'item_number'}, $@);
501 elsif ($params{'barcode_type'} eq 'COOP2OF5') {
502 $bar_length = '9.43333333333333';
503 $tot_bar_length = ($bar_length * $num_of_bars) + ($guard_length * 2);
504 $x_scale_factor = ($params{'width'} / $tot_bar_length) * 0.9;
506 PDF::Reuse::Barcode::COOP2of5(
509 value => "*$params{barcode}*",
510 xSize => $x_scale_factor,
511 ySize => $params{'y_scale_factor'},
516 syslog("LOG_ERR", "Barcode generation failed for item %s with this error: %s", $self->{'item_number'}, $@);
519 elsif ( $params{'barcode_type'} eq 'INDUSTRIAL2OF5' ) {
520 $bar_length = '13.1333333333333';
521 $tot_bar_length = ($bar_length * $num_of_bars) + ($guard_length * 2);
522 $x_scale_factor = ($params{'width'} / $tot_bar_length) * 0.9;
524 PDF::Reuse::Barcode::Industrial2of5(
527 value => "*$params{barcode}*",
528 xSize => $x_scale_factor,
529 ySize => $params{'y_scale_factor'},
534 syslog("LOG_ERR", "Barcode generation failed for item %s with this error: %s", $self->{'item_number'}, $@);
541 my $label_fields = _get_text_fields($self->{'format_string'});
542 my $item = _get_label_item($self->{'item_number'});
543 my $bib_record = GetMarcBiblio($item->{biblionumber});
544 my @csv_data = (map { _get_barcode_data($_->{'code'},$item,$bib_record) } @$label_fields);
553 Mason James <mason@katipo.co.nz>
554 Chris Nighswonger <cnighswonger AT foundations DOT edu>