MTGPrint/lib/TgMagicPdf/PdfBuilder.pm

309 lines
9.5 KiB
Perl

package TgMagicPdf::PdfBuilder;
use v5.38.2;
use strict;
use warnings;
use feature 'signatures';
use Moo;
use DBI;
use DBD::SQLite;
use Mojo::Promise;
use Mojo::UserAgent;
use PDF::API2;
use Path::Tiny;
has _db_all_printings => ( is => 'lazy', );
has last_invalid_card => (
is => 'rw',
default => sub { "" }
);
has _ua => ( is => 'lazy', );
sub _build__ua {
my $ua = Mojo::UserAgent->new->with_roles('+Queued');
$ua->max_active(20);
$ua->inactivity_timeout(60);
return $ua;
}
our $ERR_TOO_MANY_CARDS = 'TOO_MANY_CARDS';
our $ERR_INVALID_CARD = 'INVALID_CARD';
our $ERR_UNABLE_TO_FIND_IMAGE = 'UNABLE_TO_FIND_IMAGE';
our $MAX_CARDS = 9 * 50;
sub _build__db_all_printings ($self) {
return DBI->connect('dbi:SQLite:AllPrintings.sqlite');
}
sub from_text ( $self, $text ) {
my $promise = Mojo::Promise->new;
{
my @lines = split /\n+/, $text;
@lines = grep { $self->_filter_lines($_); } @lines;
if ( scalar @lines > $MAX_CARDS ) {
$promise->reject($ERR_TOO_MANY_CARDS);
next;
}
my @cards = map { s/^\s*(.*?)\s*$/$1/; $self->_parse_card($_) } @lines;
my $n_cards = 0;
for my $card (@cards) {
$n_cards += $card->{quantity};
}
if ( $n_cards > $MAX_CARDS ) {
$promise->reject($ERR_TOO_MANY_CARDS);
next;
}
eval {
@cards = map { $self->_fill_scryfall_id($_) } @cards;
};
if ($@) {
$promise->reject($@);
next;
}
$self->_get_cards_images( \@cards )->then(
sub ($images) {
my $real_number_of_cards = 0;
for my $card (@cards) {
$real_number_of_cards += $card->{quantity} * scalar
keys $images->{ $card->{scryfallId} }->%*;
}
if ( $real_number_of_cards > $MAX_CARDS ) {
$promise->reject($ERR_TOO_MANY_CARDS);
}
my $pdf = $self->_generate_pdf( \@cards, $images );
$promise->resolve($pdf);
}
)->catch(
sub ($err) {
$promise->reject($err);
}
);
}
return $promise;
}
sub _generate_pdf ( $self, $cards, $images ) {
my $number_image = 0;
my $pdf = PDF::API2->new;
my $page = $pdf->page;
$page->size('A4');
my @rectangle = $page->size;
my $width_paper = $rectangle[2];
my $height_paper = $rectangle[3];
my $n_pixels_per_cm_width = $width_paper / 21.0;
my $n_pixels_per_cm_height = $height_paper / 29.7;
for my $card (@$cards) {
my $images_card = $images->{ $card->{scryfallId} };
for my $image_kind ( keys %$images_card ) {
for ( my $i = 0 ; $i < $card->{quantity} ; $i++ ) {
if ( $number_image > 8 ) {
$number_image = 0;
$page = $pdf->page;
$page->size('A4');
}
my $margin_bottom = 25;
my $mtg_card_width = 6.35;
my $mtg_card_height = 8.89;
my $small_line_to_cut_better_size = 0.5;
open my $fh, '<', \$images_card->{$image_kind};
my $image = $pdf->image( $fh, format => 'jpeg' );
my $margin_left =
( $width_paper -
( $n_pixels_per_cm_width * $mtg_card_width * 3 ) -
( $small_line_to_cut_better_size * 2 ) ) / 2;
my $page_position_x = $number_image % 3;
my $page_position_y = abs( int( $number_image / 3 ) - 2 );
$page->object(
$image,
$margin_left +
$page_position_x * $mtg_card_width * $n_pixels_per_cm_width +
$small_line_to_cut_better_size * $page_position_x,
$margin_bottom +
$page_position_y * $mtg_card_height * $n_pixels_per_cm_height +
$small_line_to_cut_better_size * $page_position_y,
$n_pixels_per_cm_width * $mtg_card_width,
$n_pixels_per_cm_height * $mtg_card_height,
);
$number_image++;
}
}
}
return $pdf->to_string;
}
sub _get_cards_images ( $self, $cards ) {
my %card_images;
my @promises;
my $ua = $self->_ua;
for my $card (@$cards) {
my $scryfallId = $card->{scryfallId};
my ( $first, $second ) = $scryfallId =~ /^(.)(.)/;
my $url_front =
"https://cards.scryfall.io/normal/front/$first/$second/$scryfallId.jpg";
my $url_back =
"https://cards.scryfall.io/normal/back/$first/$second/$scryfallId.jpg";
my $promise_front = Mojo::Promise->new;
my $promise_back = Mojo::Promise->new;
$ua->get_p($url_front)->then(
sub ($res) {
my $content_type = $res->result->headers->content_type;
if ( $content_type ne 'image/jpeg' ) {
$self->last_invalid_card(
"@{[$card->{quantity}]} $scryfallId @{[$card->{type}]} @{[$card->{name}]} @{[$card->{set_code}]}"
);
say "FAIL: $url_front";
$promise_front->reject($ERR_UNABLE_TO_FIND_IMAGE);
return;
}
$card_images{$scryfallId}{front} = $res->result->body;
$promise_front->resolve;
}
)->catch(
sub ($err) {
$promise_front->reject($err);
}
);
$ua->get_p($url_back)->then(
sub ($res) {
my $content_type = $res->result->headers->content_type;
if ( $content_type ne 'image/jpeg' ) {
$promise_back->resolve;
return;
}
$card_images{$scryfallId}{back} = $res->result->body;
$promise_back->resolve;
}
)->catch(
sub ($err) {
$promise_back->reject($err);
}
);
push @promises, $promise_front;
push @promises, $promise_back;
}
my $final_promise = Mojo::Promise->new;
Mojo::Promise->all(@promises)->then(
sub {
$final_promise->resolve( \%card_images );
}
)->catch(
sub ($err) {
$final_promise->reject($err);
}
);
return $final_promise;
}
sub _fill_scryfall_id ( $self, $card ) {
if ( $card->{type} eq 'token' ) {
return $self->_fill_scryfall_id_token($card);
}
return $self->_fill_scryfall_id_card($card);
}
sub _fill_scryfall_id_card ( $self, $card ) {
my $db = $self->_db_all_printings;
my $query = <<'EOF';
select scryfallId
from cards
inner join cardIdentifiers
on cards.uuid = cardIdentifiers.uuid
where cards.name = ? and setCode = ? and number = ?;
EOF
my ( $name, $set_code, $number ) = $card->@{ 'name', 'set_code', 'number' };
my @args_query = ( $name, $set_code, $number );
my $result = $db->selectrow_hashref( $query, undef, @args_query );
if ( !defined $result ) {
$self->last_invalid_card(
"@{[$card->{quantity}]} @{[$card->{name}]} @{[$card->{set_code}]} @{[$card->{number}]}"
);
die $ERR_INVALID_CARD;
}
$card->{scryfallId} = $result->{scryfallId};
return { %$card, };
}
sub _fill_scryfall_id_token ( $self, $token ) {
my $db = $self->_db_all_printings;
my $query = <<'EOF';
SELECT scryfallId
FROM tokens
INNER JOIN tokenIdentifiers
ON tokens.uuid = tokenIdentifiers.uuid
WHERE tokens.name = ?
EOF
my @args = ( $token->{name} );
$query .= ' AND LOWER(tokens.setCode) = LOWER(?)'
if defined $token->{set_code};
push @args, $token->{set_code} if defined $token->{set_code};
$query .= ' LIMIT 1';
my $result = $db->selectrow_hashref( $query, undef, @args );
if ( !defined $result ) {
my $last_invalid_card = "@{[$token->{quantity}]} @{[$token->{name}]}";
$last_invalid_card .= " @{[$token->{set_code}]}"
if defined $token->{set_code};
$self->last_invalid_card($last_invalid_card);
die $ERR_INVALID_CARD;
}
$token->{scryfallId} = $result->{scryfallId};
return {%$token};
}
sub _filter_lines ( $self, $arg ) {
return 0 if $arg =~ /^\w+:\s*$/;
return 0 if $arg =~ /^\s*$/;
return 1;
}
sub _parse_token ( $self, $line ) {
my ( $quantity, $name, $set_code );
if (
!(
( $quantity, $name, $set_code ) =
/^\s*(\d+)?\s*(.*?)\s*(?:\[(.*?)\])?\s*$/
)
)
{
$self->last_invalid_card($line);
die $ERR_INVALID_CARD;
}
$quantity //= 1;
return {
quantity => $quantity,
name => $name,
set_code => $set_code,
type => 'token',
};
}
sub _parse_card ( $self, $line ) {
if ( $line !~ /\(/ ) {
return $self->_parse_token($line);
}
my ( $quantity, $name, $set_code, $number, $foil );
if (
!(
( $quantity, $name, $set_code, $number, $foil ) =
$line =~ /^\s*(\d+)\s+(.*?)\s*\((\w+)\)\s*(\S+)\s*(\*F\*)?$/
)
)
{
$self->last_invalid_card($line);
die $ERR_INVALID_CARD;
}
return {
number => $number,
name => $name,
set_code => $set_code,
quantity => $quantity,
foil => 0 + ( !!$foil ),
type => 'card',
};
}
1;