#!/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; }