#! /usr/bin/perl -w
use strict;
# in inches
my $unit = 0.01; # width of smallest element
my $height = 0.6; # height of readable area
my $quiet = 0.25; # white area around barcode
my $factor = 5; # pixels per element
# ---------
use POSIX;
use CGI;
my $q = new CGI;
defined( $q->param('factor') ) and ($q->param('factor') =~ /^([1-9])/) and $factor = $1;
if( defined( $q->param('q') ) )
{
$factor=1;
$height = 0.18;
$quiet = 0.1;
};
my $image = '';
my $err = '';
sub hyphenate
{
my ($isbn) = @_;
# source: http://usin.org/software/servers/ISBN-ISSN.phps
my @country_group_partition = ( 0, 80, 950, 9960, 99900 );
my %country_group_map = (
0 => [ '00',200,7000,85000,900000,9500000 ],
1 => [ '00000000',55000,869800,9999900 ],
2 => [ '00',200,40000000,500,7000,84000,900000,9500000 ],
3 => [ '00',200,7000,85000,900000,9500000 ],
4 => ['00',200,7000,85000,900000,9500000],
5 => ['00',200,7000,85000,900000,9500000],
7 => ['00',100,5000,80000,900000],
80 => ['00',200,7000,85000,900000],
81 => ['00',200,7000,85000,900000],
82 => ['00',200,7000,90000,990000],
83 => ['00',200,7000,85000,900000],
84 => ['00',200,7000,85000,900000,95000,9700],
85 => ['00',200,7000,85000,900000],
86 => ['00',300,7000,80000,900000],
87 => ['00',400,7000,85000,970000],
88 => ['00',200,7000,85000,900000],
89 => ['00',300,7000,85000,950000],
90 => ['00',200,5000,70000,800000,9000000],
91 => ['0',20,500,6500000,7000,8000000,85000,9500000,970000],
92 => ['0',60,800,9000],
93 => ['0000000'],
950 => ['00',500,9000,99000],
951 => ['0',20,550,8900,95000],
952 => ['00',200,5000,89,9500,99000],
953 => ['0',10,150,6000,96000],
954 => ['00',400,8000,90000],
955 => ['0',20,550,800000,9000,95000],
956 => ['00',200,7000],
957 => ['00',440,8500,97000],
958 => ['0',600,9000,95000],
959 => ['00',200,7000],
960 => ['00',200,7000,85000],
961 => ['00',200,6000,90000],
962 => ['00',200,7000,85000],
963 => ['00',200,7000,85000],
964 => ['00',300,5500,90000],
965 => ['00',200,7000,90000],
966 => ['00',500,7000,90000],
967 => ['0',60,900,9900,99900],
968 => ['000000',10,400,500000,6000,800,900000],
969 => ['0',20,400,8000],
970 => ['00',600,9000,91000],
971 => ['00',500,8500,91000],
972 => ['0',20,550,8000,95000],
973 => ['0',20,550,9000,95000],
974 => ['00',200,7000,85000,900000],
975 => ['00',300,6000,92000,980000],
976 => ['0',40,600,8000,95000],
977 => ['00',200,5000,70000],
978 => ['000',2000,30000],
979 => ['0',20,300000,400,700000,8000,95000],
980 => ['00',200,6000],
981 => ['00',200,3000],
982 => ['00',100,500000],
983 => ['000',2000,300000,50,800,9000,99000],
984 => ['00',400,8000,90000],
985 => ['00',400,6000,90000],
986 => ['000000'],
987 => ['00',500,9000,99000],
9952 => ['00000'],
9953 => ['0',20,9000],
9954 => ['00',8000],
9955 => ['00',400],
9956 => ['00000'],
9957 => ['00',8000],
9958 => ['0',10,500,7000,9000],
9959 => ['00'],
9960 => ['00',600,9000],
9961 => ['0',50,800,9500],
9962 => ['00000'],
9963 => ['0',30,550,7500],
9964 => ['0',70,950],
9965 => ['00',400,9000],
9966 => ['00',70000,800,9600],
9967 => ['00000'],
9968 => ['0',10,700,9700],
9970 => ['00',400,9000],
9971 => ['0',60,900,9900],
9972 => ['0',40,600,9000],
9973 => ['0',10,700,9700],
9974 => ['0',30,550,7500],
9975 => ['0',50,900,9500],
9976 => ['0',60,900,99000,9990],
9977 => ['00',900,9900],
9978 => ['00',950,9900],
9979 => ['0',50,800,9000],
9980 => ['0',40,900,9900],
9981 => ['0',20,800,9500],
9982 => ['00',40000,800,9900],
9983 => ['00',500,80,950,9900],
9984 => ['00',500,9000],
9985 => ['0',50,800,9000],
9986 => ['00',400,9000],
9987 => ['00',400,8800],
9988 => ['0',30,550,7500],
9989 => ['0',30,600,9700],
99901 => ['00'],
99903 => ['0',20,900],
99904 => ['0',60,900],
99905 => ['0',60,900],
99906 => ['0',60,900],
99908 => ['0',10,900],
99909 => ['0',40,950],
99910 => ['0000'],
99911 => ['00',600],
99912 => ['0',60,900],
99913 => ['0',30,600],
99914 => ['0',50,900],
99915 => ['0',50,800],
99916 => ['0',30,700],
99917 => ['0',30],
99918 => ['0',40,900],
99919 => ['0',40,900],
99920 => ['0',50,900],
99921 => ['0',20,700],
99922 => ['0',50],
99923 => ['0',20,800],
99924 => ['0',30],
99925 => ['0',40,800],
99926 => ['0000',600],
99927 => ['0',30,600],
99928 => ['0',50,800],
99929 => ['0000'],
99930 => ['0',50,800],
99931 => ['0000'],
99932 => ['0',10],
99933 => ['00',300],
99934 => ['0'],
99935 => ['0000'],
99936 => ['0000'],
99937 => ['0',20]
);
# determine country group
my $group = substr($isbn, 0, length($country_group_partition[0]));
{
my $ngroup = 1;
while( defined($country_group_partition[$ngroup]) and ( ( $country_group_partition[$ngroup] cmp substr($isbn, 0, length($country_group_partition[$ngroup])) ) < 0 ) )
{
$group = substr($isbn, 0, length($country_group_partition[$ngroup]));
$ngroup++;
}
}
# determine publisher prefix
my $prefix = substr($isbn, length($group), length($country_group_map{$group}->[0]));
{
my $nprefix = 1;
while( defined($country_group_map{$group}->[$nprefix]) and ( ( $country_group_map{$group}->[$nprefix] cmp substr($isbn, length($group), length($country_group_map{$group}->[$nprefix])) ) < 0 ) )
{
$prefix = substr($isbn, length($group), length($country_group_map{$group}->[$nprefix]));
$nprefix++;
}
}
my $itemstart = length($group) + length($prefix);
my $itemlength = length($isbn) - $itemstart - 1;
return $group . '-' . $prefix . '-' . substr($isbn, $itemstart, $itemlength) . '-' . substr($isbn, $itemstart + $itemlength, 1);
};
sub errorexit
{
print $q->header, $q->start_html;
$err =~ s/\n/
\n/g;
print '
ISBN->barcode
';
print $err;
print @_;
print $q->start_form(-method=>'GET');
print $q->textfield('isbn','1905038003',13,200);
print $q->end_form;
print 'Click here to go to ISBN lookup instead.';
print $q->end_html;
exit(0);
};
my %A =
(
'0' => '0001101',
'1' => '0011001',
'2' => '0010011',
'3' => '0111101',
'4' => '0100011',
'5' => '0110001',
'6' => '0101111',
'7' => '0111011',
'8' => '0110111',
'9' => '0001011'
);
my %B =
(
'0' => '0100111',
'1' => '0110011',
'2' => '0011011',
'3' => '0100001',
'4' => '0011101',
'5' => '0111001',
'6' => '0000101',
'7' => '0010001',
'8' => '0001001',
'9' => '0010111'
);
my %C =
(
'0' => '1110010',
'1' => '1100110',
'2' => '1101100',
'3' => '1000010',
'4' => '1011100',
'5' => '1001110',
'6' => '1010000',
'7' => '1000100',
'8' => '1001000',
'9' => '1110100'
);
my %G =
(
'L' => '101',
'C' => '01010',
'R' => '101'
);
my %tables =
(
'A' => \%A,
'B' => \%B,
'C' => \%C,
'G' => \%G
);
my %ean13 =
(
'0' => 'GAAAAAAGCCCCCCG',
'1' => 'GAABABBGCCCCCCG',
'2' => 'GAABBABGCCCCCCG',
'3' => 'GAABBBAGCCCCCCG',
'4' => 'GABAABBGCCCCCCG',
'5' => 'GABBAABGCCCCCCG',
'6' => 'GABBBAAGCCCCCCG',
'7' => 'GABABABGCCCCCCG',
'8' => 'GABABBAGCCCCCCG',
'9' => 'GABBABAGCCCCCCG'
);
my %font =
(
'height' => 10,
'width' => 7,
'0' => [
'0000000',
'0000000',
'0000000',
'0011100',
'0100010',
'0100010',
'0100010',
'0100010',
'0011100',
'0000000'
],
'1' => [
'0000000',
'0000000',
'0000000',
'0001000',
'0011000',
'0001000',
'0001000',
'0001000',
'0011100',
'0000000'
],
'2' => [
'0000000',
'0000000',
'0000000',
'0011100',
'0100010',
'0000010',
'0001100',
'0010000',
'0111110',
'0000000'
],
'3' => [
'0000000',
'0000000',
'0000000',
'0011100',
'0100010',
'0000100',
'0000100',
'0100010',
'0011100',
'0000000'
],
'4' => [
'0000000',
'0000000',
'0000000',
'0000100',
'0001100',
'0010100',
'0111110',
'0000100',
'0000100',
'0000000'
],
'5' => [
'0000000',
'0000000',
'0000000',
'0111110',
'0100000',
'0111100',
'0000010',
'0000010',
'0111100',
'0000000'
],
'6' => [
'0000000',
'0000000',
'0000000',
'0011110',
'0100000',
'0111100',
'0100010',
'0100010',
'0011100',
'0000000'
],
'7' => [
'0000000',
'0000000',
'0000000',
'0111110',
'0000010',
'0000100',
'0001000',
'0001000',
'0001000',
'0000000'
],
'8' => [
'0000000',
'0000000',
'0000000',
'0011100',
'0100010',
'0011100',
'0100010',
'0100010',
'0011100',
'0000000'
],
'9' => [
'0000000',
'0000000',
'0000000',
'0011100',
'0100010',
'0100010',
'0011110',
'0000010',
'0111100',
'0000000'
],
'I' => [
'0000000',
'0000000',
'0000000',
'0011100',
'0001000',
'0001000',
'0001000',
'0001000',
'0011100',
'0000000'
],
'S' => [
'0000000',
'0000000',
'0000000',
'0011110',
'0100000',
'0011100',
'0000010',
'0000010',
'0111100',
'0000000'
],
'B' => [
'0000000',
'0000000',
'0000000',
'0111100',
'0100010',
'0111100',
'0100010',
'0100010',
'0111100',
'0000000'
],
'N' => [
'0000000',
'0000000',
'0000000',
'0100010',
'0110010',
'0101010',
'0100110',
'0100010',
'0100010',
'0000000'
],
'x' => [
'0000000',
'0000000',
'0000000',
'0100010',
'0010100',
'0001000',
'0001000',
'0010100',
'0100010',
'0000000'
],
'X' => [
'0000000',
'0000000',
'0000000',
'0100010',
'0010100',
'0001000',
'0001000',
'0010100',
'0100010',
'0000000'
],
':' => [
'0000',
'0000',
'0000',
'0000',
'0110',
'0000',
'0000',
'0110',
'0000',
'0000'
],
'-' => [
'00000',
'00000',
'00000',
'00000',
'00000',
'01110',
'00000',
'00000',
'00000',
'00000'
],
' ' => [
'00',
'00',
'00',
'00',
'00',
'00',
'00',
'00',
'00',
'00'
],
'L' => [
'101',
'101',
'101',
'101',
'101',
'101',
'000',
'000',
'000',
'000'
],
'C' => [
'01010',
'01010',
'01010',
'01010',
'01010',
'01010',
'00000',
'00000',
'00000',
'00000'
],
'R' => [
'101',
'101',
'101',
'101',
'101',
'101',
'000',
'000',
'000',
'000'
]
);
sub isbncheckdigit
{
my($string) = @_;
my $weight = 10;
my $sum = 0;
foreach my $digit(split('', $string))
{
$digit =~ /[0-9]/ or errorexit("Not a digit: $digit.\n");
$sum += $digit * $weight;
$weight--;
}
$sum = (11 - ($sum % 11)) % 11;
if ($sum == 10)
{
$sum = 'X';
}
return $sum;
};
sub checkdigit
{
my($string) = @_;
my $weight = 1;
my $sum = 0;
foreach my $digit(split('', $string))
{
$digit =~ /[0-9]/ or errorexit("Not a digit: $digit.\n");
$sum += $digit * $weight;
$weight = 4 - $weight;
}
return (10 - ($sum % 10)) % 10;
};
sub code
{
my($code) = @_;
my $out = '';
defined($code) or return '';
foreach my $char(split('',$code))
{
$out .= $char x $factor;
}
return $out;
}
sub encode
{
my ($string) = @_;
my $out = '';
$string =~ /^(.)(......)(......)$/ or errorexit("EAN13 needs 13 digits!\n");
$string = 'L' . $2 . 'C' . $3 . 'R';
my @encoding = split('', $ean13{$1});
my @digits = split('', $string);
foreach my $digit(@digits)
{
$out .= &code( $tables{shift @encoding}->{$digit} );
}
return $out;
};
if(!defined($q->param('isbn')))
{
errorexit("Please type in a valid ISBN, then hit ENTER.
");
}
# -------------------------------------------
my $isbn = $q->param('isbn');
$err .= "String: $isbn\n";
$isbn =~ s/[^0-9xX]//g;
my $ean = undef;
# missing first digit?
if(length($isbn)==12)
{
if( $isbn =~ /^78/ )
{
$err .= "Prepending '9'.\n";
$isbn = '9' . $isbn;
$err .= "String: $isbn\n";
}
else
{
$err .= "Generating EAN check digit.\n";
$isbn = $isbn . &checkdigit($isbn);
$err .= "String: $isbn\n";
}
}
if( length($isbn) == 9 )
{
# 10-digit ISBN without its checkdigit?
$err .= "Generating ISBN check digit.\n";
$ean = '978'.$isbn . &checkdigit('978'.$isbn);
$isbn = $isbn . &isbncheckdigit($isbn);
$err .= "ISBN: $isbn\n";
}
elsif( length($isbn) == 10 )
{
$isbn =~ /((?:[0-9]){9}?)([0-9xX])/ or errorexit("Please supply a valid ISBN!\n");
my $check = $2;
$ean = $1;
my $expected = &isbncheckdigit($ean);
if( $check ne $expected )
{
errorexit("Invalid check digit: the correct digit for $ean would be $expected, but you typed $isbn. There's probably a typo there.\n");
}
$ean = '978' . $ean . &checkdigit('978' . $ean);
}
elsif( length($isbn) == 13 )
{
if( $isbn =~ /^([0-9]+)([0-9xX])$/)
{
my $i = $1;
my $c = $2;
my $e = &checkdigit($i);
if( uc($c) eq $e )
{
$ean = $isbn;
if( $isbn =~ /^978([0-9]+)([0-9xX])$/)
{
$isbn = $1 . &isbncheckdigit($1);
}
}
else
{
errorexit("Invalid check digit: the correct digit for $i would be $e, but you typed $i $c. There's probably a typo there.\n");
}
}
}
else
{
errorexit("Please supply a valid ISBN!\n");
}
$err .= "ISBN OK.\n";
$err .= "EAN13: $ean\n";
if( $ean =~ /^978/ )
{
$err .= "Will produce human-readable ISBN.\n";
}
else
{
$err .= "Will NOT produce human-readable ISBN.\n";
$isbn = undef;
}
my $scanline = &encode($ean);
my $lines = ceil(((1/$unit)*$height)*$factor);
my $gap = ceil(((1/$unit)*$quiet*$factor));
my $th = $lines + $gap + $gap;
my $tw = length($scanline) + $gap + $gap;
my $i;
# -------------------------------------------
$image .= 'P1';
$image .= '# NB. minimum width: ' . $tw * $unit / $factor . '" minimum height: ' . $th * $unit / $factor. '"' . "\n";
$err .= 'Generating netpbm image. Minimum width: ' . $tw * $unit / $factor . '" minimum height: ' . $th * $unit / $factor . '"' . "\n";
$image .= $tw . ' ' . $th . "\n";
if( $isbn )
{
(length($isbn)==10) or errorexit("ISBN needs 10 digits!\n");
$isbn = 'ISBN ' . hyphenate($isbn);
}
else
{
$isbn = ' ' . (' ' x 10);
}
my @humanreadable = split('', $isbn);
$i = 0;
while( $i++ < $gap )
{
$scanline = '';
my $csr = floor(($i - (($gap - ($font{'height'}*$factor)))) / $factor);
if( ($csr >= 0) && ($csr < $font{'height'}) )
{
foreach my $digit(@humanreadable)
{
$scanline .= &code( $font{$digit}->[$csr] );
}
$scanline = ('0' x floor(($tw - length($scanline)) / 2)) . $scanline;
$scanline .= '0' x ($tw - length($scanline));
}
else
{
$scanline = '0' x $tw;
}
$image .= $scanline;
$image .= "\n";
}
$scanline = &encode($ean);
$i = 0;
while($i++ < $lines)
{
$image .= '0' x $gap;
$image .= $scanline;
$image .= '0' x $gap;
$image .= "\n";
}
$i = 0;
$ean =~ /^(.)(......)(......)$/ or errorexit("EAN13 needs 13 digits!\n");
$ean = $1 . 'L' . $2 . 'C' . $3 . 'R';
@humanreadable = split('', $ean);
while( $i++ < $gap )
{
my $csr = floor($i / $factor);
if( $csr < $font{'height'} )
{
$scanline = '0' x ($gap - ($font{'width'} * $factor));
foreach my $digit(@humanreadable)
{
$scanline .= &code( $font{$digit}->[$csr] );
}
$scanline .= '0' x $gap;
}
else
{
$scanline = '0' x $tw;
}
$image .= $scanline;
$image .= "\n";
}
# ---------------------
my $try = 0;
while((!mkdir('/tmp/isbn')) && ($try++ < 20))
{
# if($try++ > 10)
#{
# errorexit("Can't get lock on /tmp/isbn.");
# assume something crashed
#};
sleep 1;
}
use FileHandle;
my $fh = new FileHandle;
$fh->open('> /tmp/isbn/isbn');
print $fh $image;
$fh->close();
`pnmtopng /tmp/isbn/isbn > /tmp/isbn/isbn.png`;
my @stats = stat '/tmp/isbn/isbn.png';
print $q->header(-type=>'image/png', "Content-Length"=>($stats[7]));
print `cat /tmp/isbn/isbn.png`;
unlink '/tmp/isbn/isbn.png';
unlink '/tmp/isbn/isbn';
rmdir '/tmp/isbn';