I would like to offer my visitors the option to download the entire photo gallery (displayed on dedicated [gallery] 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. Thanks.

  • What do you mean by dedicated gallery pages?
    – NoBugs
    May 19, 2013 at 3:06
  • standard posts that display ONLY the gallery shortcode [gallery columns="4" link="file"] and no other content on the page. I included that in the description just incase it helped developers. May 19, 2013 at 6:15

2 Answers 2


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="#" 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.

  • note that if you're dealing with new 3.5-style galleries, that method to retrieve images from a gallery may not work for you.
    – Milo
    May 13, 2013 at 17:16
  • To clarify I am using the latest version of Wordpress 3.5.1. Johannes, I would be overly grateful if you were able to provide some sample code when you're next at your desktop. Thanks, Paul May 13, 2013 at 18:01
  • Hey Ralf, this is some fantastic work! Thanks for sharing. In order for my gallery images to display properly I have to include link="file" in the shortcode ie: [gallery link="file"] as the shortcode is re-written through your code I think is being left out and as a result my galleries are not being displayed correctly. Is there any way to fix that argument into your code? May 15, 2013 at 12:52
  • Normally the atributes are simply passed through to the original shortcode and will not be modified. The galleries are displayed as normal, but with some HTML appended. In my tests (with twentytwelve standrad theme) the galleries are displayed correctly.
    – Ralf912
    May 15, 2013 at 15:25
  • @PaulThomson I fixed some issues in the github repository. The code wasn't clean.
    – Ralf912
    May 15, 2013 at 22:23

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 . "\n\t\t" . $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 ).'">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!

  • I don't think this is a good idea, using download.php and the $_GET parameter I can download all files I want from your web folder (also on network shares), like wp-config.php Feb 11, 2020 at 15:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.