Merge pull request #19495 from nextcloud/preview-generate-batch

optimize batch generation of previews
This commit is contained in:
Roeland Jago Douma 2020-04-10 15:57:06 +02:00 committed by GitHub
commit eba3726e1e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 273 additions and 78 deletions

View File

@ -91,22 +91,37 @@ class Generator {
* @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid)
*/
public function getPreview(File $file, $width = -1, $height = -1, $crop = false, $mode = IPreview::MODE_FILL, $mimeType = null) {
$specification = [
'width' => $width,
'height' => $height,
'crop' => $crop,
'mode' => $mode,
];
$this->eventDispatcher->dispatch(
IPreview::EVENT,
new GenericEvent($file, $specification)
);
// since we only ask for one preview, and the generate method return the last one it created, it returns the one we want
return $this->generatePreviews($file, [$specification], $mimeType);
}
/**
* Generates previews of a file
*
* @param File $file
* @param array $specifications
* @param string $mimeType
* @return ISimpleFile the last preview that was generated
* @throws NotFoundException
* @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid)
*/
public function generatePreviews(File $file, array $specifications, $mimeType = null) {
//Make sure that we can read the file
if (!$file->isReadable()) {
throw new NotFoundException('Cannot read file');
}
$this->eventDispatcher->dispatch(
IPreview::EVENT,
new GenericEvent($file, [
'width' => $width,
'height' => $height,
'crop' => $crop,
'mode' => $mode
])
);
if ($mimeType === null) {
$mimeType = $file->getMimeType();
}
@ -123,41 +138,57 @@ class Generator {
// Get the max preview and infer the max preview sizes from that
$maxPreview = $this->getMaxPreview($previewFolder, $file, $mimeType, $previewVersion);
$maxPreviewImage = null; // only load the image when we need it
if ($maxPreview->getSize() === 0) {
$maxPreview->delete();
throw new NotFoundException('Max preview size 0, invalid!');
}
list($maxWidth, $maxHeight) = $this->getPreviewSize($maxPreview, $previewVersion);
[$maxWidth, $maxHeight] = $this->getPreviewSize($maxPreview, $previewVersion);
// If both width and heigth are -1 we just want the max preview
if ($width === -1 && $height === -1) {
$width = $maxWidth;
$height = $maxHeight;
}
$preview = null;
// Calculate the preview size
list($width, $height) = $this->calculateSize($width, $height, $crop, $mode, $maxWidth, $maxHeight);
foreach ($specifications as $specification) {
$width = $specification['width'] ?? -1;
$height = $specification['height'] ?? -1;
$crop = $specification['crop'] ?? false;
$mode = $specification['mode'] ?? IPreview::MODE_FILL;
// No need to generate a preview that is just the max preview
if ($width === $maxWidth && $height === $maxHeight) {
return $maxPreview;
}
// Try to get a cached preview. Else generate (and store) one
try {
try {
$preview = $this->getCachedPreview($previewFolder, $width, $height, $crop, $maxPreview->getMimeType(), $previewVersion);
} catch (NotFoundException $e) {
$preview = $this->generatePreview($previewFolder, $maxPreview, $width, $height, $crop, $maxWidth, $maxHeight, $previewVersion);
// If both width and heigth are -1 we just want the max preview
if ($width === -1 && $height === -1) {
$width = $maxWidth;
$height = $maxHeight;
}
} catch (\InvalidArgumentException $e) {
throw new NotFoundException();
}
if ($preview->getSize() === 0) {
$preview->delete();
throw new NotFoundException('Cached preview size 0, invalid!');
// Calculate the preview size
[$width, $height] = $this->calculateSize($width, $height, $crop, $mode, $maxWidth, $maxHeight);
// No need to generate a preview that is just the max preview
if ($width === $maxWidth && $height === $maxHeight) {
// ensure correct return value if this was the last one
$preview = $maxPreview;
continue;
}
// Try to get a cached preview. Else generate (and store) one
try {
try {
$preview = $this->getCachedPreview($previewFolder, $width, $height, $crop, $maxPreview->getMimeType(), $previewVersion);
} catch (NotFoundException $e) {
if ($maxPreviewImage === null) {
$maxPreviewImage = $this->helper->getImage($maxPreview);
}
$preview = $this->generatePreview($previewFolder, $maxPreviewImage, $width, $height, $crop, $maxWidth, $maxHeight, $previewVersion);
}
} catch (\InvalidArgumentException $e) {
throw new NotFoundException("", 0, $e);
}
if ($preview->getSize() === 0) {
$preview->delete();
throw new NotFoundException('Cached preview size 0, invalid!');
}
}
return $preview;
@ -360,9 +391,8 @@ class Generator {
* @throws NotFoundException
* @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid)
*/
private function generatePreview(ISimpleFolder $previewFolder, ISimpleFile $maxPreview, $width, $height, $crop, $maxWidth, $maxHeight, $prefix) {
$preview = $this->helper->getImage($maxPreview);
private function generatePreview(ISimpleFolder $previewFolder, IImage $maxPreview, $width, $height, $crop, $maxWidth, $maxHeight, $prefix) {
$preview = $maxPreview;
if (!$preview->valid()) {
throw new \InvalidArgumentException('Failed to generate preview, failed to load image');
}
@ -380,13 +410,13 @@ class Generator {
$scaleH = $maxHeight / $widthR;
$scaleW = $width;
}
$preview->preciseResize((int)round($scaleW), (int)round($scaleH));
$preview = $preview->preciseResizeCopy((int)round($scaleW), (int)round($scaleH));
}
$cropX = (int)floor(abs($width - $preview->width()) * 0.5);
$cropY = (int)floor(abs($height - $preview->height()) * 0.5);
$preview->crop($cropX, $cropY, $width, $height);
$preview = $preview->cropCopy($cropX, $cropY, $width, $height);
} else {
$preview->resize(max($width, $height));
$preview = $maxPreview->resizeCopy(max($width, $height));
}
@ -448,7 +478,7 @@ class Generator {
case 'image/gif':
return 'gif';
default:
throw new \InvalidArgumentException('Not a valid mimetype');
throw new \InvalidArgumentException('Not a valid mimetype: "' . $mimeType . '"');
}
}
}

View File

@ -151,6 +151,22 @@ class PreviewManager implements IPreview {
return !empty($this->providers);
}
private function getGenerator(): Generator {
if ($this->generator === null) {
$this->generator = new Generator(
$this->config,
$this,
$this->appData,
new GeneratorHelper(
$this->rootFolder,
$this->config
),
$this->eventDispatcher
);
}
return $this->generator;
}
/**
* Returns a preview of a file
*
@ -169,20 +185,22 @@ class PreviewManager implements IPreview {
* @since 11.0.0 - \InvalidArgumentException was added in 12.0.0
*/
public function getPreview(File $file, $width = -1, $height = -1, $crop = false, $mode = IPreview::MODE_FILL, $mimeType = null) {
if ($this->generator === null) {
$this->generator = new Generator(
$this->config,
$this,
$this->appData,
new GeneratorHelper(
$this->rootFolder,
$this->config
),
$this->eventDispatcher
);
}
return $this->getGenerator()->getPreview($file, $width, $height, $crop, $mode, $mimeType);
}
return $this->generator->getPreview($file, $width, $height, $crop, $mode, $mimeType);
/**
* Generates previews of a file
*
* @param File $file
* @param array $specifications
* @param string $mimeType
* @return ISimpleFile the last preview that was generated
* @throws NotFoundException
* @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid)
* @since 19.0.0
*/
public function generatePreviews(File $file, array $specifications, $mimeType = null) {
return $this->getGenerator()->generatePreviews($file, $specifications, $mimeType);
}
/**

View File

@ -39,6 +39,8 @@
*
*/
use OCP\IImage;
/**
* Class for basic image manipulation
*/
@ -845,6 +847,17 @@ class OC_Image implements \OCP\IImage {
* @return bool
*/
public function resize($maxSize) {
$result = $this->resizeNew($maxSize);
imagedestroy($this->resource);
$this->resource = $result;
return is_resource($result);
}
/**
* @param $maxSize
* @return resource | bool
*/
private function resizeNew($maxSize) {
if (!$this->valid()) {
$this->logger->error(__METHOD__ . '(): No image loaded', ['app' => 'core']);
return false;
@ -861,8 +874,7 @@ class OC_Image implements \OCP\IImage {
$newHeight = $maxSize;
}
$this->preciseResize((int)round($newWidth), (int)round($newHeight));
return true;
return $this->preciseResizeNew((int)round($newWidth), (int)round($newHeight));
}
/**
@ -871,6 +883,19 @@ class OC_Image implements \OCP\IImage {
* @return bool
*/
public function preciseResize(int $width, int $height): bool {
$result = $this->preciseResizeNew($width, $height);
imagedestroy($this->resource);
$this->resource = $result;
return is_resource($result);
}
/**
* @param int $width
* @param int $height
* @return resource | bool
*/
public function preciseResizeNew(int $width, int $height) {
if (!$this->valid()) {
$this->logger->error(__METHOD__ . '(): No image loaded', ['app' => 'core']);
return false;
@ -896,9 +921,7 @@ class OC_Image implements \OCP\IImage {
imagedestroy($process);
return false;
}
imagedestroy($this->resource);
$this->resource = $process;
return true;
return $process;
}
/**
@ -969,6 +992,22 @@ class OC_Image implements \OCP\IImage {
* @return bool for success or failure
*/
public function crop(int $x, int $y, int $w, int $h): bool {
$result = $this->cropNew($x, $y, $w, $h);
imagedestroy($this->resource);
$this->resource = $result;
return is_resource($result);
}
/**
* Crops the image from point $x$y with dimension $wx$h.
*
* @param int $x Horizontal position
* @param int $y Vertical position
* @param int $w Width
* @param int $h Height
* @return resource | bool
*/
public function cropNew(int $x, int $y, int $w, int $h) {
if (!$this->valid()) {
$this->logger->error(__METHOD__ . '(): No image loaded', ['app' => 'core']);
return false;
@ -993,9 +1032,7 @@ class OC_Image implements \OCP\IImage {
imagedestroy($process);
return false;
}
imagedestroy($this->resource);
$this->resource = $process;
return true;
return $process;
}
/**
@ -1045,6 +1082,55 @@ class OC_Image implements \OCP\IImage {
return false;
}
public function copy(): IImage {
$image = new OC_Image(null, $this->logger, $this->config);
$image->resource = imagecreatetruecolor($this->width(), $this->height());
imagecopy(
$image->resource(),
$this->resource(),
0,
0,
0,
0,
$this->width(),
$this->height()
);
return $image;
}
public function cropCopy(int $x, int $y, int $w, int $h): IImage {
$image = new OC_Image(null, $this->logger, $this->config);
$image->resource = $this->cropNew($x, $y, $w, $h);
return $image;
}
public function preciseResizeCopy(int $width, int $height): IImage {
$image = new OC_Image(null, $this->logger, $this->config);
$image->resource = $this->preciseResizeNew($width, $height);
return $image;
}
public function resizeCopy(int $maxSize): IImage {
$image = new OC_Image(null, $this->logger, $this->config);
$image->resource = $this->resizeNew($maxSize);
return $image;
}
/**
* Resizes the image preserving ratio, returning a new copy
*
* @param integer $maxSize The maximum size of either the width or height.
* @return bool
*/
public function copyResize($maxSize): IImage {
}
/**
* Destroys the current image and resets the object
*/

View File

@ -190,4 +190,43 @@ interface IImage {
* @since 8.1.0
*/
public function scaleDownToFit($maxWidth, $maxHeight);
/**
* create a copy of this image
*
* @return IImage
* @since 19.0.0
*/
public function copy(): IImage;
/**
* create a new cropped copy of this image
*
* @param int $x Horizontal position
* @param int $y Vertical position
* @param int $w Width
* @param int $h Height
* @return IImage
* @since 19.0.0
*/
public function cropCopy(int $x, int $y, int $w, int $h): IImage;
/**
* create a new resized copy of this image
*
* @param int $width
* @param int $height
* @return IImage
* @since 19.0.0
*/
public function preciseResizeCopy(int $width, int $height): IImage;
/**
* create a new resized copy of this image
*
* @param integer $maxSize The maximum size of either the width or height.
* @return IImage
* @since 19.0.0
*/
public function resizeCopy(int $maxSize): IImage;
}

View File

@ -115,4 +115,17 @@ interface IPreview {
* @since 8.0.0
*/
public function isAvailable(\OCP\Files\FileInfo $file);
/**
* Generates previews of a file
*
* @param File $file
* @param array $specifications
* @param string $mimeType
* @return ISimpleFile the last preview that was generated
* @throws NotFoundException
* @throws \InvalidArgumentException if the preview would be invalid (in case the original image is invalid)
* @since 19.0.0
*/
public function generatePreviews(File $file, array $specifications, $mimeType = null);
}

View File

@ -227,19 +227,11 @@ class GeneratorTest extends \Test\TestCase {
->with($this->equalTo('256-256.png'))
->willThrowException(new NotFoundException());
$image = $this->createMock(IImage::class);
$image = $this->getMockImage(2048, 2048, 'my resized data');
$this->helper->method('getImage')
->with($this->equalTo($maxPreview))
->willReturn($image);
$image->expects($this->once())
->method('resize')
->with(256);
$image->method('data')
->willReturn('my resized data');
$image->method('valid')->willReturn(true);
$image->method('dataMimeType')->willReturn('image/png');
$previewFile->expects($this->once())
->method('putContent')
->with('my resized data');
@ -325,6 +317,27 @@ class GeneratorTest extends \Test\TestCase {
$this->generator->getPreview($file, 100, 100);
}
private function getMockImage($width, $height, $data = null) {
$image = $this->createMock(IImage::class);
$image->method('height')->willReturn($width);
$image->method('width')->willReturn($height);
$image->method('valid')->willReturn(true);
$image->method('dataMimeType')->willReturn('image/png');
$image->method('data')->willReturn($data);
$image->method('resizeCopy')->willReturnCallback(function($size) use ($data) {
return $this->getMockImage($size, $size, $data);
});
$image->method('preciseResizeCopy')->willReturnCallback(function($width, $height) use ($data) {
return $this->getMockImage($width, $height, $data);
});
$image->method('cropCopy')->willReturnCallback(function($x, $y, $width, $height) use ($data) {
return $this->getMockImage($width, $height, $data);
});
return $image;
}
public function dataSize() {
return [
[1024, 2048, 512, 512, false, IPreview::MODE_FILL, 256, 512],
@ -409,14 +422,10 @@ class GeneratorTest extends \Test\TestCase {
->with($this->equalTo($filename))
->willThrowException(new NotFoundException());
$image = $this->createMock(IImage::class);
$image = $this->getMockImage($maxX, $maxY);
$this->helper->method('getImage')
->with($this->equalTo($maxPreview))
->willReturn($image);
$image->method('height')->willReturn($maxY);
$image->method('width')->willReturn($maxX);
$image->method('valid')->willReturn(true);
$image->method('dataMimeType')->willReturn('image/png');
$preview = $this->createMock(ISimpleFile::class);
$previewFolder->method('newFile')