Skip to content

Instantly share code, notes, and snippets.

@janfabry
Created November 13, 2010 16:04
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save janfabry/675438 to your computer and use it in GitHub Desktop.
Save janfabry/675438 to your computer and use it in GitHub Desktop.
WordPress Resize img tags plugin (first public attempt)
<?php
/*
Plugin Name: Resize img tags
Plugin URI: http://www.monkeyman.be
Description: Change image src's based on width and height specified in tag
Version: 0.1
Author: Jan Fabry
This plugin rewrites <img> tags so that the src contains width and height information. It is supposed to be used together with a plugin that can generate these resized images on the fly, like monkeyman-on-demand-resizer.
The format is currently image-100x200.jpg for an image that is 100 pixels wide and 200 pixels high. Partial sizes work too, like 100x or x200. Existing size information is stripped, which can be annoying for filenames that look like they have size information (image-2.jpg is probably not a 2-pixel wide image). Maybe I should restrict the size modifier to always include the final 'x', and not make it optional (see $sizeRegex).
The plugin currently does not remove the size modifier if the size is equals to the original image. This could also be solved by creating a server-side redirect from image-300x400.jpg to image.jpg if the original image is already 300x400.
It tries to be smart and add 'tmp=1' to the query params when we are in the editor and on preview pages. This param will always be removed when we are not - maybe that should be overridable too? It probably also messes up with the WordPress TinyMCE image plugin (that ties images to their attachment metadata).
This plugin is split up in a lot of submethods, which should make it easily extendable. Probably better to also include lots of hooks.
filterContent($sContent, $idPost, $isTemp) accepts the content to be filtered and optionally an ID of the post (which could make the replacer smarter by looking at attachment metadata), and an indicator on whether to include or remove the 'tmp-1' query param.
It first fires parseImages($sContent, $idPost) to get out all image tags. This function returns an array with the offset in the text, the whole image tag, and an kses-inspired array of attributes. It should be very flexibile in the type of input it accepts (single quotes and stuff). Parsing HTML with regexes is a bad idea [ http://stackoverflow.com/questions/1732348/regex-match-open-tags-except-xhtml-self-contained-tags/1732454#1732454 ], but I don't think there is a better way without unintentionally re-formatting the message [ http://stackoverflow.com/questions/4155255/keeping-file-offsets-while-parsing-html-with-the-dom ].
Next, the returned images are filtered by imageIsInScope($imgSrc, $idPost). Currently it tries to make sure the image URL begins with our upload URL. This does not work for relative paths (but you should not use relative paths anyway).
Replacement information is created by getImageReplacement($image, $idPost, $isTemp), which gets the image struct from parseImages() and tries to add size information. It first strips off size information and then adds it back on. To build the URLs and tags I created two helper functions that probably already exist in WordPress, but I don't know whereL buildUrl($urlParts), which accepts a parse_url()-style array, and buildTag($attr, $tagName) which accepts the kses-like output of parseImages(). The latter function preserves quote-style (single vs. double) for attribute values, I think this is needed.
The ideal place to hook this plugin is a filter on content. However, if we want the post ID, I don't think we can use pre_post_content or content_save_pre, since they run before the post is saved. In practice, with all the saved drafts and stuff, it seems the ID is already "reserved" in the database (filled with draft content), and we could still use this filter. Maybe only XML-RPC calls do it the "classic" way, where sanitize_post() runs before anything is saved.
For that reason, we hook into the `save_post` hook, and re-save the post if the content is changed. This means of course we have to prevent recursive save-loops, and it probably also is not so great for other plugins that hook into this action. But I was educated with the idea that "naked" database calls are
indecent, so this is the way it is.
For adding the 'tmp=1' query param when editing the post, we can hook into the `edit_post_content` filter, because it also gets the post id.
This plugin was created with other image-modifing plugins in mind, like one that attaches hotlinked images and expects this plugin to resize them too. This is also an argument to run on `save_post` and not `content_save_pre`, since such a plugin will probably expect a complete post object to attach stuff. And what is an extra database call in the larger image of WordPress performance?
A nice addition to this plugin is probably the TinyMCE 'advanced image scale' plugin [ http://code.google.com/p/tinymce-plugin-advimagescale/ ]. This allows you to rewrite the <img> tag on resize, so we can add our image size in JavaScript.
I also left in a simple test page where you can input test content and get back the result. Don't expect too much from it.
If you think the variables have weird names ($idPost instead of $post_id), it's because I am trying to use "Apps Hungrarian" notation [ http://www.joelonsoftware.com/articles/Wrong.html ] to indicate the type of the value stored there. But I'm not consistent enough (and it clashes with WordPress coding standard), and maybe should rethink it.
*/
class Monkeyman_ResizeImgTags
{
protected $idCurrentPost = null;
public function __construct()
{
add_action('save_post', array(&$this, 'applyFilterOnPost'), 50, 2);
add_action('edit_post_content', array(&$this, 'applyFilterOnEdit'), 50, 2);
// add_action('admin_menu', array(&$this, 'addAdminPages'));
}
public function applyFilterOnPost($idPost, $post)
{
if ($idPost == $this->idCurrentPost) {
return;
}
$sContent = $post->post_content;
$isTemp = false;
if (isset($GLOBALS['action']) && $GLOBALS['action'] == 'preview') {
$isTemp = true;
}
$sNewContent = $this->filterContent($sContent, $idPost, $isTemp);
if ($sContent != $sNewContent) {
$this->idCurrentPost = $idPost;
wp_update_post(array(
'ID' => $idPost,
'post_content' => $sNewContent,
));
$this->idCurrentPost = null;
}
}
public function applyFilterOnEdit($sContent, $idPost)
{
return $this->filterContent($sContent, $idPost, true);
}
public function filterContent($sContent, $idPost = null, $isTemp = false)
{
$images = $this->parseImages($sContent, $idPost);
$replacements = array();
foreach ($images as $offset => $image) {
if ($this->imageIsInScope($image['attr']['src']['value'], $idPost)) {
$replacement = $this->getImageReplacement($image, $idPost, $isTemp);
if (false !== $replacement) {
$replacements[$offset] = array(
'offset' => $offset,
'orig' => $image['whole'],
'replacement' => $replacement,
);
}
}
}
krsort($replacements);
foreach ($replacements as $offset => $replacement) {
$sContent = substr_replace($sContent, $replacement['replacement'], $replacement['offset'], strlen($replacement['orig']));
}
return $sContent;
}
public function parseImages($sContent, $idPost = null)
{
/*
* Regexes used in other plugins
* - imagescaler: '/<img([^>"]|"[^"]*")*>/i'
* - autothumb: '/<img[^>]*>/'
* - choicecuts-image-juggler: "/<img[^<>]+>/", "/\<img(.*)\/\>/"
* - hungred-image-fit: '/(<img(.*?)"?\'?\s*?\/?>)/i'
* - jd-redesign-images: '/<img[^>]+>/is'
*/
$images = array();
if (preg_match_all('/<img\s[^>]*>/is', $sContent, $aImgTagMatches, PREG_OFFSET_CAPTURE)) {
foreach ($aImgTagMatches[0] as $imgTagMatch) {
$imgTag = $imgTagMatch[0];
$imgTagOffset = $imgTagMatch[1];
$attributes = array();
foreach (wp_kses_hair($imgTag, array('http', 'https')) as $attrName => $ksesAttr) {
// 'name', 'value', 'whole', 'vless'
$quoteChar = substr($ksesAttr['whole'], -1);
$attributes[$attrName] = array(
'value' => $ksesAttr['value'],
'quoteChar' => $quoteChar,
'whole' => $ksesAttr['whole'],
'isValueless' => ($ksesAttr['vless'] == 'y'),
);
}
$images[$imgTagOffset] = array(
'whole' => $imgTag,
'attr' => $attributes,
);
}
}
return $images;
}
public function getImageReplacement($image, $idPost = NULL, $isTemp = false)
{
// $image['attr']['src']['value'] .= '-banana';
$imgUrl = $image['attr']['src']['value'];
$imgWidth = (isset($image['attr']['width']) ? $image['attr']['width']['value'] : '');
$imgHeight = (isset($image['attr']['height']) ? $image['attr']['height']['value'] : '');
$sizeString = '';
if ($imgWidth || $imgHeight) {
$sizeString = '-' . $imgWidth . 'x' . $imgHeight;
}
$imgUrlInfo = parse_url($imgUrl);
$imgPathInfo = pathinfo($imgUrlInfo['path']);
$imgFileName = $imgPathInfo['filename'];
// Regex for:
// - 200, 200x
// - x400
// - 200x400
$sizeRegex = '((\d+)x?|x(\d+)|(\d+)x(\d+))';
$imgFileName = preg_replace('/-' . $sizeRegex . '$/i', '', $imgFileName) . $sizeString;
$imgUrlInfo['path'] = $imgPathInfo['dirname'] . '/' . $imgFileName . '.' . $imgPathInfo['extension'];
$queryParams = array();
if (!empty($imgUrlInfo['query'])) {
parse_str($imgUrlInfo['query'], $queryParams);
}
if ($isTemp) {
$queryParams['tmp'] = 1;
} else {
unset($queryParams['tmp']);
}
$imgUrlInfo['query'] = $queryParams;
$newImgUrl = $this->buildUrl($imgUrlInfo);
$image['attr']['src']['value'] = $newImgUrl;
return $this->buildTag($image['attr']);
}
public function buildUrl($urlParts)
{
$url = '';
if (!empty($urlParts['scheme'])) {
$url .= $urlParts['scheme'] . '://';
if (!empty($urlParts['username'])) {
$url .= $urlParts['username'];
if (!empty($urlParts['password'])) {
$url .= ':' . $urlParts['password'];
}
$url .= '@';
}
$url .= $urlParts['host'];
if (!empty($urlParts['port'])) {
$url .= ':' . $urlParts['port'];
}
}
$url .= $urlParts['path'];
if (!empty($urlParts['query'])) {
$url .= '?';
if (is_array($urlParts['query'])) {
$url .= http_build_query($urlParts['query']);
} else {
$url .= $urlParts['query'];
}
}
if (!empty($urlParts['fragment'])) {
$url .= '#' . $urlParts['fragment'];
}
return $url;
}
public function buildTag($attr, $tagName = 'img')
{
$tag = '<' . $tagName;
foreach ($attr as $name => $data) {
$tag .= ' ' . $name;
if (!$data['isValueless']) {
$tag .= '=' . $data['quoteChar'];
$tag .= $data['value'];
$tag .= $data['quoteChar'];
}
}
$tag .= ' />';
return $tag;
}
protected static $uploadInfo = null;
public function imageIsInScope($imgSrc, $idPost = NULL)
{
if (is_null(self::$uploadInfo)) {
self::$uploadInfo = wp_upload_dir();
}
$baseUploadUrl = self::$uploadInfo['baseurl'];
return (0 === strncmp($imgSrc, $baseUploadUrl, strlen($baseUploadUrl)));
}
/**
* Test pages
*/
public function addAdminPages()
{
add_management_page( 'Resize img tags test', 'Resize img tags test', 'edit_posts', __FILE__, array(&$this, 'doTestPage'));
}
public function doTestPage()
{
$content = isset($_REQUEST['content']) ? stripslashes($_REQUEST['content']) : '';
$filteredContent = $this->filterContent($content);
echo <<<EOF
<form method="post">
<textarea name="content" rows="10" cols="100">{$content}</textarea>
<textarea name="filteredContent" rows="10" cols="100">{$filteredContent}</textarea>
<input type="submit"/>
</form>
<div>{$content}</div>
<div>{$filteredContent}</div>
EOF;
}
}
$monkeyman_ResizeImgTags_instance = new Monkeyman_ResizeImgTags();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment