Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Waste] Limit image size on Echo backed Bulky Collection reports. #4939

Merged
merged 1 commit into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
40 changes: 40 additions & 0 deletions perllib/FixMyStreet/App/Model/PhotoSet.pm
Original file line number Diff line number Diff line change
Expand Up @@ -416,4 +416,44 @@ sub redact_image {
return $new_set;
}

# Shrinks any images over the given size until they are small enough.
# First tries shrinking to the given percentage of the original size.
# If this isn't small enough, next tries shrinking the original to the given percentage squared,
# and so on.
# E.g. for 90% it would next try 81%, then 72% etc.
# Returns the new photoset and a bool indicating if any images were shrunk.
sub shrink_all_to_size {
my ($self, $size_bytes, $resize_percent) = @_;

my $shrunk = 0;
my @images = $self->all_ids;
foreach my $i (0.. $#images) {
my $original_blob = $self->get_raw_image($i)->{data};

if (length $original_blob <= $size_bytes) {
next;
}

$shrunk = 1;

my $percent = $resize_percent;
my $shrunk_blob;
do {
$shrunk_blob = FixMyStreet::ImageMagick->new(blob => $original_blob)
->shrink_to_percentage($percent)
->as_blob;
$percent = $percent * $resize_percent;
} while (length $shrunk_blob > $size_bytes);

$images[$i] = $shrunk_blob;
}

my $new_set = (ref $self)->new({
data_items => \@images,
object => $self->object,
});
$self->delete_cached();
return ($new_set, $shrunk);
}

1;
6 changes: 6 additions & 0 deletions perllib/FixMyStreet/ImageMagick.pm
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ sub shrink {
return $self->strip;
}

# Shrinks a picture to the specified percentage of the original, but keeping in proportion.
sub shrink_to_percentage {
my ($self, $percentage) = @_;
return $self->image->Scale(geometry => "$percentage%");
}

# Shrinks a picture to a given dimension (defaults to 90x60(, cropping so that
# it is exactly that.
sub crop {
Expand Down
28 changes: 28 additions & 0 deletions perllib/FixMyStreet/Roles/CobrandEcho.pm
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@ use v5.14;
use warnings;
use DateTime;
use DateTime::Format::Strptime;
use List::Util qw(min);
use Moo::Role;
use POSIX qw(floor);
use Sort::Key::Natural qw(natkeysort_inplace);
use FixMyStreet::DateRange;
use FixMyStreet::DB;
use FixMyStreet::WorkingDays;
use Open311::GetServiceRequestUpdates;

with 'FixMyStreet::Roles::EnforcePhotoSizeOpen311PreSend';

requires 'waste_containers';
requires 'waste_service_to_containers';
requires 'waste_quantity_max';
Expand Down Expand Up @@ -1217,4 +1221,28 @@ sub send_bulky_payment_echo_update_failed {
}
}

sub per_photo_size_limit_for_report_in_bytes {
my ($self, $report, $image_count) = @_;

# We only need to check bulky collections at present.
return 0 unless $report->cobrand_data eq 'waste' && $report->contact->category eq 'Bulky collection';

my $cfg = FixMyStreet->config('COBRAND_FEATURES');
return 0 unless $cfg;

my $echo_cfg = $cfg->{'echo'};
return 0 unless $echo_cfg;

my $max_size_per_image = $echo_cfg->{'max_size_per_image_bytes'};
my $max_size_images_total = $echo_cfg->{'max_size_image_total_bytes'};

return 0 unless $max_size_per_image || $max_size_images_total;
return $max_size_per_image if !$max_size_images_total;

my $max_size_per_image_from_total = floor($max_size_images_total / $image_count);
return $max_size_per_image_from_total if !$max_size_per_image;

return min($max_size_per_image, $max_size_per_image_from_total);
};

1;
57 changes: 57 additions & 0 deletions perllib/FixMyStreet/Roles/EnforcePhotoSizeOpen311PreSend.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package FixMyStreet::Roles::EnforcePhotoSizeOpen311PreSend;
use Moo::Role;

=head1 NAME

FixMyStreet::Roles::EnforcePhotoSizeOpen311PreSend - limit report photo sizes on open311 pre-send

=head1 SYNOPSIS

Applied to a cobrand class to shrink any images larger than a given size as an open311 pre-send action.

Oversized images are repeatedly shrunk until they conform.

A 'photo_size_limit_applied_<bytes>' metadata flag is set on the report to indicate it has been processed
and prevent reprocessing.

=cut

=head1 REQUIRED METHODS

=cut

=head2 per_photo_size_limit_for_report_in_bytes

Takes the report and the number of images.
Returns the max number of bytes for each photo on the report.
0 indicates no max to apply.

=cut

requires 'per_photo_size_limit_for_report_in_bytes';

sub open311_pre_send { }
dracos marked this conversation as resolved.
Show resolved Hide resolved

after open311_pre_send => sub {
my ($self, $report, $open311) = @_;
my $photoset = $report->get_photoset;
return unless $photoset->num_images > 0;

my $limit = $self->per_photo_size_limit_for_report_in_bytes($report, $photoset->num_images);
return unless $limit > 0;

my $limit_applied_flag = "photo_size_limit_applied_" . $limit;
return if $report->get_extra_metadata($limit_applied_flag);

# Keep shrinking oversized images to 90% of their original size until they conform.
my ($new, $shrunk) = $photoset->shrink_all_to_size($limit, 90);

if ($shrunk) {
$report->update({ photo => $new->data });
}

$report->set_extra_metadata( $limit_applied_flag => 1 );
$report->update;
};

1;
73 changes: 73 additions & 0 deletions t/roles/cobrandecho.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
use strict;
use warnings;

use FixMyStreet;
BEGIN { FixMyStreet->test_mode(1); }

package FixMyStreet::Cobrand::CobrandEchoTest;
use parent 'FixMyStreet::Cobrand::UKCouncils';

use Moo;
with 'FixMyStreet::Roles::CobrandEcho';

sub waste_bulky_missed_blocked_codes {}
sub waste_containers {}
sub waste_service_to_containers {}
sub waste_quantity_max {}
sub waste_extra_service_info {}

sub garden_subscription_event_id {}
sub garden_echo_container_name {}
sub garden_container_data_extract {}
sub garden_due_days {}
sub garden_service_id {}

package main;
use Test::More;
use FixMyStreet;
use FixMyStreet::TestMech;

my $mech = FixMyStreet::TestMech->new;
my $cobrand = FixMyStreet::Cobrand::CobrandEchoTest->new;
my $body = $mech->create_body_ok(1, 'body');
my $bulky_contact = $mech->create_contact_ok(
body_id => $body->id,
category => 'Bulky collection',
email => '',
);
my $non_bulky_contact = $mech->create_contact_ok(
body_id => $body->id,
category => 'Non bulky',
email => '',
);
my ($bulky_report) = $mech->create_problems_for_body(1, $body->id, 'report', {
category => $bulky_contact->category,
cobrand_data => 'waste',
});

FixMyStreet::override_config {
COBRAND_FEATURES => {
echo => {
max_size_per_image_bytes => 100,
max_size_image_total_bytes => 201,
},
},
}, sub {
subtest 'Image size limit is applied correctly' => sub {
subtest 'Image size calculate from config for bulky reports' => sub {
is $cobrand->per_photo_size_limit_for_report_in_bytes($bulky_report, 1), 100;
is $cobrand->per_photo_size_limit_for_report_in_bytes($bulky_report, 2), 100;
is $cobrand->per_photo_size_limit_for_report_in_bytes($bulky_report, 4), 50;
};

subtest 'No size limit for non-bulky reports' => sub {
my ($non_bulky_report) = $mech->create_problems_for_body(1, $body->id, 'report', {
category => $non_bulky_contact->category,
cobrand_data => 'waste',
});
is $cobrand->per_photo_size_limit_for_report_in_bytes($non_bulky_report, 1), 0;
};
};
};

done_testing;
107 changes: 107 additions & 0 deletions t/roles/enforcephotosizeopen311presend.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
use strict;
use warnings;

use FixMyStreet;
BEGIN { FixMyStreet->test_mode(1); }

package FixMyStreet::Cobrand::NoSizeLimit;
use parent 'FixMyStreet::Cobrand::Default';
use Moo;
with 'FixMyStreet::Roles::EnforcePhotoSizeOpen311PreSend';
sub per_photo_size_limit_for_report_in_bytes { 0 }

package FixMyStreet::Cobrand::SizeLimit;
use parent 'FixMyStreet::Cobrand::Default';
use Moo;
with 'FixMyStreet::Roles::EnforcePhotoSizeOpen311PreSend';
sub per_photo_size_limit_for_report_in_bytes { 100 }

package main;
use Test::MockModule;
use Test::More;
use FixMyStreet::Script::Reports;
use FixMyStreet::TestMech;

my $photoset = Test::MockModule->new('FixMyStreet::App::Model::PhotoSet');
my @shrink_all_to_size_arguments;

$photoset->mock('shrink_all_to_size', sub {
my ($self, $size_bytes, $resize_percent) = @_;
push @shrink_all_to_size_arguments, [$size_bytes, $resize_percent];
return ($self, 1);
});

my $mech = FixMyStreet::TestMech->new;
my $mock_open311 = Test::MockModule->new('FixMyStreet::SendReport::Open311');

my $body = $mech->create_body_ok(1, 'Photo Enforce Size Limit Test Body', {
endpoint => 'e',
api_key => 'key',
jurisdiction => 'j',
send_method => 'Open311',
});

my $contact = $mech->create_contact_ok(
body_id => $body->id,
category => 'Photo Size Limit Enforced Category',
email => '',
);

my $tag_name = 'photo_size_limit_applied_100';

FixMyStreet::override_config {
ALLOWED_COBRANDS => ['nosizelimit', 'sizelimit'],
STAGING_FLAGS => { send_reports => 1 },
}, sub {
subtest 'Skips report if there are no photos' => sub {
my ($report) = $mech->create_problems_for_body(1, $body->id, 'report', {
cobrand => 'sizelimit',
category => $contact->category,
photo => undef,
});
FixMyStreet::Script::Reports::send();
$report->discard_changes;
is scalar @shrink_all_to_size_arguments, 0, "shrink_all_to_size shouldn't be called";
is $report->get_extra_metadata($tag_name), undef, "tag shouldn't be set";
};

subtest 'Skips report if no limit is returned' => sub {
my ($report) = $mech->create_problems_for_body(1, $body->id, 'report', {
cobrand => 'nosizelimit',
category => $contact->category,
});
FixMyStreet::Script::Reports::send();
$report->discard_changes;
is scalar @shrink_all_to_size_arguments, 0, "shrink_all_to_size shouldn't be called";
is $report->get_extra_metadata($tag_name), undef, "tag shouldn't be set";
};

subtest 'Skips report if tag says limit already applied' => sub {
my ($report) = $mech->create_problems_for_body(1, $body->id, 'report', {
cobrand => 'sizelimit',
category => $contact->category,
});
$report->set_extra_metadata($tag_name => 1);
$report->update;
FixMyStreet::Script::Reports::send();
$report->discard_changes;
is scalar @shrink_all_to_size_arguments, 0, "shrink_all_to_size shouldn't be called";
is $report->get_extra_metadata($tag_name), 1, "tag shouldn't be cleared";
};

subtest 'Applies shrink and sets tag' => sub {
my ($report) = $mech->create_problems_for_body(1, $body->id, 'report', {
cobrand => 'sizelimit',
category => $contact->category,
});
FixMyStreet::Script::Reports::send();
$report->discard_changes;
is scalar @shrink_all_to_size_arguments, 1, "shrink_all_to_size should have been called";
my ($size_limit, $percent) = @{$shrink_all_to_size_arguments[0]};
is $size_limit, 100, "size limit should be 100 bytes";
is $percent, 90, "resize percent should be 90%";
is $report->get_extra_metadata($tag_name), 1, "tag should be set";
};
};

done_testing;