From 9988c9af88f3080181be7065c489eb6579b0bc32 Mon Sep 17 00:00:00 2001 From: Aaron Carlino Date: Mon, 3 May 2021 13:30:25 +1200 Subject: [PATCH] NEW: More complete block content, unique images with watermarks --- code/tasks/FTFileMakerTask.php | 75 ++++++++++++++++++++++++++++++++- code/tasks/FTPageMakerTask.php | 63 ++++++++++++++++++++++----- images/silverstripe.png | Bin 0 -> 11526 bytes 3 files changed, 126 insertions(+), 12 deletions(-) create mode 100644 images/silverstripe.png diff --git a/code/tasks/FTFileMakerTask.php b/code/tasks/FTFileMakerTask.php index ab34a5e..82ddfd9 100644 --- a/code/tasks/FTFileMakerTask.php +++ b/code/tasks/FTFileMakerTask.php @@ -9,6 +9,8 @@ use GuzzleHttp\Promise; use SilverStripe\Security\Member; use SilverStripe\Security\Security; +use SilverStripe\Core\Path; +use SilverStripe\Core\Manifest\ModuleResourceLoader; /** * Creates sample folder and file structure, useful to test performance, @@ -106,7 +108,14 @@ class FTFileMakerTask extends BuildTask private static $depth = 2; /** - * Number of folders to create certain hierachy. + * When true, watermark images for unique image binary per Image record + * @var bool + * @config + */ + private static $uniqueImages = true; + + /** + * Number of folders to create certain hierarchy. * @var int[] * @config */ @@ -301,6 +310,11 @@ protected function generateFiles($fixtureFilePaths, $depth = 0, $prefix = "0", $ $doSetOldCreationDate = (bool) self::config()->get('doSetOldCreationDate'); $doRandomlyPublish = (bool) self::config()->get('doRandomlyPublish'); + $uniqueImages = (bool) self::config()->get('uniqueImages'); + $watermarkPath = ModuleResourceLoader::singleton()->resolvePath( + 'silverstripe/frameworktest: images/silverstripe.png' + ); + for ($i = 1; $i <= $folderCount; $i++) { $folder = new Folder([ 'ParentID' => $parentID, @@ -336,6 +350,16 @@ protected function generateFiles($fixtureFilePaths, $depth = 0, $prefix = "0", $ $class = $this->fixtureFileTypes[$randomFileName]; + // If we're uniquifying images, copy the path and watermark it. + if ($class === Image::class && $uniqueImages) { + $copyPath = Path::join(dirname($randomFilePath), $fileName); + copy($randomFilePath, $copyPath); + $newPath = $this->watermarkImage($watermarkPath, $copyPath); + if ($newPath) { + $randomFilePath = $newPath; + } + } + $file = new $class([ 'ParentID' => $folder->ID, 'Title' => $fileName, @@ -343,6 +367,7 @@ protected function generateFiles($fixtureFilePaths, $depth = 0, $prefix = "0", $ ]); $file->File->setFromLocalFile($randomFilePath, $folder->getFilename() . $fileName); + // Randomly set old created date (for testing) if ($doSetOldCreationDate) { if (rand(0, 10) === 0) { @@ -391,4 +416,52 @@ protected function putProtectedFilesInPublicAssetStore() } } + /** + * @param string $stampPath + * @param string $targetPath + * @return null + */ + protected function watermarkImage(string $stampPath, string $targetPath): ?string + { + // Load the stamp and the photo to apply the watermark to + $ext = strtolower(pathinfo($targetPath, PATHINFO_EXTENSION)); + $functions = null; + if (in_array($ext, ['jpeg', 'jpg'])) { + $functions = ['imagecreatefromjpeg', 'imagejpeg']; + } elseif ($ext === 'png') { + $functions = ['imagecreatefrompng', 'imagepng']; + } + if (!$functions) { + return null; + } + + $stamp = imagecreatefrompng($stampPath); + $targetImage = call_user_func($functions[0], $targetPath); + + // Set the margins for the stamp and get the height/width of the stamp image + $targetX = imagesx($targetImage); + $targetY = imagesy($targetImage); + $stampX = imagesx($stamp); + $stampY = imagesy($stamp); + + $marge_right = rand($stampX, $targetX - $stampX); + $marge_bottom = rand($stampY, $targetY - $stampY); + + // Copy the stamp image onto our photo using the margin offsets and the photo + // width to calculate positioning of the stamp. + imagecopy( + $targetImage, + $stamp, + $targetX - $stampX - $marge_right, + $targetY - $stampY - $marge_bottom, + 0, + 0, + $stampX, + $stampY + ); + call_user_func($functions[1], $targetImage, $targetPath); + + return $targetPath; + } + } diff --git a/code/tasks/FTPageMakerTask.php b/code/tasks/FTPageMakerTask.php index 958a87c..f7f93df 100644 --- a/code/tasks/FTPageMakerTask.php +++ b/code/tasks/FTPageMakerTask.php @@ -2,13 +2,13 @@ use DNADesign\Elemental\Models\ElementContent; use SilverStripe\Assets\File; -use SilverStripe\Assets\Image; use SilverStripe\Dev\BuildTask; use SilverStripe\Core\ClassInfo; -use DNADesign\Elemental\Models\BaseElement; use SilverStripe\ElementalBannerBlock\Block\BannerBlock; use SilverStripe\ElementalFileBlock\Block\FileBlock; - +use SilverStripe\CMS\Model\SiteTree; +use DNADesign\Elemental\Extensions\ElementalPageExtension; +use DNADesign\Elemental\Models\BaseElement; /** * Creates sample page structure, useful to test tree performance, @@ -56,6 +56,15 @@ public function run($request) throw new \LogicException('withBlocks requested, but BaseElement class not found'); } + // Allow pageCountByDepth to be passed as comma-separated value, e.g. pageCounts=5,100,1,1 + $pageCounts = $request->getVar('pageCounts'); + if ($pageCounts) { + $counts = explode(',', $pageCounts); + $this->pageCountByDepth = array_map(function ($int) { + return (int) trim($int); + }, $counts); + } + $this->generatePages(0, "", 0, $withBlocks); } @@ -102,7 +111,7 @@ protected function generateBlocksForPage(Page $page) foreach(range($range[0], array_rand(range($range[0], $range[1]))) as $i) { $class = array_rand($classes); $callable = $classes[$class]; - $block = call_user_func($callable); + $block = call_user_func($callable, $page); // Add block to page $page->ElementalArea()->Elements()->add($block); @@ -116,36 +125,68 @@ protected function generateBlocksForPage(Page $page) } } - public static function generateContentBlock() + /** + * @param SiteTree&ElementalPageExtension|null $page + * @return ElementContent + * @throws \SilverStripe\ORM\ValidationException + */ + public static function generateContentBlock(?SiteTree $page = null) { + $count = $page ? $page->ElementalArea()->Elements()->count() : ''; + $content = $page ? "Page {$page->Title}" : "Page"; $block = new ElementContent([ - 'HTML' => 'test 123' + 'Title' => sprintf('Block #%s (Content Block)', $count), + 'ShowTitle' => rand(0,1) === 1, + 'HTML' => sprintf('Content block for %s', $content), ]); $block->write(); return $block; } - public static function generateFileBlock() + /** + * @param SiteTree&ElementalPageExtension|null $page + * @return FileBlock + * @throws \SilverStripe\ORM\ValidationException + */ + public static function generateFileBlock(?SiteTree $page = null): FileBlock { + $count = $page ? $page->ElementalArea()->Elements()->count() : ''; + // Supports both images and files $file = File::get()->shuffle()->First(); if (!$file) { throw new \LogicException('No files found to associate with FileBlock'); } - $block = new FileBlock(); + $block = new FileBlock([ + 'Title' => sprintf('Block #%s (File Block)', $count), + 'ShowTitle' => rand(0,1) === 1, + ]); $block->FileID = $file->ID; $block->write(); return $block; } - public static function generateBannerBlock() + /** + * @param SiteTree&ElementalPageExtension|null $page + * @return BannerBlock + * @throws \SilverStripe\ORM\ValidationException + */ + public static function generateBannerBlock(?SiteTree $page = null): BannerBlock { + $count = $page ? $page->ElementalArea()->Elements()->count() : ''; + $content = $page ? "Page {$page->Title}" : "Page"; + $block = new BannerBlock([ - 'Content' => 'test 123', - 'CallToActionLink' => 'http://example.com', + 'Title' => sprintf('Block #%s (Banner Block)', $count), + 'ShowTitle' => rand(0,1) === 1, + 'Content' => sprintf('Banner block for %s', $content), + 'CallToActionLink' => json_encode([ + 'PageID' => SiteTree::get()->shuffle()->first()->ID, + 'Text' => sprintf('Link for page %s', $page->Title), + ]), ]); $block->write(); diff --git a/images/silverstripe.png b/images/silverstripe.png new file mode 100644 index 0000000000000000000000000000000000000000..16951d933b4d703dd766e4c5b4f165d2958da260 GIT binary patch literal 11526 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 literal 0 HcmV?d00001