use strict;
use warnings;

package Printer::ESCPOS::Profiles::Generic;

# PODNAME: Printer::ESCPOS::Profiles::Generic
# ABSTRACT: Generic Profile for Printers for L<Printer::ESCPOS>. Most common functions are included here.
#
# This file is part of Printer-ESCPOS
#
# This software is copyright (c) 2016 by Shantanu Bhadoria.
#
# This is free software; you can redistribute it and/or modify it under
# the same terms as the Perl 5 programming language system itself.
#
our $VERSION = '1.004'; # VERSION

# Dependencies
use 5.010;
use Moo;
with 'Printer::ESCPOS::Roles::Profile';
use Carp;
use Scalar::Util::Numeric qw(isint);
use GD::Barcode::QRcode;

use Pango;
use utf8;

use constant {
    _ESC => "\x1b",
    _GS  => "\x1d",
    _DLE => "\x10",
    _FS  => "\x1c",

    # Level 2 Constants
    _FF  => "\x0c",
    _SP  => "\x20",
    _EOT => "\x04",
    _DC4 => "\x14",
};


sub init {
    my ($self) = @_;

    $self->driver->print( _ESC . '@' );
}


sub enable {
    my ( $self, $n ) = @_;

    if ( $n == 1 ) {
        $self->driver->print( _ESC . '=' . chr(1) );
    }
    elsif ( $n == 0 ) {
        $self->driver->print( _ESC . '=' . chr(2) );
    }
    else {
        confess "Invalid parameter please use '0' or '1'";
    }
}


sub qr {
    my ( $self, $string, $ecc, $version, $moduleSize ) = @_;
    $ecc        ||= 'L';
    $version    ||= 5;
    $moduleSize ||= 3;

    my %eccAllowedValues;
    @eccAllowedValues{qw(L M Q H)} = ();
    confess "Ecc must be one of 'L', 'M', 'Q' or 'H'"
      unless ( exists $eccAllowedValues{$ecc} );
    confess "Version must be between 1 to 40"
      unless ( $version <= 40 and $version >= 1 and $version =~ /\d?\d/ );
    confess "Module size must be between a positive integer"
      unless ( isint $moduleSize == 1 );

    my $qrImage =
      GD::Barcode::QRcode->new( $string,
        { Ecc => $ecc, Version => $version, ModuleSize => $moduleSize } )
      ->plot();
    $self->image($qrImage);
}


sub utf8ImagedText {
    my ( $self, $string, %params ) = @_;
    my $fontFamily = $params{fontFamily} // "Purisa";
    my $fontStyle  = $params{fontStyle}  // "Normal";
    my $fontSize   = $params{fontSize}   // 20;
    my $lineHeight = $params{lineHeight} // 42;
    my $paperWidth = $params{paperWidth} // 500;

    my $surface =
      Cairo::ImageSurface->create( 'argb32', $paperWidth, $lineHeight );
    my $cr = Cairo::Context->create($surface);
    $cr->set_antialias('none');
    $cr->set_source_rgb( 255, 255, 255 );
    $cr->paint();
    $cr->set_source_rgb( 0, 0, 0 );
    my $layout = Pango::Cairo::create_layout($cr);
    $layout->set_text($string);
    my $font =
      Pango::FontDescription->from_string("$fontFamily $fontStyle $fontSize");
    $layout->set_font_description($font);

    Pango::Cairo::show_layout( $cr, $layout );
    my $tempdir = File::Temp::tempdir();
    $surface->write_to_png( $tempdir . '/cairopangoprinterimage.png' );
    my $img = newFromPng GD::Image( $tempdir . '/cairopangoprinterimage.png' )
      || die "Error $!";
    $self->image($img);
}


sub image {
    my ( $self, $img ) = @_;
    my $paddingLeft  = '';
    my $paddingRight = '';

    if ( $img->width > 512 ) {
        carp
'Width is greater than 512 pixels and could be truncated at print time';
    }
    if ( $img->height > 255 ) {
        confess 'Height is greated than 255 pixels';
    }

    my @padding = $self->_pad_image_size( $img->width );
    for ( 1 .. $padding[0] ) {
        $paddingLeft .= '0';
    }
    for ( 1 .. $padding[1] ) {
        $paddingRight .= '0';
    }

    my $pixelLine = '';
    my $switch    = 0;
    my @imageSize = ( 0, 0 );
    for my $y ( 0 .. $img->height - 1 ) {
        $imageSize[1]++;
        $pixelLine .= $paddingLeft;
        $imageSize[0] += $padding[0];
        for my $x ( 0 .. $img->width - 1 ) {
            $imageSize[0]++;
            my $index         = $img->getPixel( $x, $y );
            my @rgb           = $img->rgb($index);
            my $imageColour   = $rgb[0] + $rgb[1] + $rgb[2];
            my $imagePattern  = "1X0";
            my $patternLength = length $imagePattern;
            $switch = ( $switch - 1 ) * (-1);
            for my $x ( 1 .. $patternLength ) {

                if ( $imageColour <= ( 255 * 3 / $patternLength * $x ) ) {
                    my $patternAtX = substr( $imagePattern, $x - 1, 1 );
                    if ( $patternAtX eq 'X' ) {
                        $pixelLine .= $switch;
                    }
                    else {
                        $pixelLine .= $patternAtX;
                    }
                    last;
                }
                elsif (
                    $imageColour > ( 255 * 3 / $patternLength * $patternLength )
                    and $imageColour <= ( 255 * 3 ) )
                {
                    $pixelLine .= substr( $imagePattern, -1, 1 );
                    last;
                }
            }
        }
        $pixelLine .= $paddingRight;
        $imageSize[0] += $padding[1];
    }
    $self->_print_image( $pixelLine, \@imageSize );
}

sub _pad_image_size {
    my ( $self, $width ) = @_;

    if ( $width % 32 == 0 ) {
        return ( 0, 0 );
    }
    else {
        my $border = 32 - ( $width % 32 );
        if ( $border % 2 == 0 ) {
            return ( $border / 2, $border / 2 );
        }
        else {
            return ( $border / 2 - .5, $border / 2 + .5 );
        }
    }
}

sub _print_image {
    my ( $self, $pixelLine, $imageSize ) = @_;

    $self->driver->write( _GS . "v\x30\x00" );
    my $buffer = sprintf(
        "%02X%02X%02X%02X",
        (
            ( ( $imageSize->[0] / $imageSize->[1] ) / 8 ), 0, $imageSize->[1],
            0
        )
    );
    $self->driver->write( pack( "H*", $buffer ) );

    $buffer = "";
    my $i     = 0;
    my $count = 0;
    while ( $i < length($pixelLine) ) {
        my $octalString = oct( "0b" . substr( $pixelLine, $i, 8 ) );
        $buffer .= sprintf( "%02X", $octalString );
        $i += 8;
        $count++;
        if ( $count % 4 == 0 ) {
            $self->driver->write( pack( "H*", $buffer ) );
            $buffer = "";
            $count  = 0;
        }
    }
}


sub printAreaWidth {
    my ( $self, $width ) = @_;

    confess
"Width must be a integer between 0 and 65535 in printAreaWidth(). Invalid value '$width'.
        Usage: \n\t\$device->printer->printAreaWidth(\$width)\n"
      unless ( isint $width == 1 and $width <= 65535 and $width >= 1 );

    my $nH = $width >> 8;
    my $nL = $width - ( $nH << 8 );

    $self->driver->write( _GS . 'W' . chr($nL) . chr($nH) );
}


sub tabPositions {
    my ( $self, @positions ) = @_;
    my $pos = '';

    for (@positions) {
        confess "Tab position must be a positive integer. Invalid value '$_'.
        Usage: \n\t\$device->printer->tabPositions(4,8,16 ...)\n"
          unless isint $_ == 1;
    }

    $pos .= chr($_) for @positions;
    $self->driver->write( _ESC . 'D' . $pos . chr(0) );
}


sub tab {
    my ($self) = @_;

    $self->driver->write("\t");
}


sub lf {
    my ($self) = @_;

    $self->driver->write("\n");
}


sub ff {
    my ($self) = @_;

    $self->driver->write("\x0c");
}


sub cr {
    my ($self) = @_;

    $self->driver->write("\x0d");
}


sub cancel {
    my ($self) = @_;

    $self->driver->write("\x18");
}


sub font {
    my ( $self, $font ) = @_;
    $font ||= 'a';

    my %fontMap = (
        a => "\x00",
        b => "\x01",
        c => "\x02",
    );

    confess "Invalid value for font '$font'. Use 'a', 'b' or 'c'.
        Usage: \n\t\$device->printer->font('a')\n"
      unless exists $fontMap{$font};

    $self->fontStyle($font);
    if ( $self->usePrintMode && $font ne 'c' ) {
        $self->_updatePrintMode;
    }
    else {
        $self->driver->write( _ESC . 'M' . $fontMap{$font} );
    }
}


sub bold {
    my ( $self, $bold ) = @_;
    $bold ||= 0;

    confess "Invalid value for bold '$bold'. Use '0' or '1'.
        Usage: \n\t\$device->printer->bold(1)\n"
      unless ( $bold == 1 or $bold == 0 );

    $self->emphasizedStatus($bold);
    if ( $self->usePrintMode ) {
        $self->_updatePrintMode;
    }
    else {
        $self->driver->write( _ESC . 'E' . int($bold) );
    }
}


sub doubleStrike {
    my ( $self, $doubleStrike ) = @_;
    $doubleStrike ||= 0;

    confess "Invalid value for doubleStrike '$doubleStrike'. Use '0' or '1'.
        Usage: \n\t\$device->printer->doubleStrike(1)\n"
      unless ( $doubleStrike == 1 or $doubleStrike == 0 );

    $self->driver->write( _ESC . 'G' . int($doubleStrike) );
}


sub underline {
    my ( $self, $underline ) = @_;
    $underline ||= 0;

    confess "Invalid value for underline '$underline'. Use '0', '1' or '2'.
        Usage: \n\t\$device->printer->underline(1)\n"
      unless ( $underline == 2 or $underline == 1 or $underline == 0 );

    $self->underlineStatus($underline);
    if ( $self->usePrintMode ) {
        $self->_updatePrintMode;
    }
    else {
        $self->driver->write( _ESC . '-' . $underline );
    }
}


sub invert {
    my ( $self, $invert ) = @_;
    $invert ||= 0;

    confess "Invalid value for invert '$invert'. Use '0' or '1'.
        Usage: \n\t\$device->printer->invert(1)\n"
      unless ( $invert == 1 or $invert == 0 );

    $self->driver->write( _GS . 'B' . chr($invert) );
}


sub color {
    my ( $self, $color ) = @_;
    $color ||= 0;

    confess "Invalid value for color '$color'. Use '0' or a positive integer.
        Usage: \n\t\$device->printer->color(1)\n" unless ( isint $color >= 0 );

    $self->driver->write( _ESC . 'r' . chr($color) );
}


sub justify {
    my ( $self, $justify ) = @_;
    $justify ||= 'left';
    my %jmap = (
        left   => 0,
        center => 1,
        right  => 2,
        full   => 3,
    );

    confess
"Invalid value for justify '$justify'. Use 'full', 'left', 'center' or 'right'.
        Usage: \n\t\$device->printer->justify('left')\n"
      unless ( exists $jmap{$justify} );

    $self->driver->write( _ESC . 'a' . int( $jmap{ lc $justify } ) );
}


sub upsideDown {
    my ( $self, $upsideDown ) = @_;
    $upsideDown ||= 0;

    confess "Invalid value for upsideDown '$upsideDown'. Use '0' or '1'.
        Usage: \n\t\$device->printer->upsideDown(1)\n"
      unless ( $upsideDown == 1 or $upsideDown == 0 );

    $self->lf();
    $self->driver->write( _ESC . '{' . int($upsideDown) );
}


sub fontHeight {
    my ( $self, $height ) = @_;
    $height ||= 0;
    my $width = $self->widthStatus;

    confess
"Invalid value for fontHeight '$height'. Use a integer between '0' and '7'.
        Usage: \n\t\$device->printer->fontHeight(5)\n"
      unless ( isint $height >= 0 and $height <= 7 );

    $self->heightStatus($height);
    if ( $self->usePrintMode ) {
        $self->_updatePrintMode;
    }
    else {
        $self->driver->write( _GS . '!' . chr( $width << 4 | $height ) );
    }
}


sub fontWidth {
    my ( $self, $width ) = @_;
    $width ||= 0;
    my $height = $self->heightStatus;

    confess
      "Invalid value for fontWidth '$width'. Use a integer between '0' and '7'.
        Usage: \n\t\$device->printer->fontWidth(5)\n"
      unless ( isint $width >= 0 and $width <= 7 );

    $self->widthStatus($width);
    if ( $self->usePrintMode ) {
        $self->_updatePrintMode;
    }
    else {
        $self->driver->write(
            _GS . '!' . chr( int($width) << 4 | int($height) ) );
    }
}


sub charSpacing {
    my ( $self, $charSpacing ) = @_;
    $charSpacing ||= 0;

    confess
"Invalid value for charSpacing '$charSpacing'. Use a integer between '0' and '255'.
        Usage: \n\t\$device->printer->charSpacing(5)\n"
      unless ( isint $charSpacing >= 0 and $charSpacing <= 255 );

    $self->driver->write( _ESC . _SP . chr($charSpacing) );
}


sub lineSpacing {
    my ( $self, $lineSpacing, $commandSet ) = @_;
    $lineSpacing ||= 30;
    $commandSet  ||= '3';

    if ( $commandSet eq '+' or $commandSet eq '3' ) {
        confess
"Invalid value for lineSpacing '$lineSpacing'. Use a integer between '0' and '255' with this commandSet.
            Usage: \n\t\$device->printer->lineSpacing(5, 'A')\n"
          unless ( isint $lineSpacing >= 0 and $lineSpacing <= 255 );
    }
    elsif ( $commandSet eq 'A' ) {
        confess
"Invalid value for lineSpacing '$lineSpacing'. Use a integer between '0' and '85' with commandSet 'A'.
            Usage: \n\t\$device->printer->lineSpacing(5, 'A')\n"
          unless ( isint $lineSpacing >= 0 and $lineSpacing <= 85 );
    }
    else {
        confess
          "Invalid value for commandSet '$commandSet'. Use 'A', '3' or '+'.
            Usage: \n\t\$device->printer->lineSpacing(5, 'A')\n";
    }

    $self->driver->write( _ESC . $commandSet . chr($lineSpacing) );
}


sub selectDefaultLineSpacing {
    my ($self) = @_;
    $self->driver->write( _ESC . '2' );
}


sub printPosition {
    my ( $self, $length, $height ) = @_;

    confess
      "Invalid value for length '$length'. Use a integer between '0' and '255'.
        Usage: \n\t\$device->printer->printPosition(5, 6)\n"
      unless ( isint $length >= 0 and $length <= 255 );
    confess
      "Invalid value for length '$height'. Use a integer between '0' and '255'.
        Usage: \n\t\$device->printer->printPosition(5, 6)\n"
      unless ( isint $height >= 0 and $height <= 255 );

    $self->driver->write( _ESC . '$' . chr($length) . chr($height) );
}


sub leftMargin {
    my ( $self, $leftMargin ) = @_;

    confess
"Invalid value for leftMargin '$leftMargin'. Use a integer between '0' and '255'.
        Usage: \n\t\$device->printer->leftMargin(30)\n"
      unless ( isint $leftMargin >= 0 and $leftMargin <= 255 );

    my $nH = $leftMargin >> 8;
    my $nL = $leftMargin - ( $nH << 8 );

    $self->driver->write( _GS . 'L' . chr($nL) . chr($nH) );
}


sub rot90 {
    my ( $self, $rotate ) = @_;

    confess "Invalid value for rot90 '$rotate'. Use '0' or '1'.
        Usage: \n\t\$device->printer->rot90(1)\n"
      unless ( $rotate == 1 or $rotate == 0 );

    $self->driver->write( _ESC . 'V' . chr($rotate) );
}

# This is a redundant function in ESCPOS which updates the printer
sub _updatePrintMode {
    my ($self) = @_;
    my %fontMap = (
        a => 0,
        b => 1,
    );

    my $value =
        $fontMap{ $self->fontStyle } . '00'
      . $self->emphasizedStatus
      . ( $self->heightStatus ? '1' : '0' )
      . ( $self->widthStatus  ? '1' : '0' ) . '0'
      . $self->underlineStatus;
    $self->driver->write( _ESC . '!' . pack( "b*", $value ) );
}

# BEGIN: BARCODE functions


sub barcode {
    my ( $self, %params ) = @_;

    my %map = (
        none          => 0,
        above         => 1,
        below         => 2,
        aboveandbelow => 3,
    );

    $self->driver->write(
        _GS . 'H' . chr( $map{ $params{HRIPosition} || 'below' } ) );

    %map = (
        a => 0,
        b => 1,
    );
    $self->driver->write( _GS . 'f' . chr( $map{ $params{font} || 'b' } ) );

    $self->driver->write( _GS . 'h' . chr( $params{height} || 50 ) );

    $self->driver->write( _GS . 'w' . chr( $params{width} || 2 ) );

    %map = (
        'UPC-A' => 0,
        'UPC-B' => 1,
        JAN13   => 2,
        JAN8    => 3,
        CODE39  => 4,
        ITF     => 5,
        CODABAR => 6,
        CODE93  => 7,
        CODE128 => 8,
    );
    $params{system} ||= 'CODE93';

    if ( exists $map{ $params{system} } ) {
        $self->driver->write( _GS . 'k'
              . chr( $map{ $params{system} } + 65 )
              . chr( length $params{barcode} )
              . $params{barcode} );
    }
    else {
        confess "Invalid system in barcode";
    }
}

# END: BARCODE functions

# BEGIN: Bitmap printing methods


sub printNVImage {
    my ( $self, $flag ) = @_;

    $self->driver->write( _FS . 'p' . chr(1) . chr($flag) );
}


sub printImage {
    my ( $self, $flag ) = @_;

    $self->driver->write( _GS . '/' . chr($flag) );
}

# END: Bitmap printing methods

# BEGIN: Peripheral and cutter Control Commands


sub cutPaper {
    my ( $self, %params ) = @_;
    $params{feed} ||= 0;

    $self->lf();
    if ( $params{feed} == 0 ) {
        $self->driver->write( _GS . 'V' . chr(1) );
    }
    else {
        $self->driver->write( _GS . 'V' . chr(66) . chr(0) );
    }

}


sub drawerKickPulse {
    my ( $self, $pin, $time ) = @_;
    $pin  = defined $pin  ? $pin  : 0;
    $time = defined $time ? $time : 8;

    $self->driver->write( _DLE . _DC4 . "\x01" . chr($pin) . chr($time) );
}

# End Peripheral Control Commands

# BEGIN: Printer STATUS methods


sub printerStatus {
    my ($self) = @_;

    my @flags =
      split( //,
        unpack( "B*", $self->driver->read( _DLE . _EOT . "\x01", 255 ) ) );
    return {
        drawer_pin3_high            => $flags[5],
        offline                     => $flags[4],
        waiting_for_online_recovery => $flags[2],
        feed_button_pressed         => $flags[1],
    };
}


sub offlineStatus {
    my ($self) = @_;

    my @flags =
      split( //,
        unpack( "B*", $self->driver->read( _DLE . _EOT . "\x02", 255 ) ) );
    return {
        cover_is_closed     => $flags[5],
        feed_button_pressed => $flags[4],
        paper_end           => $flags[2],
        error               => $flags[1],
    };
}


sub errorStatus {
    my ($self) = @_;

    my @flags =
      split( //,
        unpack( "B*", $self->driver->read( _DLE . _EOT . "\x03", 255 ) ) );
    return {
        auto_cutter_error     => $flags[4],
        unrecoverable_error   => $flags[2],
        autorecoverable_error => $flags[1],
    };
}


sub paperSensorStatus {
    my ($self) = @_;

    my @flags =
      split( //,
        unpack( "B*", $self->driver->read( _DLE . _EOT . "\x04", 255 ) ) );
    return {
        paper_roll_near_end_sensor_1 => $flags[5],
        paper_roll_near_end_sensor_2 => $flags[4],
        paper_roll_status_sensor_1   => $flags[2],
        paper_roll_status_sensor_2   => $flags[1],
    };
}


sub inkStatusA {
    my ($self) = @_;

    my @flags = split(
        //,
        unpack(
            "B*", $self->driver->read( _DLE . _EOT . "\x07" . "\x01", 255 )
        )
    );
    return {
        ink_near_end          => $flags[5],
        ink_end               => $flags[4],
        ink_cartridge_missing => $flags[2],
        cleaning_in_progress  => $flags[1],
    };
}


sub inkStatusB {
    my ($self) = @_;

    my @flags = split(
        //,
        unpack(
            "B*", $self->driver->read( _DLE . _EOT . "\x07" . "\x02", 255 )
        )
    );
    return {
        ink_near_end          => $flags[5],
        ink_end               => $flags[4],
        ink_cartridge_missing => $flags[2],
    };
}

# END: Printer STATUS methods

no Moo;
__PACKAGE__->meta->make_immutable;

1;

__END__

=pod

=head1 NAME

Printer::ESCPOS::Profiles::Generic - Generic Profile for Printers for L<Printer::ESCPOS>. Most common functions are included here.

=head1 VERSION

version 1.004

=head1 METHODS

=head2 init

Initializes the Printer. Clears the data in print buffer and resets the printer to the mode that was in effect when the
power was turned on. This function is automatically called on creation of printer object.

=head2 enable

Enables/Disables the printer with a '_ESC =' command (Set peripheral device). When disabled, the printer ignores all
commands except enable() or other real-time commands.

Pass B<1> to enable, pass B<0> to disable

    $device->printer->enable(0) # disabled
    $device->printer->enable(1) # enabled

=head2 qr

Prints a qr code to the printer. In Generic profile, this creates a QR Code image using L<GD::Barcode::QRcode>. A native
implementation may be created using a printer model specific profile.

    $device->printer->qr('Print this QR Code');
    $device->printer->qr('WIFI:T:WPA;S:ShantanusWifi;P:wifipasswordhere;;')  # Create a QR code for connecting to a Wifi

You may also pass in optional QR Code format parameters like Ecc, Version and moduleSize. Read more about these params
at L<http://www.qrcode.com/en/about/version.html>.

I<string>: String to be printed as QR code.

I<ecc> (optional, default B<'L'>): error correction level. There are four available error correction schemes in QR codes.

=over

=item *

Level B<L> - up to 7% damage

=item *

Level B<M> - up to 15% damage

=item *

Level B<Q> - up to 25% damage

=item *

Level B<H> - up to 30% damage

=back

I<version> (optional, default B<5>): The symbol versions of QR Code range from Version B<1> to Version B<40>. Each
version has a different module configuration or number of modules. (The module refers to the black and white dots that
make up QR Code.)

Each QR Code symbol version has the maximum data capacity according to the amount of data, character type and error
correction level. In other words, as the amount of data increases, more modules are required to comprise QR Code,
resulting in larger QR Code symbols.

I<moduleSize> (optional, default B<3>): width of each module in pixels.

    my $ecc = 'L'; # Default value
    my $version = 5; # Default value
    my $moduleSize = 3; # Default value
    $device->printer->qr("Don't Panic!", $ecc, $version, $moduleSize);

You may also call align() before calling qr() to set alignment on the page.

=head2 utf8ImagedText

    use utf8;

    $device->printer->utf8ImagedText("Hello World\x{8A9E}",
      fontFamily => "Rubik",
      fontStyle => "Normal",
      fontSize => 25,
      lineHeight => 40
    );

This method uses native fonts to print utf8 compatible characters including international wide characters. This method
is slower than direct text printing but it allows exceptional styling options allowing you to print text using system
fonts in a wide range of font sizes and styles with many more choices than what a thermal printer otherwise provides.

In the background this function uses L<Pango> and L<Cairo> libraries to create a one line image from a given font styles,
font family in a given font size. Note that you must not use this method to print more than a single line at a time.
When you want to print the next line call this method again to print to the next line.

I<string>: String to be printed in the line.

I<fontFamily> (optional, default B<'Purisa'>): Font family to use. On linux systems with font config installed use the
following command to choose from the list of available fonts:

    fc-list | sed 's/.*:\(.*,\|\s\)\(.*\):.*/\2/'

You may also install more fonts from https://fonts.google.com/ to your system fonts( copy the font to /usr/share/fonts )

I<fontStyle> (optional, default B<'Normal'>): Font style like Bold, Normal, Italic etc.

I<fontSize> (optional, default B<20>): Font size

I<lineHeight> (optional, default B<42>): Line Height in pixels, make sure this is bigger than the font height in pixels for your chosen font size.

I<paperWidth> (optional, default B<500>): This is set to 500 pixels by default as this is the most common width for receipt printers. Change this
as per your printer specs.

=head2 image

Prints a image to the printer. Takes a L<GD> Image object as input. <Maximum printable image dimensions are 512x255

I<image>: L<GD> image object for the image to be printed.

    use GD;

    my $image = newFromGif GD::Image('header.gif') || die "Error $!";
    $device->printer->image($image);

You may also call align() before calling qr() to set alignment on the page.

=head2 printAreaWidth

Sets the Print area width specified by width.

    width x basic calculated pitch

I<width>: width is a 16 bits value range, i.e. int between B<0> to B<65535> specifying print area width in basic
calculated pitch. This command is effective only when processed at the beginning of the line when standard mode is being
used. Printable area width setting is effective until init is executed, the printer is reset, or the power is turned
off.

    $device->printer->printAreaWidth( $width );

Note: If you are using Printer::ESCPOS version prior to v1.* Please check documentation for older version of this module
the nL and nH syntax for this method.

=head2 tabPositions

Sets horizontal tab positions for tab stops. Upto 32 tab positions can be set in most receipt printers.

I<tabPositions>: a list of positions for tab().

    $device->printer->tabPositions( 5, 9, 13 );
    printer.tabPositions(5, 9, 13);

    for my $plu (@plus):
        $device->printer->text($plu{quantity});
        $device->printer->tab();
        $device->printer->text(' x ' . plu{name});
        $device->printer->tab();
        $device->printer->text('$' . plu{price});

    This would print a well aligned receipt like so::

    10 x Guiness Beer              $24.00
    2  x Pizza                     $500.50
    1  x Tandoori Chicken          $50.20

Common tab positions are usually in intervals of 8 chars (9, 17, 25) etc.

=head2 tab

moves the cursor to next horizontal tab position like a "\t". This command is ignored unless the next horizontal tab
position has been set. You may substitute this command with a "\t" as well.

This

    $device->printer->text("blah blah");
    $device->printer->tab();
    $device->printer->text("blah2 blah2");

is same as this

    $device->printer->text("blah blah\tblah2 blah2");

=head2 lf

line feed. Moves to the next line. You can substitute this method with {"\n"} in your print or write method e.g. :

This

    $device->printer->text("blah blah");
    $device->printer->lf();
    $device->printer->text("blah2 blah2");

is same as this

    $device->printer->text("blah blah\nblah2 blah2");

=head2 ff

When in page mode, print data in the buffer and return back to standard mode

=head2 cr

Print and carriage return

When automatic line feed is enabled this method works the same as lf , else it is ignored.

=head2 cancel

Cancel (delete) page data in page mode

=head2 font

Set Font style, you can pass *a*, *b* or *c*. Many printers don't support style *c* and only have two supported styles.

I<font> (optional, default 'a'): Font to set for the printer

    $device->printer->font('a');
    $device->printer->text('Writing in Font A');
    $device->printer->font('b');
    $device->printer->text('Writing in Font B');

=head2 bold

Set bold mode *0* for off and *1* for on. Also called emphasized mode in some printer manuals

I<bold> (optional, default 0): 1 or 0 to set or unset bold.

    $device->printer->bold(1);
    $device->printer->text("This is Bold Text\n");
    $device->printer->bold(0);
    $device->printer->text("This is not Bold Text\n");

=head2 doubleStrike

Set double-strike mode *0* for off and *1* for on

I<doubleStrike> (optional, default 0): 1 or 0 to doubleStrike or unset doubleStrike.

    $device->printer->doubleStrike(1);
    $device->printer->text("This is Double Striked Text\n");
    $device->printer->doubleStrike(0);
    $device->printer->text("This is not Double Striked  Text\n");

=head2 underline

Set underline, *0* for off, *1* for on and *2* for double thickness

I<underline> (optional, default 0): 1 or 0 to underline or unset underline.

    $device->printer->underline(1);
    $device->printer->text("This is Underlined Text\n");
    $device->printer->underline(2);
    $device->printer->text("This is Underlined Text with thicker underline\n");
    $device->printer->underline(0);
    $device->printer->text("This is not Underlined Text\n");

=head2 invert

Reverse white/black printing mode pass *0* for off and *1* for on

I<invert> (optional, default 0): 1 or 0 to invert or unset invert.

    $device->printer->invert(1);
    $device->printer->text("This is Inverted Text\n");
    $device->printer->invert(0);
    $device->printer->text("This is not Inverted Text\n");

=head2 color

Most thermal printers support just one color, black. Some ESCPOS printers(especially dot matrix) also support a second
color, usually red. A few rarer models also support upto 7 different colors. In many models, this only works when the
color is set at the beginning of a new line before any text is printed. Pass *0* or *1* to switch between the two
colors.

I<color> (optional, default 0): color number 0, 1 ...

    $device->printer->lf();
    $device->printer->color(0); #black
    $device->printer->text("black");
    $device->printer->lf();
    $device->printer->color(1); #red
    $device->printer->text("Red");
    $device->printer->print();

=head2 justify

Set Justification. Options B<full>, B<left>, B<right> and B<center>

I<justify> (optional, default 'left'): B<full>, B<left>, B<right> or B<center>

    $device->printer->justify( 'right' );
    $device->printer->text("This is right justified");

=head2 upsideDown

Sets Upside Down Printing on/off (pass *0* or *1*)

I<upsideDown> (optional, default 0): B<0> or B<1>

    $device->printer->upsideDownPrinting(1);
    $device->printer->text("This text is upside down");

=head2 fontHeight

Set font height. Only supports *0* or *1* for printmode set to 1, supports values *0*, *1*, *2*, *3*, *4*, *5*, *6* and
*7* for non-printmode state (default)

I<height> (optional, default 0): B<0> to B<7>

    $device->printer->fontHeight(1);
    $device->printer->text("double height\n");
    $device->printer->fontHeight(2);
    $device->printer->text("triple height\n");
    $device->printer->fontHeight(3);
    $device->printer->text("quadruple height\n");
    . . .

=head2 fontWidth

Set font width. Only supports *0* or *1* for printmode set to 1, supports values *0*, *1*, *2*, *3*, *4*, *5*, *6* and
*7* for non-printmode state (default)

I<width> (optional, default 0): B<0> to B<7>

    $device->printer->fontWidth(1);
    $device->printer->text("double width\n");
    $device->printer->fontWidth(2);
    $device->printer->text("triple width\n");
    $device->printer->fontWidth(3);
    $device->printer->text("quadruple width\n");
    . . .

=head2 charSpacing

Sets character spacing takes a value between 0 and 255

I<charSpacing> (optional, default 0): B<0> to B<255>

    $device->printer->charSpacing(5);
    $device->printer->text("Blah Blah Blah\n");
    $device->printer->print();

=head2 lineSpacing

Sets line spacing i.e the spacing between each line of printout. Note that some printers may not support all
command sets for setting a line spacing. The most commonly available I<commandSet>('3') is used by default.

I<lineSpacing>: ranges from 0 to 255 when commandSet is '+' or '3',

Line spacing is set to lineSpacing/360 of an inch if commandSet is '+', lineSpacing/180 of an inch if commandSet is '3'
and lineSpacing/60 of an inch if commandSet is 'A' (default: 30)

I<commandSet>: ESCPOS provides three alternate commands for setting line spacing i.e. '+', '3', 'A' (default : '3').

    $device->printer->lineSpacing($lineSpacing); # Use default commandSet '3'
    $device->printer->lineSpacing($lineSpacing, $commandSet);

=head2 selectDefaultLineSpacing

Reverts to default line spacing for the printer

    $device->printer->selectDefaultLineSpacing();

=head2 printPosition

Sets the distance from the beginning of the line to the position at which characters are to be printed.

I<length>: ranges from 0 to 255

I<height>: ranges from 0 to 255

    $device->printer->printPosition( $length, $height );

* 0 <= $length <= 255
* 0 <= $height <= 255

=head2 leftMargin

Sets the left margin for printing. Set the left margin at the beginning of a line. The printer ignores any data
preceding this command on the same line in the buffer.

In page mode sets the left margin to leftMargin x (horizontal motion unit) from the left edge of the printable area

I<leftMargin>: Left Margin, range: B<0> to B<65535>. If the margin exceeds the printable area, the left margin is
automatically set to the maximum value of the printable area.

    $device->printer->leftMargin($leftMargin);

Note: If you are using Printer::ESCPOS version prior to v1.* Please check documentation for older version of this module
the nL and nH syntax for this method.

=head2 rot90

Rotate printout by 90 degrees

I<rotate> (optional, default 0): B<0> or B<1>

    $device->printer->rot90(1);
    $device->printer->text("This is rotated 90 degrees\n");
    $device->printer->rot90(0);
    $device->printer->text("This is not rotated 90 degrees\n");

=head2 barcode

This method prints a barcode to the printer. This can be bundled with other text formatting commands at the appropriate
point where you would like to print a barcode on your print out. takes argument ~barcode~ as the barcode value.

In the simplest form you can use this command as follows:

    #Default barcode printed in code93 system with a width of 2 and HRI Chars printed below the barcode
    $device->printer->barcode(
        barcode     => 'SHANTANU BHADORIA',
    );

However there are several customizations available including barcode ~system~, ~font~, ~height~ etc.

    my $hripos = 'above';
    my $font   = 'a';
    my $height = 100;
    my $system = 'UPC-A';
    $device->printer->barcode(
        HRIPosition => $hripos,        # Position of Human Readable characters
                                       # 'none','above','below','aboveandbelow'
        font        => $font,          # Font for HRI characters. 'a' or 'b'
        height      => $height,        # no of dots in vertical direction
        system      => $system,        # Barcode system
        width       => 2               # 2:0.25mm, 3:0.375mm, 4:0.5mm, 5:0.625mm, 6:0.75mm
        barcode     => '123456789012', # Check barcode system you are using for allowed
                                       # characters in barcode
    );
    $device->printer->barcode(
        system      => 'CODE39',
        HRIPosition => 'above',
        barcode     => '*1-I.I/ $IA*',
    );
    $device->printer->barcode(
        system      => 'CODE93',
        HRIPosition => 'above',
        barcode     => 'Shan',
    );

I<HRIPosition> (optional, default 'below'): 'none', 'above', 'below', 'aboveandbelow'

I<font> (optional, default 'b'): 'a' or 'b'

I<height> (optional, default 50): height integer between 0 and 255

I<width> (optional, default 50): width integer between 0 and 255

I<system> (optional, default 'CODE93'): B<UPC-A>, B<UPC-B>, B<JAN13>, B<JAN8>, B<CODE39>, B<ITF>, B<CODABAR>, B<CODE93>,
B<CODE128>

I<barcode>: String to print as barcode.

=head2 printNVImage

Prints bit image stored in Non-Volatile (NV) memory of the printer.

    $device->printer->printNVImage($flag);

I<flag>: height and width

* $flag = 0 # Normal width and Normal Height
* $flag = 1 # Double width and Normal Height
* $flag = 2 # Normal width and Double Height
* $flag = 3 # Double width and Double Height

=head2 printImage

Prints bit image stored in Volatile memory of the printer. This image gets erased when printer is reset.

    $device->printer->printImage($flag);

* $flag = 0 # Normal width and Normal Height
* $flag = 1 # Double width and Normal Height
* $flag = 2 # Normal width and Double Height
* $flag = 3 # Double width and Double Height

=head2 cutPaper

Cuts the paper,

I<feed> (optional, default 0): if ~feed~ is set to B<0> then printer doesnt feed paper to cutting position before
cutting it. The default behavior is that the printer doesn't feed paper to cutting position before cutting. One
pre-requisite line feed is automatically executed before paper cut though.

    $device->printer->cutPaper( feed => 0 )

While not strictly a text formatting option, in receipt printer the cut paper instruction is sent along with the rest of
the text and text formatting data and the printer cuts the paper at the appropriate points wherever this command is
used.

=head2 drawerKickPulse

Trigger drawer kick. Used to open cash drawer connected to the printer. In some use cases it may be used to trigger
other devices by close contact.

    $device->printer->drawerKickPulse( $pin, $time );

I<pin> (optional, default 0): $pin is either 0( for pin 2 ) and 1( for pin5 )

I<pin> (optional, default 8): $time is a value between 1 to 8 and the pulse duration in multiples of 100ms.

For default values use without any params to kick drawer pin 2 with a 800ms pulse

    $device->printer->drawerKickPulse();

Again like cutPaper command this is obviously not a text formatting command but this command is sent along with the rest
of the text and text formatting data and the printer sends the pulse at the appropriate points wherever this command is
used. While originally designed for triggering a cash drawer to open, in practice this port can be used for all sorts of
devices like pulsing light, or sound alarm etc.

=head2 printerStatus

Returns printer status in a hashref.

    return {
        drawer_pin3_high            => $flags[5],
        offline                     => $flags[4],
        waiting_for_online_recovery => $flags[2],
        feed_button_pressed         => $flags[1],
    };

=head2 offlineStatus

Returns a hashref for paper cover closed status, feed button pressed status, paper end stop status, and a aggregate
error status either of which will prevent the printer from processing a printing request.

    return {
        cover_is_closed     => $flags[5],
        feed_button_pressed => $flags[4],
        paper_end           => $flags[2],
        error               => $flags[1],
    };

=head2 errorStatus

Returns hashref with error flags for auto_cutter_error, unrecoverable error and auto-recoverable error

    return {
        auto_cutter_error     => $flags[4],
        unrecoverable_error   => $flags[2],
        autorecoverable_error => $flags[1],
    };

=head2 paperSensorStatus

Gets printer paper Sensor status. Returns a hashref with four sensor statuses. Two paper near end sensors and two paper
end sensors for printers supporting this feature. The exact returned status might differ based on the make of your
printer. If any of the flags is set to 1 it implies that the paper is out or near end.

    return {
        paper_roll_near_end_sensor_1 => $flags[5],
        paper_roll_near_end_sensor_2 => $flags[4],
        paper_roll_status_sensor_1 => $flags[2],
        paper_roll_status_sensor_2 => $flags[1],
    };

=head2 inkStatusA

Only available for dot-matrix and other ink consuming printers. Gets printer ink status for inkA(usually black ink).
Returns a hashref with ink statuses.

    return {
        ink_near_end          => $flags[5],
        ink_end               => $flags[4],
        ink_cartridge_missing => $flags[2],
        cleaning_in_progress  => $flags[1],
    };

=head2 inkStatusB

Only available for dot-matrix and other ink consuming printers. Gets printer ink status for inkB(usually red ink).
Returns a hashref with ink statuses.

    return {
        ink_near_end          => $flags[5],
        ink_end               => $flags[4],
        ink_cartridge_missing => $flags[2],
    };

=head1 AUTHOR

Shantanu Bhadoria <shantanu@cpan.org> L<https://www.shantanubhadoria.com>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2016 by Shantanu Bhadoria.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut
