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 { return Mojo::UserAgent->new; } 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;