309 lines
9.5 KiB
Perl
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;
|