From 838926085cac00ec65ee5aecb67e6102ea8b6f20 Mon Sep 17 00:00:00 2001 From: Jonathon Menz Date: Fri, 12 Jun 2015 16:42:51 -0700 Subject: [PATCH] API New and renamed image functions Renamed image functions with more expressive names. Added CropWidth & CropHeight functions. Added no-upsampling capabilities. Cleaned up Image docs. Closes #4211 --- .../01_Tutorials/02_Extending_A_Basic_Site.md | 8 +- .../14_Files/01_File_Management.md | 40 +++ .../02_Developer_Guides/14_Files/01_Image.md | 135 -------- .../02_Developer_Guides/14_Files/02_Images.md | 163 ++++++++++ docs/en/02_Developer_Guides/14_Files/index.md | 45 +-- docs/en/04_Changelogs/3.2.0.md | 12 + docs/en/_images/image-methods.jpg | Bin 0 -> 33416 bytes filesystem/File.php | 2 +- forms/HtmlEditorField.php | 2 +- forms/UploadField.php | 4 +- model/Image.php | 293 ++++++++++++++++-- .../Includes/HtmlEditorField_viewfile.ss | 2 +- tests/filesystem/GDTest.php | 14 +- tests/model/GDImageTest.php | 6 +- tests/model/ImageTest.php | 144 +++++---- 15 files changed, 603 insertions(+), 267 deletions(-) create mode 100644 docs/en/02_Developer_Guides/14_Files/01_File_Management.md delete mode 100644 docs/en/02_Developer_Guides/14_Files/01_Image.md create mode 100644 docs/en/02_Developer_Guides/14_Files/02_Images.md create mode 100644 docs/en/_images/image-methods.jpg diff --git a/docs/en/01_Tutorials/02_Extending_A_Basic_Site.md b/docs/en/01_Tutorials/02_Extending_A_Basic_Site.md index 499dea2be..36b484a3c 100644 --- a/docs/en/01_Tutorials/02_Extending_A_Basic_Site.md +++ b/docs/en/01_Tutorials/02_Extending_A_Basic_Site.md @@ -512,7 +512,7 @@ The staff section templates aren't too difficult to create, thanks to the utilit <% loop $Children %>

$Title

- $Photo.SetWidth(150) + $Photo.ScaleWidth(150)

$Content.FirstParagraph

Read more >>
@@ -521,7 +521,7 @@ The staff section templates aren't too difficult to create, thanks to the utilit -This template is very similar to the *ArticleHolder* template. The *SetWidth* method of the `[api:Image]` class +This template is very similar to the *ArticleHolder* template. The *ScaleWidth* method of the `[api:Image]` class will resize the image before sending it to the browser. The resized image is cached, so the server doesn't have to resize the image every time the page is viewed. @@ -537,13 +537,13 @@ The *StaffPage* template is also very straight forward.

$Title

- $Photo.SetWidth(433) + $Photo.ScaleWidth(433) $Content
$Form -Here we use the *SetWidth* method to get a different sized image from the same source image. You should now have +Here we use the *ScaleWidth* method to get a different sized image from the same source image. You should now have a complete staff section. ![](../_images/tutorial2_einstein.jpg) diff --git a/docs/en/02_Developer_Guides/14_Files/01_File_Management.md b/docs/en/02_Developer_Guides/14_Files/01_File_Management.md new file mode 100644 index 000000000..f1d957a29 --- /dev/null +++ b/docs/en/02_Developer_Guides/14_Files/01_File_Management.md @@ -0,0 +1,40 @@ +summary: Learn how to work with File and Image records + +# File Management + +## Files, Images and Folders as database records + +All files, images and folders in the 'assets' directory are stored in the database. Each record has the following database fields: + +| Field name | Description | +| ---------- | ----------- | +| `ClassName` | The class name of the file (e.g. File, Image or Folder). | +| `Name` | The 'basename' of the file, or the folder name. For example 'my-image.jpg', or 'images' for a folder. | +| `Title` | The optional, human-readable title of the file for display only (doesn't apply to folders). | +| `Filename` | The path to the file/folder, relative to the webroot. For example 'assets/images/my-image.jpg', or 'assets/images/' for a folder. | +| `Content` | Typically unused, but handy for a textual representation of files. For example for fulltext indexing of PDF documents. | +| `ShowInSearch` | Whether the file should be shown in search results, defaults to '1'. See ["Tutorial 4 - Site Search"](/tutorials/site_search) for enabling search. | +| `ParentID` | The ID of the parent Folder that this File/Folder is in. A ParentID of '0' indicates that the File/Folder is in the 'assets' directory. | +| `OwnerID` | The ID of the Member that 'owns' the File/Folder (not related to filesystem permissions). | + +## Management through the "Files" section of the CMS + +If you have the CMS module installed, you can manage files, folders and images in the "Files" section of the CMS. Inside this section, you will see a list of files and folders like below: + +![](../../_images/assets.png) + +You can click on any file to edit it, or click on any folder to open it. To delete a file or a folder, simply click the red 'X' symbol next to it. If you click to open a folder, you can go back up one level by clicking the 'up' arrow above the folder name (highlighted below): + +![](../../_images/assets_up.png) + +Once you click to edit a file, you will see a form similar to the one below, in which you can edit the file's title, filename, owner, or even change which folder the file is located in: + +![](../../_images/assets_editform.png) + +You may also notice the 'Sync files' button (highlighted below). This button allows CMS users to 'synchronise' the database (remember, all files/folders are stored as database records) with the filesystem. This is particularly useful if someone has uploaded or removed files/folders via FTP, for example. + +![](../../_images/assets_sync.png) + +## Upload + +Files can be managed through a `FileField` or an `UploadField`. The `[api:FileField]` class provides a simple HTML input with a type of "file", whereas an `[api:UploadField]` provides a much more feature-rich field (including AJAX-based uploads, previews, relationship management and file data management). See [`Reference - UploadField`](/developer_guides/forms/field_types/uploadfield) for more information about how to use the `UploadField` class. \ No newline at end of file diff --git a/docs/en/02_Developer_Guides/14_Files/01_Image.md b/docs/en/02_Developer_Guides/14_Files/01_Image.md deleted file mode 100644 index bd3b4537a..000000000 --- a/docs/en/02_Developer_Guides/14_Files/01_Image.md +++ /dev/null @@ -1,135 +0,0 @@ -# Image - -## Introduction - -Represents an image object through the `[api:Image]` class, inheriting all base functionality from the `[api:File]` class with extra functionality including resizing. - -## Usage - -### Managing images through form fields - -Images can be uploaded like any other file, through `[api:FileField]`. -More advanced usage is possible through `[api:UploadField]`, -which provides thumbnails, a detail view of the image properties, -and management of relationships to other DataObject instances. -Allows upload of images through limiting file extensions with `setAllowedExtensions()`. - -### Inserting images into the WYSIWYG editor - -See [Topics: Rich Text Editing](/topics/rich-text-editing). - -### Resizing Images in PHP - -The following are methods defined on the GD class which you can call on Image Objects. Note to get the following to work -you need to have GD2 support in your PHP installation and because these generate files you must have write access to -your tmp folder. - - :::php - // manipulation functions - $image->resize(width,height); // Basic resize, just skews the image - $image->resizeRatio(width,height) // Resizes an image with max width and height - $image->paddedResize(width,height) // Adds padding after resizing to width or height. - $image->croppedImage(width,height) // Crops the image from the centre, to given values. - $image->resizeByHeight(height) // Maximum height the image resizes to, keeps proportion - $image->resizeByWidth(width) // Maximum width the image resizes to, keeps proportion - $image->greyscale(r,g,b) // alters image channels === - - // values - $image->getHeight() // Returns the height of the image. - $image->getWidth() // Returns the width of the image - $image->getOrienation() // Returns a class constant: ORIENTATION_SQUARE or ORIENTATION_PORTRAIT or ORIENTATION_LANDSCAPE - - -You can also create your own functions by extending the image class, for example - - :::php - class MyImage extends Image { - public function generateRotateClockwise(GD $gd) { - return $gd->rotate(90); - } - - public function generateRotateCounterClockwise(GD $gd) { - return $gd->rotate(270); - } - - public function clearResampledImages() { - $files = glob(Director::baseFolder().'/'.$this->Parent()->Filename."_resampled/*-$this->Name"); - foreach($files as $file) {unlink($file);} - } - - public function Landscape() { - return $this->getWidth() > $this->getHeight(); - } - - public function Portrait() { - return $this->getWidth() < $this->getHeight(); - } - - public function generatePaddedImageByWidth(GD $gd,$width=600,$color="fff"){ - return $gd->paddedResize($width, round($gd->getHeight()/($gd->getWidth()/$width),0),$color); - } - - public function Exif(){ - //http://www.v-nessa.net/2010/08/02/using-php-to-extract-image-exif-data - $image = $this->AbsoluteURL; - $d=new ArrayList(); - $exif = exif_read_data($image, 0, true); - foreach ($exif as $key => $section) { - $a=new ArrayList(); - foreach ($section as $name => $val) - $a->push(new ArrayData(array("Title"=>$name,"Content"=>$val))); - $d->push(new ArrayData(array("Title"=>strtolower($key),"Content"=>$a))); - } - return $d; - } - } - -### Resizing in Templates - -You can call certain resize functions directly from the template, to use the inbuilt GD functions as the template parser -supports these, for example SetWidth() or SetHeight(). - -For output of an image tag with the image automatically resized to 80px width, you can use: - - :::php - $Image.SetWidth(80) // returns a image 80px wide, ratio kept the same - $Image.SetHeight(80) // returns a image 80px tall, ratio kept the same - $Image.SetSize(80,80) // returns a 80x80px padded image - $Image.SetRatioSize(80,80) // Returns an image scaled proportional, with its greatest diameter scaled to 80px - $Image.CroppedImage(80,80) // Returns an 80x80 image cropped from the center. - $Image.PaddedImage(80, 80, FFFFFF) // Returns an 80x80 image. Unused space is padded white. No crop. No stretching - $Image.Width // returns width of image - $Image.Height // returns height of image - $Image.Orientation // returns Orientation - $Image.Filename // returns filename - $Image.URL // returns filename - - -### Form Upload - -For usage on a website form, see `[api:FileField]`. -If you want to upload images within the CMS, see `[api:UploadField]`. - -### Image Quality - -To adjust the quality of the generated images when they are resized add the following to your mysite/config/config.yml file: - - :::yml - GDBackend: - default_quality: 90 - -The default value is 75. - -### Clearing Thumbnail Cache - -Images are (like all other Files) synchronized with the SilverStripe database. -This syncing happens whenever you load the "Files & Images" interface, -and whenever you upload or modify an Image through SilverStripe. - -If you encounter problems with images not appearing, or have mysteriously disappeared, you can try manually flushing the -image cache. - - http://localhost/dev/tasks/FlushGeneratedImagesTask - -## API Documentation -`[api:Image]` diff --git a/docs/en/02_Developer_Guides/14_Files/02_Images.md b/docs/en/02_Developer_Guides/14_Files/02_Images.md new file mode 100644 index 000000000..46babf387 --- /dev/null +++ b/docs/en/02_Developer_Guides/14_Files/02_Images.md @@ -0,0 +1,163 @@ +summary: Learn how to crop and resize images in templates and PHP code + +# Image + +Represents an image object through the `[api:Image]` class, inheriting all base functionality from the `[api:File]` class with extra functionality including resizing. + +## Usage + +### Managing images through form fields + +Images can be uploaded like any other file, through `[api:FileField]`. +More advanced usage is possible through `[api:UploadField]`, +which provides thumbnails, a detail view of the image properties, +and management of relationships to other DataObject instances. +Allows upload of images through limiting file extensions with `setAllowedExtensions()`. + +### Inserting images into the WYSIWYG editor + +See [Topics: Rich Text Editing](/topics/rich-text-editing). + +### Manipulating images in Templates + +You can manipulate images directly from templates to create images that are +resized and cropped to suit your needs. This doesn't affect the original +image or clutter the CMS with any additional files, and any images you create +in this way are cached for later use. In most cases the pixel aspect ratios of +images are preserved (meaning images are not stretched). + +![](../../_images/image-methods.jpg) + +Here are some examples, assuming the `$Image` object has dimensions of 200x100px: + + :::ss + // Scaling functions + $Image.ScaleWidth(150) // Returns a 150x75px image + $Image.ScaleMaxWidth(100) // Returns a 100x50px image (like ScaleWidth but prevents up-sampling) + $Image.ScaleHeight(150) // Returns a 300x150px image (up-sampled. Try to avoid doing this) + $Image.ScaleMaxHeight(150) // Returns a 200x100px image (like ScaleHeight but prevents up-sampling) + $Image.Fit(300,300) // Returns an image that fits within a 300x300px boundary, resulting in a 300x150px image (up-sampled) + $Image.FitMax(300,300) // Returns a 200x100px image (like Fit but prevents up-sampling) + + // Cropping functions + $Image.Fill(150,150) // Returns a 150x150px image resized and cropped to fill specified dimensions (up-sampled) + $Image.FillMax(150,150) // Returns a 100x100px image (like Fill but prevents up-sampling) + $Image.CropWidth(150) // Returns a 150x100px image (trims excess pixels off the x axis from the center) + $Image.CropHeight(50) // Returns a 200x50px image (trims excess pixels off the y axis from the center) + + // Padding functions (add space around an image) + $Image.Pad(100,100) // Returns a 100x100px padded image, with white bars added at the top and bottom + $Image.Pad(100, 100, CCCCCC) // Same as above but with a grey background + + // Metadata + $Image.Width // Returns width of image + $Image.Height // Returns height of image + $Image.Orientation // Returns Orientation + $Image.Title // Returns the friendly file name + $Image.Name // Returns the actual file name + $Image.FileName // Returns the actual file name including directory path from web root + $Image.Link // Returns relative URL path to image + $Image.AbsoluteLink // Returns absolute URL path to image + +Image methods are chainable. Example: + + :::ss + + +### Manipulating images in PHP + +The image manipulation functions can be used in your code with the same names, example: `$image->Fill(150,150)`. + +Some of the MetaData functions need to be prefixed with 'get', example `getHeight()`, `getOrientation()` etc. + +Please refer to the `[api:Image]` API documentation for specific functions. + +### Creating custom image functions + +You can also create your own functions by extending the image class, for example + + :::php + class MyImage extends DataExtension { + + public function Landscape() { + return $this->owner->getWidth() > $this->owner->getHeight(); + } + + public function Portrait() { + return $this->owner->getWidth() < $this->owner->getHeight(); + } + + public function PerfectSquare() { + return $this->owner->getFormattedImage('PerfectSquare'); + } + + public function generatePerfectSquare(Image_Backend $backend) { + return $backend->croppedResize(100,100); + } + + public function Exif(){ + //http://www.v-nessa.net/2010/08/02/using-php-to-extract-image-exif-data + $image = $this->owner->AbsoluteLink(); + $d=new ArrayList(); + $exif = exif_read_data($image, 0, true); + foreach ($exif as $key => $section) { + $a=new ArrayList(); + foreach ($section as $name => $val) + $a->push(new ArrayData(array("Title"=>$name,"Content"=>$val))); + $d->push(new ArrayData(array("Title"=>strtolower($key),"Content"=>$a))); + } + return $d; + } + } + + :::yml + Image: + extensions: + - MyImage + +### Form Upload + +For usage on a website form, see `[api:FileField]`. +If you want to upload images within the CMS, see `[api:UploadField]`. + +### Image Quality + +To adjust the quality of the generated images when they are resized add the +following to your mysite/config/config.yml file: + + :::yml + GDBackend: + default_quality: 90 + # or + ImagickBackend: + default_quality: 90 + +The default value is 75. + +By default SilverStripe image functions will not resample an image if no +cropping or resizing is taking place. You can tell SilverStripe to always to +always produce resampled output by adding this to your +mysite/config/config.yml file: + + :::yml + Image: + force_resample: true + +If you are intending to resample images with SilverStripe it is good practice +to upload high quality (minimal compression) images as these will produce +better results when resampled. Very high resolution images may cause GD to +crash so a good size for website images is around 2000px on the longest edge. + +### Clearing Thumbnail Cache + +Images are (like all other Files) synchronized with the SilverStripe database. +This syncing happens whenever you load the "Files & Images" interface, +and whenever you upload or modify an Image through SilverStripe. + +If you encounter problems with images not appearing, or have mysteriously +disappeared, you can try manually flushing the image cache. + + http://localhost/dev/tasks/FlushGeneratedImagesTask + +## API Documentation +`[api:Image]` diff --git a/docs/en/02_Developer_Guides/14_Files/index.md b/docs/en/02_Developer_Guides/14_Files/index.md index daff8a2cb..4c2c14c9b 100644 --- a/docs/en/02_Developer_Guides/14_Files/index.md +++ b/docs/en/02_Developer_Guides/14_Files/index.md @@ -1,40 +1,11 @@ -summary: Learn how to deal with File and Image records +title: Files +summary: Upload, manage and manipulate files and images. +introduction: Upload, manage and manipulate files and images. -# Files, Images and Folders +[CHILDREN] -## Files, Images and Folders as database records +## API Documentation -All files, images and folders in the 'assets' directory are stored in the database. Each record has the following database fields: - -| Field name | Description | -| ---------- | ----------- | -| `ClassName` | The class name of the file (e.g. File, Image or Folder). | -| `Name` | The 'basename' of the file, or the folder name. For example 'my-image.jpg', or 'images' for a folder. | -| `Title` | The optional, human-readable title of the file for display only (doesn't apply to folders). | -| `Filename` | The path to the file/folder, relative to the webroot. For example 'assets/images/my-image.jpg', or 'assets/images/' for a folder. | -| `Content` | Typically unused, but handy for a textual representation of files. For example for fulltext indexing of PDF documents. | -| `ShowInSearch` | Whether the file should be shown in search results, defaults to '1'. See ["Tutorial 4 - Site Search"](/tutorials/site_search) for enabling search. | -| `ParentID` | The ID of the parent Folder that this File/Folder is in. A ParentID of '0' indicates that the File/Folder is in the 'assets' directory. | -| `OwnerID` | The ID of the Member that 'owns' the File/Folder (not related to filesystem permissions). | - -## Management through the "Files" section of the CMS - -If you have the CMS module installed, you can manage files, folders and images in the "Files" section of the CMS. Inside this section, you will see a list of files and folders like below: - -![](../../_images/assets.png) - -You can click on any file to edit it, or click on any folder to open it. To delete a file or a folder, simply click the red 'X' symbol next to it. If you click to open a folder, you can go back up one level by clicking the 'up' arrow above the folder name (highlighted below): - -![](../../_images/assets_up.png) - -Once you click to edit a file, you will see a form similar to the one below, in which you can edit the file's title, filename, owner, or even change which folder the file is located in: - -![](../../_images/assets_editform.png) - -You may also notice the 'Sync files' button (highlighted below). This button allows CMS users to 'synchronise' the database (remember, all files/folders are stored as database records) with the filesystem. This is particularly useful if someone has uploaded or removed files/folders via FTP, for example. - -![](../../_images/assets_sync.png) - -## Upload - -Files can be managed through a `FileField` or an `UploadField`. The `[api:FileField]` class provides a simple HTML input with a type of "file", whereas an `[api:UploadField]` provides a much more feature-rich field (including AJAX-based uploads, previews, relationship management and file data management). See [`Reference - UploadField`](/developer_guides/forms/field_types/uploadfield) for more information about how to use the `UploadField` class. \ No newline at end of file +* [api:File] +* [api:Image] +* [api:Folder] \ No newline at end of file diff --git a/docs/en/04_Changelogs/3.2.0.md b/docs/en/04_Changelogs/3.2.0.md index bb21b2306..1ff719cfd 100644 --- a/docs/en/04_Changelogs/3.2.0.md +++ b/docs/en/04_Changelogs/3.2.0.md @@ -20,6 +20,9 @@ * Implementation of new "Archive" concept for page removal, which supercedes "delete". Where deletion removed pages only from draft, archiving removes from both draft and live simultaneously. * Support for multiple HtmlEditorConfigs on the same page. + * Most of the `Image` manipulation methods have been renamed + * New `Image` methods `CropWidth` and `CropHeight` added + * 'Max' versions of `Image` methods introduced to prevent up-sampling #### Deprecated classes/methods removed @@ -393,6 +396,15 @@ In order to remove the new "archive" action and restore the old "delete" action - CMSBatchAction_Delete +### Update Image method names in PHP code and templates + + * `SetRatioSize` -> `Fit` + * `CroppedImage` -> `Fill` + * `PaddedImage` -> `Pad` + * `SetSize` -> `Pad` + * `SetWidth` -> `ScaleWidth` + * `SetHeight` -> `ScaleHeight` + ### Other * Helper function `DB::placeholders` can be used to generate a comma separated list of placeholders diff --git a/docs/en/_images/image-methods.jpg b/docs/en/_images/image-methods.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c1a2e8b007f0d7d333b8c02f77751c743e989858 GIT binary patch literal 33416 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 - 'SetWidth', + 'ScaleWidth', Config::inst()->get('Image', 'asset_preview_width') ); $thumbnail = $formattedImage ? $formattedImage->URL : ''; diff --git a/forms/HtmlEditorField.php b/forms/HtmlEditorField.php index ded9c1459..9817431e8 100644 --- a/forms/HtmlEditorField.php +++ b/forms/HtmlEditorField.php @@ -673,7 +673,7 @@ class HtmlEditorField_Toolbar extends RequestHandler { */ protected function getFieldsForImage($url, $file) { if($file->File instanceof Image) { - $formattedImage = $file->File->generateFormattedImage('SetWidth', + $formattedImage = $file->File->generateFormattedImage('ScaleWidth', Config::inst()->get('Image', 'asset_preview_width')); $thumbnailURL = Convert::raw2att($formattedImage ? $formattedImage->URL : $url); } else { diff --git a/forms/UploadField.php b/forms/UploadField.php index d09a6379f..e623a9ead 100644 --- a/forms/UploadField.php +++ b/forms/UploadField.php @@ -873,8 +873,8 @@ class UploadField extends FileField { return $file->getThumbnail($width, $height)->getURL(); } elseif ($file->hasMethod('getThumbnailURL')) { return $file->getThumbnailURL($width, $height); - } elseif ($file->hasMethod('SetRatioSize')) { - return $file->SetRatioSize($width, $height)->getURL(); + } elseif ($file->hasMethod('Fit')) { + return $file->Fit($width, $height)->getURL(); } else { return $file->Icon(); } diff --git a/model/Image.php b/model/Image.php index 31561ade2..d488c689a 100644 --- a/model/Image.php +++ b/model/Image.php @@ -216,30 +216,271 @@ class Image extends File implements Flushable { } /** - * Resize the image by preserving aspect ratio, keeping the image inside the - * $width and $height + * Scale image proportionally to fit within the specified bounds * * @param integer $width The width to size within * @param integer $height The height to size within * @return Image */ - public function SetRatioSize($width, $height) { + public function Fit($width, $height) { // Prevent divide by zero on missing/blank file if(!$this->getWidth() || !$this->getHeight()) return null; // Check if image is already sized to the correct dimension $widthRatio = $width / $this->getWidth(); $heightRatio = $height / $this->getHeight(); + if( $widthRatio < $heightRatio ) { // Target is higher aspect ratio than image, so check width if($this->isWidth($width) && !Config::inst()->get('Image', 'force_resample')) return $this; } else { - // Target is wider aspect ratio than image, so check height + // Target is wider or same aspect ratio as image, so check height if($this->isHeight($height) && !Config::inst()->get('Image', 'force_resample')) return $this; } // Item must be regenerated - return $this->getFormattedImage('SetRatioSize', $width, $height); + return $this->getFormattedImage('Fit', $width, $height); + } + + /** + * Scale image proportionally to fit within the specified bounds + * + * @param Image_Backend $backend + * @param integer $width The width to size within + * @param integer $height The height to size within + * @return Image_Backend + */ + public function generateFit(Image_Backend $backend, $width, $height) { + return $backend->resizeRatio($width, $height); + } + + /** + * Proportionally scale down this image if it is wider or taller than the specified dimensions. + * Similar to Fit but without up-sampling. Use in templates with $FitMax. + * + * @uses Image::Fit() + * @param integer $width The maximum width of the output image + * @param integer $height The maximum height of the output image + * @return Image + */ + public function FitMax($width, $height) { + // Temporary $force_resample support for 3.x, to be removed in 4.0 + if (Config::inst()->get('Image', 'force_resample') && $this->getWidth() <= $width && $this->getHeight() <= $height) return $this->Fit($this->getWidth(),$this->getHeight()); + + return $this->getWidth() > $width || $this->getHeight() > $height + ? $this->Fit($width,$height) + : $this; + } + + /** + * Resize and crop image to fill specified dimensions. + * Use in templates with $Fill + * + * @param integer $width Width to crop to + * @param integer $height Height to crop to + * @return Image + */ + public function Fill($width, $height) { + return $this->isSize($width, $height) && !Config::inst()->get('Image', 'force_resample') + ? $this + : $this->getFormattedImage('Fill', $width, $height); + } + + /** + * Resize and crop image to fill specified dimensions. + * Use in templates with $Fill + * + * @param Image_Backend $backend + * @param integer $width Width to crop to + * @param integer $height Height to crop to + * @return Image_Backend + */ + public function generateFill(Image_Backend $backend, $width, $height) { + return $backend->croppedResize($width, $height); + } + + /** + * Crop this image to the aspect ratio defined by the specified width and height, + * then scale down the image to those dimensions if it exceeds them. + * Similar to Fill but without up-sampling. Use in templates with $FillMax. + * + * @uses Image::Fill() + * @param integer $width The relative (used to determine aspect ratio) and maximum width of the output image + * @param integer $height The relative (used to determine aspect ratio) and maximum height of the output image + * @return Image + */ + public function FillMax($width, $height) { + // Prevent divide by zero on missing/blank file + if(!$this->getWidth() || !$this->getHeight()) return null; + + // Temporary $force_resample support for 3.x, to be removed in 4.0 + if (Config::inst()->get('Image', 'force_resample') && $this->isSize($width, $height)) return $this->Fill($width, $height); + + // Is the image already the correct size? + if ($this->isSize($width, $height)) return $this; + + // If not, make sure the image isn't upsampled + $imageRatio = $this->getWidth() / $this->getHeight(); + $cropRatio = $width / $height; + // If cropping on the x axis compare heights + if ($cropRatio < $imageRatio && $this->getHeight() < $height) return $this->Fill($this->getHeight()*$cropRatio, $this->getHeight()); + // Otherwise we're cropping on the y axis (or not cropping at all) so compare widths + if ($this->getWidth() < $width) return $this->Fill($this->getWidth(), $this->getWidth()/$cropRatio); + + return $this->Fill($width, $height); + } + + /** + * Fit image to specified dimensions and fill leftover space with a solid colour (default white). Use in templates with $Pad. + * + * @param integer $width The width to size to + * @param integer $height The height to size to + * @return Image + */ + public function Pad($width, $height, $backgroundColor='FFFFFF') { + return $this->isSize($width, $height) && !Config::inst()->get('Image', 'force_resample') + ? $this + : $this->getFormattedImage('Pad', $width, $height, $backgroundColor); + } + + /** + * Fit image to specified dimensions and fill leftover space with a solid colour (default white). Use in templates with $Pad. + * + * @param Image_Backend $backend + * @param integer $width The width to size to + * @param integer $height The height to size to + * @return Image_Backend + */ + public function generatePad(Image_Backend $backend, $width, $height, $backgroundColor='FFFFFF') { + return $backend->paddedResize($width, $height, $backgroundColor); + } + + /** + * Scale image proportionally by width. Use in templates with $ScaleWidth. + * + * @param integer $width The width to set + * @return Image + */ + public function ScaleWidth($width) { + return $this->isWidth($width) && !Config::inst()->get('Image', 'force_resample') + ? $this + : $this->getFormattedImage('ScaleWidth', $width); + } + + /** + * Scale image proportionally by width. Use in templates with $ScaleWidth. + * + * @param Image_Backend $backend + * @param int $width The width to set + * @return Image_Backend + */ + public function generateScaleWidth(Image_Backend $backend, $width) { + return $backend->resizeByWidth($width); + } + + /** + * Proportionally scale down this image if it is wider than the specified width. + * Similar to ScaleWidth but without up-sampling. Use in templates with $ScaleMaxWidth. + * + * @uses Image::ScaleWidth() + * @param integer $width The maximum width of the output image + * @return Image + */ + public function ScaleMaxWidth($width) { + // Temporary $force_resample support for 3.x, to be removed in 4.0 + if (Config::inst()->get('Image', 'force_resample') && $this->getWidth() <= $width) return $this->ScaleWidth($this->getWidth()); + + return $this->getWidth() > $width + ? $this->ScaleWidth($width) + : $this; + } + + /** + * Scale image proportionally by height. Use in templates with $ScaleHeight. + * + * @param integer $height The height to set + * @return Image + */ + public function ScaleHeight($height) { + return $this->isHeight($height) && !Config::inst()->get('Image', 'force_resample') + ? $this + : $this->getFormattedImage('ScaleHeight', $height); + } + + /** + * Scale image proportionally by height. Use in templates with $ScaleHeight. + * + * @param Image_Backend $backend + * @param integer $height The height to set + * @return Image_Backend + */ + public function generateScaleHeight(Image_Backend $backend, $height){ + return $backend->resizeByHeight($height); + } + + /** + * Proportionally scale down this image if it is taller than the specified height. + * Similar to ScaleHeight but without up-sampling. Use in templates with $ScaleMaxHeight. + * + * @uses Image::ScaleHeight() + * @param integer $height The maximum height of the output image + * @return Image + */ + public function ScaleMaxHeight($height) { + // Temporary $force_resample support for 3.x, to be removed in 4.0 + if (Config::inst()->get('Image', 'force_resample') && $this->getHeight() <= $height) return $this->ScaleHeight($this->getHeight()); + + return $this->getHeight() > $height + ? $this->ScaleHeight($height) + : $this; + } + + /** + * Crop image on X axis if it exceeds specified width. Retain height. + * Use in templates with $CropWidth. Example: $Image.ScaleHeight(100).$CropWidth(100) + * + * @uses Image::Fill() + * @param integer $width The maximum width of the output image + * @return Image + */ + public function CropWidth($width) { + // Temporary $force_resample support for 3.x, to be removed in 4.0 + if (Config::inst()->get('Image', 'force_resample') && $this->getWidth() <= $width) return $this->Fill($this->getWidth(), $this->getHeight()); + + return $this->getWidth() > $width + ? $this->Fill($width, $this->getHeight()) + : $this; + } + + /** + * Crop image on Y axis if it exceeds specified height. Retain width. + * Use in templates with $CropHeight. Example: $Image.ScaleWidth(100).CropHeight(100) + * + * @uses Image::Fill() + * @param integer $height The maximum height of the output image + * @return Image + */ + public function CropHeight($height) { + // Temporary $force_resample support for 3.x, to be removed in 4.0 + if (Config::inst()->get('Image', 'force_resample') && $this->getHeight() <= $height) return $this->Fill($this->getWidth(), $this->getHeight()); + + return $this->getHeight() > $height + ? $this->Fill($this->getWidth(), $height) + : $this; + } + + /** + * Resize the image by preserving aspect ratio, keeping the image inside the + * $width and $height + * + * @param integer $width The width to size within + * @param integer $height The height to size within + * @return Image + * @deprecated 4.0 Use Fit instead + */ + public function SetRatioSize($width, $height) { + Deprecation::notice('4.0', 'Use Fit instead'); + return $this->Fit($width, $height); } /** @@ -250,8 +491,10 @@ class Image extends File implements Flushable { * @param integer $width The width to size within * @param integer $height The height to size within * @return Image_Backend + * @deprecated 4.0 Use generateFit instead */ public function generateSetRatioSize(Image_Backend $backend, $width, $height) { + Deprecation::notice('4.0', 'Use generateFit instead'); return $backend->resizeRatio($width, $height); } @@ -260,11 +503,11 @@ class Image extends File implements Flushable { * * @param integer $width The width to set * @return Image + * @deprecated 4.0 Use ScaleWidth instead */ public function SetWidth($width) { - return $this->isWidth($width) && !Config::inst()->get('Image', 'force_resample') - ? $this - : $this->getFormattedImage('SetWidth', $width); + Deprecation::notice('4.0', 'Use ScaleWidth instead'); + return $this->ScaleWidth($width); } /** @@ -273,8 +516,10 @@ class Image extends File implements Flushable { * @param Image_Backend $backend * @param int $width The width to set * @return Image_Backend + * @deprecated 4.0 Use generateScaleWidth instead */ public function generateSetWidth(Image_Backend $backend, $width) { + Deprecation::notice('4.0', 'Use generateScaleWidth instead'); return $backend->resizeByWidth($width); } @@ -283,11 +528,11 @@ class Image extends File implements Flushable { * * @param integer $height The height to set * @return Image + * @deprecated 4.0 Use ScaleHeight instead */ public function SetHeight($height) { - return $this->isHeight($height) && !Config::inst()->get('Image', 'force_resample') - ? $this - : $this->getFormattedImage('SetHeight', $height); + Deprecation::notice('4.0', 'Use ScaleHeight instead'); + return $this->ScaleHeight($height); } /** @@ -296,8 +541,10 @@ class Image extends File implements Flushable { * @param Image_Backend $backend * @param integer $height The height to set * @return Image_Backend + * @deprecated 4.0 Use generateScaleHeight instead */ public function generateSetHeight(Image_Backend $backend, $height){ + Deprecation::notice('4.0', 'Use generateScaleHeight instead'); return $backend->resizeByHeight($height); } @@ -308,11 +555,11 @@ class Image extends File implements Flushable { * @param integer $width The width to size to * @param integer $height The height to size to * @return Image + * @deprecated 4.0 Use Pad instead */ public function SetSize($width, $height) { - return $this->isSize($width, $height) && !Config::inst()->get('Image', 'force_resample') - ? $this - : $this->getFormattedImage('SetSize', $width, $height); + Deprecation::notice('4.0', 'Use Pad instead'); + return $this->Pad($width, $height); } /** @@ -322,8 +569,10 @@ class Image extends File implements Flushable { * @param integer $width The width to size to * @param integer $height The height to size to * @return Image_Backend + * @deprecated 4.0 Use generatePad instead */ public function generateSetSize(Image_Backend $backend, $width, $height) { + Deprecation::notice('4.0', 'Use generatePad instead'); return $backend->paddedResize($width, $height); } @@ -370,11 +619,11 @@ class Image extends File implements Flushable { * @param integer $width The width to size to * @param integer $height The height to size to * @return Image + * @deprecated 4.0 Use Pad instead */ public function PaddedImage($width, $height, $backgroundColor='FFFFFF') { - return $this->isSize($width, $height) && !Config::inst()->get('Image', 'force_resample') - ? $this - : $this->getFormattedImage('PaddedImage', $width, $height, $backgroundColor); + Deprecation::notice('4.0', 'Use Pad instead'); + return $this->Pad($width, $height, $backgroundColor); } /** @@ -384,8 +633,10 @@ class Image extends File implements Flushable { * @param integer $width The width to size to * @param integer $height The height to size to * @return Image_Backend + * @deprecated 4.0 Use generatePad instead */ public function generatePaddedImage(Image_Backend $backend, $width, $height, $backgroundColor='FFFFFF') { + Deprecation::notice('4.0', 'Use generatePad instead'); return $backend->paddedResize($width, $height, $backgroundColor); } @@ -547,11 +798,11 @@ class Image extends File implements Flushable { * @param integer $width Width to crop to * @param integer $height Height to crop to * @return Image + * @deprecated 4.0 Use Fill instead */ public function CroppedImage($width, $height) { - return $this->isSize($width, $height) && !Config::inst()->get('Image', 'force_resample') - ? $this - : $this->getFormattedImage('CroppedImage', $width, $height); + Deprecation::notice('4.0', 'Use Fill instead'); + return $this->Fill($width, $height); } /** @@ -562,8 +813,10 @@ class Image extends File implements Flushable { * @param integer $width Width to crop to * @param integer $height Height to crop to * @return Image_Backend + * @deprecated 4.0 Use generateFill instead */ public function generateCroppedImage(Image_Backend $backend, $width, $height) { + Deprecation::notice('4.0', 'Use generateFill instead'); return $backend->croppedResize($width, $height); } diff --git a/templates/Includes/HtmlEditorField_viewfile.ss b/templates/Includes/HtmlEditorField_viewfile.ss index a1cf734cb..b2c3251dd 100644 --- a/templates/Includes/HtmlEditorField_viewfile.ss +++ b/templates/Includes/HtmlEditorField_viewfile.ss @@ -3,7 +3,7 @@
<% if $Width %> - $Preview.SetRatioSize(30, 40) + $Preview.Fit(30, 40) <% else %> <% end_if %> diff --git a/tests/filesystem/GDTest.php b/tests/filesystem/GDTest.php index dfc3b79af..1cca549c8 100644 --- a/tests/filesystem/GDTest.php +++ b/tests/filesystem/GDTest.php @@ -161,7 +161,7 @@ class GDTest extends SapphireTest { $fullPath = realpath(dirname(__FILE__) . '/gdtest/test_jpg.jpg'); try { - $gdFailure = new GDBackend_Failure($fullPath, array('SetWidth', 123)); + $gdFailure = new GDBackend_Failure($fullPath, array('ScaleWidth', 123)); $this->fail('GDBackend_Failure should throw an exception when setting image resource'); } catch (GDBackend_Failure_Exception $e) { $cache = SS_Cache::factory('GDBackend_Manipulations'); @@ -169,8 +169,8 @@ class GDTest extends SapphireTest { $data = unserialize($cache->load($key)); - $this->assertArrayHasKey('SetWidth|123', $data); - $this->assertTrue($data['SetWidth|123']); + $this->assertArrayHasKey('ScaleWidth|123', $data); + $this->assertTrue($data['ScaleWidth|123']); } } @@ -183,12 +183,12 @@ class GDTest extends SapphireTest { $fullPath = realpath(dirname(__FILE__) . '/gdtest/test_jpg.jpg'); try { - $gdFailure = new GDBackend_Failure($fullPath, array('SetWidth-failed', 123)); + $gdFailure = new GDBackend_Failure($fullPath, array('ScaleWidth-failed', 123)); $this->fail('GDBackend_Failure should throw an exception when setting image resource'); } catch (GDBackend_Failure_Exception $e) { - $gd = new GDBackend($fullPath, array('SetWidth', 123)); - $this->assertTrue($gd->failedResample($fullPath, 'SetWidth-failed|123')); - $this->assertFalse($gd->failedResample($fullPath, 'SetWidth-not-failed|123')); + $gd = new GDBackend($fullPath, array('ScaleWidth', 123)); + $this->assertTrue($gd->failedResample($fullPath, 'ScaleWidth-failed|123')); + $this->assertFalse($gd->failedResample($fullPath, 'ScaleWidth-not-failed|123')); } } diff --git a/tests/model/GDImageTest.php b/tests/model/GDImageTest.php index f950ff977..f095e3ceb 100644 --- a/tests/model/GDImageTest.php +++ b/tests/model/GDImageTest.php @@ -34,13 +34,13 @@ class GDImageTest extends ImageTest { try { // Simluate a failed manipulation - $gdFailure = new GDBackend_Failure($fullPath, array('SetWidth', 123)); + $gdFailure = new GDBackend_Failure($fullPath, array('ScaleWidth', 123)); $this->fail('GDBackend_Failure should throw an exception when setting image resource'); } catch (GDBackend_Failure_Exception $e) { // Check that the cache has stored the manipulation failure $data = unserialize($cache->load($key)); - $this->assertArrayHasKey('SetWidth|123', $data); - $this->assertTrue($data['SetWidth|123']); + $this->assertArrayHasKey('ScaleWidth|123', $data); + $this->assertTrue($data['ScaleWidth|123']); // Delete the image object $image->delete(); diff --git a/tests/model/ImageTest.php b/tests/model/ImageTest.php index fe1e467e1..e9d87d8dc 100644 --- a/tests/model/ImageTest.php +++ b/tests/model/ImageTest.php @@ -100,7 +100,7 @@ class ImageTest extends SapphireTest { public function testMultipleGenerateManipulationCalls() { $image = $this->objFromFixture('Image', 'imageWithoutTitle'); - $imageFirst = $image->SetWidth(200); + $imageFirst = $image->ScaleWidth(200); $this->assertNotNull($imageFirst); $expected = 200; $actual = $imageFirst->getWidth(); @@ -124,27 +124,27 @@ class ImageTest extends SapphireTest { $this->assertTrue($image->isSize(300, 300)); // Set width to 300 pixels - $imageSetWidth = $image->SetWidth(300); - $this->assertEquals($imageSetWidth->getWidth(), 300); - $this->assertEquals($image->Filename, $imageSetWidth->Filename); + $imageScaleWidth = $image->ScaleWidth(300); + $this->assertEquals($imageScaleWidth->getWidth(), 300); + $this->assertEquals($image->Filename, $imageScaleWidth->Filename); // Set height to 300 pixels - $imageSetHeight = $image->SetHeight(300); - $this->assertEquals($imageSetHeight->getHeight(), 300); - $this->assertEquals($image->Filename, $imageSetHeight->Filename); + $imageScaleHeight = $image->ScaleHeight(300); + $this->assertEquals($imageScaleHeight->getHeight(), 300); + $this->assertEquals($image->Filename, $imageScaleHeight->Filename); // Crop image to 300 x 300 - $imageCropped = $image->CroppedImage(300, 300); + $imageCropped = $image->Fill(300, 300); $this->assertTrue($imageCropped->isSize(300, 300)); $this->assertEquals($image->Filename, $imageCropped->Filename); // Resize (padded) to 300 x 300 - $imageSized = $image->SetSize(300, 300); + $imageSized = $image->Pad(300, 300); $this->assertTrue($imageSized->isSize(300, 300)); $this->assertEquals($image->Filename, $imageSized->Filename); // Padded image 300 x 300 (same as above) - $imagePadded = $image->PaddedImage(300, 300); + $imagePadded = $image->Pad(300, 300); $this->assertTrue($imagePadded->isSize(300, 300)); $this->assertEquals($image->Filename, $imagePadded->Filename); @@ -153,16 +153,16 @@ class ImageTest extends SapphireTest { $this->assertTrue($imageStretched->isSize(300, 300)); $this->assertEquals($image->Filename, $imageStretched->Filename); - // SetRatioSize (various options) - $imageSetRatioSize = $image->SetRatioSize(300, 600); - $this->assertTrue($imageSetRatioSize->isSize(300, 300)); - $this->assertEquals($image->Filename, $imageSetRatioSize->Filename); - $imageSetRatioSize = $image->SetRatioSize(600, 300); - $this->assertTrue($imageSetRatioSize->isSize(300, 300)); - $this->assertEquals($image->Filename, $imageSetRatioSize->Filename); - $imageSetRatioSize = $image->SetRatioSize(300, 300); - $this->assertTrue($imageSetRatioSize->isSize(300, 300)); - $this->assertEquals($image->Filename, $imageSetRatioSize->Filename); + // Fit (various options) + $imageFit = $image->Fit(300, 600); + $this->assertTrue($imageFit->isSize(300, 300)); + $this->assertEquals($image->Filename, $imageFit->Filename); + $imageFit = $image->Fit(600, 300); + $this->assertTrue($imageFit->isSize(300, 300)); + $this->assertEquals($image->Filename, $imageFit->Filename); + $imageFit = $image->Fit(300, 300); + $this->assertTrue($imageFit->isSize(300, 300)); + $this->assertEquals($image->Filename, $imageFit->Filename); } /** @@ -178,27 +178,27 @@ class ImageTest extends SapphireTest { Config::inst()->update('Image', 'force_resample', true); // Set width to 300 pixels - $imageSetWidth = $image->SetWidth(300); - $this->assertEquals($imageSetWidth->getWidth(), 300); - $this->assertNotEquals($image->Filename, $imageSetWidth->Filename); + $imageScaleWidth = $image->ScaleWidth(300); + $this->assertEquals($imageScaleWidth->getWidth(), 300); + $this->assertNotEquals($image->Filename, $imageScaleWidth->Filename); // Set height to 300 pixels - $imageSetHeight = $image->SetHeight(300); - $this->assertEquals($imageSetHeight->getHeight(), 300); - $this->assertNotEquals($image->Filename, $imageSetHeight->Filename); + $imageScaleHeight = $image->ScaleHeight(300); + $this->assertEquals($imageScaleHeight->getHeight(), 300); + $this->assertNotEquals($image->Filename, $imageScaleHeight->Filename); // Crop image to 300 x 300 - $imageCropped = $image->CroppedImage(300, 300); + $imageCropped = $image->Fill(300, 300); $this->assertTrue($imageCropped->isSize(300, 300)); $this->assertNotEquals($image->Filename, $imageCropped->Filename); // Resize (padded) to 300 x 300 - $imageSized = $image->SetSize(300, 300); + $imageSized = $image->Pad(300, 300); $this->assertTrue($imageSized->isSize(300, 300)); $this->assertNotEquals($image->Filename, $imageSized->Filename); // Padded image 300 x 300 (same as above) - $imagePadded = $image->PaddedImage(300, 300); + $imagePadded = $image->Pad(300, 300); $this->assertTrue($imagePadded->isSize(300, 300)); $this->assertNotEquals($image->Filename, $imagePadded->Filename); @@ -207,16 +207,16 @@ class ImageTest extends SapphireTest { $this->assertTrue($imageStretched->isSize(300, 300)); $this->assertNotEquals($image->Filename, $imageStretched->Filename); - // SetRatioSize (various options) - $imageSetRatioSize = $image->SetRatioSize(300, 600); - $this->assertTrue($imageSetRatioSize->isSize(300, 300)); - $this->assertNotEquals($image->Filename, $imageSetRatioSize->Filename); - $imageSetRatioSize = $image->SetRatioSize(600, 300); - $this->assertTrue($imageSetRatioSize->isSize(300, 300)); - $this->assertNotEquals($image->Filename, $imageSetRatioSize->Filename); - $imageSetRatioSize = $image->SetRatioSize(300, 300); - $this->assertTrue($imageSetRatioSize->isSize(300, 300)); - $this->assertNotEquals($image->Filename, $imageSetRatioSize->Filename); + // Fit (various options) + $imageFit = $image->Fit(300, 600); + $this->assertTrue($imageFit->isSize(300, 300)); + $this->assertNotEquals($image->Filename, $imageFit->Filename); + $imageFit = $image->Fit(600, 300); + $this->assertTrue($imageFit->isSize(300, 300)); + $this->assertNotEquals($image->Filename, $imageFit->Filename); + $imageFit = $image->Fit(300, 300); + $this->assertTrue($imageFit->isSize(300, 300)); + $this->assertNotEquals($image->Filename, $imageFit->Filename); Config::inst()->update('Image', 'force_resample', $origForceResample); } @@ -225,20 +225,52 @@ class ImageTest extends SapphireTest { $this->assertTrue($image->isSize(300, 300)); // Test normal resize - $resized = $image->SetSize(150, 100); + $resized = $image->Pad(150, 100); $this->assertTrue($resized->isSize(150, 100)); // Test cropped resize - $cropped = $image->CroppedImage(100, 200); + $cropped = $image->Fill(100, 200); $this->assertTrue($cropped->isSize(100, 200)); // Test padded resize - $padded = $image->PaddedImage(200, 100); + $padded = $image->Pad(200, 100); $this->assertTrue($padded->isSize(200, 100)); - // Test SetRatioSize - $ratio = $image->SetRatioSize(80, 160); + // Test Fit + $ratio = $image->Fit(80, 160); $this->assertTrue($ratio->isSize(80, 80)); + + // Test FitMax + $fitMaxDn = $image->FitMax(200, 100); + $this->assertTrue($fitMaxDn->isSize(100, 100)); + $fitMaxUp = $image->FitMax(500, 400); + $this->assertTrue($fitMaxUp->isSize(300, 300)); + + //Test ScaleMax + $scaleMaxWDn = $image->ScaleMaxWidth(200); + $this->assertTrue($scaleMaxWDn->isSize(200, 200)); + $scaleMaxWUp = $image->ScaleMaxWidth(400); + $this->assertTrue($scaleMaxWUp->isSize(300, 300)); + $scaleMaxHDn = $image->ScaleMaxHeight(200); + $this->assertTrue($scaleMaxHDn->isSize(200, 200)); + $scaleMaxHUp = $image->ScaleMaxHeight(400); + $this->assertTrue($scaleMaxHUp->isSize(300, 300)); + + // Test FillMax + $cropMaxDn = $image->FillMax(200, 100); + $this->assertTrue($cropMaxDn->isSize(200, 100)); + $cropMaxUp = $image->FillMax(400, 200); + $this->assertTrue($cropMaxUp->isSize(300, 150)); + + // Test Clip + $clipWDn = $image->CropWidth(200); + $this->assertTrue($clipWDn->isSize(200, 300)); + $clipWUp = $image->CropWidth(400); + $this->assertTrue($clipWUp->isSize(300, 300)); + $clipHDn = $image->CropHeight(200); + $this->assertTrue($clipHDn->isSize(300, 200)); + $clipHUp = $image->CropHeight(400); + $this->assertTrue($clipHUp->isSize(300, 300)); } /** @@ -247,15 +279,15 @@ class ImageTest extends SapphireTest { public function testGenerateImageWithInvalidParameters() { $image = $this->objFromFixture('Image', 'imageWithoutTitle'); $image->setHeight('String'); - $image->PaddedImage(600,600,'XXXXXX'); + $image->Pad(600,600,'XXXXXX'); } public function testCacheFilename() { $image = $this->objFromFixture('Image', 'imageWithoutTitle'); - $imageFirst = $image->SetSize(200,200); + $imageFirst = $image->Pad(200,200,'CCCCCC'); $imageFilename = $imageFirst->getFullPath(); // Encoding of the arguments is duplicated from cacheFilename - $neededPart = 'SetSize' . base64_encode(json_encode(array(200,200))); + $neededPart = 'Pad' . base64_encode(json_encode(array(200,200,'CCCCCC'))); $this->assertContains($neededPart, $imageFilename, 'Filename for cached image is correctly generated'); } @@ -263,7 +295,7 @@ class ImageTest extends SapphireTest { $image = $this->objFromFixture('Image', 'imageWithoutTitle'); $folder = new SS_FileFinder(); - $imageFirst = $image->SetSize(200,200); + $imageFirst = $image->Pad(200,200); $this->assertNotNull($imageFirst); $expected = 200; $actual = $imageFirst->getWidth(); @@ -276,7 +308,7 @@ class ImageTest extends SapphireTest { $actual = $imageSecond->getHeight(); $this->assertEquals($expected, $actual); - $imageThird = $imageSecond->PaddedImage(600,600,'0F0F0F'); + $imageThird = $imageSecond->Pad(600,600,'0F0F0F'); // Encoding of the arguments is duplicated from cacheFilename $argumentString = base64_encode(json_encode(array(600,600,'0F0F0F'))); $this->assertNotNull($imageThird); @@ -300,7 +332,7 @@ class ImageTest extends SapphireTest { public function testRegenerateImages() { $image = $this->objFromFixture('Image', 'imageWithMetacharacters'); - $image_generated = $image->SetWidth(200); + $image_generated = $image->ScaleWidth(200); $p = $image_generated->getFullPath(); $this->assertTrue(file_exists($p), 'Resized image exists after creation call'); $this->assertEquals(1, $image->regenerateFormattedImages(), 'Cached images were regenerated correct'); @@ -315,7 +347,7 @@ class ImageTest extends SapphireTest { */ public function testRegenerateImagesWithRenaming() { $image = $this->objFromFixture('Image', 'imageWithMetacharacters'); - $image_generated = $image->SetWidth(200); + $image_generated = $image->ScaleWidth(200); $p = $image_generated->getFullPath(); $this->assertTrue(file_exists($p), 'Resized image exists after creation call'); @@ -337,7 +369,7 @@ class ImageTest extends SapphireTest { public function testGeneratedImageDeletion() { $image = $this->objFromFixture('Image', 'imageWithMetacharacters'); - $image_generated = $image->SetWidth(200); + $image_generated = $image->ScaleWidth(200); $p = $image_generated->getFullPath(); $this->assertTrue(file_exists($p), 'Resized image exists after creation call'); $numDeleted = $image->deleteFormattedImages(); @@ -351,11 +383,11 @@ class ImageTest extends SapphireTest { public function testMultipleGenerateManipulationCallsImageDeletion() { $image = $this->objFromFixture('Image', 'imageWithMetacharacters'); - $firstImage = $image->SetWidth(200); + $firstImage = $image->ScaleWidth(200); $firstImagePath = $firstImage->getFullPath(); $this->assertTrue(file_exists($firstImagePath)); - $secondImage = $firstImage->SetHeight(100); + $secondImage = $firstImage->ScaleHeight(100); $secondImagePath = $secondImage->getFullPath(); $this->assertTrue(file_exists($secondImagePath)); @@ -369,11 +401,11 @@ class ImageTest extends SapphireTest { */ public function testPathPropertiesCachedImage() { $image = $this->objFromFixture('Image', 'imageWithMetacharacters'); - $firstImage = $image->SetWidth(200); + $firstImage = $image->ScaleWidth(200); $firstImagePath = $firstImage->getRelativePath(); $this->assertEquals($firstImagePath, $firstImage->Filename); - $secondImage = $firstImage->SetHeight(100); + $secondImage = $firstImage->ScaleHeight(100); $secondImagePath = $secondImage->getRelativePath(); $this->assertEquals($secondImagePath, $secondImage->Filename); }