Skip to content

Commit

Permalink
[Waste] Limit image size on Echo backed Bulky Collection reports.
Browse files Browse the repository at this point in the history
  • Loading branch information
neprune committed May 7, 2024
1 parent 3adcce8 commit 4afb9b8
Show file tree
Hide file tree
Showing 6 changed files with 311 additions and 0 deletions.
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 { }

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;

0 comments on commit 4afb9b8

Please sign in to comment.