Skip to content

Commit de83f82

Browse files
authored
Merge pull request #2522 from magento-performance/MAGETWO-91164
[performance] MAGETWO-91164: Optimize images resizing
2 parents 73b48b4 + 0b00940 commit de83f82

File tree

9 files changed

+335
-302
lines changed

9 files changed

+335
-302
lines changed

app/code/Magento/Catalog/Console/Command/ImagesResizeCommand.php

Lines changed: 169 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -5,47 +5,101 @@
55
*/
66
namespace Magento\Catalog\Console\Command;
77

8+
use Magento\Catalog\Api\ProductRepositoryInterface;
9+
use Magento\Catalog\Model\Product\Image\CacheFactory;
10+
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
11+
use Magento\Catalog\Model\ResourceModel\Product\Image as ProductImage;
12+
use Magento\Framework\App\Area;
13+
use Magento\Framework\App\ObjectManager;
14+
use Magento\Framework\App\State;
15+
use Magento\Catalog\Helper\Image as ImageHelper;
16+
use Magento\Framework\Console\Cli;
17+
use Symfony\Component\Console\Command\Command;
818
use Symfony\Component\Console\Input\InputInterface;
919
use Symfony\Component\Console\Output\OutputInterface;
20+
use Magento\Framework\View\ConfigInterface as ViewConfig;
21+
use Magento\Theme\Model\ResourceModel\Theme\Collection as ThemeCollection;
22+
use Magento\Catalog\Model\Product\Image;
23+
use Magento\Catalog\Model\Product\ImageFactory as ProductImageFactory;
24+
use Symfony\Component\Console\Helper\ProgressBar;
1025

11-
class ImagesResizeCommand extends \Symfony\Component\Console\Command\Command
26+
/**
27+
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
28+
*/
29+
class ImagesResizeCommand extends Command
1230
{
1331
/**
14-
* @var \Magento\Framework\App\State
32+
* @var State
1533
*/
1634
protected $appState;
1735

1836
/**
19-
* @var \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory
37+
* @deprecated
38+
* @var CollectionFactory
2039
*/
2140
protected $productCollectionFactory;
2241

2342
/**
24-
* @var \Magento\Catalog\Api\ProductRepositoryInterface
43+
* @deprecated
44+
* @var ProductRepositoryInterface
2545
*/
2646
protected $productRepository;
2747

2848
/**
29-
* @var \Magento\Catalog\Model\Product\Image\CacheFactory
49+
* @deprecated
50+
* @var CacheFactory
3051
*/
3152
protected $imageCacheFactory;
3253

3354
/**
34-
* @param \Magento\Framework\App\State $appState
35-
* @param \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory
36-
* @param \Magento\Catalog\Api\ProductRepositoryInterface $productRepository
37-
* @param \Magento\Catalog\Model\Product\Image\CacheFactory $imageCacheFactory
55+
* @var ProductImage
56+
*/
57+
private $productImage;
58+
59+
/**
60+
* @var ViewConfig
61+
*/
62+
private $viewConfig;
63+
64+
/**
65+
* @var ThemeCollection
66+
*/
67+
private $themeCollection;
68+
69+
/**
70+
* @var ProductImageFactory
71+
*/
72+
private $productImageFactory;
73+
74+
/**
75+
* @param State $appState
76+
* @param CollectionFactory $productCollectionFactory
77+
* @param ProductRepositoryInterface $productRepository
78+
* @param CacheFactory $imageCacheFactory
79+
* @param ProductImage $productImage
80+
* @param ViewConfig $viewConfig
81+
* @param ThemeCollection $themeCollection
82+
* @param ProductImageFactory $productImageFactory
3883
*/
3984
public function __construct(
40-
\Magento\Framework\App\State $appState,
41-
\Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory,
42-
\Magento\Catalog\Api\ProductRepositoryInterface $productRepository,
43-
\Magento\Catalog\Model\Product\Image\CacheFactory $imageCacheFactory
85+
State $appState,
86+
CollectionFactory $productCollectionFactory,
87+
ProductRepositoryInterface $productRepository,
88+
CacheFactory $imageCacheFactory,
89+
ProductImage $productImage = null,
90+
ViewConfig $viewConfig = null,
91+
ThemeCollection $themeCollection = null,
92+
ProductImageFactory $productImageFactory = null
4493
) {
4594
$this->appState = $appState;
4695
$this->productCollectionFactory = $productCollectionFactory;
4796
$this->productRepository = $productRepository;
4897
$this->imageCacheFactory = $imageCacheFactory;
98+
$this->productImage = $productImage ?: ObjectManager::getInstance()->get(ProductImage::class);
99+
$this->viewConfig = $viewConfig ?: ObjectManager::getInstance()->get(ViewConfig::class);
100+
$this->themeCollection = $themeCollection ?: ObjectManager::getInstance()->get(ThemeCollection::class);
101+
$this->productImageFactory = $productImageFactory
102+
?: ObjectManager::getInstance()->get(ProductImageFactory::class);
49103
parent::__construct();
50104
}
51105

@@ -63,49 +117,122 @@ protected function configure()
63117
*/
64118
protected function execute(InputInterface $input, OutputInterface $output)
65119
{
66-
$this->appState->setAreaCode(\Magento\Framework\App\Area::AREA_GLOBAL);
67-
68-
/** @var \Magento\Catalog\Model\ResourceModel\Product\Collection $productCollection */
69-
$productCollection = $this->productCollectionFactory->create();
70-
$productIds = $productCollection->getAllIds();
71-
if (!count($productIds)) {
72-
$output->writeln("<info>No product images to resize</info>");
73-
return \Magento\Framework\Console\Cli::RETURN_SUCCESS;
74-
}
120+
$this->appState->setAreaCode(Area::AREA_GLOBAL);
75121

76-
$errorMessage = '';
77122
try {
78-
foreach ($productIds as $productId) {
79-
try {
80-
/** @var \Magento\Catalog\Model\Product $product */
81-
$product = $this->productRepository->getById($productId);
82-
} catch (\Magento\Framework\Exception\NoSuchEntityException $e) {
83-
continue;
84-
}
123+
$count = $this->productImage->getCountAllProductImages();
124+
if (!$count) {
125+
$output->writeln("<info>No product images to resize</info>");
126+
return Cli::RETURN_SUCCESS;
127+
}
85128

86-
try {
87-
/** @var \Magento\Catalog\Model\Product\Image\Cache $imageCache */
88-
$imageCache = $this->imageCacheFactory->create();
89-
$imageCache->generate($product);
90-
} catch (\Magento\Framework\Exception\RuntimeException $e) {
91-
$errorMessage = $e->getMessage();
92-
}
129+
$productImages = $this->productImage->getAllProductImages();
130+
131+
$themes = $this->themeCollection->loadRegisteredThemes();
132+
$viewImages = $this->getViewImages($themes->getItems());
133+
134+
$progress = new ProgressBar($output, $count);
135+
$progress->setFormat(
136+
"%current%/%max% [%bar%] %percent:3s%% %elapsed% %memory:6s% \t| <info>%message%</info>"
137+
);
93138

94-
$output->write(".");
139+
if ($output->getVerbosity() !== OutputInterface::VERBOSITY_NORMAL) {
140+
$progress->setOverwrite(false);
141+
}
142+
143+
foreach ($productImages as $image) {
144+
$originalImageName = $image['filepath'];
145+
146+
foreach ($viewImages as $viewImage) {
147+
$image = $this->makeImage($originalImageName, $viewImage);
148+
$image->resize();
149+
$image->saveFile();
150+
}
151+
$progress->setMessage($originalImageName);
152+
$progress->advance();
95153
}
96154
} catch (\Exception $e) {
97155
$output->writeln("<error>{$e->getMessage()}</error>");
98156
// we must have an exit code higher than zero to indicate something was wrong
99-
return \Magento\Framework\Console\Cli::RETURN_FAILURE;
157+
return Cli::RETURN_FAILURE;
100158
}
101159

102160
$output->write("\n");
103161
$output->writeln("<info>Product images resized successfully.</info>");
104162

105-
if ($errorMessage !== '') {
106-
$output->writeln("<comment>{$errorMessage}</comment>");
163+
return 0;
164+
}
165+
166+
/**
167+
* Make image
168+
* @param string $originalImagePath
169+
* @param array $imageParams
170+
* @return Image
171+
*/
172+
private function makeImage(string $originalImagePath, array $imageParams): Image
173+
{
174+
$image = $this->productImageFactory->create();
175+
176+
if (isset($imageParams['height'])) {
177+
$image->setHeight($imageParams['height']);
178+
}
179+
if (isset($imageParams['width'])) {
180+
$image->setWidth($imageParams['width']);
181+
}
182+
if (isset($imageParams['aspect_ratio'])) {
183+
$image->setKeepAspectRatio($imageParams['aspect_ratio']);
184+
}
185+
if (isset($imageParams['frame'])) {
186+
$image->setKeepFrame($imageParams['frame']);
187+
}
188+
if (isset($imageParams['transparency'])) {
189+
$image->setKeepTransparency($imageParams['transparency']);
190+
}
191+
if (isset($imageParams['constrain'])) {
192+
$image->setConstrainOnly($imageParams['constrain']);
193+
}
194+
if (isset($imageParams['background'])) {
195+
$image->setBackgroundColor($imageParams['background']);
107196
}
108197

109-
return 0;
198+
$image->setDestinationSubdir($imageParams['type']);
199+
$image->setBaseFile($originalImagePath);
200+
201+
return $image;
202+
}
203+
204+
/**
205+
* Get view images data from themes
206+
* @param array $themes
207+
* @return array
208+
*/
209+
private function getViewImages(array $themes): array
210+
{
211+
$viewImages = [];
212+
foreach ($themes as $theme) {
213+
$config = $this->viewConfig->getViewConfig([
214+
'area' => Area::AREA_FRONTEND,
215+
'themeModel' => $theme,
216+
]);
217+
$images = $config->getMediaEntities('Magento_Catalog', ImageHelper::MEDIA_TYPE_CONFIG_NODE);
218+
foreach ($images as $imageId => $imageData) {
219+
$uniqIndex = $this->getUniqImageIndex($imageData);
220+
$imageData['id'] = $imageId;
221+
$viewImages[$uniqIndex] = $imageData;
222+
}
223+
}
224+
return $viewImages;
225+
}
226+
227+
/**
228+
* Get uniq image index
229+
* @param array $imageData
230+
* @return string
231+
*/
232+
private function getUniqImageIndex(array $imageData): string
233+
{
234+
ksort($imageData);
235+
unset($imageData['type']);
236+
return md5(json_encode($imageData));
110237
}
111238
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Catalog\Model\ResourceModel\Product;
9+
10+
use Magento\Framework\DB\Adapter\AdapterInterface;
11+
use Magento\Framework\DB\Query\Generator;
12+
use Magento\Framework\DB\Select;
13+
use Magento\Framework\App\ResourceConnection;
14+
15+
/**
16+
* Class for fast retrieval of all product images
17+
*/
18+
class Image
19+
{
20+
/**
21+
* @var AdapterInterface
22+
*/
23+
private $connection;
24+
25+
/**
26+
* @var Generator
27+
*/
28+
private $batchQueryGenerator;
29+
30+
/**
31+
* @var ResourceConnection
32+
*/
33+
private $resourceConnection;
34+
35+
/**
36+
* @var int
37+
*/
38+
private $batchSize;
39+
40+
/**
41+
* @param Generator $generator
42+
* @param ResourceConnection $resourceConnection
43+
* @param int $batchSize
44+
*/
45+
public function __construct(
46+
Generator $generator,
47+
ResourceConnection $resourceConnection,
48+
$batchSize = 100
49+
) {
50+
$this->batchQueryGenerator = $generator;
51+
$this->resourceConnection = $resourceConnection;
52+
$this->connection = $this->resourceConnection->getConnection();
53+
$this->batchSize = $batchSize;
54+
}
55+
56+
/**
57+
* Returns product images
58+
*
59+
* @return \Generator
60+
*/
61+
public function getAllProductImages(): \Generator
62+
{
63+
$batchSelectIterator = $this->batchQueryGenerator->generate(
64+
'value_id',
65+
$this->getVisibleImagesSelect(),
66+
$this->batchSize,
67+
\Magento\Framework\DB\Query\BatchIteratorInterface::NON_UNIQUE_FIELD_ITERATOR
68+
);
69+
70+
foreach ($batchSelectIterator as $select) {
71+
foreach ($this->connection->fetchAll($select) as $key => $value) {
72+
yield $key => $value;
73+
}
74+
}
75+
}
76+
77+
/**
78+
* Get the number of unique pictures of products
79+
* @return int
80+
*/
81+
public function getCountAllProductImages(): int
82+
{
83+
$select = $this->getVisibleImagesSelect()->reset('columns')->columns('count(*)');
84+
return (int) $this->connection->fetchOne($select);
85+
}
86+
87+
/**
88+
* @return Select
89+
*/
90+
private function getVisibleImagesSelect(): Select
91+
{
92+
return $this->connection->select()->distinct()
93+
->from(
94+
['images' => $this->resourceConnection->getTableName(Gallery::GALLERY_TABLE)],
95+
'value as filepath'
96+
)->where(
97+
'disabled = 0'
98+
);
99+
}
100+
}

app/code/Magento/Catalog/Model/View/Asset/Image.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
*/
2020
class Image implements LocalInterface
2121
{
22+
private $sourceContentType;
23+
2224
/**
2325
* @var string
2426
*/
@@ -67,6 +69,12 @@ public function __construct(
6769
$filePath,
6870
array $miscParams = []
6971
) {
72+
if (array_key_exists('image_type', $miscParams)) {
73+
$this->sourceContentType = $miscParams['image_type'];
74+
unset($miscParams['image_type']);
75+
} else {
76+
$this->sourceContentType = $this->contentType;
77+
}
7078
$this->mediaConfig = $mediaConfig;
7179
$this->context = $context;
7280
$this->filePath = $filePath;
@@ -129,7 +137,7 @@ public function getSourceFile()
129137
*/
130138
public function getSourceContentType()
131139
{
132-
return $this->contentType;
140+
return $this->sourceContentType;
133141
}
134142

135143
/**

0 commit comments

Comments
 (0)