File Coverage

blib/lib/IIIF/ImageAPI.pm
Criterion Covered Total %
statement 44 122 36.0
branch 0 46 0.0
condition 0 34 0.0
subroutine 15 25 60.0
pod 2 10 20.0
total 61 237 25.7


line stmt bran cond sub pod time code
1             package IIIF::ImageAPI;
2 1     1   501 use 5.014001;
  1         2  
3              
4             our $VERSION = "0.07";
5              
6 1     1   4 use parent 'Plack::Component';
  1         1  
  1         8  
7              
8 1     1   3574 use IIIF::Magick qw(info convert);
  1         2  
  1         49  
9 1     1   5 use File::Spec;
  1         2  
  1         18  
10 1     1   442 use Try::Tiny;
  1         1618  
  1         45  
11 1     1   418 use Plack::Request;
  1         48073  
  1         31  
12 1     1   366 use IIIF::Request;
  1         2  
  1         25  
13 1     1   586 use JSON::PP;
  1         11111  
  1         82  
14 1     1   9 use File::Temp qw(tempdir);
  1         8  
  1         53  
15 1     1   6 use Digest::MD5 qw(md5_hex);
  1         1  
  1         35  
16 1     1   407 use HTTP::Date;
  1         3100  
  1         50  
17              
18 1     1   366 use Plack::MIME;
  1         1740  
  1         45  
19             Plack::MIME->add_type( '.jp2', 'image/jp2' );
20             Plack::MIME->add_type( '.webp', 'image/webp' );
21              
22 1     1   6 use Cwd;
  1         2  
  1         91  
23 1     1   6 use Plack::Util;
  1         2  
  1         26  
24              
25             use Plack::Util::Accessor
26 1     1   4 qw(images base cache formats rights service canonical magick_args preferredFormats maxWidth maxHeight maxArea);
  1         1  
  1         6  
27              
28             our @FORMATS = qw(jpg png gif);
29              
30             sub new {
31 0     0 1   my $class = shift;
32 0           my $self = $class->SUPER::new(@_);
33              
34 0 0         $self->images('.') unless $self->images;
35 0 0         $self->formats( [qw{jpg png gif}] ) unless $self->formats;
36              
37 0           $self;
38             }
39              
40             sub call {
41 0     0 1   my ( $self, $env ) = @_;
42 0           my $req = Plack::Request->new($env);
43              
44 0 0         if ( $req->path_info =~ qr{^/([^/]+)/?(.*)$} ) {
45 0           my ( $identifier, $request ) = ( $1, $2 );
46 0 0         if ( my $file = $self->file($identifier) ) {
47 0   0       $file->{id} = ( $self->base // $req->base ) . $identifier;
48 0           return $self->response( $file, $request );
49             }
50             }
51              
52 0           return error_response( 404, "Not Found" );
53             }
54              
55             sub response {
56 0     0 0   my ( $self, $file, $local ) = @_;
57              
58             # Image Information Request
59 0 0         if ( $local eq '' ) {
    0          
60 0           return redirect( $file->{id} . "/info.json" );
61             }
62             elsif ( $local eq 'info.json' ) {
63 0           return $self->info_response($file);
64             }
65              
66             # allow abbreviated requests, redirect to full form
67 0           my $request = eval { IIIF::Request->new($local) };
  0            
68 0 0         if ($@) {
69 0           return error_response( 400, ( split( " at ", $@ ) )[0] );
70             }
71              
72             $request->{format} = $request->{format}
73 0   0       // ( $self->{preferredFormats} || [] )->[0] // $file->{format};
      0        
      0        
74              
75             return error_response( 400, "unsupported format" )
76 0 0         unless grep { $_ eq $request->{format} } @{ $self->formats };
  0            
  0            
77              
78 0 0 0       if ( !$self->canonical && "$request" ne $local ) {
79 0           return redirect( $file->{id} . "/$request" );
80             }
81              
82 0           my $info = info( $file->{path} );
83 0 0         my $norm = $request->canonical( $info->{width}, $info->{height}, %$self )
84             or return error_response();
85 0           $request = IIIF::Request->new($norm);
86 0           my $canonical = $file->{id} . "/$request";
87              
88 0 0 0       if ( $self->canonical && "$request" ne $local ) {
89 0           return redirect($canonical);
90             }
91              
92             # Image Request
93              
94             # directly serve unmodified image
95 0 0 0       if ( $request->is_default && $request->{format} eq $file->{format} ) {
96 0           return image_response( $file->{path}, $canonical );
97             }
98              
99             # cache image segment and directly serve if found
100 0   0       my $cache = $self->cache // $self->cache( tempdir( CLEANUP => 1 ) );
101 0           my $cache_file = File::Spec->catfile( $cache,
102             md5_hex("$request") . ".$request->{format}" );
103              
104 0 0         if ( -r $cache_file ) {
105 0           return image_response( $cache_file, $canonical );
106             }
107             else {
108 0           my @args = ( $request, $file->{path}, $cache_file );
109 0 0         push @args, @{ $self->{magick_args} || [] };
  0            
110 0 0         return image_response( $cache_file, $canonical ) if convert(@args);
111             }
112              
113 0           error_response( 500, "Conversion failed" );
114             }
115              
116             sub info_response {
117 0     0 0   my ( $self, $file ) = @_;
118              
119             my $info = info(
120             $file->{path},
121             id => $file->{id},
122 0           profile => 'level2',
123              
124             extraQualities => [qw(color gray bitonal default)],
125             extraFormats => $self->formats,
126             extraFeatures => [
127             qw(
128             baseUriRedirect canonicalLinkHeader cors jsonldMediaType mirroring
129             profileLinkHeader
130             regionByPct regionByPx regionSquare rotationArbitrary rotationBy90s
131             sizeByConfinedWh sizeByH sizeByPct sizeByW sizeByWh sizeUpscaling
132             )
133             ]
134             );
135              
136             $info->{$_} = $self->{$_}
137 0           for grep { $self->{$_} } qw(maxWidth maxHeight maxArea);
  0            
138              
139 0 0         if ( $self->preferredFormats ) {
140 0           $info->{preferredFormats} = $self->preferredFormats;
141             }
142              
143             # TODO: canonicalLinkHeader?
144              
145 0 0         $info->{rights} = $self->rights if $self->rights;
146 0 0         $info->{service} = $self->service if $self->service;
147              
148 0           return json_response( 200, $info,
149             'application/ld+json;profile="http://iiif.io/api/image/3/context.json"'
150             );
151             }
152              
153             sub find_file {
154 0     0 0   my ( $self, $identifier ) = @_;
155              
156 0           foreach ( @{ $self->formats } ) {
  0            
157 0           my $file = File::Spec->catfile( $self->images, "$identifier.$_" );
158 0 0         return $file if -r $file;
159             }
160             }
161              
162             sub file {
163 0     0 0   my ( $self, $identifier ) = @_;
164              
165 0 0         my $path =
166             ref $self->images eq 'CODE'
167             ? $self->images->($identifier)
168             : $self->find_file($identifier);
169              
170 0 0 0       if ( -f $path && $path =~ /\.([^.]+)$/ ) {
171 0 0         if ( grep { $1 eq $_ } @{ $self->formats } ) {
  0            
  0            
172             return {
173 0           path => $path,
174             format => $1
175             };
176             }
177             }
178             }
179              
180             sub redirect {
181 0     0 0   return [ 303, [ Location => $_[0] ], [] ];
182             }
183              
184             # adopted from Plack::App::File
185             sub image_response {
186 0     0 0   my ( $file, $canonical ) = @_;
187              
188 0 0         open my $fh, "<:raw", $file
189             or return error_response( 403, " Forbidden " );
190              
191 0   0       my $type = Plack::MIME->mime_type($file) // 'image';
192 0           my @stat = stat $file;
193              
194 0           Plack::Util::set_io_path( $fh, Cwd::realpath($file) );
195              
196             return [
197 0           200,
198             [
199             'Content-Type' => $type,
200             'Content-Length' => $stat[7],
201             'Last-Modified' => HTTP::Date::time2str( $stat[9] ),
202             'Link' => ';rel="profile"',
203             'Link' => "<$canonical>;rel=\"canonical\""
204             ],
205             $fh,
206             ];
207             }
208              
209             sub json_response {
210 0     0 0   my ( $code, $body, $type ) = @_;
211              
212 0           state $JSON = JSON::PP->new->pretty->canonical(1);
213              
214             [
215 0   0       $code,
216             [
217             'Content-Type' => $type // 'application/json',
218             'Link' => ';rel="profile"'
219             ],
220             [ $JSON->encode($body) ]
221             ];
222             }
223              
224             sub error_response {
225 0   0 0 0   my $code = shift // 400;
226 0   0       my $message = shift
227             // " Invalid IIIF Image API Request : region or size out of bounds ";
228 0           json_response( $code, { message => $message } );
229             }
230              
231             1;
232              
233             =head1 NAME
234              
235             IIIF::ImageAPI - IIIF Image API implementation as Plack application
236              
237             =head1 SYNOPSIS
238              
239             use Plack::Builder;
240             use IIIF::ImageAPI;
241              
242             builder {
243             enable 'CrossOrigin', origins => '*';
244             IIIF::ImageAPI->new(
245             images => 'path/to/images',
246             base => 'https://example.org/iiif/',
247             formats => [qw(jpg png gif tif pdf webp jp2)],
248             );
249             }
250              
251             =head1 CONFIGURATION
252              
253             =over
254              
255             =item images
256              
257             Either an image directory (set to the current directory by default) or a code
258             reference of a function that maps image identifiers to image files.
259              
260             =item cache
261              
262             Cache directory. Set to a temporary per-process directory by default. Please
263             use different cache directories for different settings of C and
264             C.
265              
266             =item base
267              
268             Base URI which the service is hosted at, including trailing slash. Likely
269             required if the service is put behind a web proxy.
270              
271             =item canonical
272              
273             Redirect requests to the L
274             and include (disabled by default). A canonical Link header is set anyway.
275              
276             =item formats
277              
278             List of supported image formats. Set to C<['jpg', 'png', 'gif']> by default. On
279             configuration with other formats make sure ImageMagick supports them (see
280             L).
281              
282             =item preferredFormats
283              
284             Optional list of preferred image formats. MUST be a subset of or equal to
285             C. The first preferred format, if given, will be used as default if a
286             request does no specify a file format.
287              
288             =item maxWidth
289              
290             Optional maximum width in pixels to be supported.
291              
292             =item maxHeight
293              
294             Optional maximum height in pixels to be supported.
295              
296             =item maxArea
297              
298             Optional maximum pixel area (width x height) to be supported.
299              
300             =item rights
301              
302             Optional string that identifies a license or rights statement for all images,
303             to be included in image information responses.
304              
305             =item service
306              
307             Optional array with L
308             to be included in image information responses.
309              
310             =item magick_args
311              
312             Additional command line arguments always used when calling ImageMagick. For
313             instance C<[qw(-limit memory 1GB -limit disk 1GB)]> to limit resources.
314              
315             =back
316              
317             =cut