Adding inital support for printing.
This commit is contained in:
commit
572f3e278f
BIN
diploma-currado.png
Normal file
BIN
diploma-currado.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 47 KiB |
385
print.pl
Normal file
385
print.pl
Normal file
@ -0,0 +1,385 @@
|
|||||||
|
#!/usr/bin/env perl
|
||||||
|
|
||||||
|
use v5.38.2;
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
use feature 'signatures';
|
||||||
|
|
||||||
|
use Printer::ESCPOS;
|
||||||
|
use GD::Image;
|
||||||
|
use Cairo;
|
||||||
|
use Path::Tiny;
|
||||||
|
use Encode qw/decode/;
|
||||||
|
use Pango;
|
||||||
|
use Imager::QRCode;
|
||||||
|
|
||||||
|
use Data::Dumper;
|
||||||
|
|
||||||
|
my $debug = $ARGV[0] // 0;
|
||||||
|
my $vendor_id = 0x0416;
|
||||||
|
my $product_id = 0x5011;
|
||||||
|
|
||||||
|
my $deviceFilePath = '/dev/usb/lp0';
|
||||||
|
|
||||||
|
my $device = Printer::ESCPOS->new(
|
||||||
|
driverType => 'File',
|
||||||
|
deviceFilePath => $deviceFilePath,
|
||||||
|
);
|
||||||
|
|
||||||
|
my $diploma = get_gd_image('diploma-currado.png');
|
||||||
|
my $black = $diploma->colorAllocate(0,0,0);
|
||||||
|
$diploma->stringFT($black, '/usr/share/fonts/noto-cjk/NotoSansCJK-Medium.ttc', 20, 0, 500, 190, 'Diploma al merito.');
|
||||||
|
$diploma = $diploma->copyRotate90();
|
||||||
|
my $tempdir = Path::Tiny->tempdir(CLEANUP=>0);
|
||||||
|
my $diploma_image = $tempdir->child('image.png');
|
||||||
|
$diploma->_file($diploma_image);
|
||||||
|
image($device, ''.$diploma_image);
|
||||||
|
|
||||||
|
$device->printer->lf for ( 0 .. 3 );
|
||||||
|
|
||||||
|
$device->printer->print;
|
||||||
|
|
||||||
|
sub qr($device, $url) {
|
||||||
|
my $tmpdir = Path::Tiny->tempdir;
|
||||||
|
my $image_file = $tmpdir->child('image.png');
|
||||||
|
my $qrcode = Imager::QRCode->new( size => 8 );
|
||||||
|
my $img = $qrcode->plot( $url, {} );
|
||||||
|
$img->write( file => '' . $image_file ) or die $img->errstr;
|
||||||
|
image($device, $image_file);
|
||||||
|
print_text($device, $url, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub qr_as_gd_image($device, $url) {
|
||||||
|
my $tmpdir = Path::Tiny->tempdir;
|
||||||
|
my $image_file = $tmpdir->child('image.png');
|
||||||
|
my $qrcode = Imager::QRCode->new( size => 8 );
|
||||||
|
my $img = $qrcode->plot( $url, {} );
|
||||||
|
$img->write( file => '' . $image_file ) or die $img->errstr;
|
||||||
|
return get_gd_image($image_file);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub get_gd_image($file) {
|
||||||
|
$file = path($file);
|
||||||
|
my ($extension) = $file =~ /\.(.*?)$/;
|
||||||
|
my $tempdir = Path::Tiny->tempdir;
|
||||||
|
my $new_image = $tempdir->child( 'image.' . $extension );
|
||||||
|
system 'convert', $file, '-background', 'white', '-alpha', 'remove',
|
||||||
|
$new_image;
|
||||||
|
$file = $new_image;
|
||||||
|
my $image;
|
||||||
|
if ( $file =~ /\.png$/ ) {
|
||||||
|
$image = GD::Image->newFromPng( $file . '' );
|
||||||
|
}
|
||||||
|
elsif ( $file =~ /\.jpg$/ ) {
|
||||||
|
$image = GD::Image->newFromJpeg( $file . '' );
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
die "$image format not recognized";
|
||||||
|
}
|
||||||
|
return $image;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
sub image( $device, $file ) {
|
||||||
|
my $image = get_gd_image($file);
|
||||||
|
my $width = $image->width;
|
||||||
|
my $height = $image->height;
|
||||||
|
my $dest_width = 380;
|
||||||
|
my $dest_height = $dest_width * ( $height / $width );
|
||||||
|
my $image_dest = GD::Image->new( $dest_width, $dest_height );
|
||||||
|
$image_dest->copyResampled( $image, 0, 0, 0, 0, $dest_width, $dest_height,
|
||||||
|
$width, $height, );
|
||||||
|
|
||||||
|
for (
|
||||||
|
my $single_image_y = 0 ;
|
||||||
|
$single_image_y < $dest_height + 255 ;
|
||||||
|
$single_image_y += 255
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if ( $single_image_y >= $dest_height ) {
|
||||||
|
last;
|
||||||
|
}
|
||||||
|
my $final_height = 255;
|
||||||
|
if ( $single_image_y + 255 > $dest_height ) {
|
||||||
|
$final_height = $dest_height - $single_image_y;
|
||||||
|
}
|
||||||
|
my $image_final = GD::Image->new( $dest_width, $final_height );
|
||||||
|
$image_final->copy( $image_dest, 0, 0, 0, $single_image_y, $dest_width,
|
||||||
|
$final_height );
|
||||||
|
my ($alpha) = $image_final->colorAllocateAlpha( 0, 0, 0, 127 );
|
||||||
|
$image_final->colorDeallocate($alpha);
|
||||||
|
$image_final->colorAllocate( 255, 255, 255 );
|
||||||
|
$device->printer->image($image_final);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sub hex_to_char {
|
||||||
|
return pack 'C', shift;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub split_text_lines( $text, $max_width, $font, $font_size ) {
|
||||||
|
my @return;
|
||||||
|
my @lines;
|
||||||
|
if ( !ref $text ) {
|
||||||
|
@lines = map { $_ } split "\n", $text, -1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
for my $element (@$text) {
|
||||||
|
if ( ref $element ) {
|
||||||
|
push @lines, $element;
|
||||||
|
next;
|
||||||
|
}
|
||||||
|
$element = decode 'utf-8', $element;
|
||||||
|
push @lines, $element;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
my $surface = Cairo::ImageSurface->create( 'argb32', 380, 100 );
|
||||||
|
my $cr = Cairo::Context->create($surface);
|
||||||
|
my $layout = Pango::Cairo::create_layout($cr);
|
||||||
|
$layout->set_font_description($font);
|
||||||
|
while (@lines) {
|
||||||
|
my $current_line = get_next_line( \@lines );
|
||||||
|
my ( $w, $h ) = _get_size_text($cr, $layout, $font, $font_size, $current_line );
|
||||||
|
my $final_line = [];
|
||||||
|
my $remainder = [];
|
||||||
|
if ( $w > $max_width ) {
|
||||||
|
LOOP_SPLIT: while (1) {
|
||||||
|
my $word = get_word($current_line);
|
||||||
|
if (!defined $word) {
|
||||||
|
die 'wtf?';
|
||||||
|
}
|
||||||
|
my ( $w, $h ) =
|
||||||
|
_get_size_text($cr, $layout, $font, $font_size,
|
||||||
|
join_line_with_word( $final_line, $word ) );
|
||||||
|
if ( $w > $max_width ) {
|
||||||
|
my ( $w_cutted_word, $h_cutted_word ) =
|
||||||
|
_get_size_text($cr, $layout, $font, $font_size, [$word] );
|
||||||
|
if ( $w_cutted_word > $max_width ) {
|
||||||
|
my $i = 0;
|
||||||
|
$final_line = join_line_with_word( $final_line, $word );
|
||||||
|
while (1) {
|
||||||
|
my $possible_cut =
|
||||||
|
substr_line( $final_line, 0, $i );
|
||||||
|
my ( $w_cutted_word, $h_cutted_word ) =
|
||||||
|
_get_size_text($cr, $layout, $font, $font_size, $possible_cut );
|
||||||
|
if ( $w_cutted_word > $max_width ) {
|
||||||
|
$remainder = substr_line( [@$final_line, @$current_line], $i - 1 );
|
||||||
|
$final_line =
|
||||||
|
substr_line( $final_line, 0, $i - 1 );
|
||||||
|
$current_line = $final_line;
|
||||||
|
last LOOP_SPLIT;
|
||||||
|
}
|
||||||
|
$i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$remainder = unshift_line_with_word( $word, $current_line );
|
||||||
|
$current_line = $final_line;
|
||||||
|
last;
|
||||||
|
}
|
||||||
|
$final_line = join_line_with_word( $final_line, $word );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$remainder->[0] =~ s/^\s+// if defined $remainder->[0] && !(ref $remainder->[0]);
|
||||||
|
unshift @lines, @$remainder if scalar @$remainder > 0;
|
||||||
|
push @return, $current_line;
|
||||||
|
}
|
||||||
|
return @return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub substr_line( $text, $offset, $how_much = undef ) {
|
||||||
|
my $result = [];
|
||||||
|
my $cur_offset = 0;
|
||||||
|
my $element = 0;
|
||||||
|
my $needed_offset = 0;
|
||||||
|
while ( $cur_offset < $offset ) {
|
||||||
|
if ( !defined $text->[$element] ) {
|
||||||
|
die 'Overflow';
|
||||||
|
}
|
||||||
|
if ( ref $text->[$element] ) {
|
||||||
|
$cur_offset += 1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
my $length = (length $text->[$element]) - 1;
|
||||||
|
if ( $cur_offset + $length > $offset ) {
|
||||||
|
$needed_offset = $offset - $cur_offset;
|
||||||
|
last;
|
||||||
|
}
|
||||||
|
$cur_offset += $length;
|
||||||
|
}
|
||||||
|
$element++;
|
||||||
|
}
|
||||||
|
my $gotten = 0;
|
||||||
|
while ( !defined $how_much || $gotten < $how_much ) {
|
||||||
|
if ( !defined $text->[$element] ) {
|
||||||
|
last;
|
||||||
|
}
|
||||||
|
if ( ref $text->[$element] ) {
|
||||||
|
push @$result, $text->[$element];
|
||||||
|
$gotten += 1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
my $substr_length = (length $text->[$element]) - $needed_offset;
|
||||||
|
if ( defined $how_much && $gotten + $substr_length > $how_much ) {
|
||||||
|
$substr_length = $how_much - $gotten;
|
||||||
|
}
|
||||||
|
$result = join_line_with_word( $result, substr $text->[$element],
|
||||||
|
$needed_offset, $substr_length );
|
||||||
|
$needed_offset = 0;
|
||||||
|
$gotten += $substr_length;
|
||||||
|
}
|
||||||
|
$element++;
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub unshift_line_with_word( $word, $line ) {
|
||||||
|
my $result = [@$line];
|
||||||
|
if (defined $result->[0] && !ref $result->[0]) {
|
||||||
|
$result->[0] = $word.$result->[0];
|
||||||
|
} else {
|
||||||
|
unshift @$result, $word;
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
sub join_line_with_word( $line, $word ) {
|
||||||
|
my $result = [@$line];
|
||||||
|
if ( scalar @$result == 0 || scalar @$result == 0 || ref $word || ref $result->[$#$result] ) {
|
||||||
|
push @$result, $word;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$result->[$#$result] .= $word;
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
sub get_word($line) {
|
||||||
|
if ( ref $line->[0] ) {
|
||||||
|
return shift @$line;
|
||||||
|
}
|
||||||
|
return unless @$line;
|
||||||
|
$line->[0] =~ /^(\s*\S*)(.*)$/;
|
||||||
|
if ($2 eq '') {
|
||||||
|
shift @$line;
|
||||||
|
} else {
|
||||||
|
$line->[0] = $2;
|
||||||
|
}
|
||||||
|
return $1;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub get_next_line($lines) {
|
||||||
|
my @result;
|
||||||
|
my $last_is_image = 0;
|
||||||
|
my $first = 1;
|
||||||
|
my $finish = 0;
|
||||||
|
state $calls = 0;
|
||||||
|
$calls++;
|
||||||
|
while (1) {
|
||||||
|
my $current_line = shift @$lines;
|
||||||
|
my $pre_split;
|
||||||
|
my $post_split;
|
||||||
|
if ( !defined $current_line ) {
|
||||||
|
last;
|
||||||
|
}
|
||||||
|
if ( (!(ref $current_line) && !$first && !$last_is_image)
|
||||||
|
|| ($current_line =~ /^(.*?)?\n(.*)$/s)) {
|
||||||
|
$pre_split = $1 if defined $1;
|
||||||
|
$post_split = $2 if defined $2;
|
||||||
|
$finish = 1;
|
||||||
|
}
|
||||||
|
$first = 0;
|
||||||
|
$last_is_image = !!(ref $current_line);
|
||||||
|
if ($finish) {
|
||||||
|
if (defined $post_split) {
|
||||||
|
push @result, $pre_split if defined $pre_split;
|
||||||
|
unshift @$lines, $post_split;
|
||||||
|
} else {
|
||||||
|
unshift @$lines, $current_line
|
||||||
|
}
|
||||||
|
last;
|
||||||
|
}
|
||||||
|
push @result, $current_line;
|
||||||
|
}
|
||||||
|
return \@result;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub print_text( $device, $text, $font_size ) {
|
||||||
|
if ( !(ref $text) ) {
|
||||||
|
$text = decode 'utf-8', $text;
|
||||||
|
}
|
||||||
|
my $tempdir = Path::Tiny->tempdir;
|
||||||
|
my $file = $tempdir->child('result.png');
|
||||||
|
my $font = Pango::FontDescription->from_string(
|
||||||
|
'Noto Sans CJK JP ' . ( $font_size - ( 2 * 50 / 30 ) ) );
|
||||||
|
|
||||||
|
my @lines = split_text_lines( $text, 380, $font, $font_size );
|
||||||
|
while (@lines) {
|
||||||
|
my $current_line = shift @lines;
|
||||||
|
my $height = $font_size * 30 / 17;
|
||||||
|
my $surface = Cairo::ImageSurface->create( 'argb32', 380, $height );
|
||||||
|
my $cr = Cairo::Context->create($surface);
|
||||||
|
$cr->set_source_rgb( 1, 1, 1 );
|
||||||
|
|
||||||
|
$cr->rectangle( 0, 0, 380, $font_size * 30 / 17 );
|
||||||
|
|
||||||
|
$cr->fill;
|
||||||
|
$cr->set_source_rgb( 0, 0, 0 );
|
||||||
|
|
||||||
|
my $x = 0;
|
||||||
|
my $layout = Pango::Cairo::create_layout($cr);
|
||||||
|
for my $part (@$current_line) {
|
||||||
|
$cr->move_to( 0, 0 );
|
||||||
|
if (!ref $part) {
|
||||||
|
Pango::Cairo::update_layout($cr, $layout);
|
||||||
|
$layout->set_text($part);
|
||||||
|
$layout->set_font_description($font);
|
||||||
|
$cr->move_to( $x, 0 );
|
||||||
|
my ($new_x) = $layout->get_pixel_size;
|
||||||
|
$x += $new_x;
|
||||||
|
Pango::Cairo::show_layout( $cr, $layout );
|
||||||
|
next;
|
||||||
|
}
|
||||||
|
my $gd_image = $part;
|
||||||
|
my $tempdir = Path::Tiny->tempdir(CLEANUP=>0);
|
||||||
|
my $image_file = $tempdir->child('out.png');
|
||||||
|
$gd_image->_file($image_file);
|
||||||
|
my $image = Cairo::ImageSurface->create_from_png(
|
||||||
|
$image_file );
|
||||||
|
my $ori_width = $gd_image->width;
|
||||||
|
my $ori_height = $gd_image->height;
|
||||||
|
my $scale_w = $font_size / $ori_width;
|
||||||
|
my $total_height = $font_size * 30 / 17 / $scale_w;
|
||||||
|
my $scale_h = $scale_w;
|
||||||
|
$cr->save;
|
||||||
|
$cr->scale($scale_w, $scale_h);
|
||||||
|
$cr->set_source_surface( $image, $x/$scale_w,($total_height - $x * $scale_w) / 4);
|
||||||
|
$cr->paint;
|
||||||
|
$cr->restore;
|
||||||
|
$x+= $font_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
$surface->write_to_png( $file . '' );
|
||||||
|
my $img = GD::Image->newFromPng( $file . '' );
|
||||||
|
$device->printer->image($img);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sub _get_size_text( $cr, $layout, $font, $font_size, $text ) {
|
||||||
|
my $x = 0;
|
||||||
|
my $i;
|
||||||
|
for my $element (@$text) {
|
||||||
|
$cr->move_to( $x, 0 );
|
||||||
|
if ( ref $element ) {
|
||||||
|
$x += $font_size;
|
||||||
|
next;
|
||||||
|
}
|
||||||
|
$layout->set_text($element);
|
||||||
|
$layout->set_font_description($font);
|
||||||
|
my ($new_x) = $layout->get_pixel_size;
|
||||||
|
$x += $new_x;
|
||||||
|
}
|
||||||
|
return $x;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user