diff --git a/Web/Models/Entities/Photo.php b/Web/Models/Entities/Photo.php index 628589db..5627a4fe 100644 --- a/Web/Models/Entities/Photo.php +++ b/Web/Models/Entities/Photo.php @@ -1,6 +1,8 @@ getWidth() / $image->getHeight()) > ($px / $py)) { - # For some weird reason using resize with EXACT flag causes system to consume an unholy amount of RAM - $image->crop(0, 0, "100%", (int) ceil(($px * $image->getWidth()) / $py)); + if(($image->getImageWidth() / $image->getImageHeight()) > ($px / $py)) { + $height = ceil(($px * $image->getImageWidth()) / $py); + $image->cropImage($image->getImageWidth(), $height, 0, 0); $res[0] = true; } } - + if(isset($size["maxSize"])) { $maxSize = (int) $size["maxSize"]; - $image->resize($maxSize, $maxSize, Image::SHRINK_ONLY | Image::FIT); + $sizes = Image::calculateSize($image->getImageWidth(), $image->getImageHeight(), $maxSize, $maxSize, Image::SHRINK_ONLY | Image::FIT); + $image->resizeImage($sizes[0], $sizes[1], \Imagick::FILTER_POINT, 1); } else if(isset($size["maxResolution"])) { $resolution = explode("x", (string) $size["maxResolution"]); - $image->resize((int) $resolution[0], (int) $resolution[1], Image::SHRINK_ONLY | Image::FIT); + $sizes = Image::calculateSize( + $image->getImageWidth(), $image->getImageHeight(), (int) $resolution[0], (int) $resolution[1], Image::SHRINK_ONLY | Image::FIT + ); + $image->resizeImage($sizes[0], $sizes[1], \Imagick::FILTER_POINT, 1); } else { throw new \RuntimeException("Malformed size description: " . (string) $size["id"]); } - - $res[1] = $image->getWidth(); - $res[2] = $image->getHeight(); + + $res[1] = $image->getImageWidth(); + $res[2] = $image->getImageHeight(); if($res[1] <= 300 || $res[2] <= 300) - $image->save("$outputDir/" . (string) $size["id"] . ".gif"); + $image->writeImage("$outputDir/$size[id].gif"); else - $image->save("$outputDir/" . (string) $size["id"] . ".jpeg"); - - imagedestroy($image->getImageResource()); + $image->writeImage("$outputDir/$size[id].jpeg"); + + $res[3] = true; + $image->destroy(); unset($image); - + return $res; } - private function saveImageResizedCopies(string $filename, string $hash): void + private function saveImageResizedCopies(?\Imagick $image, string $filename, string $hash): void { + if(!$image) { + $image = new \Imagick; + $image->readImage($filename); + } + $dir = dirname($this->pathFromHash($hash)); $dir = "$dir/$hash" . "_cropped"; if(!is_dir($dir)) { @@ -67,8 +83,13 @@ class Photo extends Media throw new \RuntimeException("Could not load photosizes.xml!"); $sizesMeta = []; - foreach($sizes->Size as $size) - $sizesMeta[(string) $size["id"]] = $this->resizeImage($filename, $dir, $size); + if(OPENVK_ROOT_CONF["openvk"]["preferences"]["photos"]["photoSaving"] === "quick") { + foreach($sizes->Size as $size) + $sizesMeta[(string)$size["id"]] = [false, false, false, false]; + } else { + foreach($sizes->Size as $size) + $sizesMeta[(string)$size["id"]] = $this->resizeImage(clone $image, $dir, $size); + } $sizesMeta = MessagePack::pack($sizesMeta); $this->stateChanges("sizes", $sizesMeta); @@ -76,13 +97,19 @@ class Photo extends Media protected function saveFile(string $filename, string $hash): bool { - $image = Image::fromFile($filename); - if(($image->height >= ($image->width * Photo::ALLOWED_SIDE_MULTIPLIER)) || ($image->width >= ($image->height * Photo::ALLOWED_SIDE_MULTIPLIER))) + $image = new \Imagick; + $image->readImage($filename); + $h = $image->getImageHeight(); + $w = $image->getImageWidth(); + if(($h >= ($w * Photo::ALLOWED_SIDE_MULTIPLIER)) || ($w >= ($h * Photo::ALLOWED_SIDE_MULTIPLIER))) throw new ISE("Invalid layout: image is too wide/short"); - - $image->resize(8192, 4320, Image::SHRINK_ONLY | Image::FIT); - $image->save($this->pathFromHash($hash), 92, Image::JPEG); - $this->saveImageResizedCopies($filename, $hash); + + $sizes = Image::calculateSize( + $image->getImageWidth(), $image->getImageHeight(), 8192, 4320, Image::SHRINK_ONLY | Image::FIT + ); + $image->resizeImage($sizes[0], $sizes[1], \Imagick::FILTER_POINT, 1); + $image->writeImage($this->pathFromHash($hash)); + $this->saveImageResizedCopies($image, $filename, $hash); return true; } @@ -114,8 +141,8 @@ class Photo extends Media $sizes = $this->getRecord()->sizes; if(!$sizes || $forceUpdate) { if($forceUpdate || $upgrade || OPENVK_ROOT_CONF["openvk"]["preferences"]["photos"]["upgradeStructure"]) { - $hash = $this->getRecord()->hash; - $this->saveImageResizedCopies($this->pathFromHash($hash), $hash); + $hash = $this->getRecord()->hash; + $this->saveImageResizedCopies(NULL, $this->pathFromHash($hash), $hash); $this->save(); return $this->getSizes(); @@ -127,6 +154,16 @@ class Photo extends Media $res = []; $sizes = MessagePack::unpack($sizes); foreach($sizes as $id => $meta) { + if(isset($meta[3]) && !$meta[3]) { + $res[$id] = (object) [ + "url" => "/photos/thumbnails/" . $this->getId() . "_$id.jpeg", + "width" => NULL, + "height" => NULL, + "crop" => NULL + ]; + continue; + } + $url = $this->getURL(); $url = str_replace(".$this->fileExtension", "_cropped/$id.", $url); $url .= ($meta[1] <= 300 || $meta[2] <= 300) ? "gif" : "jpeg"; @@ -149,6 +186,47 @@ class Photo extends Media return $res; } + + function forceSize(string $sizeName): bool + { + $hash = $this->getRecord()->hash; + $sizes = MessagePack::unpack($this->getRecord()->sizes); + $size = $sizes[$sizeName] ?? false; + if(!$size) + return $size; + + if(!isset($size[3]) || $size[3] === true) + return true; + + $path = $this->pathFromHash($hash); + $dir = dirname($this->pathFromHash($hash)); + $dir = "$dir/$hash" . "_cropped"; + if(!is_dir($dir)) { + @unlink($dir); + mkdir($dir); + } + + $sizeMetas = simplexml_load_file(OPENVK_ROOT . "/data/photosizes.xml"); + if(!$sizeMetas) + throw new \RuntimeException("Could not load photosizes.xml!"); + + $sizeInfo = NULL; + foreach($sizeMetas->Size as $size) + if($size["id"] == $sizeName) + $sizeInfo = $size; + + if(!$sizeInfo) + return false; + + $pic = new \Imagick; + $pic->readImage($path); + $sizes[$sizeName] = $this->resizeImage($pic, $dir, $sizeInfo); + + $this->stateChanges("sizes", MessagePack::pack($sizes)); + $this->save(); + + return $sizes[$sizeName][3]; + } function getVkApiSizes(): ?array { diff --git a/Web/Presenters/PhotosPresenter.php b/Web/Presenters/PhotosPresenter.php index 3dd2a774..02d6ae46 100644 --- a/Web/Presenters/PhotosPresenter.php +++ b/Web/Presenters/PhotosPresenter.php @@ -185,6 +185,18 @@ final class PhotosPresenter extends OpenVKPresenter $this->renderPhoto($photo->getOwner(true)->getId(), $photo->getVirtualId()); } + function renderThumbnail($id, $size): void + { + $photo = $this->photos->get($id); + if(!$photo || $photo->isDeleted()) + $this->notFound(); + + if(!$photo->forceSize($size)) + chandler_http_panic(588, "Gone", "This thumbnail cannot be generated due to server misconfiguration"); + + $this->redirect($photo->getURLBySizeId($size), 8); + } + function renderEditPhoto(int $ownerId, int $photoId): void { $this->assertUserLoggedIn(); diff --git a/Web/routes.yml b/Web/routes.yml index e5639a08..bcb6d289 100644 --- a/Web/routes.yml +++ b/Web/routes.yml @@ -163,6 +163,8 @@ routes: handler: "Photos->uploadPhoto" - url: "/photo{num}_{num}" handler: "Photos->photo" + - url: "/photos/thumbnails/{num}_{text}.jpeg" + handler: "Photos->thumbnail" - url: "/photos/{text}" handler: "Photos->absolutePhoto" - url: "/photo{num}_{num}/edit" diff --git a/openvk-example.yml b/openvk-example.yml index 47eacd24..c2f794e4 100644 --- a/openvk-example.yml +++ b/openvk-example.yml @@ -18,7 +18,8 @@ openvk: forbiddenNames: - "index.php" photos: - upgradeStructure: true + upgradeStructure: false + photoSaving: "quick" apps: withdrawTax: 8 security: