For “fun” in the last day or two I have been tinkering and revamping one of my own WordPress sites. One of the things I most enjoy about these episodes is that I typically enter with one goal in mind, and in the process of doing, figuring out, I end up working on things I did not expect to, or finding new things not on my list of “objectives”.

I’ve been sharing photos on flickr since 2004 and for some, that is a sufficient enough archive/collection. I forget the exact reason, maybe it was just to stretch my WordPress chops, in 2010 I decided to make a site for my favorite photos- I call it Barking Dog Studios (as if it were a real studio).

One of the happy accidental discoveries in 2010 was noting something in the images I uploaded that were exported from Aperture (my photo organizing tool; I know most people are bailing in it because Apple said they are not updating, but that does not mean its not functional now). When added to WordPress, the title of the image and the caption I entered in Aperture showed up in the WordPress media library. So digging in some more, I found that WordPress also imported other metadata in the image- the camera type, the aperture, shutter speed, lens info, ISO, and even a copyright statement.

So I wrote first about how this made my site work– that I could compose a post simply by uploading the image. Two years later, I wrote about some other WordPress features I was able to bake into the site.

barking-dog-studios

It’s been four years, and I’m back at it. The rest is a lot of code and stuff, so if that’s not your bag, just check out the new version of my photo blog http://barkingdog.me

Some of this was knowing that the theme I was using, Fullscreen from Graph Paper Press was tolerable on mobile devices, but was not responsive.

But also a comment from Pat Lockley about another WordPress site of mine got me thinking about a better way to design this one.

Without going too deep into details (I might already be there), Pat’s comment was on a post about the Daily Create, where on each single post page, I append some info to the bottom of the post. What I did there, was like I usually do, I modified the single.php template to add content I wanted to see.

What Pat suggested was using the WordPress filter the_content as a better way to tack on something to every post. It’s better because (a) I could modify it at anytime by editing one function; (b) this way it becomes part of posts content and so appears in its RSS feed; (c) probably some other reasons.

My Barking Dog site was a good way to try this out, since what I do on every single post is to find the attachment for the first image in the post, and then dig into the data to pull out the photo metadata. I was doing it before by changing the single.php template, but saw how I could do it differently.

At the same time, I looked at a new theme from Graph Paper Press called Lookbook that was responsive and had another nifty feature I will get to later.

Since my previous work was done as a Child Theme, I could make a copy of that and see all of the customizations I had made. Many would need to be redone for the new theme, but I made a copy of it to save as a copy just in case.

This is how the new site works for creating a photo entry post (and yes, in my screencast I stumbled on my own bug, its now fixed)

A first problem was that the old theme did not use Featured Images (it used the first image in the post). Easy fix with the Auto Post Thumbnail plugin. It creates a featured image out of the first image in a new post, and has an admin tool to do it for all old posts.

To get the new approach working for getting the exif data, I had to set the filter up for the_content this means, any place in WordPress that asks for the body of a post, my extra function gets called too.


// add a filter to append to the content
add_filter( 'the_content', 'barkingdog_content_filter', 20 );

function barkingdog_content_filter( $content ) {

    global $post; 

    // we only want to go into action for a single post
    if ( is_single() ) {
    
    	// append and EXIF info we can find from first image
    	$content .= get_exif( $post->ID );
    }
 
    // Return the content
    return $content;
}

All of the work is done by my function get_exif() which gets sent the ID value for the post in question. I use a query that gets all of the attachments for the post- this is everything uploaded from the post. When I add the image, that’s the file that has my metadata in it. But WordPress also creates copies for all the other sizes (thumbnail, medium, large, etc).

What I figured out was that the attachment with the oldest data was the original, so I set the parameters to make the creation date the way I ordered my results, oldest first. This way, I only need to ask for 1 attachment.

Then there is a bunch of crazy code (stuff I wrote in 2010) to get all the metadata from the image, and we return it all as a pile of text.

function get_exif( $pid ) {
	/*
	
	This function creates content for a photo  site where the only content
	is an uploaded image that has embedded EXIF data (e.g. exports from Aperture).
	
	Will be called for a filter on the_content
		
	Based a bit on 
	www.kristarella.com/2008/12/geo-exif-data-in-wordpress
	*/
	
	
	// set up arguments for query to get post attachments ordered by date, oldest first--
	// the oldest date should be the original image which has the camera meta data
	$args = array(
		'post_type' 	=> 'attachment',
		'numberposts' 	=> 1,
		'post_status' 	=> null,
		'order_by' 		=> date,
		'order' 		=> ASC,
		'post_parent' 	=> $pid
	);
	
	// get all attachments for the post
	$attachments = get_posts($args);
	
	// The first array item (actually only) returned should be the original image
	$pic = $attachments[0];

	// get image metadata for this attachment ID		
	$imgmeta = wp_get_attachment_metadata( $pic->ID ); 

	$exif_str = '<div id="photo-exif">';
	
		// use the photo caption as content
		if (!empty($imgmeta['image_meta']['caption'])) $exif_str.= "<p>" . nl2br( make_links_clickable( $imgmeta['image_meta']['caption'] ) ) . "</p>";
	
		// start list for photo metadata
		$exif_str.= "<h3>Photo Metadata</h3> <ul>";
	
		// time stamp
		if (!empty($imgmeta['image_meta']['created_timestamp'])) $exif_str .=  "<li><strong>When:</strong> " . date("M d, Y h:i:s a", $imgmeta['image_meta']['created_timestamp']) ."</li>";
	
		// camera type
		if (!empty($imgmeta['image_meta']['camera'])) $exif_str .=  "<li><strong>Camera:</strong> " . $imgmeta['image_meta']['camera']."</li>";
		
		// lens
		if (!empty($imgmeta['image_meta']['focal_length'])) $exif_str .=  "<li><strong>Focal Length:</strong> " . $imgmeta['image_meta']['focal_length']."mm</li>";
		
		// ISO
		if (!empty($imgmeta['image_meta']['iso'])) $exif_str .=  "<li><strong>ISO:</strong> " . $imgmeta['image_meta']['iso']."</li>";
	
		// aperture
		if (!empty($imgmeta['image_meta']['aperture'])) $exif_str .=  "<li><strong>Aperture:</strong> f/" . $imgmeta['image_meta']['aperture']."</li>";
		
		
		// Shutter speed
		if (!empty($imgmeta['image_meta']['shutter_speed'])) {
			$exif_str .=  "<li><strong>Shutter Speed:</strong> ";
				if ((1 / $imgmeta['image_meta']['shutter_speed']) > 1) {
					$exif_str .=  "1/";
				
					if ((number_format((1 / $imgmeta['image_meta']['shutter_speed']), 1)) == 1.3
					or number_format((1 / $imgmeta['image_meta']['shutter_speed']), 1) == 1.5
					or number_format((1 / $imgmeta['image_meta']['shutter_speed']), 1) == 1.6
					or number_format((1 / $imgmeta['image_meta']['shutter_speed']), 1) == 2.5) {
						$exif_str .=  number_format((1 / $imgmeta['image_meta']['shutter_speed']), 1, '.', '') . " s</li>";
					
					} else {
						$exif_str .=  number_format((1 / $imgmeta['image_meta']['shutter_speed']), 0, '.', '') . " s";
					}
				
				} else {
					$exif_str .=  $imgmeta['image_meta']['shutter_speed']." s";
				}
		}
		
		// license
		if (!empty($imgmeta['image_meta']['copyright'])) $exif_str .=  "<li><strong>Rights :</strong> " . $imgmeta['image_meta']['copyright']."</li>";
						
		$exif_str.= '</ul></div>';
		
		return ($exif_str);	
	}
}

And that works. For a photo of a sunset over the Superstition wilderness area, my “post” is just a single IMG tag

moon post

but my script extracts the rest of the information to add the caption and the photo metadata:

metadata

(Wow, not bad for an iPhone photo). So I had pretty much re-did the functionality I did before. But I am looking at all the photo metadata thinking… this is data… what can I do with it.

What if I could make things like the aperture and the camera model a link, and show all my other photos with the same info?

The first step was to create a custom taxonomy for the photo information (EXIF data it’s called) for my posts. By adding a custom taxonomy, the tags do not get mingled with my ordinary post tags which describe the imagery.

This is how I added a taxonomy called ‘Exif Tags’.

add_action( 'init', 'barkingdog_make_tax' );

function barkingdog_make_tax() {
	// create custom taxonomy for photo metadata	
	
	register_taxonomy(
		'exif', // Taxonomy name
		array( 'post') , // Post Types
		array( 
			'labels' => array(
						'name' => __( 'Exif Tags'),
						'singular_name' => __('Exif'),
						'search_items'               => __( 'Search Exif Tags' ),
						'all_items'                  => __( 'All Exif Tags' ),
						'edit_item'                  => __( 'Edit Exif Tags' ),
						'update_item'                => __( 'Update Exif Tag' ),
						'add_new_item'               => __( 'Add New Exif Tag' ),
						'new_item_name'              => __( 'New Exif Tag' ),
						'separate_items_with_commas' => __( 'Separate Exif Tags with commas' ),
						'add_or_remove_items'        => __( 'Add or remove exif tags' ),
						'choose_from_most_used'      => __( 'Choose from the most used exif tags' ),
						'not_found'                  => __( 'No exif tags found.' ),
						),
			'show_ui' => true,
			'show_admin_column' => false,
			'show_tagcloud' => true,
			'hierarchical' => false,
		)
	);
}

To make this go into action, I am back into WordPress filter land — this time I want to trigger an action every time a post is saved, so I tap into the save_post process with a hook.

// Save Post Action to convert the meta data to the custom taxonomy
add_action( 'save_post', 'barkingdog_tag_exif' );

function barkingdog_tag_exif ( $post_id ) {

	// If this is just a revision, skip
	if ( wp_is_post_revision( $post_id ) )
		return;

	get_exif( $post_id, 'tagexif' );
}

Okay, I sort of skipped a step. You might notice I am using the same get_exif() function I listed above, I am passing as a parameter the post ID so my function knows which post to look for attachments form.

But I am also sending a second parameter 'tagexif' because I want to use the same function but it will do something different.

The way I wrote the function, it returns a bunch of stuff used to output to the screen. But for the adding to the new taxonomy, it’s the same kind of logic, but its steps are different.

Rather than copying the code and making a second function, I can make it work like this, I have a new parameter, which by defining at the top, I can give it a default value if it is called without the second parameter.

function get_exif( $pid, $mode = 'post' ) {

:
:

// do some set up stuff

    if ( $mode == 'post' ) {
    
        // do the stuff it was doing before, this is defaul

    } elseif ( $mode == 'tagexif' ) {

        // do the new stuff I want to do

    }
}

So the stuff I want it to do if the function is sent a value of ‘tagexif’ is taking the data and adding it to the post’s custom taxonomy. And for the normal output, I added some code that generates links to the custom taxonomy links (using the helpful santize_title function to make the link right).

Okay, this is a long swoop of code. I doubt anyone is reading here now. But hey, this post is for me.

function get_exif( $pid, $mode = 'post' ) {
	/*
	
	This function creates content for a photo  site where the only content
	is an uploaded image that has embedded EXIF data (e.g. exports from Aperture).
	
	Will be called for a filter on the_content
		
	Based a bit on 
	www.kristarella.com/2008/12/geo-exif-data-in-wordpress
	*/
	
	
	// set up arguments for query to get post attachments ordered by date, oldest first--
	// the oldest date should be the original image which has the camera meta data
	$args = array(
		'post_type' 	=> 'attachment',
		'numberposts' 	=> 1,
		'post_status' 	=> null,
		'order_by' 		=> date,
		'order' 		=> ASC,
		'post_parent' 	=> $pid
	);
	
	// get all attachments for the post
	$attachments = get_posts($args);
	
	// The first array item (actually only) returned should be the original image
	$pic = $attachments[0];

	// get image metadata for this attachment ID		
	$imgmeta = wp_get_attachment_metadata( $pic->ID ); 

	if ( $mode == 'post' ) {
		// output to display meta data on post
		
		// wrap it in a div for custom formatting
		$exif_str = '<div id="photo-exif">';
	
		// use the photo caption as content
		if (!empty($imgmeta['image_meta']['caption'])) $exif_str.= "<p>" . nl2br( make_links_clickable( $imgmeta['image_meta']['caption'] ) ) . "</p>";
	
		// start list for photo metadata
		$exif_str.= "<h3>Photo Metadata</h3> <ul>";
	
		// time stamp
		if (!empty($imgmeta['image_meta']['created_timestamp'])) $exif_str.= "<li><strong>When:</strong> " . date("M d, Y h:i:s a", $imgmeta['image_meta']['created_timestamp']) ."</li>";
		
		// camera type
		if (!empty($imgmeta['image_meta']['camera'])) $exif_str.= '<li><strong>Camera:</strong> <a href="' . site_url() .  '/?exif=' . sanitize_title( $imgmeta['image_meta']['camera'] ) . '">' .  $imgmeta['image_meta']['camera'] . '</a></li>';
	
		// lens
		if (!empty($imgmeta['image_meta']['focal_length'])) $exif_str.= '<li><strong>Focal Length:</strong> <a href="' . site_url() .  '/?exif=' . sanitize_title( $imgmeta['image_meta']['focal_length'] . 'mm'  ) . '">' .  $imgmeta['image_meta']['focal_length'] . 'mm' . '</a></li>';
	
		// ISO
		if (!empty($imgmeta['image_meta']['iso'])) $exif_str.= '<li><strong>ISO:</strong> <a href="' . site_url() .  '/?exif=' . sanitize_title( 'ISO ' . $imgmeta['image_meta']['iso'] ) . '">' .  $imgmeta['image_meta']['iso'] . '</a></li>';

		// aperture
		if (!empty($imgmeta['image_meta']['aperture'])) $exif_str.= '<li><strong>Aperture:</strong> <a href="' . site_url() .  '/?exif=' . sanitize_title( 'f/' . $imgmeta['image_meta']['aperture'] ) . '">f/' . $imgmeta['image_meta']['aperture'] . '</a></li>';
	
	
		// Shutter speed
		if (!empty($imgmeta['image_meta']['shutter_speed'])) {
			$exif_str.= '<li><strong>Shutter Speed:</strong> <a href="' . site_url() .  '/?exif=';
				if ((1 / $imgmeta['image_meta']['shutter_speed']) > 1) {
			
					if ( (number_format((1 / $imgmeta['image_meta']['shutter_speed']), 1)) == 1.3
					or number_format((1 / $imgmeta['image_meta']['shutter_speed']), 1) == 1.5
					or number_format((1 / $imgmeta['image_meta']['shutter_speed']), 1) == 1.6
					or number_format((1 / $imgmeta['image_meta']['shutter_speed']), 1) == 2.5) {
					
						
						$exif_str .= sanitize_title( '1/' . number_format( (1 / $imgmeta['image_meta']['shutter_speed']), 1, '.', '') . 'sec') . '"/>' . '1/' . number_format( (1 / $imgmeta['image_meta']['shutter_speed']), 1, '.', '') . 'sec</a></li>';
				
					} else {
						
						$exif_str .= sanitize_title( '1/' . number_format( (1 / $imgmeta['image_meta']['shutter_speed'] ), 0, '.', '') .'sec' ) . '"/>' . '1/' . number_format( (1 / $imgmeta['image_meta']['shutter_speed']), 0, '.', '') . 'sec';
					}
			
				} else {
				
					$exif_str .= sanitize_title( $imgmeta['image_meta']['shutter_speed'] . 'sec' ) . '"/>' . $imgmeta['image_meta']['shutter_speed'] . 'sec';
				}
		}
	
	
		// license
		if (!empty($imgmeta['image_meta']['copyright'])) $exif_str.= "<li><strong>Rights :</strong> " . $imgmeta['image_meta']['copyright'] . '</li>';
						
		$exif_str.= '</ul></div>';
		
		return ($exif_str);
		
	} elseif ( $mode == 'tagexif' ) {
		// add metadata to post
		
		$exit_terms = [];
	
		// add camera type to post exif tags
		if ( !empty($imgmeta['image_meta']['camera'] ) )  $exit_terms[] = $imgmeta['image_meta']['camera'];
	
		// add lens to post exif tags 
		if (!empty($imgmeta['image_meta']['focal_length']) )   $exit_terms[] = $imgmeta['image_meta']['focal_length'] . 'mm';
	
		// add ISO to post exif tags 
		if (!empty($imgmeta['image_meta']['iso'])  )  $exit_terms[] = 'ISO ' . $imgmeta['image_meta']['iso'];

		// aperture
		if (!empty($imgmeta['image_meta']['aperture']) )  $exit_terms[] =  'f/' . $imgmeta['image_meta']['aperture'];
	
		// Shutter speed
		if (!empty($imgmeta['image_meta']['shutter_speed']) ) {
				if ((1 / $imgmeta['image_meta']['shutter_speed']) > 1) {
					$shutter = "1/";
			
					if ((number_format((1 / $imgmeta['image_meta']['shutter_speed']), 1)) == 1.3
					or number_format((1 / $imgmeta['image_meta']['shutter_speed']), 1) == 1.5
					or number_format((1 / $imgmeta['image_meta']['shutter_speed']), 1) == 1.6
					or number_format((1 / $imgmeta['image_meta']['shutter_speed']), 1) == 2.5) {
						$shutter .= number_format((1 / $imgmeta['image_meta']['shutter_speed']), 1, '.', '') . "sec";
				
					} else {
						$shutter .= number_format((1 / $imgmeta['image_meta']['shutter_speed']), 0, '.', '') . "sec";
					}
			
				} else {
					$shutter =  $imgmeta['image_meta']['shutter_speed'] . "sec";
				}
				$exit_terms[] =  $shutter;
		}
		
		wp_set_post_terms( $pid, $exit_terms, 'exif' );
	
	}
}

So upload a new photo, and it works (mostly). But now I have a different problem. Like 800 existing posts that do not have the new taxonomy settings.

I re-used some stuff I did before for the DS106 site, and was able to add a menu item to the Dashboard, under Tools, that marches through all posts and resets their Exif Tags (my custom taxonomy) based on the image in the posts.

I added my own special admin tool that resets the tags for all posts
I added my own special admin tool that resets the tags for all posts

Here is that code.

// create an Admin menu items to run a function that updates all photos for the taxonomy
add_action('admin_menu', 'barkingdog_tag_exif_update_page');

function barkingdog_tag_exif_update_page() {
	add_submenu_page( 'tools.php', 'Update EXIF Tags', 'Update EXIF Tags', 'manage_options', 'exit-update', 'barkingdog_exif_update_callback' );
}

function barkingdog_exif_update_callback() {
	// Admin page for updating all the EXIF taxonomy links
	echo '<div class="wrap"><div id="icon-tools" class="icon32"></div>';
	echo '<h2>Update EXIF Tags</h2><p>Updating now (chug chug chug)...</p><ul> ';

	$args = array( 'posts_per_page' => -1 );
	$allposts = get_posts( $args );
	
	foreach ( $allposts as $post ) :
  
  		setup_postdata( $post );
  		echo '<li>Finding and tagging EXIF for <strong><a href="' . get_edit_post_link( $post->ID ) .'">' . get_the_title( $post->ID ) . '</a></strong>';
  		get_exif( $post->ID, 'tagexif' );
  		echo ' DONE!</li>';
	
	endforeach; 
	wp_reset_postdata();
	
	echo '</ul></div>';

}

But wait, there’s more code! I have stuff working that shows me all the photos I have taken with my 300mm lens

exif 300mm

There’s something missing- how many photos have I taken with that lens? What I want (and made) is something like this:

exif plus

This was done by making a copy of the theme’s archive.php template in my child theme, and changing the part:

<header class="entry-header">
    <?php the_archive_title( '<h1 class="page-title">', '</h1>' ); ?>
</header>

to be

<header class="entry-header">
    <?php the_archive_title( '<h1 class="page-title">',  
           ' (' . $wp_query->post_count . ' out of ' 
           .  $GLOBALS['wp_query']->found_posts 
           . ' Photos)</h1>' ); ?>
</header>

Okay– $wp_query->post_count gives you the number of posts it is showing per page — 10 — (this comes out of your Reading Settings) while $GLOBALS['wp_query']->found_posts shows you the total number of posts — 36 — that fit the criteria that it used to get all matches for the archive from the database.

That’s quite a lot of new features, and I pushed some of my chops to make the stuff work. But there is one more bonus from this theme- and why it is called “Lookbook”. As you are looking through the front or category/archive views, you can click an image and see it has a “save” flag added.

saved

lookbookAnd with click a folder icon lights up in the top right corner with a red number on it. You can build a collection of images, that get added to it like a shopping cart (but there is nothing being sold here)… it’s more like a playlist or collection of photos.

When I go to the Lookbook, I can see all of the images I have selected:

lookbook

I can turn off the name and email fields on the form, but hey, cannot I get some user data too? But what it does is pretty cool- it generates a dynamic PDF book of all the images. The format is not stellar, but good enough (I hacked it to include the URLs for the posts on my site that contain the photo). Check it out!

Barking Dog Studio Lookbook example (PDF)

I’d add for those into reclaiming or PAPI-ing, this is another way I am managing in my own space media I push outward to other places. I also use the JetPack Publicize module to push all the photos to tumblr — I have no idea what else I have been pushing there since I have 37,000 items in it! https://cogdogblog.tumblr.com/

Anyhow, I am happy with the new look to my site, and hammering out some new code. With this new approach, I could turn the part it that does the photo EXIF data into a proper plugin, but I wonder how many people build photo sites from photos that have metadata? It really is here to produce something that works in my flow.

Mostly, a chunk of time getting some WordPress mods to work is a good thing all the way around.


Top / Featured image credit: Screenshot for the new iteration of my photo blog / gallery / WordPress rumpus room at http://barkingdog.me/

The post "New WP Theme / Under the Hood Features for Barking Dog Studios" was originally pulled like taffy through a needle's eye at CogDogBlog (http://cogdogblog.com/2016/04/under-the-hood-barking-dog/) on April 10, 2016.

1 Comment

  • Tom

    That is slick and, as always, I appreciate the in depth tutorials. I’ll have to keep it in mind as I intend a complete overhaul of my blog and mostly-dead portfolio site.

Leave a Comment

All fields are required. Your email address will not be published.