Finishing image storage in .exd and adding save and open capability.

This commit is contained in:
Sergiotarxz 2024-10-16 19:57:50 +02:00
parent 3285ac3987
commit de0bc9a655
4 changed files with 432 additions and 15 deletions

View File

@ -10,8 +10,12 @@ use Moo;
use Path::Tiny;
use Archive::Zip;
use Exd::Utils;
use Digest::SHA qw/sha256_hex/;
use Exd::FileFormat::DB;
has dir => ( is => 'rw' );
sub new_tmp($class) {
@ -22,21 +26,85 @@ sub new_tmp($class) {
has _images_dir => ( is => 'lazy' );
sub _build__images_dir($self) {
my $dir = $self->dir;
my $dir = $self->dir;
my $images = $dir->child('images');
$images->mkpath;
return $images;
}
sub add_png_image( $self, $file_contents ) {
sub add_png_image( $self, $file_contents, $label = '' ) {
my $sha_image = sha256_hex($file_contents);
$self->_images_dir->child("$sha_image.png")->spew_raw($file_contents);
my $file = $self->_images_dir->child("$sha_image.png");
$file->spew_raw($file_contents);
if ( defined $label && '' . $label ) {
$self->_register_label( $label, $sha_image );
}
}
sub _image_hashes($self) {
my $glob = ( '' . $self->_images_dir ) . '/*.png';
my @images = glob( $glob );
@images = map {
my $image = path($_);
$image = $image->basename;
$image =~ s/\.png$//r;
} @images;
return \@images;
}
sub image_hashes_to_label($self) {
my %images;
for my $image_hash ( $self->_image_hashes->@* ) {
my $dbh = $self->_dbh;
my $hashes = $dbh->selectall_arrayref(
'SELECT label FROM label_to_image_hash WHERE hash = ?',
{Slice => {}}, $image_hash );
$images{$image_hash} = [ sort { $a cmp $b } map { $_->{label} } @$hashes ];
}
return \%images;
}
sub _dbh($self) {
return Exd::FileFormat::DB->connect( '' . $self->dir->child('db.sqlite3') );
}
sub _register_label( $self, $label, $sha_image ) {
my $dbh = $self->_dbh;
eval {
$dbh->do(
'INSERT INTO label_to_image_hash (label, hash) VALUES (?, ?);',
{}, $label, $sha_image );
};
if ($@) {
warn $@;
}
}
sub get_image_gd_from_label ( $self, $label ) {
return $self->get_image_gd( $self->_get_hash_from_label($label) );
}
sub _get_hash_from_label($self, $label) {
my $dbh = $self->_dbh;
my $row = $dbh->selectrow_hashref(
'SELECT hash FROM label_to_image_hash WHERE label = ?',
{}, $label );
die 'No such image in file' if !defined $row || !defined $row->{hash};
return $row->{hash};
}
sub get_image_from_label ( $self, $label ) {
return $self->get_image( $self->_get_hash_from_label($label) );
}
sub get_image( $self, $sha ) {
return $self->_images_dir->child("$sha.png");
}
sub get_image_gd ( $self, $sha ) {
return Exd::Utils::get_gd_image( $self->get_image($sha) );
}
sub get_script($self) {
return $self->dir->child('script.pl')->slurp_raw;
}
@ -51,7 +119,7 @@ sub to_zip($self) {
sub( $path, $state ) {
return if $path->is_dir;
my $path_relative = $path->relative( $self->dir );
$zip->addFile( $path, $path_relative );
$zip->addFile( ''.$path, ''.$path_relative );
},
{ recurse => 1 }
);
@ -61,7 +129,7 @@ sub to_zip($self) {
sub from_zip_file( $class, $zip_file ) {
my $tempdir = Path::Tiny->tempdir();
my $zip = Archive::Zip->new($zip_file);
$zip->extractTree( '.', $tempdir );
$zip->extractTree( '', $tempdir );
return $class->new( dir => $tempdir );
}
@ -74,7 +142,7 @@ sub execute( $self, $printer ) {
if ($@) {
die $@;
}
$sub->($self, $printer);
$sub->( $self, $printer );
}
sub save( $self, $output_file ) {

95
lib/Exd/FileFormat/DB.pm Normal file
View File

@ -0,0 +1,95 @@
package Exd::FileFormat::DB;
use v5.40.0;
use strict;
use warnings;
use utf8;
use DBI;
use Path::Tiny;
use Exd::FileFormat::DB::Migrations;
my $dbh;
sub connect {
if ( defined $dbh ) {
return $dbh;
}
my $class = shift or die 'Missing arg class';
my $database = shift or die 'Missing arg database file';
$dbh = DBI->connect(
"dbi:SQLite:dbname=$database",
,
undef, undef,
{
RaiseError => 1,
},
);
$class->_migrate($dbh);
return $dbh;
}
sub _migrate {
my $class = shift;
my $dbh = shift;
local $dbh->{RaiseError} = 0;
local $dbh->{PrintError} = 0;
my @migrations = Exd::FileFormat::DB::Migrations::MIGRATIONS();
if ( $class->get_current_migration($dbh) > @migrations ) {
warn "Something happened there, wrong migration number.";
}
if ( $class->get_current_migration($dbh) >= @migrations ) {
say STDERR "Migrations already applied.";
return;
}
$class->_apply_migrations( $dbh, \@migrations );
}
sub _apply_migrations {
my $class = shift;
my $dbh = shift;
my $migrations = shift;
for (
my $i = $class->get_current_migration($dbh) ;
$i < @$migrations ;
$i++
)
{
local $dbh->{RaiseError} = 1;
my $current_migration = $migrations->[$i];
my $migration_number = $i + 1;
$class->_apply_migration( $dbh, $current_migration, $migration_number );
}
}
sub _apply_migration {
my $class = shift;
my $dbh = shift;
my $current_migration = shift;
my $migration_number = shift;
{
if (ref $current_migration eq 'CODE') {
$current_migration->($dbh);
next;
}
$dbh->do($current_migration);
}
$dbh->do( <<'EOF', undef, 'current_migration', $migration_number );
INSERT INTO options
VALUES ($1, $2)
ON CONFLICT (name) DO
UPDATE SET value = $2;
EOF
}
sub get_current_migration {
my $class = shift;
my $dbh = shift;
my $result = $dbh->selectrow_hashref( <<'EOF', undef, 'current_migration' );
select value from options where name = ?;
EOF
return int( $result->{value} // 0 );
}
1;

View File

@ -0,0 +1,23 @@
package Exd::FileFormat::DB::Migrations;
use v5.40.0;
use strict;
use warnings;
use utf8;
use feature 'signatures';
sub MIGRATIONS {
return (
'CREATE TABLE options (
name TEXT PRIMARY KEY,
value TEXT
);',
'CREATE TABLE label_to_image_hash (
label TEXT NOT NULL PRIMARY KEY,
hash TEXT NOT NULL
);',
);
}
1;

View File

@ -38,6 +38,12 @@ Glib::Object::Introspection->setup(
package => 'Gdk',
);
Glib::Object::Introspection->setup(
basename => 'GdkWayland',
version => '4.0',
package => 'GdkWayland',
);
Glib::Object::Introspection->setup(
basename => 'GtkSource',
version => '5',
@ -66,7 +72,6 @@ has _file_format => (
is => 'rw',
default => sub {
my $tmp = Exd::FileFormat->new_tmp;
say $tmp->dir;
$tmp->set_script(<<'EOF');
sub ($exd, $printer) {
$printer->print_text(
@ -106,6 +111,11 @@ sub start($self) {
$self->_daemon_script_runner;
my $app = Gtk4::Application->new( 'me.sergiotarxz.Exd', 'default-flags' );
$self->_app($app);
$app->signal_connect(
startup => sub {
$self->_startup;
}
);
$app->signal_connect(
activate => sub {
$self->_activate;
@ -114,6 +124,54 @@ sub start($self) {
$app->run;
}
sub _save_action($self) {
my $file_format = $self->_file_format;
my $dialog = Gtk4::FileDialog->new;
$dialog->set_initial_name('project.exd');
$dialog->save($self->window, undef, sub ($source, $res, $data) {
eval {
my $file = $dialog->save_finish($res);
$file = $file->get_path;
my $zip = $file_format->to_zip;
$zip->writeToFileNamed(''.$file);
};
});
}
sub _open_action($self) {
my $file_format = $self->_file_format;
my $dialog = Gtk4::FileDialog->new;
$dialog->set_initial_name('project.exd');
$dialog->open($self->window, undef, sub ($source, $res, $data) {
eval {
my $file = $dialog->open_finish($res);
$file = $file->get_path;
$self->_file_format(Exd::FileFormat->from_zip_file($file));
$self->_update_editor_buffer;
};
});
}
sub _startup($self) {
my $app = $self->_app;
my $menu_model = Glib::IO::Menu->new;
my $file_menu = Glib::IO::Menu->new;
my $open_action = Glib::IO::SimpleAction->new('open', undef);
my $save_action = Glib::IO::SimpleAction->new('save', undef);
$open_action->signal_connect('activate', sub {
$self->_open_action;
});
$save_action->signal_connect('activate', sub {
$self->_save_action;
});
$app->add_action($open_action);
$app->add_action($save_action);
$file_menu->append('Open', 'app.open');
$file_menu->append('Save', 'app.save');
$menu_model->append_submenu('File', $file_menu);
$app->set_menubar($menu_model);
}
sub _generate_preview_file( $self, $verbose = 1 ) {
$self->_preview_file(1) if -f $self->_preview_file;
my $device =
@ -137,7 +195,7 @@ sub _run_script( $self, $device = undef, $verbose = 1 ) {
my $fh = $self->_write_to_script;
print $fh JSON::to_json(
{
exd_dir => ''.$self->_file_format->dir,
exd_dir => '' . $self->_file_format->dir,
device => $device->serialize,
verbose => $verbose,
}
@ -179,8 +237,8 @@ sub _daemon_script_runner($self) {
my $new_pid = fork;
if ( !$new_pid ) {
eval {
$self->_on_run_script( path($exd_dir), $device, $write_to_parent,
$verbose );
$self->_on_run_script( path($exd_dir), $device,
$write_to_parent, $verbose );
};
if ($@) {
warn $@;
@ -219,12 +277,10 @@ sub _on_run_script( $self, $exd_dir, $device, $write_to_parent, $verbose = 1 ) {
my $printer = Exd::Printer->new( device => $device );
local $| = 1;
my $exd = Exd::FileFormat->new(dir => $exd_dir);
my $exd = Exd::FileFormat->new( dir => $exd_dir );
my ( $stdout, $stderr, $exit ) = capture {
eval {
$exd->execute($printer);
};
eval { $exd->execute($printer); };
if ($@) {
if ($verbose) {
print $write_to_parent $@ . "\n";
@ -279,6 +335,12 @@ sub _on_preview($self) {
}
}
sub _update_editor_buffer($self) {
my $editor = $self->_editor;
my $buffer = $editor->get_buffer();
$buffer->set_text( $self->_file_format->get_script, -1 );
}
sub _populate_editor( $self, $box_editor_preview ) {
my $editor = Gtk4::Source::View->new;
$self->_editor($editor);
@ -291,7 +353,7 @@ sub _populate_editor( $self, $box_editor_preview ) {
$editor->set_smart_backspace(1);
$editor->set_show_line_numbers(1);
my $buffer = $editor->get_buffer();
$buffer->set_text( $self->_file_format->get_script, -1 );
$self->_update_editor_buffer;
$buffer->set_language(
Gtk4::Source::LanguageManager::get_default()->get_language('perl') );
$buffer->set_highlight_syntax(1);
@ -337,6 +399,166 @@ sub _populate_preview( $self, $box_editor_preview ) {
$self->_generate_preview_file;
}
sub _open_gallery($self) {
my $window = Gtk4::Window->new;
$window->set_transient_for( $self->window );
$window->set_default_size( 600, 600 );
$window->set_title('Image gallery');
my $box = Gtk4::Box->new( 'vertical', 1 );
my $box_upper_menu = Gtk4::Box->new( 'horizontal', 10 );
my $add_image = Gtk4::Button->new_with_label('Add PNG image');
$add_image->set_halign('start');
my $label_entry_label =
Gtk4::Label->new('(Recommended) Put a label to the new image:');
my $entry_label = Gtk4::Entry->new;
$box_upper_menu->append($label_entry_label);
$box_upper_menu->append($entry_label);
my $images_scroll = Gtk4::ScrolledWindow->new;
$add_image->signal_connect(
'clicked',
sub {
my $dialog = Gtk4::FileDialog->new;
$dialog->open(
$window, undef,
sub( $source, $res, $data ) {
eval {
my $file = $dialog->open_finish($res);
my $buffer = $entry_label->get_buffer;
my $label = $buffer->get_text;
$file = $file->get_path;
if ( $file !~ /\.png$/ ) {
$self->write_to_parent->say("$file is not png.");
$self->write_to_parent->flush;
return;
}
eval {
$self->_file_format->add_png_image(
path($file)->slurp_raw, $label );
$buffer->set_text( '', -1 );
$self->_update_gallery_images( $window,
$images_scroll );
};
if ($@) {
warn $@;
$self->write_to_parent->print($@);
$self->write_to_parent->flush;
}
};
if ($@) {
warn $@;
return;
}
}
);
}
);
$self->_update_gallery_images( $window, $images_scroll );
$box->append($add_image);
$box->append($box_upper_menu);
$box->append($images_scroll);
$images_scroll->set_vexpand(1);
$window->set_child($box);
$window->present;
}
sub _update_gallery_images( $self, $window, $images_scroll ) {
my %image_hashes_to_label = $self->_file_format->image_hashes_to_label->%*;
my $gallery_box = Gtk4::Box->new( 'vertical', 10 );
my $i = 0;
my $row_image;
for my $hash ( sort { $a cmp $b } keys %image_hashes_to_label ) {
my @labels = $image_hashes_to_label{$hash}->@*;
if ( scalar @labels == 0 ) {
$row_image = $self->_get_row_image( $i, $gallery_box, $row_image );
my $picture = $self->_create_gallery_image($hash);
my $box_image = Gtk4::Box->new( 'vertical', 1 );
$box_image->set_property( 'width-request', 180 );
my $copy = Gtk4::Button->new_with_label('Copy to clipboard');
my $copy_gd = Gtk4::Button->new_with_label('Copy GD to clipboard');
$copy->signal_connect(
'clicked',
sub {
$self->_set_clipboard("\$exd->get_image('$hash')");
$window->close;
}
);
$copy_gd->signal_connect(
'clicked',
sub {
$self->_set_clipboard("\$exd->get_image_gd('$hash')");
$window->close;
}
);
$box_image->append($picture);
my $scroll = Gtk4::ScrolledWindow->new;
$scroll->set_child( Gtk4::Label->new($hash) );
$box_image->append($scroll);
$box_image->append($copy);
$box_image->append($copy_gd);
$row_image->append($box_image);
$i++;
next;
}
for my $label (@labels) {
$row_image = $self->_get_row_image( $i, $gallery_box, $row_image );
my $picture = $self->_create_gallery_image($hash);
my $copy = Gtk4::Button->new_with_label('Copy to clipboard');
my $copy_gd = Gtk4::Button->new_with_label('Copy GD to clipboard');
my $box_image = Gtk4::Box->new( 'vertical', 1 );
$box_image->set_property( 'width-request', 180 );
$copy->signal_connect(
'clicked',
sub {
$self->_set_clipboard("\$exd->get_image_from_label('$label')");
$window->close;
}
);
$copy_gd->signal_connect(
'clicked',
sub {
$self->_set_clipboard("\$exd->get_image_gd_from_label('$label')");
$window->close;
}
);
$box_image->append($picture);
my $scroll = Gtk4::ScrolledWindow->new;
$scroll->set_child( Gtk4::Label->new($label) );
$box_image->append($scroll);
$box_image->append($copy);
$box_image->append($copy_gd);
$row_image->append($box_image);
$i++;
}
}
$images_scroll->set_child($gallery_box);
}
sub _set_clipboard($self, $text) {
my $display = Gdk::Display::get_default();
my $clipboard = $display->get_clipboard;
my $wrapper =
Glib::Object::Introspection::GValueWrapper->new( 'Glib::String', $text );
$clipboard->set($wrapper);
}
sub _get_row_image( $self, $i, $gallery_box, $row_image ) {
if ( $i % 3 == 0 ) {
$row_image = Gtk4::Box->new( 'horizontal', 10 );
$gallery_box->append($row_image);
}
return $row_image;
}
sub _create_gallery_image( $self, $hash ) {
my $picture = Gtk4::Picture->new;
my $image_file = $self->_file_format->get_image($hash);
$picture->set_property( 'width-request', 180 );
$picture->set_property( 'height-request', 180 );
$picture->set_content_fit('fill');
$picture->set_filename($image_file);
return $picture;
}
sub _activate($self) {
Glib::Timeout->add(
1000,
@ -347,12 +569,20 @@ sub _activate($self) {
);
my $app = $self->_app;
my $win = Gtk4::ApplicationWindow->new($app);
$win->set_show_menubar(1);
$self->window($win);
my $box_vertical = Gtk4::Box->new( 'vertical', 10 );
my $execute_log = Gtk4::TextView->new;
my $run_button = Gtk4::Button->new_with_label('Run');
my $preview_button = Gtk4::Button->new_with_label('Preview');
my $select_printer = Gtk4::Button->new_with_label('Select Printer');
my $open_gallery = Gtk4::Button->new_with_label('Open image gallery');
$open_gallery->signal_connect(
'clicked',
sub {
$self->_open_gallery;
}
);
$select_printer->signal_connect(
'clicked',
sub {
@ -388,6 +618,7 @@ sub _activate($self) {
$box_buttons->append($run_button);
$box_buttons->append($preview_button);
$box_buttons->append($select_printer);
$box_buttons->append($open_gallery);
$self->_populate_editor_and_preview($box_vertical);
$box_vertical->append($box_buttons);
my $execute_log_window = Gtk4::ScrolledWindow->new;