Skip to content

Instantly share code, notes, and snippets.

@Gkiokan
Last active March 14, 2023 17:18
Show Gist options
  • Save Gkiokan/26ef3ed0a6a7e34f3b00e1c5d2adf208 to your computer and use it in GitHub Desktop.
Save Gkiokan/26ef3ed0a6a7e34f3b00e1c5d2adf208 to your computer and use it in GitHub Desktop.
Image Proxy Controller
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Intervention\Image\Facades\Image;
use Storage;
use Http;
use Cache;
use Log;
class ProxyController extends Controller
{
public $forceWidth = null;
public $forceCompression = null;
public $folder = 'images/';
/**
* Entry Points
*/
public function image(Request $r, $url=null){
return $this->proxy($r, $url);
}
public function cover(Request $r, $url=null){
$this->forceWidth = 300;
$this->forceCompression = 50;
$this->folder = 'covers/';
return $this->proxy($r, $url);
}
public function actor(Request $r, $url=null){
$this->forceWidth = 200;
$this->forceCompression = 40;
$this->folder = 'actors/';
return $this->proxy($r, $url);
}
/**
* General Image Proxy
*/
public function proxy(Request $request, $url=null){
$imageUrl = $this->validateUrl($url);
// Check if the image already exists in the storage
$filename = basename($imageUrl);
$filename = hash('fnv1a32', $filename);
$filePath = $this->folder . $filename;
// return file if we have it already
if (Storage::disk('cdn')->exists($filePath)){
$image = Storage::disk('cdn')->get($filePath);
$mime = Storage::disk('cdn')->mimeType($filePath);
return response($image, 200)->header('Content-Type', $mime);
}
// Fetch the remote image
try {
$response = Http::get($imageUrl);
$contentType = $response->header('Content-Type');
}
catch( \Exception $e){
return response($e->getMessage(), 400);
}
// url check
if( strstr($response->handlerStats()['url'], 'nopicture') )
return response('Probably no-image', 404);
// check mime type
if (!str_starts_with($contentType, 'image/'))
return response('Invalid content type', 400);
// put this in a job
$this->createJob($imageUrl, $filePath);
// Return the image as a response directly for now
return response($response->body())->header('Content-Type', $contentType);
}
function validateUrl($url=null){
if( !$url )
return response('Invalid URL', 400);
if( strstr($url, 'nopicture') )
return response(null, 404);
if (!filter_var($url, FILTER_VALIDATE_URL)) {
return response('Invalid URL', 400);
}
$urlComponents = parse_url($url);
if (!isset($urlComponents['scheme']) || !in_array($urlComponents['scheme'], ['http', 'https'])) {
return response('Invalid URL scheme', 400);
}
$host = $urlComponents['host'];
$isValidDomain = Cache::remember('dns:' . $host, 60 * 60 * 30, function () use ($host) {
return checkdnsrr($host, 'A');
});
if (!$isValidDomain) {
return response('Invalid domain', 400);
}
return $url;
}
public function resizeImage($body=null, $forceWidth=null, $forceCompression=null){
$image = Image::make($body);
$size = strlen($body); // $image->filesize() // not working because we have the stream and not the file
$softLimit = 1000000; // 1 * 1024 * 1024;
$limit = 4000000; // 4 * 1024 * 1024;
$hardLimit = 8000000; // 8 * 1024 * 1024;
$compression = 60;
$maxWidthPercent = 60;
$factor = 1;
// debugging
// dd($image, $image->getWidth(), $size, $softLimit, $size > $softLimit);
// if limit exceeds
// if ($size > $softLimit):
if ( true ):
/**
* Prepare the Compression logic
*/
if ($size > $softLimit):
$compression = 40;
$maxWidthPercent = 50;
$factor = 0.9;
endif;
if( $size > $hardLimit ):
$factor = 0.6;
endif;
// resize the image based on the filesize
$width = $image->getWidth();
$newWidth = round($width * ($maxWidthPercent / 100));
// set the compression higher for 4MB+
if( $size > $limit )
$compression = 25;
$mimeToExtension = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/gif' => 'gif',
'image/webp' => 'webp',
'image/svg+xml' => 'svg',
'image/bmp' => 'bmp',
'image/tiff' => 'tiff',
// add more MIME types as keys and their corresponding extensions as values
];
$extension = $mimeToExtension[$image->mime()] ?? false;
// simple check
if( $extension == false )
return response('Not supported file format', 400);
// Override values if given
if( $forceWidth )
$newWidth = $forceWidth;
if( $forceCompression )
$compression = $forceCompression * $factor;
/**
* Do the Magic here
*/
Log::info("Image Resize and patch to $newWidth Compression $compression");
// resize
$image->resize($newWidth, null, function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
});
// encode as WebP if original image is JPEG or PNG
if ($image->mime() === 'image/jpeg' || $image->mime() === 'image/png')
$image->encode('webp', $compression);
// otherwise, encode using the original extension
else
$image->encode($extension, $compression);
endif;
return $image->stream()->__toString();
}
public function createJob($imageUrl, $filePath){
dispatch( function() use ($imageUrl, $filePath){
Log::info("Patching CDN Image \n URL: $imageUrl \n Width: $this->forceWidth \n Compression: $this->forceCompression");
// catch the image again
$response = Http::get($imageUrl);
// url check
if( strstr($response->handlerStats()['url'], 'nopicture') )
return response("Probably a No-Picture file", 400);
// No Image size 14282
if( strlen($response->body()) == 14282)
return response("Probably a No-Picture file", 400);
// Resize the image if it's larger than 8MB while maintaining its proportions
$image = $this->resizeImage($response->body(), $this->forceWidth, $this->forceCompression);
// Save the image to the application
Storage::disk('cdn')->put($filePath, $image);
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment