[Patches] [PATCH] Bug 1633: Add support for uploading images to Koha

koha-patchbot at kohaaloha.com koha-patchbot at kohaaloha.com
Tue Dec 13 22:05:11 NZDT 2011


From: Jared Camins-Esakov <jcamins at cpbibliography.com>
Date: Sun, 6 Nov 2011 11:13:23 +0530
Subject: [PATCH] Bug 1633: Add support for uploading images to Koha
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Content-Type: text/plain; charset="UTF-8"

A frequently-requested feature for Koha, especially by special libraries, is the
ability to upload local cover images into Koha. This patch adds that ability.

This counter-patch builds on the work by Koustubha Kale at Anant Corporation,
but adds the following additional features:
1. Moves the code for handling image retrieval/saving into the Koha namespace
2. The ability to have multiple cover images for a biblio
3. Handling for "full size" (800x600) and thumbnail-size (200x140) images
4. A separate image viewer page
5. Javascript-based handling of cover image placement for the cover images in
   the OPAC (as opposed to static <img /> tags embedded into pages)
6. Local cover display in the staff client
7. Uploading images directly from the record view

How to use/test :
Assign user permission to the user Tools > (upload_local_cover_images Upload
local cover images).  In order to upload local  images, login to the staff
client. Go to Home › Tools › Upload Cover Images. Here you can upload cover
images either singly or in bulk in the form of a zip file.  If uploading singly,
click on image file, browse the image from your local disk, type in the biblio
number of the catalogue entry and press upload.  If uploading in bulk as a zip
file, the zip file must contain (in addition to cover images) one text file named
either datalink.txt OR idlink.txt. This file should have mapping of biblionumber
to image file name in the zip one per line with comma or tab as delimiters. For
example:

1, scanned_cover_image_of_bib_no_1.jpg
2, scanned_cover_image_of_bib_no_1.jpg

Cover images will be resized to a large image of 800x600 and a thumbnail of
200x140. Depending on the setting of AllowMultipleCovers, it is possible to
upload multiple images for a single bibliographic record. However, even if
multiple covers are permitted, you have the option of replacing the existing
covers by checking the "Replace existing covers" option on the upload screen.

1. The patch adds a menu link in Tools from where you can upload local cover
   images
2. It adds a user permission to enable access control to this menu item under
   Tools
3. It adds a system preference OPACLocalCoverImages under Enhanced Content.
   This needs to be turned on to show local cover images in OPAC.

Once you have uploaded local images, if you search for the biblio, the local
cover should show up in search as well as search detail pages in the OPAC, and
the details view in the Intranet.

Koustubha Kale is working on another patch which will allow us to set a cover
image source priority in system preferences, and which will then gracefully
fail over to the next source if image is not available from the first choice
source.

Detailed test plan:
1.  Install update (database updates are in
    installer/data/mysql/atomicupdate/local_cover_images.pl)
2.  Enable LocalCoverImages and OPACLocalCoverImages in the Enhanced content
    preferences screen
3.  Upload an image of type GIF, JPEG, PNG, or XPM either from the "Local Cover
    Image" tool and choosing "Image file" as its type and entering a valid
    biblionumber, or by viewing a record and clicking the "Upload Image" option
    under the Edit button in the toolbar
4.  View the record in the Intranet and OPAC. The image should show up directly
    in OPAC search results and details, and there should be an "Images" tab on
    both the Intranet and the OPAC detail view
5.  Upload another image and attach it to the same bibliographic record.
6.  Confirm that the new image (only) shows up in the OPAC and Intranet
7.  Change the AllowMultipleCovers to "Allow" in the Enhanced content preferences
    screen
8.  Add several images to a bibliographic record
9.  Confirm that they all show up in the "Images" tab on the Intranet and OPAC
    and only one shows up in the main display in the OPAC details and search
    results pages
10. Create a zip file containing at least one image and a text file "idlink.txt"
    with one or more lines that looks like this:
    1, filename.jpg
    Replace the '1' with a valid biblionumber, and 'filename.jpg' with the name
    of the image file you are adding to the zip file
11. Upload the zip file from the "Local Cover Image" tool (being sure to choose
    "zip file" as the filetype)
12. Check that the image(s) have been attached to the appropriate bibliographic
    record(s)

Special thanks to Koustubha Kale and Anant Corporation for the initial
implementation of local cover images, and to Chris Nighswonger of Foundation
Bible College for his prior work on patron images.
---
 C4/Auth.pm                                         |    5 +-
 C4/UploadedFile.pm                                 |   18 ++
 Koha/Images.pm                                     |  149 +++++++++++++++++
 catalogue/detail.pl                                |    6 +
 catalogue/image.pl                                 |  111 +++++++++++++
 catalogue/imageviewer.pl                           |   51 ++++++
 .../data/mysql/atomicupdate/local_cover_images.pl  |   20 +++
 .../data/mysql/en/mandatory/userpermissions.sql    |    1 +
 installer/data/mysql/kohastructure.sql             |   15 ++
 installer/data/mysql/sysprefs.sql                  |    4 +-
 .../intranet-tmpl/prog/en/css/staff-global.css     |    3 +
 .../intranet-tmpl/prog/en/includes/cat-toolbar.inc |    3 +-
 .../prog/en/includes/doc-head-close.inc            |    8 +
 .../intranet-tmpl/prog/en/includes/tools-menu.inc  |    3 +
 koha-tmpl/intranet-tmpl/prog/en/js/localcovers.js  |   44 +++++
 .../admin/preferences/enhanced_content.pref        |   19 +++
 .../prog/en/modules/catalogue/detail.tt            |   12 ++
 .../prog/en/modules/catalogue/imageviewer.tt       |   43 +++++
 .../prog/en/modules/tools/tools-home.tt            |    5 +
 .../prog/en/modules/tools/upload-images.tt         |  130 +++++++++++++++
 .../opac-tmpl/prog/en/includes/doc-head-close.inc  |    9 +
 koha-tmpl/opac-tmpl/prog/en/js/localcovers.js      |   44 +++++
 koha-tmpl/opac-tmpl/prog/en/modules/opac-detail.tt |   18 ++
 .../opac-tmpl/prog/en/modules/opac-imageviewer.tt  |   43 +++++
 .../opac-tmpl/prog/en/modules/opac-results.tt      |    2 +
 opac/opac-detail.pl                                |   11 ++
 opac/opac-image.pl                                 |  111 +++++++++++++
 opac/opac-imageviewer.pl                           |   51 ++++++
 opac/opac-search.pl                                |    4 +
 tools/upload-cover-image.pl                        |  167 ++++++++++++++++++++
 30 files changed, 1107 insertions(+), 3 deletions(-)
 create mode 100644 Koha/Images.pm
 create mode 100755 catalogue/image.pl
 create mode 100755 catalogue/imageviewer.pl
 create mode 100644 installer/data/mysql/atomicupdate/local_cover_images.pl
 create mode 100644 koha-tmpl/intranet-tmpl/prog/en/js/localcovers.js
 create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/imageviewer.tt
 create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/tools/upload-images.tt
 create mode 100644 koha-tmpl/opac-tmpl/prog/en/js/localcovers.js
 create mode 100644 koha-tmpl/opac-tmpl/prog/en/modules/opac-imageviewer.tt
 create mode 100755 opac/opac-image.pl
 create mode 100755 opac/opac-imageviewer.pl
 create mode 100755 tools/upload-cover-image.pl

diff --git a/C4/Auth.pm b/C4/Auth.pm
index e360e10..2b997dd 100755
--- a/C4/Auth.pm
+++ b/C4/Auth.pm
@@ -389,7 +389,9 @@ sub get_template_and_user {
             virtualshelves              => C4::Context->preference("virtualshelves"),
             StaffSerialIssueDisplayCount => C4::Context->preference("StaffSerialIssueDisplayCount"),
             NoZebra                     => C4::Context->preference('NoZebra'),
-		EasyAnalyticalRecords => C4::Context->preference('EasyAnalyticalRecords'),
+            EasyAnalyticalRecords       => C4::Context->preference('EasyAnalyticalRecords'),
+            LocalCoverImages            => C4::Context->preference('LocalCoverImages'),
+            AllowMultipleCovers         => C4::Context->preference('AllowMultipleCovers'),
         );
     }
     else {
@@ -494,6 +496,7 @@ sub get_template_and_user {
             SyndeticsAwards              => C4::Context->preference("SyndeticsAwards"),
             SyndeticsSeries              => C4::Context->preference("SyndeticsSeries"),
             SyndeticsCoverImageSize      => C4::Context->preference("SyndeticsCoverImageSize"),
+            OPACLocalCoverImages         => C4::Context->preference("OPACLocalCoverImages"),
         );
 
         $template->param(OpacPublic => '1') if ($user || C4::Context->preference("OpacPublic"));
diff --git a/C4/UploadedFile.pm b/C4/UploadedFile.pm
index da29c1f..e8c9080 100644
--- a/C4/UploadedFile.pm
+++ b/C4/UploadedFile.pm
@@ -159,6 +159,24 @@ sub name {
     }
 }
 
+=head2 filename
+
+  my $filename = $uploaded_file->filename();
+
+Accessor method for the name by which the file is to be known.
+
+=cut
+
+sub filename {
+    my $self = shift;
+    if (@_) {
+        $self->{'tmp_file_name'} = shift;
+        $self->_serialize();
+    } else {
+        return $self->{'tmp_file_name'};
+    }
+}
+
 =head2 max_size
 
   my $max_size = $uploaded_file->max_size();
diff --git a/Koha/Images.pm b/Koha/Images.pm
new file mode 100644
index 0000000..80ef437
--- /dev/null
+++ b/Koha/Images.pm
@@ -0,0 +1,149 @@
+package Koha::Images;
+use strict;
+use warnings;
+use 5.010;
+
+use C4::Context;
+use GD;
+
+use vars qw($debug $VERSION @ISA @EXPORT);
+
+BEGIN {
+	# set the version for version checking
+	$VERSION = 3.03;
+	require Exporter;
+	@ISA    = qw(Exporter);
+	@EXPORT = qw(
+        &PutImage 
+        &RetrieveImage
+        &ListImagesForBiblio
+        &DelImage
+    );
+	$debug = $ENV{KOHA_DEBUG} || $ENV{DEBUG} || 0;
+}
+
+=head2 PutImage
+
+    PutImage($biblionumber, $srcimage, $replace);
+
+Stores binary image data and thumbnail in database, optionally replacing existing images for the given biblio.
+
+=cut
+
+sub PutImage {
+    my ($biblionumber, $srcimage, $replace) = @_;
+
+    return -1 unless defined($srcimage);
+
+    if ($replace) {
+        foreach (ListImagesForBiblio($biblionumber)) {
+            DelImage($_);
+        }
+    }
+
+    my $dbh = C4::Context->dbh;
+    my $query = "INSERT INTO biblioimages (biblionumber, mimetype, imagefile, thumbnail) VALUES (?,?,?,?);";
+    my $sth = $dbh->prepare($query);
+
+    my $mimetype = 'image/png';	# GD autodetects three basic image formats: PNG, JPEG, XPM; we will convert all to PNG which is lossless...
+# Check the pixel size of the image we are about to import...
+    my $thumbnail = _scale_image($srcimage, 140, 200);    # MAX pixel dims are 140 X 200 for thumbnail...
+    my $fullsize = _scale_image($srcimage, 600, 800);   # MAX pixel dims are 600 X 800 for full-size image...
+    $debug and warn "thumbnail is " . length($thumbnail) . " bytes.";
+
+    $sth->execute($biblionumber,$mimetype,$fullsize->png(),$thumbnail->png());
+    my $dberror = $sth->errstr;
+    warn "Error returned inserting $biblionumber.$mimetype." if $sth->errstr;
+    undef $thumbnail;
+    undef $fullsize;
+    return $dberror;
+}
+
+=head2 RetrieveImage
+    my ($imagedata, $error) = RetrieveImage($imagenumber);
+
+Retrieves the specified image.
+
+=cut
+
+sub RetrieveImage {
+    my ($imagenumber) = @_;
+
+    my $dbh = C4::Context->dbh;
+    my $query = 'SELECT mimetype, imagefile, thumbnail FROM biblioimages WHERE imagenumber = ?';
+    my $sth = $dbh->prepare($query);
+    $sth->execute($imagenumber);
+    my $imagedata = $sth->fetchrow_hashref;
+    warn "Database error!" if $sth->errstr;
+    return $imagedata, $sth->errstr;
+}
+
+=head2 ListImagesForBiblio
+    my (@images) = ListImagesForBiblio($biblionumber);
+
+Gets a list of all images associated with a particular biblio.
+
+=cut
+
+
+sub ListImagesForBiblio {
+    my ($biblionumber) = @_;
+
+    my @imagenumbers;
+    my $dbh = C4::Context->dbh;
+    my $query = 'SELECT imagenumber FROM biblioimages WHERE biblionumber = ?';
+    my $sth = $dbh->prepare($query);
+    $sth->execute($biblionumber);
+    if (!$sth->errstr) {
+        while (my $row = $sth->fetchrow_hashref) {
+            push @imagenumbers, $row->{'imagenumber'};
+        }
+    }
+    warn "Database error!" if $sth->errstr;
+    return @imagenumbers, $sth->errstr;
+}
+
+=head2 DelImage
+
+    my ($dberror) = DelImage($imagenumber);
+
+Removes the image with the supplied imagenumber.
+
+=cut
+
+sub DelImage {
+    my ($imagenumber) = @_;
+    warn "Imagenumber passed to DelImage is $imagenumber" if $debug;
+    my $dbh = C4::Context->dbh;
+    my $query = "DELETE FROM biblioimages WHERE imagenumber = ?;";
+    my $sth = $dbh->prepare($query);
+    $sth->execute($imagenumber);
+    my $dberror = $sth->errstr;
+    warn "Database error!" if $sth->errstr;
+    return $dberror;
+}
+
+sub _scale_image {
+    my ($image, $maxwidth, $maxheight) = @_;
+    my ($width, $height) = $image->getBounds();
+    $debug and warn "image is $width pix X $height pix.";
+    if ($width > $maxwidth || $height > $maxheight) {
+#        $debug and warn "$filename exceeds the maximum pixel dimensions of $maxwidth X $maxheight. Resizing...";
+        my $percent_reduce;    # Percent we will reduce the image dimensions by...
+            if ($width > $maxwidth) {
+                $percent_reduce = sprintf("%.5f",($maxwidth/$width));    # If the width is oversize, scale based on width overage...
+            } else {
+                $percent_reduce = sprintf("%.5f",($maxheight/$height));    # otherwise scale based on height overage.
+            }
+        my $width_reduce = sprintf("%.0f", ($width * $percent_reduce));
+        my $height_reduce = sprintf("%.0f", ($height * $percent_reduce));
+        $debug and warn "Reducing image by " . ($percent_reduce * 100) . "\% or to $width_reduce pix X $height_reduce pix";
+        my $newimage = GD::Image->new($width_reduce, $height_reduce, 1); #'1' creates true color image...
+        $newimage->copyResampled($image,0,0,0,0,$width_reduce,$height_reduce,$width,$height);
+        return $newimage;
+    } else {
+        return $image;
+    }
+}
+
+1;
diff --git a/catalogue/detail.pl b/catalogue/detail.pl
index 580949d..7719c77 100755
--- a/catalogue/detail.pl
+++ b/catalogue/detail.pl
@@ -37,6 +37,7 @@ use C4::External::Amazon;
 use C4::Search;		# enabled_staff_search_views
 use C4::VirtualShelves;
 use C4::XSLT;
+use Koha::Images;
 
 # use Smart::Comments;
 
@@ -379,6 +380,11 @@ if ( C4::Context->preference("AmazonEnabled") == 1 ) {
     }
 }
 
+if ( C4::Context->preference("LocalCoverImages") == 1 ) {
+    my @images = ListImagesForBiblio($biblionumber);
+    $template->{VARS}->{localimages} = \@images;
+}
+
 # Get OPAC URL
 if (C4::Context->preference('OPACBaseURL')){
      $template->param( OpacUrl => C4::Context->preference('OPACBaseURL') );
diff --git a/catalogue/image.pl b/catalogue/image.pl
new file mode 100755
index 0000000..8721299
--- /dev/null
+++ b/catalogue/image.pl
@@ -0,0 +1,111 @@
+#!/usr/bin/perl
+#
+# based on patronimage.pl
+#
+# This file is part of Koha.
+#
+# Koha is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 2 of the License, or (at your option) any later
+# version.
+#
+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with Koha; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+#
+#
+
+use strict;
+use warnings;
+
+use CGI; #qw(:standard escapeHTML);
+use C4::Context;
+use Koha::Images;
+
+$|=1;
+
+my $DEBUG = 1;
+my $data = new CGI;
+my $imagenumber;
+
+=head1 NAME
+
+image.pl - Script for retrieving and formatting local cover images for display
+
+=head1 SYNOPSIS
+
+<img src="image.pl?imagenumber=X" />
+<img src="image.pl?biblionumber=X" />
+<img src="image.pl?imagenumber=X&thumbnail=1" />
+<img src="image.pl?biblionumber=X&thumbnail=1" />
+
+=head1 DESCRIPTION
+
+This script, when called from within HTML and passed a valid imagenumber or
+biblionumber, will retrieve the image data associated with that biblionumber
+if one exists, format it in proper HTML format and pass it back to be displayed.
+If the parameter thumbnail has been provided, a thumbnail will be returned
+rather than the full-size image. When a biblionumber is provided rather than an
+imagenumber, a random image is selected.
+
+=cut
+
+if (defined $data->param('imagenumber')) {
+    $imagenumber = $data->param('imagenumber');
+} elsif (defined $data->param('biblionumber')) {
+    my @imagenumbers = ListImagesForBiblio($data->param('biblionumber'));
+    if (@imagenumbers) {
+        $imagenumber = $imagenumbers[0];
+    } else {
+        warn "No images for this biblio" if $DEBUG;
+        error();
+    }
+} else {
+    $imagenumber = shift;
+}
+
+if ($imagenumber) {
+    warn "imagenumber passed in: $imagenumber" if $DEBUG;
+    my ($imagedata, $dberror) = RetrieveImage($imagenumber);
+
+    if ($dberror) {
+        warn "Database Error!" if $DEBUG;
+        error();
+    }
+
+    if ($imagedata) {
+        my $image;
+        if ($data->param('thumbnail')) {
+            $image = $imagedata->{'thumbnail'};
+        } else {
+            $image = $imagedata->{'imagefile'};
+        }
+        print $data->header (-type => $imagedata->{'mimetype'}, -'Cache-Control' => 'no-store', -expires => 'now', -Content_Length => length ($image)), $image;
+        exit;
+    } else {
+        warn "No image exists for $imagenumber" if $DEBUG;
+        error();
+    }
+} else {
+    error();
+}
+
+error();
+
+sub error {
+    print $data->header ( -status=> '404', -expires => 'now' );
+    exit;
+}
+
+=head1 AUTHOR
+
+Chris Nighswonger cnighswonger <at> foundations <dot> edu
+
+modified for local cover images by Koustubha Kale kmkale <at> anantcorp <dot> com
+
+=cut
diff --git a/catalogue/imageviewer.pl b/catalogue/imageviewer.pl
new file mode 100755
index 0000000..fae6243
--- /dev/null
+++ b/catalogue/imageviewer.pl
@@ -0,0 +1,51 @@
+#!/usr/bin/perl
+
+# Copyright 2011 C & P Bibliography Services
+#
+# This file is part of Koha.
+#
+# Koha is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 2 of the License, or (at your option) any later
+# version.
+#
+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with Koha; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+use strict;
+use warnings;
+
+use CGI;
+use C4::Auth;
+use C4::Biblio;
+use C4::Output;
+use Koha::Images;
+
+my $query = new CGI;
+my ( $template, $borrowernumber, $cookie ) = get_template_and_user(
+    {
+        template_name   => "catalogue/imageviewer.tmpl",
+        query           => $query,
+        type            => "intranet",
+        authnotrequired => 0,
+        flagsrequired   => { catalogue => 1 },
+    }
+);
+
+my $biblionumber = $query->param('biblionumber') || $query->param('bib');
+my ($count, $biblio) = GetBiblio($biblionumber);
+
+if (C4::Context->preference("LocalCoverImages")) {
+    my @images = ListImagesForBiblio($biblionumber);
+    $template->{VARS}->{'LocalCoverImages'} = 1;
+    $template->{VARS}->{'images'} = \@images;
+}
+
+$template->{VARS}->{'biblio'} = $biblio;
+
+output_html_with_http_headers $query, $cookie, $template->output;
diff --git a/installer/data/mysql/atomicupdate/local_cover_images.pl b/installer/data/mysql/atomicupdate/local_cover_images.pl
new file mode 100644
index 0000000..a698e7f
--- /dev/null
+++ b/installer/data/mysql/atomicupdate/local_cover_images.pl
@@ -0,0 +1,20 @@
+#! /usr/bin/perl
+use strict;
+use warnings;
+use C4::Context;
+my $dbh=C4::Context->dbh;
+
+$dbh->do( q|CREATE TABLE `biblioimages` (
+      `imagenumber` int(11) NOT NULL AUTO_INCREMENT,
+      `biblionumber` int(11) NOT NULL,
+      `mimetype` varchar(15) NOT NULL,
+      `imagefile` mediumblob NOT NULL,
+      `thumbnail` mediumblob NOT NULL,
+      PRIMARY KEY (`imagenumber`),
+      CONSTRAINT `bibliocoverimage_fk1` FOREIGN KEY (`biblionumber`) REFERENCES `biblio` (`biblionumber`) ON DELETE CASCADE ON UPDATE CASCADE
+      ) ENGINE=InnoDB DEFAULT CHARSET=utf8|);
+$dbh->do( q|INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES ('OPACLocalCoverImages','0','Display local cover images on OPAC search and details pages.','1','YesNo')|);
+$dbh->do( q|INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES ('LocalCoverImages','0','Display local cover images on intranet search and details pages.','1','YesNo')|);
+$dbh->do( q|INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES ('AllowMultipleCovers','0','Allow multiple cover images to be attached to each bibliographic record.','1','YesNo')|);
+$dbh->do( q|INSERT INTO permissions (module_bit, code, description) VALUES (13, 'upload_local_cover_images', 'Upload local cover images')|);
+print "Upgrade done (Added support for local cover images)\n";
diff --git a/installer/data/mysql/en/mandatory/userpermissions.sql b/installer/data/mysql/en/mandatory/userpermissions.sql
index ec61ea0..873089a 100644
--- a/installer/data/mysql/en/mandatory/userpermissions.sql
+++ b/installer/data/mysql/en/mandatory/userpermissions.sql
@@ -36,6 +36,7 @@ INSERT INTO permissions (module_bit, code, description) VALUES
    (13, 'manage_csv_profiles', 'Manage CSV export profiles'),
    (13, 'moderate_tags', 'Moderate patron tags'),
    (13, 'rotating_collections', 'Manage rotating collections'),
+   (13, 'upload_local_cover_images', 'Upload local cover images'),
    (15, 'check_expiration', 'Check the expiration of a serial'),
    (15, 'claim_serials', 'Claim missing serials'),
    (15, 'create_subscription', 'Create a new subscription'),
diff --git a/installer/data/mysql/kohastructure.sql b/installer/data/mysql/kohastructure.sql
index 452173d..7340eee 100644
--- a/installer/data/mysql/kohastructure.sql
+++ b/installer/data/mysql/kohastructure.sql
@@ -2667,6 +2667,21 @@ CREATE TABLE `fieldmapping` ( -- koha to keyword mapping
   PRIMARY KEY  (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
+--
+-- Table structure for table `bibliocoverimage`
+--
+
+DROP TABLE IF EXISTS `bibliocoverimage`;
+
+CREATE TABLE `bibliocoverimage` (
+ `imagenumber` int(11) NOT NULL AUTO_INCREMENT,
+ `biblionumber` int(11) NOT NULL,
+ `mimetype` varchar(15) NOT NULL,
+ `imagefile` mediumblob NOT NULL,
+ `thumbnail` mediumblob NOT NULL,
+ PRIMARY KEY (`imagenumber`),
+ CONSTRAINT `bibliocoverimage_fk1` FOREIGN KEY (`biblionumber`) REFERENCES `biblio` (`biblionumber`) ON DELETE CASCADE ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8
 
 /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
 /*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
diff --git a/installer/data/mysql/sysprefs.sql b/installer/data/mysql/sysprefs.sql
index ae2c1cb..99e88c1 100755
--- a/installer/data/mysql/sysprefs.sql
+++ b/installer/data/mysql/sysprefs.sql
@@ -328,4 +328,6 @@ INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('
 INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES ('OpacKohaUrl','1',"Show 'Powered by Koha' text on OPAC footer.",NULL,NULL);
 INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES('EasyAnalyticalRecords','0','If on, display in the catalogue screens tools to easily setup analytical record relationships','','YesNo');
 INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('OpacShowRecentComments',0,'If ON a link to recent comments will appear in the OPAC masthead',NULL,'YesNo');
-
+INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES ('OPACLocalCoverImages','0','Display local cover images on OPAC search and details pages.','1','YesNo');
+INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES ('LocalCoverImages','0','Display local cover images on intranet details pages.','1','YesNo');
+INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES ('AllowMultipleCovers','0','Allow multiple cover images to be attached to each bibliographic record.','1','YesNo');
diff --git a/koha-tmpl/intranet-tmpl/prog/en/css/staff-global.css b/koha-tmpl/intranet-tmpl/prog/en/css/staff-global.css
index 7710eda..c1d4b43 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/css/staff-global.css
+++ b/koha-tmpl/intranet-tmpl/prog/en/css/staff-global.css
@@ -2093,3 +2093,6 @@ div.pager input.pagedisplay {
 	font-weight: bold;
 	text-align : center;
 }
+.localimage {
+    padding: .3em;
+}
diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/cat-toolbar.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/cat-toolbar.inc
index 4b54c40..64f5c4c 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/includes/cat-toolbar.inc
+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/cat-toolbar.inc
@@ -101,7 +101,8 @@ function confirm_items_deletion() {
 	        [% IF ( CAN_user_editcatalogue_edit_catalogue ) %]{ text: _("Edit Record"), url: "/cgi-bin/koha/cataloguing/addbiblio.pl?biblionumber=[% biblionumber %]&frameworkcode=&op=" },[% END %]
 	        [% IF ( CAN_user_editcatalogue_edit_items ) %]{ text: _("Edit Items"), url: "/cgi-bin/koha/cataloguing/additem.pl?biblionumber=[% biblionumber %]" },[% END %]
 	        [% IF ( CAN_user_editcatalogue_edit_items ) %]{ text: _("Attach Item"), url: "/cgi-bin/koha/cataloguing/moveitem.pl?biblionumber=[% biblionumber %]" },[% END %]
-                [% IF ( EasyAnalyticalRecords ) %][% IF ( CAN_user_editcatalogue_edit_items ) %]{ text: _("Link to Host Item"), url: "/cgi-bin/koha/cataloguing/linkitem.pl?biblionumber=[% biblionumber %]" },[% END %][% END %]
+            [% IF ( EasyAnalyticalRecords ) %][% IF ( CAN_user_editcatalogue_edit_items ) %]{ text: _("Link to Host Item"), url: "/cgi-bin/koha/cataloguing/linkitem.pl?biblionumber=[% biblionumber %]" },[% END %][% END %]
+            [% IF ( LocalCoverImages ) %][% IF ( CAN_user_tools_upload_local_cover_images ) %]{ text: _("Upload Image"), url: "/cgi-bin/koha/tools/upload-cover-image.pl?biblionumber=[% biblionumber %]&filetype=image" },[% END %][% END %]
 	        [% IF ( CAN_user_editcatalogue_edit_catalogue ) %]{ text: _("Edit as New (Duplicate)"), url: "/cgi-bin/koha/cataloguing/addbiblio.pl?biblionumber=[% biblionumber %]&frameworkcode=&op=duplicate" },[% END %]
 			[% IF ( CAN_user_editcatalogue_edit_catalogue ) %]{ text: _("Replace Record via Z39.50"), onclick: {fn: PopupZ3950 } },[% END %]
 			[% IF ( CAN_user_editcatalogue_edit_catalogue ) %]{ text: _("Delete Record"), onclick: {fn: confirm_deletion }[% IF ( count ) %],id:'disabled'[% END %] },[% END %]
diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/doc-head-close.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/doc-head-close.inc
index 6a2dae0..53f6843 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/includes/doc-head-close.inc
+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/doc-head-close.inc
@@ -99,3 +99,11 @@
     [% IF ( virtualshelves || intranetbookbag ) %]
         <script type="text/javascript" language="javascript" src="[% themelang %]/js/basket.js"></script>
     [% END %]
+[% IF LocalCoverImages %]
+<script type="text/javascript" language="javascript" src="[% themelang %]/js/localcovers.js"></script>
+<script type="text/javascript" language="javascript">
+//<![CDATA[
+var NO_LOCAL_JACKET = _("No cover image available");
+//]]>
+</script>
+[% END %]
diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/tools-menu.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/tools-menu.inc
index e3896cc..8a739d5 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/includes/tools-menu.inc
+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/tools-menu.inc
@@ -70,6 +70,9 @@
     [% IF ( CAN_user_tools_manage_staged_marc ) %]
 	<li><a href="/cgi-bin/koha/tools/manage-marc-import.pl">Staged MARC management</a></li>
     [% END %]
+    [% IF ( CAN_user_tools_upload_local_cover_images ) %]
+	<li><a href="/cgi-bin/koha/tools/upload-cover-image.pl">Upload Local Cover Image</a></li>
+    [% END %]
 </ul>
 <h5>Additional Tools</h5>
 <ul>
diff --git a/koha-tmpl/intranet-tmpl/prog/en/js/localcovers.js b/koha-tmpl/intranet-tmpl/prog/en/js/localcovers.js
new file mode 100644
index 0000000..981af75
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/en/js/localcovers.js
@@ -0,0 +1,44 @@
+if (typeof KOHA == "undefined" || !KOHA) {
+    var KOHA = {};
+}
+
+/**
+ * A namespace for local cover related functions.
+ */
+KOHA.LocalCover = {
+
+
+    /**
+     * Search all:
+     *    <div title="biblionumber" id="isbn" class="openlibrary-thumbnail"></div>
+     * or
+     *    <div title="biblionumber" id="isbn" class="openlibrary-thumbnail-preview"></div>
+     * and run a search with all collected isbns to Open Library Book Search.
+     * The result is asynchronously returned by OpenLibrary and catched by
+     * olCallBack().
+     */
+    GetCoverFromBibnumber: function(uselink) {
+        $("div [id^=local-thumbnail]").each(function(i) {
+            var mydiv = this;
+            var message = document.createElement("span");
+            $(message).attr("class","no-image");
+            $(message).html(NO_LOCAL_JACKET);
+            $(mydiv).append(message);
+            var img = $("<img />").attr('src',
+                '/cgi-bin/koha/catalogue/image.pl?thumbnail=1&biblionumber=' + $(mydiv).attr("class"))
+                .load(function () {
+                    if (!this.complete || typeof this.naturalWidth == "undefined" || this.naturalWidth == 0) {
+                    } else {
+                        if (uselink) {
+                            var a = $("<a />").attr('href', '/cgi-bin/koha/catalogue/imageviewer.pl?biblionumber=' + $(mydiv).attr("class"));
+                            $(a).append(img);
+                            $(mydiv).append(a);
+                        } else {
+                            $(mydiv).append(img);
+                        }
+                        $(mydiv).children('.no-image').remove();
+                    }
+                })
+        });
+    }
+};
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/enhanced_content.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/enhanced_content.pref
index 171b5f4..f5828ae 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/enhanced_content.pref
+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/enhanced_content.pref
@@ -311,3 +311,22 @@ Enhanced Content:
             - pref: TagsExternalDictionary
               class: file
             - on the server to be approved without moderation.
+    Local Cover Images:
+        -
+            - pref: LocalCoverImages
+              choices:
+                  yes: Display
+                  no: "Don't display"
+            - local cover images on intranet search and details pages.
+        -
+            - pref: OPACLocalCoverImages
+              choices:
+                  yes: Display
+                  no: "Don't display"
+            - local cover images on OPAC search and details pages.
+        -
+            - pref: AllowMultipleCovers
+              choices:
+                  yes: Allow
+                  no: "Don't allow"
+            - multiple images to be attached to each bibliographic record.
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/detail.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/detail.tt
index 019fa10..9174d94 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/detail.tt
+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/detail.tt
@@ -228,6 +228,7 @@ function verify_images() {
 [% IF ( subscriptionsnumber ) %]<li><a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% biblionumber %]#subscriptions">Subscriptions</a></li>[% END %]
 [% IF ( FRBRizeEditions ) %][% IF ( XISBNS ) %]<li><a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% biblionumber %]#editions">Editions</a></li>[% END %][% END %]
 [% IF ( AmazonSimilarItems ) %]<li><a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% biblionumber %]#related">Related Titles</a></li>[% END %]
+[% IF ( LocalCoverImages ) %]<li><a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% biblionumber %]#images">Images</a></li>[% END %]
  </ul>
 
 <div id="holdings">
@@ -516,6 +517,17 @@ function verify_images() {
 </div>
 [% END %][% END %]
 
+[% IF ( LocalCoverImages ) %]
+<div id="images">
+<div>Click on an image to view it in the image viewer</div>
+[% FOREACH image IN localimages %]
+[% IF image %]
+<span class="localimage"><a href="/cgi-bin/koha/catalogue/imageviewer.pl?biblionumber=[% biblionumber %]&imagenumber=[% image %]"><img alt="img" src="/cgi-bin/koha/catalogue/image.pl?thumbnail=1&imagenumber=[% image %]" /></a></span>
+[% END %]
+[% END %]
+</div>
+[% END %]
+
 </div><!-- /bibliodetails -->
 
 <div class="yui-g" id="export" style="margin-top: 1em;">
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/imageviewer.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/imageviewer.tt
new file mode 100644
index 0000000..c5ab657
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/imageviewer.tt
@@ -0,0 +1,43 @@
+[% INCLUDE 'doc-head-open.inc' %]
+[% IF ( LibraryNameTitle ) %][% LibraryNameTitle %][% ELSE %]Koha Online[% END %] Catalog › Images for: [% biblio.title |html %]
+[% INCLUDE 'doc-head-close.inc' %]
+<script type="text/JavaScript" language="JavaScript">
+//<![CDATA[
+
+$(document).ready(function(){
+    showCover($('.thumbnail').attr('id'));
+});
+
+function showCover(img) {
+    $('.thumbnail').attr('class', 'thumbnail');
+    $('#largeCoverImg').attr('src', '/cgi-bin/koha/catalogue/image.pl?imagenumber=' + img);
+    $('#' + img + '.thumbnail').attr('class', 'thumbnail selected');
+}
+//]]>
+</script>
+<style type="text/css">
+img.thumbnail {
+    border-style: solid;
+    border-width: 3px;
+    border-color: white;
+}
+
+img.selected {
+    border-color: black;
+}
+</style>
+<script type="text/javascript" src="[% themelang %]/lib/jquery/plugins/jquery.tablesorter.min.js"></script>
+<body id="imageviewer">
+<div id="largeCover"><img id="largeCoverImg" alt="Large view" /></div>
+[% IF LocalCoverImages == 1 %]
+[% FOREACH img IN images %]
+[% IF img %]
+<a href='#' onclick='showCover([% img %])'><img class='thumbnail' id='[% img %]' src='/cgi-bin/koha/catalogue/image.pl?imagenumber=[% img %]&thumbnail=1' alt='Image'/></a>
+[% END %]
+[% END %]
+[% biblio.title %] [% biblio.author %]
+[% ELSE %]
+Unfortunately, images are not enabled for this catalog at this time.
+[% END %]
+</body>
+</html>
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/tools-home.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/tools-home.tt
index 05bdb47..b71a1ee 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/tools-home.tt
+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/tools-home.tt
@@ -140,6 +140,11 @@
     <dd>Managed staged MARC records, including completing and reversing imports</dd>
     [% END %]
 
+    [% IF ( CAN_user_tools_upload_local_cover_images ) %]
+    <dt><a href="/cgi-bin/koha/tools/upload-cover-image.pl">Upload Local Cover Image</a></dt>
+    <dd>Utility to upload scanned cover images for display in OPAC</dd>
+    [% END %]
+
 </dl>
 </div>
 
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/upload-images.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/upload-images.tt
new file mode 100644
index 0000000..1f1b964
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/upload-images.tt
@@ -0,0 +1,130 @@
+[% INCLUDE 'doc-head-open.inc' %]
+<title>Koha › Tools › Upload Images</title>
+[% INCLUDE 'doc-head-close.inc' %]
+[% INCLUDE 'file-upload.inc' %]
+[% INCLUDE 'background-job.inc' %]
+<style type="text/css">
+	#uploadpanel,#fileuploadstatus,#fileuploadfailed,#jobpanel,#jobstatus,#jobfailed { display : none; }
+	#fileuploadstatus,#jobstatus { margin:.4em; }
+	#fileuploadprogress,#jobprogress{ width:150px;height:10px;border:1px solid #666;background:url('/intranet-tmpl/prog/img/progress.png') -300px 0px no-repeat; }</style>
+<script type="text/javascript">
+//<![CDATA[
+$(document).ready(function(){
+	$("#processfile").hide();
+	$("#zipfile").click(function(){
+		$("#bibnum").hide();
+	});
+	$("#image").click(function(){
+		$("#bibnum").show();
+	});
+});
+function CheckForm(f) {
+    if ($("#fileToUpload").value == '') {
+        alert(_('Please upload a file first.'));
+    } else {
+        return submitBackgroundJob(f);
+    }
+    return false;
+}
+
+//]]>
+</script>
+</head>
+<body>
+[% INCLUDE 'header.inc' %]
+[% INCLUDE 'cat-search.inc' %]
+
+<div id="breadcrumbs"><a href="/cgi-bin/koha/mainpage.pl">Home</a> › <a href="/cgi-bin/koha/tools/tools-home.pl">Tools</a> › [% IF ( uploadimage ) %]<a href="/cgi-bin/koha/tools/upload-cover-image.pl">Upload Local Cover Image</a> › Upload Results[% ELSE %]Upload Local Cover Image[% END %]</div>
+
+<div id="doc3" class="yui-t2">
+   
+   <div id="bd">
+	<div id="yui-main">
+	<div class="yui-b">
+
+<h1>Upload Local Cover Image</h1>
+[% IF ( uploadimage ) %]
+<p>Image upload results :</p>
+<ul>
+	<li>[% total %]  images found</li>
+    [% IF ( error ) %]
+    <div class="dialog alert">
+    [% IF ( error == 'UZIPFAIL' ) %]<p><b>Failed to unzip archive.<br />Please ensure you are uploading a valid zip file and try again.</b></p>
+    [% ELSIF ( error == 'OPNLINK' ) %]<p><b>Cannot open folder index (idlink.txt or datalink.txt) to read.<br />Please verify that it exists.</b></p>
+    [% ELSIF ( error == 'OPNIMG' ) %]<p><b>Cannot process file as an image.<br />Please ensure you only upload GIF, JPEG, PNG, or XPM images.</b></p>
+    [% ELSIF ( error == 'DELERR' ) %]<p><b>Unrecognized or missing field delimiter.<br />Please verify that you are using either a single quote or a tab.</b></p>
+    [% ELSIF ( error == 'DBERR' ) %]<p><b>Unable to save image to database.</b></p>
+    [% ELSE %]<p><b>An unknown error has occurred.<br />Please review the error log for more details.</b></p>[% END %]
+    </div>
+    </li>
+    [% END %]
+    <li><a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% biblionumber %]">View final record</a></li>
+	<li><a href="/cgi-bin/koha/tools/tools-home.pl">Back</a></li>
+</ul>
+<hr />
+[% END %]
+<ul>
+	<li>Select an image file or ZIP file to upload. The tool will accept images in GIF, JPEG, PNG, and XPM formats.</li>
+</ul>
+<form method="post" action="[% SCRIPT_NAME %]" id="uploadfile" enctype="multipart/form-data">
+<fieldset class="rows" id="uploadform">
+<legend>Upload images</legend>
+<ol>
+	<li>
+        <div id="fileuploadform">
+		<label for="fileToUpload">Select the file to upload: </label>
+		<input type="file" id="fileToUpload" name="fileToUpload" />
+        </div>	</li>
+</ol>
+  <fieldset class="action"><button class="submit" onclick="return ajaxFileUpload();">Upload file</button></fieldset>
+</fieldset>
+		
+        <div id="uploadpanel"><div id="fileuploadstatus">Upload progress: <div id="fileuploadprogress"></div> <span id="fileuploadpercent">0</span>%</div>
+        <div id="fileuploadfailed"></div></div>
+</form>
+
+    <form method="post" id="processfile" action="[% SCRIPT_NAME %]" enctype="multipart/form-data">
+<fieldset class="rows">
+        <input type="hidden" name="uploadedfileid" id="uploadedfileid" value="" />
+        <input type="hidden" name="runinbackground" id="runinbackground" value="" />
+        <input type="hidden" name="completedJobID" id="completedJobID" value="" />
+	</fieldset>
+  <fieldset class="rows">
+    <legend>File type</legend>
+    <ol>
+      <li class="radio">
+        <input type="radio" id="zipfile" name="filetype" value="zip" [% IF (filetype != 'image' ) %]checked="checked"[% END %] />
+        <label for="zipfile">ZIP file</label>
+      </li>
+      <li class="radio">
+        <input type="radio" id="image" name="filetype" value="image" [% IF (filetype == 'image' ) %]checked="checked"[% END %] />
+        <label for="imagefile">Image file</label>
+      </li>
+      <li class="radio">
+        [% IF ( filetype == 'image' ) %]<span id="bibnum">[% ELSE %]<span id="bibnum" style="display: none">[% END %]<label for="biblionumber">Enter cover biblionumber: </label><input type="text" id="biblionumber" name="biblionumber" value="[% biblionumber %]" size="15" /></span>
+      </li>
+    </ol>
+  </fieldset>
+  <fieldset class="rows">
+    <legend>Options</legend>
+    <ol>
+      <li class="checkbox">
+        <input type="checkbox" id="replace" name="replace" [% IF AllowMultipleCovers == 0 %]checked="checked" disabled="disabled"[% END %] />
+        <label for="replace">Replace existing covers</label>
+      </li>
+    </ol>
+  </fieldset>
+  <fieldset class="action"><input type="submit" value="Process images" /></fieldset>
+ 
+       <div id="jobpanel"><div id="jobstatus">Job progress: <div id="jobprogress"></div> <span id="jobprogresspercent">0</span>%</div>
+     <div id="jobfailed"></div></div>
+  
+</form>
+
+</div>
+</div>
+<div class="yui-b">
+[% INCLUDE 'tools-menu.inc' %]
+</div>
+</div>
+[% INCLUDE 'intranet-bottom.inc' %]
diff --git a/koha-tmpl/opac-tmpl/prog/en/includes/doc-head-close.inc b/koha-tmpl/opac-tmpl/prog/en/includes/doc-head-close.inc
index 15bea79..18e5917 100644
--- a/koha-tmpl/opac-tmpl/prog/en/includes/doc-head-close.inc
+++ b/koha-tmpl/opac-tmpl/prog/en/includes/doc-head-close.inc
@@ -98,6 +98,15 @@ var NO_OL_JACKET = _("No cover image available");
 </script>
 [% END %]
 
+[% IF OPACLocalCoverImages %]
+<script type="text/javascript" language="javascript" src="[% themelang %]/js/localcovers.js"></script>
+<script type="text/javascript" language="javascript">
+//<![CDATA[
+var NO_LOCAL_JACKET = _("No cover image available");
+//]]>
+</script>
+[% END %]
+
 [% IF ( BakerTaylorEnabled ) %]<script type="text/javascript" language="javascript" src="[% themelang %]/js/bakertaylorimages.js"></script>
 <script type="text/javascript" language="javascript">
 	//<![CDATA[
diff --git a/koha-tmpl/opac-tmpl/prog/en/js/localcovers.js b/koha-tmpl/opac-tmpl/prog/en/js/localcovers.js
new file mode 100644
index 0000000..298a20e
--- /dev/null
+++ b/koha-tmpl/opac-tmpl/prog/en/js/localcovers.js
@@ -0,0 +1,44 @@
+if (typeof KOHA == "undefined" || !KOHA) {
+    var KOHA = {};
+}
+
+/**
+ * A namespace for local cover related functions.
+ */
+KOHA.LocalCover = {
+
+
+    /**
+     * Search all:
+     *    <div title="biblionumber" id="isbn" class="openlibrary-thumbnail"></div>
+     * or
+     *    <div title="biblionumber" id="isbn" class="openlibrary-thumbnail-preview"></div>
+     * and run a search with all collected isbns to Open Library Book Search.
+     * The result is asynchronously returned by OpenLibrary and catched by
+     * olCallBack().
+     */
+    GetCoverFromBibnumber: function(uselink) {
+        $("div [id^=local-thumbnail]").each(function(i) {
+            var mydiv = this;
+            var message = document.createElement("span");
+            $(message).attr("class","no-image");
+            $(message).html(NO_LOCAL_JACKET);
+            $(mydiv).append(message);
+            var img = $("<img />").attr('src',
+                '/cgi-bin/koha/opac-image.pl?thumbnail=1&biblionumber=' + $(mydiv).attr("class"))
+                .load(function () {
+                    if (!this.complete || typeof this.naturalWidth == "undefined" || this.naturalWidth == 0) {
+                    } else {
+                        if (uselink) {
+                            var a = $("<a />").attr('href', '/cgi-bin/koha/opac-imageviewer.pl?biblionumber=' + $(mydiv).attr("class"));
+                            $(a).append(img);
+                            $(mydiv).append(a);
+                        } else {
+                            $(mydiv).append(img);
+                        }
+                        $(mydiv).children('.no-image').remove();
+                    }
+                })
+        });
+    }
+};
diff --git a/koha-tmpl/opac-tmpl/prog/en/modules/opac-detail.tt b/koha-tmpl/opac-tmpl/prog/en/modules/opac-detail.tt
index 3175d47..fc2da3f 100755
--- a/koha-tmpl/opac-tmpl/prog/en/modules/opac-detail.tt
+++ b/koha-tmpl/opac-tmpl/prog/en/modules/opac-detail.tt
@@ -39,6 +39,9 @@
 	[% IF OpenLibraryCovers %]
 	KOHA.OpenLibrary.GetCoverFromIsbn();
 	[% END %]
+	[% IF OPACLocalCoverImages %]
+	KOHA.LocalCover.GetCoverFromBibnumber(true);
+	[% END %]
         [% IF ( NovelistSelectProfile ) %]
         novSelect.loadContentForISBN('[% normalized_isbn %]','[% NovelistSelectProfile %]', '[% NovelistSelectPassword %]', function(d){});
         [% END %]
@@ -224,6 +227,7 @@ YAHOO.util.Event.onContentReady("furtherm", function () {
     <div id="catalogue_detail_biblio">
 
     <div id="bookcover">
+    [% IF ( OPACLocalCoverImages ) %]<div style="block" title="[% biblionumber |url %]" class="[% biblionumber %]" id="local-thumbnail-preview"></div>[% END %]
     [% IF ( OPACAmazonEnabled ) %][% IF ( OPACAmazonCoverImages ) %][% IF ( OPACurlOpenInNewWindow ) %]<a href="http://www.amazon[% AmazonTld %]/gp/reader/[% normalized_isbn %]/ref=sib_dp_pt/002-7879865-0184864#reader-link" target="_blank"><img border="0" src="http://images.amazon.com/images/P/[% normalized_isbn %].01.MZZZZZZZ.jpg" alt="Cover Image" /></a>[% ELSE %]<a href="http://www.amazon[% AmazonTld %]/gp/reader/[% normalized_isbn %]/ref=sib_dp_pt/002-7879865-0184864#reader-link"><img border="0" src="http://images.amazon.com/images/P/[% normalized_isbn %].01.MZZZZZZZ.jpg" alt="Cover Image" /></a>[% END %][% END %][% END %]
 
     [% IF ( SyndeticsEnabled ) %][% IF ( SyndeticsCoverImages ) %][% IF ( content_identifier_exists ) %][% IF ( using_https ) %]
@@ -543,6 +547,8 @@ YAHOO.util.Event.onContentReady("furtherm", function () {
 		[% ELSE %]<li>[% END %]
 		<a href="/cgi-bin/koha/opac-detail.pl?biblionumber=[% biblionumber %]#serialcollection">Serial Collection</a></li>
     [% END %]
+
+    [% IF ( OPACLocalCoverImages ) %]<li><a href="/cgi-bin/koha/opac-detail.pl?biblionumber=[% biblionumber %]#images">Images</a></li>[% END %]
 </ul>
 
 [% IF ( serialcollection ) %]
@@ -979,6 +985,18 @@ YAHOO.util.Event.onContentReady("furtherm", function () {
 [% END %]
 
 
+[% IF ( OPACLocalCoverImages ) %]
+<div id="images">
+<div>Click on an image to view it in the image viewer</div>
+[% FOREACH image IN localimages %]
+[% IF image %]
+<span class="localimage"><a href="/cgi-bin/koha/opac-imageviewer.pl?biblionumber=[% biblionumber %]&imagenumber=[% image %]"><img alt="img" src="/cgi-bin/koha/opac-image.pl?thumbnail=1&imagenumber=[% image %]" /></a></span>
+[% END %]
+[% END %]
+</div>
+[% END %]
+
+
 </div>
 [% IF ( NovelistSelectProfile ) %][% IF ( NovelistSelectView == 'below' ) %]
 <div id="NovelistSelect">
diff --git a/koha-tmpl/opac-tmpl/prog/en/modules/opac-imageviewer.tt b/koha-tmpl/opac-tmpl/prog/en/modules/opac-imageviewer.tt
new file mode 100644
index 0000000..d6633e1
--- /dev/null
+++ b/koha-tmpl/opac-tmpl/prog/en/modules/opac-imageviewer.tt
@@ -0,0 +1,43 @@
+[% INCLUDE 'doc-head-open.inc' %]
+[% IF ( LibraryNameTitle ) %][% LibraryNameTitle %][% ELSE %]Koha Online[% END %] Catalog › Images for: [% biblio.title |html %]
+[% INCLUDE 'doc-head-close.inc' %]
+<script type="text/JavaScript" language="JavaScript">
+//<![CDATA[
+
+$(document).ready(function(){
+    showCover($('.thumbnail').attr('id'));
+});
+
+function showCover(img) {
+    $('.thumbnail').attr('class', 'thumbnail');
+    $('#largeCoverImg').attr('src', '/cgi-bin/koha/opac-image.pl?imagenumber=' + img);
+    $('#' + img + '.thumbnail').attr('class', 'thumbnail selected');
+}
+//]]>
+</script>
+<style type="text/css">
+img.thumbnail {
+    border-style: solid;
+    border-width: 3px;
+    border-color: white;
+}
+
+img.selected {
+    border-color: black;
+}
+</style>
+<script type="text/javascript" src="[% themelang %]/lib/jquery/plugins/jquery.tablesorter.min.js"></script>
+<body id="opac-imageviewer">
+<div id="largeCover"><img id="largeCoverImg" alt="Large view" /></div>
+[% IF OPACLocalCoverImages == 1 %]
+[% FOREACH img IN images %]
+[% IF img %]
+<a href='#' onclick='showCover([% img %])'><img class='thumbnail' id='[% img %]' src='/cgi-bin/koha/opac-image.pl?imagenumber=[% img %]&thumbnail=1' alt='Image'/></a>
+[% END %]
+[% END %]
+[% biblio.title %] [% biblio.author %]
+[% ELSE %]
+Unfortunately, images are not enabled for this catalog at this time.
+[% END %]
+</body>
+</html>
diff --git a/koha-tmpl/opac-tmpl/prog/en/modules/opac-results.tt b/koha-tmpl/opac-tmpl/prog/en/modules/opac-results.tt
index 7201a82..2565c8f 100755
--- a/koha-tmpl/opac-tmpl/prog/en/modules/opac-results.tt
+++ b/koha-tmpl/opac-tmpl/prog/en/modules/opac-results.tt
@@ -229,6 +229,7 @@ $(document).ready(function(){
         [% END %]
     [% END %][% END %]
     [% IF OpenLibraryCovers %]KOHA.OpenLibrary.GetCoverFromIsbn();[% END %]
+	[% IF OPACLocalCoverImages %]KOHA.LocalCover.GetCoverFromBibnumber(false);[% END %]
     [% IF ( GoogleJackets ) %]KOHA.Google.GetCoverFromIsbn();[% END %]
 });
 //]]>
@@ -528,6 +529,7 @@ $(document).ready(function(){
 				</span>
 				</td><td>
 					<a class="p1" href="/cgi-bin/koha/opac-detail.pl?biblionumber=[% SEARCH_RESULT.biblionumber %]">
+            [% IF ( OPACLocalCoverImages ) %]<div style="block" title="[% SEARCH_RESULT.biblionumber |url %]" class="[% SEARCH_RESULT.biblionumber %]" id="local-thumbnail[% loop.count %]"></div>[% END %]
                     [% IF ( OPACAmazonEnabled ) %][% IF ( OPACAmazonCoverImages ) %][% IF ( SEARCH_RESULT.normalized_isbn ) %]<img src="http://images.amazon.com/images/P/[% SEARCH_RESULT.normalized_isbn %].01.TZZZZZZZ.jpg" alt="" class="thumbnail" />[% ELSE %]<span class="no-image">No cover image available</span>[% END %][% END %][% END %]
 
 					[% IF ( SyndeticsEnabled ) %][% IF ( SyndeticsCoverImages ) %][% IF ( using_https ) %]
diff --git a/opac/opac-detail.pl b/opac/opac-detail.pl
index f82dde2..714ea24 100755
--- a/opac/opac-detail.pl
+++ b/opac/opac-detail.pl
@@ -45,6 +45,7 @@ use C4::Charset;
 use MARC::Record;
 use MARC::Field;
 use List::MoreUtils qw/any none/;
+use Koha::Images;
 
 BEGIN {
 	if (C4::Context->preference('BakerTaylorEnabled')) {
@@ -686,6 +687,11 @@ if (scalar(@serialcollections) > 0) {
 	serialcollections => \@serialcollections);
 }
 
+# Local cover Images stuff
+if (C4::Context->preference("OPACLocalCoverImages")){
+		$template->param(OPACLocalCoverImages => 1);
+}
+
 # Amazon.com Stuff
 if ( C4::Context->preference("OPACAmazonEnabled") ) {
     $template->param( AmazonTld => get_amazon_tld() );
@@ -911,4 +917,9 @@ my $defaulttab =
         ? 'serialcollection' : 'subscription';
 $template->param('defaulttab' => $defaulttab);
 
+if (C4::Context->preference('OPACLocalCoverImages') == 1) {
+    my @images = ListImagesForBiblio($biblionumber);
+    $template->{VARS}->{localimages} = \@images;
+}
+
 output_html_with_http_headers $query, $cookie, $template->output;
diff --git a/opac/opac-image.pl b/opac/opac-image.pl
new file mode 100755
index 0000000..de44152
--- /dev/null
+++ b/opac/opac-image.pl
@@ -0,0 +1,111 @@
+#!/usr/bin/perl
+#
+# based on patronimage.pl
+#
+# This file is part of Koha.
+#
+# Koha is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 2 of the License, or (at your option) any later
+# version.
+#
+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with Koha; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+#
+#
+
+use strict;
+use warnings;
+
+use CGI; #qw(:standard escapeHTML);
+use C4::Context;
+use Koha::Images;
+
+$|=1;
+
+my $DEBUG = 1;
+my $data = new CGI;
+my $imagenumber;
+
+=head1 NAME
+
+opac-image.pl - Script for retrieving and formatting local cover images for display
+
+=head1 SYNOPSIS
+
+<img src="opac-image.pl?imagenumber=X" />
+<img src="opac-image.pl?biblionumber=X" />
+<img src="opac-image.pl?imagenumber=X&thumbnail=1" />
+<img src="opac-image.pl?biblionumber=X&thumbnail=1" />
+
+=head1 DESCRIPTION
+
+This script, when called from within HTML and passed a valid imagenumber or
+biblionumber, will retrieve the image data associated with that biblionumber
+if one exists, format it in proper HTML format and pass it back to be displayed.
+If the parameter thumbnail has been provided, a thumbnail will be returned
+rather than the full-size image. When a biblionumber is provided rather than an
+imagenumber, a random image is selected.
+
+=cut
+
+if (defined $data->param('imagenumber')) {
+    $imagenumber = $data->param('imagenumber');
+} elsif (defined $data->param('biblionumber')) {
+    my @imagenumbers = ListImagesForBiblio($data->param('biblionumber'));
+    if (@imagenumbers) {
+        $imagenumber = $imagenumbers[0];
+    } else {
+        warn "No images for this biblio" if $DEBUG;
+        error();
+    }
+} else {
+    $imagenumber = shift;
+}
+
+if ($imagenumber) {
+    warn "imagenumber passed in: $imagenumber" if $DEBUG;
+    my ($imagedata, $dberror) = RetrieveImage($imagenumber);
+
+    if ($dberror) {
+        warn "Database Error!" if $DEBUG;
+        error();
+    }
+
+    if ($imagedata) {
+        my $image;
+        if ($data->param('thumbnail')) {
+            $image = $imagedata->{'thumbnail'};
+        } else {
+            $image = $imagedata->{'imagefile'};
+        }
+        print $data->header (-type => $imagedata->{'mimetype'}, -'Cache-Control' => 'no-store', -expires => 'now', -Content_Length => length ($image)), $image;
+        exit;
+    } else {
+        warn "No image exists for $imagenumber" if $DEBUG;
+        error();
+    }
+} else {
+    error();
+}
+
+error();
+
+sub error {
+    print $data->header ( -status=> '404', -expires => 'now' );
+    exit;
+}
+
+=head1 AUTHOR
+
+Chris Nighswonger cnighswonger <at> foundations <dot> edu
+
+modified for local cover images by Koustubha Kale kmkale <at> anantcorp <dot> com
+
+=cut
diff --git a/opac/opac-imageviewer.pl b/opac/opac-imageviewer.pl
new file mode 100755
index 0000000..6d2f326
--- /dev/null
+++ b/opac/opac-imageviewer.pl
@@ -0,0 +1,51 @@
+#!/usr/bin/perl
+
+# Copyright 2011 C & P Bibliography Services
+#
+# This file is part of Koha.
+#
+# Koha is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 2 of the License, or (at your option) any later
+# version.
+#
+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with Koha; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+use strict;
+use warnings;
+
+use CGI;
+use C4::Auth;
+use C4::Biblio;
+use C4::Output;
+use Koha::Images;
+
+my $query = new CGI;
+my ( $template, $borrowernumber, $cookie ) = get_template_and_user(
+    {
+        template_name   => "opac-imageviewer.tmpl",
+        query           => $query,
+        type            => "opac",
+        authnotrequired => ( C4::Context->preference("OpacPublic") ? 1 : 0 ),
+        flagsrequired   => { borrow => 1 },
+    }
+);
+
+my $biblionumber = $query->param('biblionumber') || $query->param('bib');
+my ($count, $biblio) = GetBiblio($biblionumber);
+
+if (C4::Context->preference("OPACLocalCoverImages")) {
+    my @images = ListImagesForBiblio($biblionumber);
+    $template->{VARS}->{'OPACLocalCoverImages'} = 1;
+    $template->{VARS}->{'images'} = \@images;
+}
+
+$template->{VARS}->{'biblio'} = $biblio;
+
+output_html_with_http_headers $query, $cookie, $template->output;
diff --git a/opac/opac-search.pl b/opac/opac-search.pl
index 42eb0ec..365bd72 100755
--- a/opac/opac-search.pl
+++ b/opac/opac-search.pl
@@ -612,6 +612,10 @@ for (my $i=0;$i<@servers;$i++) {
             $template->param(SEARCH_RESULTS => \@newresults,
                                 OPACItemsResultsDisplay => (C4::Context->preference("OPACItemsResultsDisplay") eq "itemdetails"?1:0),
                             );
+	    if (C4::Context->preference("OPACLocalCoverImages")){
+		$template->param(OPACLocalCoverImages => 1);
+		$template->param(OPACLocalCoverImagesPriority => C4::Context->preference("OPACLocalCoverImagesPriority"));
+	    }
             ## Build the page numbers on the bottom of the page
             my @page_numbers;
             # total number of pages there will be
diff --git a/tools/upload-cover-image.pl b/tools/upload-cover-image.pl
new file mode 100755
index 0000000..af45cbc
--- /dev/null
+++ b/tools/upload-cover-image.pl
@@ -0,0 +1,167 @@
+#!/usr/bin/perl
+#
+# Copyright 2011 C & P Bibliography Services
+#
+# This file is part of Koha.
+#
+# Koha is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 2 of the License, or (at your option) any later
+# version.
+#
+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# Koha; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
+# Suite 330, Boston, MA  02111-1307 USA
+#
+#
+#
+=head1 NAME
+
+upload-cover-image.pl - Script for handling uploading of both single and bulk coverimages and importing them into the database.
+
+=head1 SYNOPSIS
+
+upload-cover-image.pl
+
+=head1 DESCRIPTION
+
+This script is called and presents the user with an interface allowing him/her to upload a single cover image or bulk cover images via a zip file.
+Files greater than 100K will be refused. Images should be 140x200 pixels. If they are larger they will be auto-resized to comply.
+
+=cut
+
+
+use strict;
+use warnings;
+
+use File::Temp;
+use CGI;
+use GD;
+use C4::Context;
+use C4::Auth;
+use C4::Output;
+use Koha::Images;
+use C4::UploadedFile;
+
+my $debug = 1;
+
+my $input = new CGI;
+
+my $fileID=$input->param('uploadedfileid');
+my ($template, $loggedinuser, $cookie)
+	= get_template_and_user({template_name => "tools/upload-images.tmpl",
+					query => $input,
+					type => "intranet",
+					authnotrequired => 0,
+					flagsrequired => { tools => 'upload_cover_images'},
+					debug => 0,
+					});
+
+my $filetype            = $input->param('filetype');
+my $biblionumber        = $input->param('biblionumber');
+my $uploadfilename      = $input->param('uploadfile');
+my $replace             = $input->param('replace');
+my $op                  = $input->param('op');
+my %cookies             = parse CGI::Cookie($cookie);
+my $sessionID           = $cookies{'CGISESSID'}->value;
+
+my $error;
+
+$template->{VARS}->{'filetype'} = $filetype;
+$template->{VARS}->{'biblionumber'} = $biblionumber;
+
+my $total = 0;
+
+if ($fileID) {
+    my $uploaded_file = C4::UploadedFile->fetch($sessionID, $fileID);
+    if ($filetype eq 'image') {
+        my $fh = $uploaded_file->fh();
+        my $srcimage = GD::Image->new($fh);
+        if (defined $srcimage) {
+            my $dberror = PutImage($biblionumber, $srcimage, $replace);
+            if ($dberror) {
+                $error = 'DBERR';
+            } else {
+                $total = 1;
+            }
+        } else {
+            $error = 'OPNIMG';
+        }
+        undef $srcimage;
+    } else {
+        my $filename = $uploaded_file->filename();
+        my $dirname = File::Temp::tempdir( CLEANUP => 1);
+        unless (system("unzip", $filename,  '-d', $dirname) == 0) {
+            $error = 'UZIPFAIL';
+        } else {
+            my @directories;
+            push @directories, "$dirname";
+            foreach my $recursive_dir ( @directories ) {
+                my $dir;
+                opendir $dir, $recursive_dir;
+                while ( my $entry = readdir $dir ) {
+                    push @directories, "$recursive_dir/$entry" if ( -d "$recursive_dir/$entry" and $entry !~ /^[._]/ );
+                }
+                closedir $dir;
+            }
+            foreach my $dir ( @directories ) {
+                my $file;
+                if ( -e "$dir/idlink.txt" ) {
+                    $file = "$dir/idlink.txt";
+                } elsif ( -e "$dir/datalink.txt" ) {
+                    $file = "$dir/datalink.txt";
+                } else {
+                    next;
+                }
+                if (open (FILE, $file)) {
+                    while (my $line = <FILE>) {
+                        my $delim = ($line =~ /\t/) ? "\t" : ($line =~ /,/) ? "," : "";
+                        #$debug and warn "Delimeter is \'$delim\'";
+                        unless ( $delim eq "," || $delim eq "\t" ) {
+                            warn "Unrecognized or missing field delimeter. Please verify that you are using either a ',' or a 'tab'";
+                            $error = 'DELERR';
+                        } else {
+                            ($biblionumber, $filename) = split $delim, $line;
+                            $biblionumber =~ s/[\"\r\n]//g;  # remove offensive characters
+                            $filename   =~ s/[\"\r\n\s]//g;
+                            my $srcimage = GD::Image->new("$dir/$filename");
+                            if (defined $srcimage) {
+                                $total++;
+                                my $dberror = PutImage($biblionumber, $srcimage, $replace);
+                                if ($dberror) {
+                                    $error = 'DBERR';
+                                }
+                            } else {
+                                $error = 'OPNIMG';
+                            }
+                            undef $srcimage;
+                        }
+                    }
+                    close(FILE);
+                } else {
+                    $error = 'OPNLINK';
+                }
+            }
+        }
+    }
+    $template->{VARS}->{'total'} = $total;
+    $template->{VARS}->{'uploadimage'} = 1;
+    $template->{VARS}->{'error'} = $error;
+    $template->{VARS}->{'biblionumber'} = $biblionumber;
+}
+
+output_html_with_http_headers $input, $cookie, $template->output;
+
+exit 0;
+
+=head1 AUTHORS
+
+Written by Jared Camins-Esakov of C & P Bibliography Services, in part based on
+code by Koustubha Kale of Anant Corporation and Chris Nighswonger of Foundation
+Bible College.
+
+=cut
-- 
1.7.2.5


More information about the Patches mailing list