BookStack/app/Uploads/ImageRepo.php
Dan Brown 32e7f0a2e6
Made display thumbnail generation use original data if smaller
Thumbnail generation would sometimes create a file larger than the
original, if the original was already well optimized, therefore making
the thumbnail counter-productive. This change compares the sizes of the
original and the generated thumbnail, and uses the smaller of the two if
the thumbnail does not change the aspect ratio of the image.

Fixes #1751
2019-12-22 12:44:49 +00:00

230 lines
6.7 KiB
PHP

<?php namespace BookStack\Uploads;
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Entities\Page;
use BookStack\Exceptions\ImageUploadException;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Symfony\Component\HttpFoundation\File\UploadedFile;
class ImageRepo
{
protected $image;
protected $imageService;
protected $restrictionService;
protected $page;
/**
* ImageRepo constructor.
*/
public function __construct(
Image $image,
ImageService $imageService,
PermissionService $permissionService,
Page $page
) {
$this->image = $image;
$this->imageService = $imageService;
$this->restrictionService = $permissionService;
$this->page = $page;
}
/**
* Get an image with the given id.
*/
public function getById($id): Image
{
return $this->image->findOrFail($id);
}
/**
* Execute a paginated query, returning in a standard format.
* Also runs the query through the restriction system.
*/
private function returnPaginated($query, $page = 1, $pageSize = 24): array
{
$images = $query->orderBy('created_at', 'desc')->skip($pageSize * ($page - 1))->take($pageSize + 1)->get();
$hasMore = count($images) > $pageSize;
$returnImages = $images->take($pageSize);
$returnImages->each(function ($image) {
$this->loadThumbs($image);
});
return [
'images' => $returnImages,
'has_more' => $hasMore
];
}
/**
* Fetch a list of images in a paginated format, filtered by image type.
* Can be filtered by uploaded to and also by name.
*/
public function getPaginatedByType(
string $type,
int $page = 0,
int $pageSize = 24,
int $uploadedTo = null,
string $search = null,
callable $whereClause = null
): array
{
$imageQuery = $this->image->newQuery()->where('type', '=', strtolower($type));
if ($uploadedTo !== null) {
$imageQuery = $imageQuery->where('uploaded_to', '=', $uploadedTo);
}
if ($search !== null) {
$imageQuery = $imageQuery->where('name', 'LIKE', '%' . $search . '%');
}
// Filter by page access
$imageQuery = $this->restrictionService->filterRelatedEntity('page', $imageQuery, 'images', 'uploaded_to');
if ($whereClause !== null) {
$imageQuery = $imageQuery->where($whereClause);
}
return $this->returnPaginated($imageQuery, $page, $pageSize);
}
/**
* Get paginated gallery images within a specific page or book.
*/
public function getEntityFiltered(
string $type,
string $filterType = null,
int $page = 0,
int $pageSize = 24,
int $uploadedTo = null,
string $search = null
): array
{
$contextPage = $this->page->findOrFail($uploadedTo);
$parentFilter = null;
if ($filterType === 'book' || $filterType === 'page') {
$parentFilter = function (Builder $query) use ($filterType, $contextPage) {
if ($filterType === 'page') {
$query->where('uploaded_to', '=', $contextPage->id);
} elseif ($filterType === 'book') {
$validPageIds = $contextPage->book->pages()->get(['id'])->pluck('id')->toArray();
$query->whereIn('uploaded_to', $validPageIds);
}
};
}
return $this->getPaginatedByType($type, $page, $pageSize, null, $search, $parentFilter);
}
/**
* Save a new image into storage and return the new image.
* @throws ImageUploadException
*/
public function saveNew(UploadedFile $uploadFile, string $type, int $uploadedTo = 0, int $resizeWidth = null, int $resizeHeight = null, bool $keepRatio = true): Image
{
$image = $this->imageService->saveNewFromUpload($uploadFile, $type, $uploadedTo, $resizeWidth, $resizeHeight, $keepRatio);
$this->loadThumbs($image);
return $image;
}
/**
* Save a drawing the the database.
* @throws ImageUploadException
*/
public function saveDrawing(string $base64Uri, int $uploadedTo): Image
{
$name = 'Drawing-' . user()->getShortName(40) . '-' . strval(time()) . '.png';
return $this->imageService->saveNewFromBase64Uri($base64Uri, $name, 'drawio', $uploadedTo);
}
/**
* Update the details of an image via an array of properties.
* @throws ImageUploadException
* @throws Exception
*/
public function updateImageDetails(Image $image, $updateDetails): Image
{
$image->fill($updateDetails);
$image->save();
$this->loadThumbs($image);
return $image;
}
/**
* Destroys an Image object along with its revisions, files and thumbnails.
* @throws Exception
*/
public function destroyImage(Image $image = null): bool
{
if ($image) {
$this->imageService->destroy($image);
}
return true;
}
/**
* Destroy all images of a certain type.
* @throws Exception
*/
public function destroyByType(string $imageType)
{
$images = $this->image->where('type', '=', $imageType)->get();
foreach ($images as $image) {
$this->destroyImage($image);
}
}
/**
* Load thumbnails onto an image object.
* @throws Exception
*/
protected function loadThumbs(Image $image)
{
$image->thumbs = [
'gallery' => $this->getThumbnail($image, 150, 150, false),
'display' => $this->getThumbnail($image, 1680, null, true)
];
}
/**
* Get the thumbnail for an image.
* If $keepRatio is true only the width will be used.
* Checks the cache then storage to avoid creating / accessing the filesystem on every check.
* @throws Exception
*/
protected function getThumbnail(Image $image, ?int $width = 220, ?int $height = 220, bool $keepRatio = false): ?string
{
try {
return $this->imageService->getThumbnail($image, $width, $height, $keepRatio);
} catch (Exception $exception) {
return null;
}
}
/**
* Get the raw image data from an Image.
*/
public function getImageData(Image $image): ?string
{
try {
return $this->imageService->getImageData($image);
} catch (Exception $exception) {
return null;
}
}
/**
* Get the validation rules for image files.
*/
public function getImageValidationRules(): string
{
return 'image_extension|no_double_extension|mimes:jpeg,png,gif,bmp,webp,tiff';
}
}