Group
Extension

Chart-Kaleido/lib/Chart/Kaleido.pm

package Chart::Kaleido;

# ABSTRACT: Base class for Chart::Kaleido

use 5.010;
use strict;
use warnings;

our $VERSION = '0.014'; # VERSION

use Moo;
use Config;
use JSON;
use Types::Standard qw(Int Str);
use File::Which qw(which);
use IPC::Run qw();
use namespace::autoclean;

use constant KALEIDO => 'kaleido';


has timeout => (
    is      => 'ro',
    isa     => Int,
    default => 30,
);

has base_args => (
    is       => 'ro',
    init_arg => 0,
    default  => sub { [] },
);

has _default_chromium_args => (
    is      => 'ro',
    default => sub {
        [
            qw(
              --disable-gpu
              --allow-file-access-from-files
              --disable-breakpad
              --disable-dev-shm-usage
            )
        ];
    }
);

has disable_gpu => (
    is      => 'ro',
    default => 1,
);

has _stall_timeout => (
    is => 'lazy',
    builder =>
      sub { IPC::Run::timeout( $_[0]->timeout, name => 'stall timeout' ) },
);

has _h => ( is => 'rw' );

has _ios => (
    is      => 'ro',
    default => sub {
        return { map { $_ => '' } qw(in out err) };
    },
);

# class attributes
sub all_formats { [] }
sub scope_name  { "" }
sub scope_flags { [] }

sub DEMOLISH {
    my ($self) = @_;
    $self->shutdown_kaleido;
}

sub _reset {
    my ($self) = @_;
    $self->_ios->{in}  = '';
    $self->_ios->{out} = '';
    $self->_ios->{err} = '';
}

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

    my @args = @{ $self->base_args };
    unless ( $self->disable_gpu ) {
        @args = grep { $_ ne '--disable-gpu' } @args;
    }

    no strict 'refs';
    push @args, map {
        my $val = $self->$_;
        if ( defined $val ) {
            my $flag = $_;
            $flag =~ s/_/-/g;

            # too bad Perl does not have a core boolean type..
            if ( ref($val) =~ /^(JSON::.*::Boolean|boolean)$/ and $val ) {
                "--$flag";
            }
            else {
                "--$flag=$val";
            }
        }
        else {
            ();
        }
    } @{ $self->scope_flags };

    return \@args;
}

sub ensure_kaleido {
    my ( $self, $override_args ) = @_;
    $override_args //= $self->kaleido_args;

    unless ( $self->_h and $self->_h->pumpable ) {
        $self->_reset;
        my $h = IPC::Run::start(
            [ KALEIDO, @{ $self->kaleido_args } ],
            \$self->_ios->{in},
            \$self->_ios->{out},
            \$self->_ios->{err},
            $self->_stall_timeout,
        );
        $self->_h($h);

        $self->_stall_timeout->start;
        my $resp = $self->_get_kaleido_out;
        if ( exists $resp->{code} and $resp->{code} == 0 ) {
            return $resp->{version};
        }
        else {
            die $resp->{message};
        }
    }
}

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

    if ( $self->_h ) {
        eval { $self->finish; };
        if ($@) {
            $self->_h->kill_kill;
        }
    }
}

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

    $self->ensure_kaleido;

    my $json = JSON->new->allow_blessed(1)->convert_blessed(1);
    $self->_ios->{in} .= $json->encode($data) . "\n";
    $self->_stall_timeout->start;
    my $resp = $self->_get_kaleido_out;
    return $resp;
}

sub version {
    my ( $class, $force_check ) = @_;

    if ( $class->_check_alien($force_check) ) {
        return Alien::Plotly::Kaleido->version;
    }
    else {
        state $version;
        if ( not $version or $force_check ) {
            $version = $class->_detect_kaleido_version;
        }
        return $version;
    }
}

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

    while (1) {
        $self->_h->pump;
        my $out   = $self->_ios->{out};
        my @lines = split( /\n/, $out );
        next unless @lines;

        for my $line (@lines) {
            my $data;
            eval { $data = decode_json($line); };
            next if $@;
            $self->_stall_timeout->reset;
            $self->_ios->{out} = '';    # clear out buffer
            return $data;
        }
    }
}

sub _check_alien {
    my ( $class, $force_check ) = @_;

    state $has_alien;

    if ( !defined $has_alien or $force_check ) {
        $has_alien = 0;
        eval { require Alien::Plotly::Kaleido; };
        if ( !$@ and Alien::Plotly::Kaleido->install_type eq 'share' ) {
            $ENV{PATH} = join(
                $Config{path_sep},
                Alien::Plotly::Kaleido->bin_dir,
                $ENV{PATH} // ''
            );
            $has_alien = 1;
        }
    }
    return $has_alien;
}

sub _kaleido_available {
    my ( $class, $force_check ) = @_;

    state $available;
    if ( !defined $available or $force_check ) {
        $available = 0;
        if ( not $class->_check_alien($force_check)
            and ( not which(KALEIDO) ) )
        {
            die "Kaleido tool (its 'kaleido' command) must be installed and "
              . "in PATH in order to export images. "
              . "Either install Alien::Plotly::Kaleido from CPAN, or install "
              . "it manually (see https://github.com/plotly/Kaleido/releases)";
        }
        $available = 1;
    }
    return $available;
}

sub _detect_kaleido_version {
    my ($class) = @_;

    my $kaleido = which('kaleido');
    if ($kaleido) {
        my $kaleido = $class->new;
        my $args    = [ 'plotly', '--disable-gpu' ];
        my $version = $kaleido->ensure_kaleido($args);
        $kaleido->shutdown_kaleido;
        return $version;
    }

    die "Failed to detect kaleido version";
}

__PACKAGE__->_kaleido_available;

1;

__END__

=pod

=encoding UTF-8

=head1 NAME

Chart::Kaleido - Base class for Chart::Kaleido

=head1 VERSION

version 0.014

=head1 SYNOPSIS

    use Chart::Kaleido::Plotly;
    use JSON;

    my $data = decode_json(<<'END_OF_TEXT');
    { "data": [{"y": [1,2,1]}] }
    END_OF_TEXT

    my $kaleido = Chart::Kaleido::Plotly->new();
    $kaleido->save( file => "foo.png", plot => $data,
                    width => 1024, height => 768 );

=head1 DESCRIPTION

This is base class that wraps plotly's kaleido command.
Instead of this class you would mostly want to use
its subclass like L<Chart::Kaleido::Plotly>.

=head1 ATTRIBUTES

=head2 timeout

=head1 SEE ALSO

L<https://github.com/plotly/Kaleido>

L<Chart::Kaleido::Plotly>,
L<Alien::Plotly::Kaleido>

=head1 AUTHOR

Stephan Loyd <sloyd@cpan.org>

=head1 CONTRIBUTOR

=for stopwords Gabor Szabo

Gabor Szabo <gabor@szabgab.com>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2020-2023 by Stephan Loyd.

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


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.