commit 572f3e278f340d8517a94cdd79455e4c76db56ea Author: sergiotarxz Date: Fri Jun 7 15:15:16 2024 +0200 Adding inital support for printing. diff --git a/diploma-currado.png b/diploma-currado.png new file mode 100644 index 0000000..d8a8f2b Binary files /dev/null and b/diploma-currado.png differ diff --git a/print.pl b/print.pl new file mode 100644 index 0000000..ffc8f79 --- /dev/null +++ b/print.pl @@ -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; +}