ZIP up all images displayed in a [gallery] and offer as download link

The question:

I would like to offer my visitors the option to download the entire photo gallery (displayed on dedicated pages) as a ZIP file displayed at the bottom of each gallery page. – The full-sized image will have to be included.

David Walsh has given some code in his post here to zip up files but I’m having troubles integrating that with WordPress functions.

I’m aware there is a NextGEN gallery download plugin but I’m not in a position to use that as I use the native wordpress gallery functions.

A similar question with an alternative (manual method) of completing the above can be found here: Plugin to download attached media files?

Any help would be greatly appreciated.

The Solutions:

Below are the methods you can try. The first solution is probably the best. Try others if the first one doesn’t work. Senior developers aren’t just copying/pasting – they read the methods carefully & apply them wisely to each case.

Method 1

First you have to get the images. How to get all images of a gallery is described here.

WordPress uses two classes for unzipping files. The PHP bilt in ZipArchive() (usage see David Walsh). And PclZip, you can found this class in wp-admin/includes/class-pclzip.php. If you got problems with ZipArchive() try the PclZip class.

Now you just to have to glue both together. Maybe I can post some sample code later, currently I am not at my desk.


Your question can be splitted into two parts. The first one is getting all images from a gallery. The second one is zipping the images and send the zip file.
I will only explain the first part, getting all images of a gallery, because zipping the files is slightly offtopic.

Maybe there are other solutions, but in this example I replace the original gallery shortcode with a custom one to get the images. The reason is, WordPress changed the galleries in v3.5 a bit.
Before 3.5, the images for a gallery are attachments of the post. After 3.5, the images are passed to the shortcode as an attribute. Since WP3.5 we can not longer get the attached images of a post, we have to fetch the list from the shortcode attributes. My strategy is to replace the original shortcode with a custom shortcode, grab the attributes and call the original shortcode to get the gallery output.

All gallery related things are within a class. To create a zip file, we can use another class that takes as input the output of the gallery class. Let’s start with a class and a simple constructor.

class GalleryZip
    private static $instance = null;

    public static $images = array();

    public static function get_instance() {
        if ( ! session_id() )

        if ( null === self::$instance )
            self::$instance = new self();

        return self::$instance;

    private final function __construct() {
        remove_shortcode( 'gallery' );
        add_shortcode( 'gallery', array( __CLASS__, 'gallery_zip_shortcode' ) );

We will call the method get_instance() later in the plugin with the hook plugins_loaded. In the constructor, we remove the original shortcode and replace it with our custom shortcode gallery_zip_shortcode(). Now we need the shortcode callback

public static function gallery_zip_shortcode( $atts ) {

    $post = get_post();

    if ( ! function_exists( 'gallery_shortcode' ) )
      require_once ABSPATH . 'wp-includes/media.php';

    self::get_gallery_images_from_shortcode( $post->ID, $atts );
    $output = gallery_shortcode( $atts );

    $gallery_id = count( self::$images[$post->ID] ) - 1;

    $link = sprintf( '<div><a href="#" rel="nofollow noreferrer noopener" gallery-id="%d" post-id="%d" class="gallery-zip">%s</a></div>', $gallery_id, $post->ID, __( 'Get as Zip' ) );
    $output .= $link;

    return $output;


The first thing in this method is to get the post because we need the post ID. Than we include wp-includes/media.php, this file contains the callback function for the original gallery shortcode. Now we call a method to get an array with all images, create the gallery output by calling the original gallery callback, create a link and append the link to the gallery output. The images itself, respectively the pathes to the images, are stored in the class variable $images, we need this array later.
The class variable $image holds an entry for each post with an gallery, so we can use the function either on frontpage or in single view. Each entry contains an array for each gallery, because there can be more than one gallery in each post.

The core of the plugin is the method to get the images from the shortcode.

protected static function get_gallery_images_from_shortcode( $id, $atts ) {

    // use the post ID if the attribute 'ids' is not set or empty
    $id = ( ! isset( $atts['ids'] ) || empty( $atts['ids'] ) ) ?
        (int) $id : $atts['ids'];

    $exclude = ( isset( $atts['exclude'] ) && ! empty( $atts['exclude'] ) ) ?
        $atts['exclude'] : '';

    if ( ! isset( self::$images[$id] ) || ! is_array( self::$images[$id] ) )
        self::$images[$id] = array();

    $images = self::get_gallery_images( $id, $exclude );

    array_push( self::$images[$id], $images );

    return $images;


At first we decide if it is a single post or a list of post IDs. If it is a list of post IDs, we handle a gallery from WP3.5+. After that, we have to handle the exclude attribute. After setup all varibles, we can finally get the images from the gallery. The retrived images will be pushed into the class var $images for later use.

protected static function get_gallery_images( $id, $exclude ) {
    $images     = array();
    $query_args = array(
            'post_status'    => 'inherit',
            'post_type'      => 'attachment',
            'post_mime_type' => 'image',

    // handle gallery WP3.5+
    // if $id contains an comma, it is a list of post IDs
    if ( false !== strpos( $id, ',' ) ) {
        $query_args['include'] = $id;
    } elseif ( ! empty( $exclude ) ) {
        // handle excluding posts
        $query_args['post_parent'] = $id;
        $query_args['exclude']     = $exclude;
    } else {
        // handle gallery before WP3.5
        $query_args['post_parent'] = $id;

    $attachments = get_posts( $query_args );

    $img_sizes = array_merge( array( 'full' ), get_intermediate_image_sizes() );

    $img_size = ( in_array( self::IMAGE_SIZE, $img_sizes ) ) ?
            self::IMAGE_SIZE : 'full';

    foreach ( $attachments as $key => $post ) {
        $img = wp_get_attachment_image_src( $post->ID, $img_size, false, false );
        $images[] = sprintf( '%s/%s', dirname( get_attached_file( $post->ID ) ), basename( $img[0] ) );

    return $images;

This is the gold of the plugin. Simply setup an array with query arguments, get the attachments with get_posts() and walk over the retrieved attachments. To handle different sizes, we get the attachment image and strip of the url. From the attached file, we take the path and put it together with the filename. In the array $images are now all images and their pathes from the gallery.

Basically your question is answered at this point. But you also want to createa zip file from the images. You could create a zip file from the array $images in the last method. But this method is called everytime a gallery is displayed and creating a zip file could take a while. Maybe noone would request the zip file you created here, this is a waste of resources.

How can we do it better? Do you remember that I put all images in the class variable $images? We can use this class var for an ajax request. But an ajax request is just another page load and we can access the images only when the output of the gallery is created. We have to save our images in a place where we can access them even after a another page request.
In this example I use a session variable to store the array with images. A session variable can be accessed even after another page reload. To store the images, I register a method with the shutdownhook. After WordPress finished rendering the page, the shutdown hook will be called. At this point, we should have collected all images from all displayed galleries. We simply store the images and can access them in an ajax request.

When the ajax request is triggered, we recall the session var and create a zip file from the data. But this is a bit off topic for this question.

I created a repository on GitHub with the complete plugin code. I hope it points you in the right direction.

Method 2

I like the idea of Ralf’s plugin to be able to download a whole gallery in one go, but I haven’t been able to get it to work. I’ve come up with a workaround that works for our purposes. The method is to replace the native WP gallery with your own one which you place at the end of your theme’s functions.php file AND add the following file, named download.php into the active theme folder. In the custom gallery a link under the file calls the file download.php which automatically forces the download of your file to the hard drive. I’ve tested this on the latest Chrome, Firefox and Safari versions and it works fine. Have been using Twenty Twelve theme, but no reason why it shouldn’t work on other ones too.

a) Add the following to the end of functions.php. This is simply taken from media.php

function gallery_with_download_links($attr) {
    $post = get_post();
    static $instance = 0;
    if ( ! empty( $attr['ids'] ) ) {
        // 'ids' is explicitly ordered, unless you specify otherwise.
        if ( empty( $attr['orderby'] ) )
            $attr['orderby'] = 'post__in';
        $attr['include'] = $attr['ids'];
    // Allow plugins/themes to override the default gallery template.
    $output = apply_filters('post_gallery', '', $attr);
    if ( $output != '' )
        return $output;
    // We're trusting author input, so let's at least make sure it looks like a valid orderby statement
    if ( isset( $attr['orderby'] ) ) {
        $attr['orderby'] = sanitize_sql_orderby( $attr['orderby'] );
        if ( !$attr['orderby'] )
            unset( $attr['orderby'] );

        'order'      => 'ASC',
        'orderby'    => 'menu_order ID',
        'id'         => $post->ID,
        'itemtag'    => 'dl',
        'icontag'    => 'dt',
        'captiontag' => 'dd',
        'columns'    => 3,
        'size'       => 'thumbnail',
        'include'    => '',
        'exclude'    => ''
    ), $attr));

    $id = intval($id);
    if ( 'RAND' == $order )
        $orderby = 'none';

    if ( !empty($include) ) {
        $_attachments = get_posts( array('include' => $include, 'post_status' => 'inherit', 'post_type' => 'attachment', 'post_mime_type' => 'image', 'order' => $order, 'orderby' => $orderby) );

        $attachments = array();
        foreach ( $_attachments as $key => $val ) {
            $attachments[$val->ID] = $_attachments[$key];
    } elseif ( !empty($exclude) ) {
        $attachments = get_children( array('post_parent' => $id, 'exclude' => $exclude, 'post_status' => 'inherit', 'post_type' => 'attachment', 'post_mime_type' => 'image', 'order' => $order, 'orderby' => $orderby) );
    } else {
        $attachments = get_children( array('post_parent' => $id, 'post_status' => 'inherit', 'post_type' => 'attachment', 'post_mime_type' => 'image', 'order' => $order, 'orderby' => $orderby) );

    if ( empty($attachments) )
        return '';

    if ( is_feed() ) {
        $output = "n";
        foreach ( $attachments as $att_id => $attachment )
            $output .= wp_get_attachment_link($att_id, $size, true) . "n";
        return $output;

    $itemtag = tag_escape($itemtag);
    $captiontag = tag_escape($captiontag);
    $icontag = tag_escape($icontag);
    $valid_tags = wp_kses_allowed_html( 'post' );
    if ( ! isset( $valid_tags[ $itemtag ] ) )
        $itemtag = 'dl';
    if ( ! isset( $valid_tags[ $captiontag ] ) )
        $captiontag = 'dd';
    if ( ! isset( $valid_tags[ $icontag ] ) )
        $icontag = 'dt';

    $columns = intval($columns);
    $itemwidth = $columns > 0 ? floor(100/$columns) : 100;
    $float = is_rtl() ? 'right' : 'left';

    $selector = "gallery-{$instance}";

    $gallery_style = $gallery_div = '';
    if ( apply_filters( 'use_default_gallery_style', true ) )
        $gallery_style = "
        <style type='text/css'>
            #{$selector} {
                margin: auto;
            #{$selector} .gallery-item {
                float: {$float};
                margin-top: 10px;
                text-align: center;
                width: {$itemwidth}%;
            #{$selector} img {
                border: 2px solid #cfcfcf;
            #{$selector} .gallery-caption {
                margin-left: 0;
        <!-- see gallery_shortcode() in wp-includes/media.php -->";
    $size_class = sanitize_html_class( $size );
    $gallery_div = "<div id='$selector' class='gallery galleryid-{$id} gallery-columns-{$columns} gallery-size-{$size_class}'>";
    $output = apply_filters( 'gallery_style', $gallery_style . "ntt" . $gallery_div );

    $i = 0;
    foreach ( $attachments as $id => $attachment ) {
        $link = isset($attr['link']) && 'file' == $attr['link'] ? wp_get_attachment_link($id, $size, false, false) : wp_get_attachment_link($id, $size, true, false);

        $output .= "<{$itemtag} class='gallery-item'>";
        $output .= "
            <{$icontag} class='gallery-icon'>
        if ( $captiontag && trim($attachment->post_excerpt) ) {
            $output .= "
                <{$captiontag} class='wp-caption-text gallery-caption'>
                " . wptexturize($attachment->post_excerpt) . "
// This is my addon which outputs a link to download the file through download.php . NB your file uri will be public! 
        $output .= '<br/ ><a href="'.get_template_directory_uri().'/download.php?file='.get_attached_file( $id ).'" rel="nofollow noreferrer noopener">Download image</a>';
        $output .= "</{$itemtag}>";
        if ( $columns > 0 && ++$i % $columns == 0 )
            $output .= '<br style="clear: both" />';

    $output .= "
            <br style='clear: both;' />

    return $output;
add_shortcode( 'gallery' , 'gallery_with_download_links' );

b) Copy and paste the following into a file called download.php in the theme’s base directory.

$file = $_GET['file'];
if (file_exists($file)) {
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename='.basename($file));
header('Content-Transfer-Encoding: binary');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($file));

c). Don’t forget to link to the file in the gallery!! Important!

All methods was sourced from or, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Comment