Merge pull request #4507 from JorisDebonnet/resampled-images-in-folders

Save resampled images into a folder structure indicating transformations
This commit is contained in:
Damian Mooyman 2015-09-01 11:16:23 +12:00
commit dc4c40f642
6 changed files with 121 additions and 46 deletions

View File

@ -238,7 +238,7 @@ class File extends DataObject {
*/ */
public static function find($filename) { public static function find($filename) {
// Get the base file if $filename points to a resampled file // Get the base file if $filename points to a resampled file
$filename = preg_replace('/_resampled\/[^-]+-/', '', $filename); $filename = Image::strip_resampled_prefix($filename);
// Split to folders and the actual filename, and traverse the structure. // Split to folders and the actual filename, and traverse the structure.
$parts = explode("/", $filename); $parts = explode("/", $filename);

View File

@ -78,6 +78,29 @@ class Filesystem extends Object {
} }
} }
/**
* Remove a directory, but only if it is empty.
*
* @param string $folder Absolute folder path
* @param boolean $recursive Remove contained empty folders before attempting to remove this one
* @return boolean True on success, false on failure.
*/
public static function remove_folder_if_empty($folder, $recursive = true) {
if (!is_readable($folder)) return false;
$handle = opendir($folder);
while (false !== ($entry = readdir($handle))) {
if ($entry != "." && $entry != "..") {
// if an empty folder is detected, remove that one first and move on
if($recursive && is_dir($entry) && self::remove_folder_if_empty($entry)) continue;
// if a file was encountered, or a subdirectory was not empty, return false.
return false;
}
}
// if we are still here, the folder is empty.
rmdir($folder);
return true;
}
/** /**
* Cleanup function to reset all the Filename fields. Visit File/fixfiles to call. * Cleanup function to reset all the Filename fields. Visit File/fixfiles to call.
*/ */

View File

@ -458,14 +458,13 @@ class HtmlEditorField_Toolbar extends RequestHandler {
// but GridField doesn't allow for this kind of metadata customization at the moment. // but GridField doesn't allow for this kind of metadata customization at the moment.
if($url = $request->getVar('FileURL')) { if($url = $request->getVar('FileURL')) {
if(Director::is_absolute_url($url) && !Director::is_site_url($url)) { if(Director::is_absolute_url($url) && !Director::is_site_url($url)) {
$url = $url;
$file = new File(array( $file = new File(array(
'Title' => basename($url), 'Title' => basename($url),
'Filename' => $url 'Filename' => $url
)); ));
} else { } else {
$url = Director::makeRelative($request->getVar('FileURL')); $url = Director::makeRelative($request->getVar('FileURL'));
$url = preg_replace('/_resampled\/[^-]+-/', '', $url); $url = Image::strip_resampled_prefix($url);
$file = File::get()->filter('Filename', $url)->first(); $file = File::get()->filter('Filename', $url)->first();
if(!$file) $file = new File(array( if(!$file) $file = new File(array(
'Title' => basename($url), 'Title' => basename($url),

View File

@ -93,6 +93,17 @@ class Image extends File implements Flushable {
return self::config()->backend; return self::config()->backend;
} }
/**
* Retrieve the original filename from the path of a transformed image.
* Any other filenames pass through unchanged.
*
* @param string $path
* @return string
*/
public static function strip_resampled_prefix($path) {
return preg_replace('/_resampled\/(.+\/|[^-]+-)/', '', $path);
}
/** /**
* Set up template methods to access the transformations generated by 'generate' methods. * Set up template methods to access the transformations generated by 'generate' methods.
*/ */
@ -114,6 +125,7 @@ class Image extends File implements Flushable {
$urlLink .= "<label class='left'>"._t('AssetTableField.URL','URL')."</label>"; $urlLink .= "<label class='left'>"._t('AssetTableField.URL','URL')."</label>";
$urlLink .= "<span class='readonly'><a href='{$this->Link()}'>{$this->RelativeLink()}</a></span>"; $urlLink .= "<span class='readonly'><a href='{$this->Link()}'>{$this->RelativeLink()}</a></span>";
$urlLink .= "</div>"; $urlLink .= "</div>";
// todo: check why the above code is here, since $urlLink is not used?
//attach the addition file information for an image to the existing FieldGroup create in the parent class //attach the addition file information for an image to the existing FieldGroup create in the parent class
$fileAttributes = $fields->fieldByName('Root.Main.FilePreview')->fieldByName('FilePreviewData'); $fileAttributes = $fields->fieldByName('Root.Main.FilePreview')->fieldByName('FilePreviewData');
@ -697,12 +709,25 @@ class Image extends File implements Flushable {
public function cacheFilename($format) { public function cacheFilename($format) {
$args = func_get_args(); $args = func_get_args();
array_shift($args); array_shift($args);
// Note: $folder holds the *original* file, while the Image we're working with
// may be a formatted image in a child directory (this happens when we're chaining formats)
$folder = $this->ParentID ? $this->Parent()->Filename : ASSETS_DIR . "/"; $folder = $this->ParentID ? $this->Parent()->Filename : ASSETS_DIR . "/";
$format = $format . Convert::base64url_encode($args); $format = $format . Convert::base64url_encode($args);
$filename = $format . "-" . $this->Name; $filename = $format . "/" . $this->Name;
$patterns = $this->getFilenamePatterns($this->Name);
if (!preg_match($patterns['FullPattern'], $filename)) { $pattern = $this->getFilenamePatterns($this->Name);
// Any previous formats need to be derived from this Image's directory, and prepended to the new filename
$prepend = array();
preg_match_all($pattern['GeneratorPattern'], $this->Filename, $matches, PREG_SET_ORDER);
foreach($matches as $formatdir) {
$prepend[] = $formatdir[0];
}
$filename = implode($prepend) . $filename;
if (!preg_match($pattern['FullPattern'], $filename)) {
throw new InvalidArgumentException('Filename ' . $filename throw new InvalidArgumentException('Filename ' . $filename
. ' that should be used to cache a resized image is invalid'); . ' that should be used to cache a resized image is invalid');
} }
@ -826,9 +851,9 @@ class Image extends File implements Flushable {
$generateFuncs = implode('|', $generateFuncs); $generateFuncs = implode('|', $generateFuncs);
$base64url_match = "[a-zA-Z0-9_~]*={0,2}"; $base64url_match = "[a-zA-Z0-9_~]*={0,2}";
return array( return array(
'FullPattern' => "/^((?P<Generator>{$generateFuncs})(?P<Args>" . $base64url_match . ")\-)+" 'FullPattern' => "/^((?P<Generator>{$generateFuncs})(?P<Args>" . $base64url_match . ")\/)+"
. preg_quote($filename) . "$/i", . preg_quote($filename) . "$/i",
'GeneratorPattern' => "/(?P<Generator>{$generateFuncs})(?P<Args>" . $base64url_match . ")\-/i" 'GeneratorPattern' => "/(?P<Generator>{$generateFuncs})(?P<Args>" . $base64url_match . ")\//i"
); );
} }
@ -842,40 +867,35 @@ class Image extends File implements Flushable {
$folder = $this->ParentID ? $this->Parent()->Filename : ASSETS_DIR . '/'; $folder = $this->ParentID ? $this->Parent()->Filename : ASSETS_DIR . '/';
$cacheDir = Director::getAbsFile($folder . '_resampled/'); $cacheDir = Director::getAbsFile($folder . '_resampled/');
// Find all paths with the same filename as this Image (the path contains the transformation info)
if(is_dir($cacheDir)) { if(is_dir($cacheDir)) {
if($handle = opendir($cacheDir)) { $files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($cacheDir));
while(($file = readdir($handle)) !== false) { foreach($files as $path => $file){
// ignore all entries starting with a dot if ($file->getFilename() == $this->Name) {
if(substr($file, 0, 1) != '.' && is_file($cacheDir . $file)) { $cachedFiles[] = $path;
$cachedFiles[] = $file;
}
} }
closedir($handle);
} }
} }
$pattern = $this->getFilenamePatterns($this->Name); $pattern = $this->getFilenamePatterns($this->Name);
foreach($cachedFiles as $cfile) { // Reconstruct the image transformation(s) from the format-folder(s) in the path
if(preg_match($pattern['FullPattern'], $cfile, $matches)) { // (if chained, they contain the transformations in the correct order)
if(Director::fileExists($cacheDir . $cfile)) { foreach($cachedFiles as $cf_path) {
$subFilename = substr($cfile, 0, -1 * strlen($this->Name)); preg_match_all($pattern['GeneratorPattern'], $cf_path, $matches, PREG_SET_ORDER);
preg_match_all($pattern['GeneratorPattern'], $subFilename, $subMatches, PREG_SET_ORDER);
$generatorArray = array();
$generatorArray = array(); foreach ($matches as $singleMatch) {
foreach ($subMatches as $singleMatch) { $generatorArray[] = array(
$generatorArray[] = array('Generator' => $singleMatch['Generator'], 'Generator' => $singleMatch['Generator'],
'Args' => Convert::base64url_decode($singleMatch['Args'])); 'Args' => Convert::base64url_decode($singleMatch['Args'])
} );
// Using array_reverse is important, as a cached image will
// have the generators settings in the filename in reversed
// order: the last generator given in the filename is the
// first that was used. Later resizements are prepended
$generatedImages[] = array ( 'FileName' => $cacheDir . $cfile,
'Generators' => array_reverse($generatorArray) );
}
} }
$generatedImages[] = array(
'FileName' => $cf_path,
'Generators' => $generatorArray
);
} }
return $generatedImages; return $generatedImages;
@ -922,8 +942,14 @@ class Image extends File implements Flushable {
$numDeleted = 0; $numDeleted = 0;
$generatedImages = $this->getGeneratedImages(); $generatedImages = $this->getGeneratedImages();
foreach($generatedImages as $singleImage) { foreach($generatedImages as $singleImage) {
unlink($singleImage['FileName']); $path = $singleImage['FileName'];
unlink($path);
$numDeleted++; $numDeleted++;
do {
$path = dirname($path);
}
// remove the folder if it's empty (and it's not the assets folder)
while(!preg_match('/assets$/', $path) && Filesystem::remove_folder_if_empty($path));
} }
return $numDeleted; return $numDeleted;

View File

@ -89,7 +89,7 @@ class HtmlEditorFieldTest extends FunctionalTest {
$this->assertEquals(20, (int)$xml[0]['height'], 'Height tag of resized image is set.'); $this->assertEquals(20, (int)$xml[0]['height'], 'Height tag of resized image is set.');
$neededFilename = 'assets/_resampled/ResizedImage' . Convert::base64url_encode(array(10,20)) . $neededFilename = 'assets/_resampled/ResizedImage' . Convert::base64url_encode(array(10,20)) .
'-HTMLEditorFieldTest_example.jpg'; '/HTMLEditorFieldTest_example.jpg';
$this->assertEquals($neededFilename, (string)$xml[0]['src'], 'Correct URL of resized image is set.'); $this->assertEquals($neededFilename, (string)$xml[0]['src'], 'Correct URL of resized image is set.');
$this->assertTrue(file_exists(BASE_PATH.DIRECTORY_SEPARATOR.$neededFilename), 'File for resized image exists'); $this->assertTrue(file_exists(BASE_PATH.DIRECTORY_SEPARATOR.$neededFilename), 'File for resized image exists');

View File

@ -119,7 +119,6 @@ class ImageTest extends SapphireTest {
* of the output image do not resample the file. * of the output image do not resample the file.
*/ */
public function testReluctanceToResampling() { public function testReluctanceToResampling() {
$image = $this->objFromFixture('Image', 'imageWithoutTitle'); $image = $this->objFromFixture('Image', 'imageWithoutTitle');
$this->assertTrue($image->isSize(300, 300)); $this->assertTrue($image->isSize(300, 300));
@ -170,7 +169,6 @@ class ImageTest extends SapphireTest {
* of the output image resample the file when force_resample is set to true. * of the output image resample the file when force_resample is set to true.
*/ */
public function testForceResample() { public function testForceResample() {
$image = $this->objFromFixture('Image', 'imageWithoutTitle'); $image = $this->objFromFixture('Image', 'imageWithoutTitle');
$this->assertTrue($image->isSize(300, 300)); $this->assertTrue($image->isSize(300, 300));
@ -315,23 +313,24 @@ class ImageTest extends SapphireTest {
$this->assertContains($argumentString, $imageThird->getFullPath(), $this->assertContains($argumentString, $imageThird->getFullPath(),
'Image contains background color for padded resizement'); 'Image contains background color for padded resizement');
$imageThirdPath = $imageThird->getFullPath(); $resampledFolder = dirname($image->getFullPath()) . "/_resampled";
$filesInFolder = $folder->find(dirname($imageThirdPath)); $filesInFolder = $folder->find($resampledFolder);
$this->assertEquals(3, count($filesInFolder), $this->assertEquals(3, count($filesInFolder),
'Image folder contains only the expected number of images before regeneration'); 'Image folder contains only the expected number of images before regeneration');
$imageThirdPath = $imageThird->getFullPath();
$hash = md5_file($imageThirdPath); $hash = md5_file($imageThirdPath);
$this->assertEquals(3, $image->regenerateFormattedImages(), $this->assertEquals(3, $image->regenerateFormattedImages(),
'Cached images were regenerated in the right number'); 'Cached images were regenerated in the right number');
$this->assertEquals($hash, md5_file($imageThirdPath), 'Regeneration of third image is correct'); $this->assertEquals($hash, md5_file($imageThirdPath), 'Regeneration of third image is correct');
/* Check that no other images exist, to ensure that the regeneration did not create other images */ /* Check that no other images exist, to ensure that the regeneration did not create other images */
$this->assertEquals($filesInFolder, $folder->find(dirname($imageThirdPath)), $this->assertEquals($filesInFolder, $folder->find($resampledFolder),
'Image folder contains only the expected image files after regeneration'); 'Image folder contains only the expected image files after regeneration');
} }
public function testRegenerateImages() { public function testRegenerateImages() {
$image = $this->objFromFixture('Image', 'imageWithMetacharacters'); $image = $this->objFromFixture('Image', 'imageWithoutTitle');
$image_generated = $image->ScaleWidth(200); $image_generated = $image->ScaleWidth(200);
$p = $image_generated->getFullPath(); $p = $image_generated->getFullPath();
$this->assertTrue(file_exists($p), 'Resized image exists after creation call'); $this->assertTrue(file_exists($p), 'Resized image exists after creation call');
@ -346,7 +345,7 @@ class ImageTest extends SapphireTest {
* ToDo: This doesn't seem like something that is worth testing - what is the point of this? * ToDo: This doesn't seem like something that is worth testing - what is the point of this?
*/ */
public function testRegenerateImagesWithRenaming() { public function testRegenerateImagesWithRenaming() {
$image = $this->objFromFixture('Image', 'imageWithMetacharacters'); $image = $this->objFromFixture('Image', 'imageWithoutTitle');
$image_generated = $image->ScaleWidth(200); $image_generated = $image->ScaleWidth(200);
$p = $image_generated->getFullPath(); $p = $image_generated->getFullPath();
$this->assertTrue(file_exists($p), 'Resized image exists after creation call'); $this->assertTrue(file_exists($p), 'Resized image exists after creation call');
@ -356,6 +355,7 @@ class ImageTest extends SapphireTest {
$newArgumentString = Convert::base64url_encode(array(300)); $newArgumentString = Convert::base64url_encode(array(300));
$newPath = str_replace($oldArgumentString, $newArgumentString, $p); $newPath = str_replace($oldArgumentString, $newArgumentString, $p);
if(!file_exists(dirname($newPath))) mkdir(dirname($newPath));
$newRelative = str_replace($oldArgumentString, $newArgumentString, $image_generated->getFileName()); $newRelative = str_replace($oldArgumentString, $newArgumentString, $image_generated->getFileName());
rename($p, $newPath); rename($p, $newPath);
$this->assertFalse(file_exists($p), 'Resized image does not exist at old path after renaming'); $this->assertFalse(file_exists($p), 'Resized image does not exist at old path after renaming');
@ -368,7 +368,7 @@ class ImageTest extends SapphireTest {
} }
public function testGeneratedImageDeletion() { public function testGeneratedImageDeletion() {
$image = $this->objFromFixture('Image', 'imageWithMetacharacters'); $image = $this->objFromFixture('Image', 'imageWithoutTitle');
$image_generated = $image->ScaleWidth(200); $image_generated = $image->ScaleWidth(200);
$p = $image_generated->getFullPath(); $p = $image_generated->getFullPath();
$this->assertTrue(file_exists($p), 'Resized image exists after creation call'); $this->assertTrue(file_exists($p), 'Resized image exists after creation call');
@ -381,7 +381,7 @@ class ImageTest extends SapphireTest {
* Tests that generated images with multiple image manipulations are all deleted * Tests that generated images with multiple image manipulations are all deleted
*/ */
public function testMultipleGenerateManipulationCallsImageDeletion() { public function testMultipleGenerateManipulationCallsImageDeletion() {
$image = $this->objFromFixture('Image', 'imageWithMetacharacters'); $image = $this->objFromFixture('Image', 'imageWithoutTitle');
$firstImage = $image->ScaleWidth(200); $firstImage = $image->ScaleWidth(200);
$firstImagePath = $firstImage->getFullPath(); $firstImagePath = $firstImage->getFullPath();
@ -400,7 +400,7 @@ class ImageTest extends SapphireTest {
* Tests path properties of cached images with multiple image manipulations * Tests path properties of cached images with multiple image manipulations
*/ */
public function testPathPropertiesCachedImage() { public function testPathPropertiesCachedImage() {
$image = $this->objFromFixture('Image', 'imageWithMetacharacters'); $image = $this->objFromFixture('Image', 'imageWithoutTitle');
$firstImage = $image->ScaleWidth(200); $firstImage = $image->ScaleWidth(200);
$firstImagePath = $firstImage->getRelativePath(); $firstImagePath = $firstImage->getRelativePath();
$this->assertEquals($firstImagePath, $firstImage->Filename); $this->assertEquals($firstImagePath, $firstImage->Filename);
@ -410,6 +410,33 @@ class ImageTest extends SapphireTest {
$this->assertEquals($secondImagePath, $secondImage->Filename); $this->assertEquals($secondImagePath, $secondImage->Filename);
} }
/**
* Tests the static function Image::strip_resampled_prefix, to ensure that
* the original filename can be extracted from the path of transformed images,
* both in current and previous formats
*/
public function testStripResampledPrefix() {
$orig_image = $this->objFromFixture('Image', 'imageWithoutTitleContainingDots');
// current format (3.3+). Example:
// assets/ImageTest/_resampled/ScaleHeightWzIwMF0=/ScaleWidthWzQwMF0=/test.image.with.dots.png;
$firstImage = $orig_image->ScaleWidth(200);
$secondImage = $firstImage->ScaleHeight(200);
$paths_1 = $firstImage->Filename;
$paths_2 = $secondImage->Filename;
// 3.2 format (did not work for multiple transformations)
$paths_3 = 'assets/ImageTest/_resampled/ScaleHeightWzIwMF0=-test.image.with.dots.png';
// 3.1 (and earlier) format (did not work for multiple transformations)
$paths_4 = 'assets/ImageTest/_resampled/ScaleHeight200-test.image.with.dots.png';
$this->assertEquals($orig_image->Filename, Image::strip_resampled_prefix($paths_1));
$this->assertEquals($orig_image->Filename, Image::strip_resampled_prefix($paths_2));
$this->assertEquals($orig_image->Filename, Image::strip_resampled_prefix($paths_3));
$this->assertEquals($orig_image->Filename, Image::strip_resampled_prefix($paths_4));
}
/** /**
* Test all generate methods * Test all generate methods
*/ */