2023-05-08 01:14:36 +02:00
|
|
|
package BurguillosInfo::Preview;
|
|
|
|
|
|
|
|
use v5.36.0;
|
|
|
|
|
|
|
|
use strict;
|
|
|
|
use warnings;
|
|
|
|
|
|
|
|
use feature 'signatures';
|
|
|
|
|
|
|
|
use SVG;
|
|
|
|
use Path::Tiny;
|
|
|
|
|
|
|
|
use Const::Fast;
|
|
|
|
use Capture::Tiny qw/capture/;
|
|
|
|
use MIME::Base64;
|
2023-09-17 09:14:53 +02:00
|
|
|
use Digest::SHA qw/sha512_hex/;
|
2023-09-17 10:17:50 +02:00
|
|
|
use Encode;
|
2023-05-08 01:14:36 +02:00
|
|
|
|
|
|
|
const my $CURRENT_FILE => __FILE__;
|
|
|
|
const my $ROOT_PROJECT => path($CURRENT_FILE)->parent->parent->parent;
|
|
|
|
const my $PUBLIC_DIR => $ROOT_PROJECT->child('public');
|
2023-08-25 18:09:27 +02:00
|
|
|
const my $BURGUILLOS_LOGO => $PUBLIC_DIR->child('img/burguillos-new-logo.svg');
|
2023-05-08 01:14:36 +02:00
|
|
|
const my $SVG_WIDTH => 1200;
|
2023-09-17 06:51:54 +02:00
|
|
|
const my $SVG_HEIGHT => 630;
|
2023-09-17 01:23:37 +02:00
|
|
|
const my $SVG_EMBEDDED_IMAGE_MAX_WIDTH => 1200;
|
|
|
|
const my $SVG_EMBEDDED_IMAGE_MAX_HEIGHT => 400;
|
2023-05-08 01:14:36 +02:00
|
|
|
|
2023-09-17 06:43:45 +02:00
|
|
|
sub Generate (
|
|
|
|
$self, $title, $content,
|
|
|
|
$image_file = undef,
|
|
|
|
$image_bottom_preview = undef
|
|
|
|
)
|
|
|
|
{
|
2023-09-17 10:17:50 +02:00
|
|
|
my $sha512 = sha512_hex(Encode::encode('utf8', $title.$content.($image_file//'').($image_bottom_preview//'')));
|
2023-09-17 09:14:53 +02:00
|
|
|
my $cached_image = path("public/img/preview.$sha512.generated.png");
|
|
|
|
if (!-f $cached_image) {
|
|
|
|
my $dom = Mojo::DOM->new($content);
|
|
|
|
$content = $dom->all_text;
|
|
|
|
|
|
|
|
my $svg = $self->_GenerateSVGPreview(
|
|
|
|
$self->_DivideTextContentInLines($title, 62)->[0],
|
|
|
|
$self->_DivideTextContentInLines($content),
|
|
|
|
$image_file, $image_bottom_preview
|
|
|
|
);
|
|
|
|
$cached_image->spew_raw($self->_SVGToPNG($svg));
|
|
|
|
}
|
|
|
|
return $cached_image->slurp_raw;
|
2023-05-08 01:14:36 +02:00
|
|
|
}
|
|
|
|
|
2023-09-17 10:17:50 +02:00
|
|
|
sub WhatsappAlternativeGenerate($self, $title, $content, $image_file = undef, $image_bottom_preview = undef) {
|
|
|
|
my $complete_png = $self->Generate($title, $content, $image_file, $image_bottom_preview);
|
|
|
|
my ( $stdout, $stderr ) = capture {
|
|
|
|
open my $fh, '|-', 'convert', '/dev/stdin', '-resize', "@{[$SVG_WIDTH/2]}x@{[$SVG_HEIGHT/2]}", 'png:fd:1';
|
|
|
|
binmode $fh, ':raw';
|
|
|
|
print $fh $complete_png;
|
|
|
|
close $fh;
|
|
|
|
};
|
|
|
|
say STDERR $stderr;
|
|
|
|
return $stdout;
|
|
|
|
}
|
|
|
|
|
2023-09-17 06:43:45 +02:00
|
|
|
sub _ToPng ( $self, $image ) {
|
|
|
|
if ( $image =~ /\.\w+$/ ) {
|
2023-05-08 01:14:36 +02:00
|
|
|
my $new_image = $image =~ s/\.\w+$/.generated.png/r;
|
|
|
|
say $new_image;
|
2023-09-17 06:43:45 +02:00
|
|
|
if ( !-e $new_image ) {
|
2023-08-25 18:13:28 +02:00
|
|
|
system 'convert', '-background', 'none', "$image", "$new_image";
|
2023-05-08 01:14:36 +02:00
|
|
|
}
|
|
|
|
$image = $new_image;
|
|
|
|
}
|
|
|
|
return path($image);
|
|
|
|
}
|
|
|
|
|
2023-09-17 06:43:45 +02:00
|
|
|
sub _GenerateSVGPreviewHeaderBar ( $self, $svg, $group ) {
|
2023-05-08 01:14:36 +02:00
|
|
|
$group->rect(
|
|
|
|
x => 0,
|
|
|
|
y => 0,
|
|
|
|
width => 1200,
|
|
|
|
height => 50,
|
|
|
|
style => { fill => 'blueviolet' }
|
|
|
|
);
|
|
|
|
$group->rect(
|
|
|
|
x => 0,
|
|
|
|
y => 50,
|
|
|
|
width => 1200,
|
|
|
|
height => 627,
|
|
|
|
style => { fill => '#F8F8FF' }
|
|
|
|
);
|
|
|
|
|
2023-09-17 06:43:45 +02:00
|
|
|
my $burguillos_logo_png = path( $self->_ToPng($BURGUILLOS_LOGO) );
|
2023-05-08 01:14:36 +02:00
|
|
|
say $burguillos_logo_png;
|
2023-09-17 06:43:45 +02:00
|
|
|
say '' . $burguillos_logo_png;
|
2023-05-08 01:14:36 +02:00
|
|
|
$group->image(
|
|
|
|
x => 10,
|
|
|
|
y => 5,
|
|
|
|
width => 40,
|
|
|
|
height => 40,
|
|
|
|
-href => 'data:image/png;base64,'
|
|
|
|
. encode_base64( $burguillos_logo_png->slurp )
|
|
|
|
);
|
|
|
|
$group->text(
|
|
|
|
x => 60,
|
|
|
|
y => 40,
|
|
|
|
style => { 'font-size' => 50, fill => '#f2eb8c' }
|
|
|
|
)->cdata('Burguillos.info');
|
|
|
|
}
|
|
|
|
|
2023-09-17 06:43:45 +02:00
|
|
|
sub _GenerateSVGPreview ( $self, $title, $content, $image_file,
|
|
|
|
$image_bottom_preview )
|
|
|
|
{
|
2023-05-08 01:14:36 +02:00
|
|
|
my @content = @$content;
|
|
|
|
my $svg = SVG->new( width => $SVG_WIDTH, height => $SVG_HEIGHT );
|
|
|
|
|
|
|
|
my $group = $svg->group(
|
|
|
|
id => 'group',
|
|
|
|
style => {
|
|
|
|
font => 'Arial',
|
|
|
|
'font-size' => 30,
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2023-09-17 06:43:45 +02:00
|
|
|
$self->_GenerateSVGPreviewHeaderBar( $svg, $group );
|
2023-05-08 01:14:36 +02:00
|
|
|
|
|
|
|
my $new_y;
|
|
|
|
|
|
|
|
if ( defined $image_file ) {
|
2023-09-17 06:43:45 +02:00
|
|
|
$new_y = $self->_AttachImageSVG( $svg, $group, $image_file,
|
|
|
|
$image_bottom_preview );
|
2023-05-08 01:14:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
$new_y //= 100;
|
|
|
|
$group->text(
|
|
|
|
x => 10,
|
|
|
|
y => $new_y,
|
2023-09-17 01:51:22 +02:00
|
|
|
style => { 'font-size' => 42 }
|
2023-05-08 01:14:36 +02:00
|
|
|
)->cdata($title);
|
|
|
|
|
|
|
|
my $n = 0;
|
|
|
|
for my $line (@content) {
|
2023-09-17 06:43:45 +02:00
|
|
|
next if $line =~ /^\s*$/;
|
2023-05-08 01:14:36 +02:00
|
|
|
$group->text(
|
|
|
|
x => 10,
|
|
|
|
y => $new_y + 40 + ( 30 * $n ),
|
2023-09-17 01:51:22 +02:00
|
|
|
style => { 'font-size' => 32 }
|
2023-05-08 01:14:36 +02:00
|
|
|
)->cdata($line);
|
|
|
|
$n++;
|
2023-09-17 06:43:45 +02:00
|
|
|
last if $n > 2;
|
2023-05-08 01:14:36 +02:00
|
|
|
}
|
|
|
|
return $svg->xmlify;
|
|
|
|
}
|
|
|
|
|
2023-09-17 06:43:45 +02:00
|
|
|
sub _SVGToPNG ( $self, $svg ) {
|
2023-05-08 01:14:36 +02:00
|
|
|
my ( $stdout, $stderr ) = capture {
|
|
|
|
open my $fh, '|-', qw{convert /dev/stdin png:fd:1};
|
2023-09-17 10:17:50 +02:00
|
|
|
binmode $fh, ':utf8';
|
2023-05-08 01:14:36 +02:00
|
|
|
print $fh $svg;
|
|
|
|
close $fh;
|
|
|
|
};
|
|
|
|
say STDERR $stderr;
|
|
|
|
return $stdout;
|
|
|
|
}
|
|
|
|
|
2023-09-17 06:43:45 +02:00
|
|
|
sub _DivideTextContentInLines ( $self, $content, $n_chars_per_line = 79 ) {
|
2023-09-17 06:24:53 +02:00
|
|
|
$content =~ s/(\s)\s+/$1/g;
|
2023-05-08 01:14:36 +02:00
|
|
|
my @content_divided_in_lines = split /\n/, $content;
|
|
|
|
my @new_content;
|
|
|
|
|
|
|
|
for my $line (@content_divided_in_lines) {
|
|
|
|
if ( length($line) <= $n_chars_per_line ) {
|
|
|
|
push @new_content, $line;
|
|
|
|
next;
|
|
|
|
}
|
|
|
|
my $last_word = '';
|
|
|
|
while ( $line =~ /(.{1,${n_chars_per_line}})/g ) {
|
|
|
|
my $new_line = $last_word . $1;
|
|
|
|
$new_line =~ s/(\S*)$//;
|
|
|
|
$last_word = $1;
|
|
|
|
push @new_content, $new_line;
|
|
|
|
}
|
|
|
|
if ($last_word) {
|
|
|
|
$new_content[$#new_content] .= $last_word;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return \@new_content;
|
|
|
|
}
|
2023-09-17 01:23:37 +02:00
|
|
|
|
2023-09-17 06:43:45 +02:00
|
|
|
sub _AttachImageSVG ( $self, $svg, $group, $image_file, $image_bottom_preview )
|
|
|
|
{
|
2023-05-08 01:14:36 +02:00
|
|
|
$image_file = $PUBLIC_DIR->child( './' . $image_file );
|
2023-09-17 06:43:45 +02:00
|
|
|
$image_file = path( $self->_ToPng($image_file) );
|
2023-05-08 01:14:36 +02:00
|
|
|
my ( $stdout, $stderr, $error ) = capture {
|
|
|
|
system qw/identify -format "%wx%h"/, $image_file;
|
|
|
|
};
|
|
|
|
if ($error) {
|
|
|
|
warn "$image_file not recognized by identify.";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
my ( $width, $height ) = $stdout =~ /^"(\d+)x(\d+)"$/;
|
2023-09-17 06:43:45 +02:00
|
|
|
$height = int( $height * 1200 / $width );
|
|
|
|
$width = 1200;
|
|
|
|
my $height_complete_image = ( 1200 / $width ) * $height;
|
2023-09-17 01:23:37 +02:00
|
|
|
|
2023-05-08 01:14:36 +02:00
|
|
|
if ( $height > $SVG_EMBEDDED_IMAGE_MAX_HEIGHT ) {
|
|
|
|
$width /= $height / $SVG_EMBEDDED_IMAGE_MAX_HEIGHT;
|
|
|
|
$width = int($width);
|
|
|
|
$height = $SVG_EMBEDDED_IMAGE_MAX_HEIGHT;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( $width > $SVG_EMBEDDED_IMAGE_MAX_WIDTH ) {
|
|
|
|
$height /= $width / $SVG_EMBEDDED_IMAGE_MAX_WIDTH;
|
|
|
|
$height = int($height);
|
|
|
|
$width = $SVG_EMBEDDED_IMAGE_MAX_WIDTH;
|
|
|
|
}
|
|
|
|
|
2023-09-17 06:43:45 +02:00
|
|
|
my $defs = $svg->defs();
|
|
|
|
my $clip_path = $defs->clipPath( id => 'cut-top' );
|
|
|
|
$clip_path->rect( x => 0, y => 50, width => 1200, height => $height );
|
2023-09-17 01:23:37 +02:00
|
|
|
|
2023-09-17 06:43:45 +02:00
|
|
|
my $x = 0;
|
|
|
|
my $y_image = 50 - $height_complete_image + $height;
|
|
|
|
if ( defined $image_bottom_preview
|
|
|
|
&& $height_complete_image > $SVG_EMBEDDED_IMAGE_MAX_HEIGHT )
|
|
|
|
{
|
|
|
|
$y_image += $height_complete_image - $image_bottom_preview;
|
2023-09-17 01:39:11 +02:00
|
|
|
}
|
2023-09-17 01:23:37 +02:00
|
|
|
my $y = 50;
|
2023-05-08 01:14:36 +02:00
|
|
|
my ($output) = capture {
|
|
|
|
system qw/file --mime-type/, $image_file;
|
|
|
|
};
|
|
|
|
my ($format) = $output =~ /(\S+)$/;
|
2023-09-17 01:23:37 +02:00
|
|
|
$group->image(
|
|
|
|
x => 0,
|
|
|
|
y => $y_image,
|
|
|
|
width => $SVG_WIDTH,
|
|
|
|
height => $height_complete_image,
|
|
|
|
-href => "data:$format;base64," . encode_base64( $image_file->slurp ),
|
2023-09-17 06:43:45 +02:00
|
|
|
'clip-path' => 'url(#cut-top)',
|
2023-09-17 01:23:37 +02:00
|
|
|
);
|
|
|
|
$group->rect(
|
|
|
|
x => 0,
|
2023-09-17 06:43:45 +02:00
|
|
|
y => $y + $height,
|
2023-09-17 01:23:37 +02:00
|
|
|
width => $SVG_WIDTH,
|
|
|
|
height => $SVG_HEIGHT,
|
2023-09-17 06:43:45 +02:00
|
|
|
style => { fill => 'azure' },
|
2023-05-08 01:14:36 +02:00
|
|
|
);
|
|
|
|
return $y + $height + 50;
|
|
|
|
}
|
|
|
|
|
|
|
|
1;
|