From b113aa47fe903e538aaba58024ed03e85d041b98 Mon Sep 17 00:00:00 2001 From: philipp lang Date: Sun, 25 Oct 2020 20:53:10 +0100 Subject: [PATCH] Add: Crop and resize images --- .gitignore | 1 + classes/CompressJob.php | 85 ++++++++++++++ classes/FirstLetterStrategy.php | 31 +++++ classes/UploadStorage.php | 62 ++++++++++ formwidgets/.Responsiveimage.php.swp | Bin 32768 -> 0 bytes formwidgets/Responsiveimage.php | 109 +++--------------- .../responsiveimage/assets/src/App.vue | 13 ++- .../partials/._responsiveimage.htm.swp | Bin 12288 -> 0 bytes 8 files changed, 202 insertions(+), 99 deletions(-) create mode 100644 classes/CompressJob.php create mode 100644 classes/FirstLetterStrategy.php create mode 100644 classes/UploadStorage.php delete mode 100644 formwidgets/.Responsiveimage.php.swp delete mode 100644 formwidgets/responsiveimage/partials/._responsiveimage.htm.swp diff --git a/.gitignore b/.gitignore index 9e142a0..ce064ce 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ /formwidgets/responsiveimage/assets/images/ /formwidgets/responsiveimage/assets/mix-manifest.json /formwidgets/responsiveimage/assets/node_modules/ +*.swp diff --git a/classes/CompressJob.php b/classes/CompressJob.php new file mode 100644 index 0000000..492370f --- /dev/null +++ b/classes/CompressJob.php @@ -0,0 +1,85 @@ +path = $data['path']; + $this->filename = $data['filename']; + $this->disk = $data['disk']; + $this->strategy = $data['strategy']; + $this->crop = $data['crop']; + + $this->sizes = Setting::get('srcx'); + + $this->crop(); + $this->createVersions(); + } + + public function crop() { + if ($this->crop === null) { + return; + } + + $fullPath = Storage::disk($this->disk)->path($this->path.'/'.$this->filename); + + Storage::disk($this->disk)->makeDirectory($this->getStrategy()->croppedPath()); + + $r = Resizer::open($fullPath); + + $r->crop(floor($this->crop['x']), floor($this->crop['y']), floor($this->crop['w']), floor($this->crop['h'])); + + $this->path = $this->getStrategy()->croppedPath($this->filename); + $this->filename = $this->getStrategy()->croppedFilename($this->filename, $this->crop); + + $r->save(Storage::disk($this->disk)->path($this->path.'/'.$this->filename)); + } + + public function createVersions() + { + $fullPath = Storage::disk($this->disk)->path($this->path.'/'.$this->filename); + [ $width, $height ] = getimagesize($fullPath); + + if ($width > $this->maxFilesize) { + $r = Resizer::open($fullPath); + $r->resize($this->maxFilesize, 0); + $r->save($fullPath); + } + + [ $width, $height ] = getimagesize($fullPath); + + Storage::disk($this->disk)->makeDirectory($this->getStrategy()->publicPath($this->filename)); + + foreach ($this->sizes as $w) { + + $filename = $this->getStrategy()->smallFilename($this->filename, $w); + + if ($width < $w) { + continue; + } + + $destination = Storage::disk($this->disk)->path($this->getStrategy()->publicPath($this->filename).'/'.$this->getStrategy()->smallFilename($this->filename, $w)); + + $r = Resizer::open($fullPath); + $r->resize($w, 0); + $r->save($destination); + } + } + + private function getStrategy() { + return app($this->strategy); + } + +} diff --git a/classes/FirstLetterStrategy.php b/classes/FirstLetterStrategy.php new file mode 100644 index 0000000..d2f30bf --- /dev/null +++ b/classes/FirstLetterStrategy.php @@ -0,0 +1,31 @@ +file = $uploadedFile; + $this->data = $data; + + $fileName = $this->getStrategy()->sourceFileBasename($this->file, $this->data); + $sourcePath = $this->getStrategy()->sourcePath($fileName); + $fileName = $this->transformFileName($sourcePath, $fileName).'.'.$this->file->getClientOriginalExtension(); + + $uploadedFile->storeAs($sourcePath, $fileName, $this->disk); + + Queue::push(CompressJob::class, [ + 'path' => $sourcePath, + 'filename' => $fileName, + 'disk' => $this->disk, + 'strategy' => $this->strategy, + 'crop' => $this->data['crop'] + ]); + + return $fileName; + } + + private function storage() { + return Storage::disk($this->disk); + } + + private function transformFileName($path, $basename) { + if (count(glob($this->storage()->path($path).'/'.$basename.'*')) == 0) { + return $basename; + } + + $i = 1; + + while(count(glob($this->storage()->path($path).'/'.$basename.'-'.$i.'*')) != 0) { + $i++; + } + + return $basename.'-'.$i; + } + + private function originalExtension() { + return $this->file->getClientOriginalExtension(); + } + + private function getStrategy() { + return app($this->strategy); + } + +} diff --git a/formwidgets/.Responsiveimage.php.swp b/formwidgets/.Responsiveimage.php.swp deleted file mode 100644 index 14d77f959ffff9779ceedef3a758a62772253617..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 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 - if (($fileId = post('file_id')) && ($file = $fileModel::find($fileId))) { - $this->getRelationObject()->remove($file, $this->sessionKey); - } - } - - /** - * Sorts file attachments. - */ - public function onSortAttachments() - { - if ($sortData = post('sortOrder')) { - $ids = array_keys($sortData); - $orders = array_values($sortData); - - $fileModel = $this->getRelationModel(); - $fileModel->setSortableOrder($ids, $orders); - } + // } /** @@ -255,20 +239,7 @@ class Responsiveimage extends FormWidgetBase */ public function onLoadAttachmentConfig() { - $fileModel = $this->getRelationModel(); - if ($file = $this->getFileRecord()) { - $file = $this->decorateFileAttributes($file); - - $this->vars['file'] = $file; - $this->vars['displayMode'] = $this->getDisplayMode(); - $this->vars['cssDimensions'] = $this->getCssDimensions(); - $this->vars['relationManageId'] = post('manage_id'); - $this->vars['relationField'] = post('_relation_field'); - - return $this->makePartial('config_form'); - } - - throw new ApplicationException('Unable to find file, it may no longer exist'); + // } /** @@ -318,70 +289,20 @@ class Responsiveimage extends FormWidgetBase */ public function onUpload() { - try { - if (!Input::hasFile('file_data')) { - throw new ApplicationException('File missing from request'); - } - - $fileModel = $this->getRelationModel(); - $uploadedFile = Input::file('file_data'); - - $validationRules = ['max:'.$fileModel::getMaxFilesize()]; - if ($fileTypes = $this->getAcceptedFileTypes()) { - $validationRules[] = 'extensions:'.$fileTypes; - } - - if ($this->mimeTypes) { - $validationRules[] = 'mimes:'.$this->mimeTypes; - } - - $validation = Validator::make( - ['file_data' => $uploadedFile], - ['file_data' => $validationRules] - ); - - if ($validation->fails()) { - throw new ValidationException($validation); - } - - if (!$uploadedFile->isValid()) { - throw new ApplicationException('File is not valid'); - } - - $fileRelation = $this->getRelationObject(); - - $file = $fileModel; - $file->data = $uploadedFile; - $file->is_public = $fileRelation->isPublic(); - $file->save(); - - /** - * Attach directly to the parent model if it exists and attachOnUpload has been set to true - * else attach via deferred binding - */ - $parent = $fileRelation->getParent(); - if ($this->attachOnUpload && $parent && $parent->exists) { - $fileRelation->add($file); - } - else { - $fileRelation->add($file, $this->sessionKey); - } - - $file = $this->decorateFileAttributes($file); - - $result = [ - 'id' => $file->id, - 'thumb' => $file->thumbUrl, - 'path' => $file->pathUrl - ]; - - $response = Response::make($result, 200); - } - catch (Exception $ex) { - $response = Response::make($ex->getMessage(), 400); + if (!Input::hasFile('file_data')) { + throw new ApplicationException('File missing from request'); } - return $response; + $data = json_decode(Input::get('extraData'), true); + $uploadedFile = Input::file('file_data'); + + if (!$uploadedFile->isValid()) { + throw new ApplicationException('File is not valid'); + } + + app(UploadStorage::class)->storeFileFromUpload($uploadedFile, $data); + + return Response::make('', 200); } /** diff --git a/formwidgets/responsiveimage/assets/src/App.vue b/formwidgets/responsiveimage/assets/src/App.vue index 15c6567..c304758 100644 --- a/formwidgets/responsiveimage/assets/src/App.vue +++ b/formwidgets/responsiveimage/assets/src/App.vue @@ -9,7 +9,7 @@ - +
@@ -77,6 +77,7 @@ export default { data: function() { return { + extraData: {}, addVisible: true, content: { sections: [], @@ -110,7 +111,7 @@ export default { dropzoneOptions() { return { paramName: 'file_data', - autoQueue: false, + autoProcessQueue: false, url: window.location, clickable: this.$refs.addQueueTrigger, previewsContainer: this.$refs.previewsContainer, @@ -147,14 +148,16 @@ export default { this.addVisible = false; this.showPopup(file).then((ret) => { - console.log(ret); + this.extraData[file.name] = ret; + this.$refs.dropzone.processQueue(); + console.log('AAA'); }).catch((err) => { this.$refs.dropzone.removeFile(file); this.addVisible = true; }); }, - upload() { - + onSend(file, xhr, formData) { + formData.append('extraData', JSON.stringify(this.extraData[file.name])); } }, mounted() { diff --git a/formwidgets/responsiveimage/partials/._responsiveimage.htm.swp b/formwidgets/responsiveimage/partials/._responsiveimage.htm.swp deleted file mode 100644 index 46aa16e818de14d7b28f2cf245c0a55ff941d281..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI2zi$&U6vtgARA?!%F}X+8B*W$Su~a3!P7IYO6sbi*2nFZPNvzxt%XS({Ran^h z1NcXn5G)}60|pov*ccdC*m(BcH4%k^L>JU&>8sP(@BH5T_??yL+_`jTZJjPPuMlW+ zgnYmMtbOuAI5qe@O{7Q?Zu|exoSn~9)IZ$tu`>)tqfrn`-5caQ$pV=Sdm_^T8}VER zifi$RtDu{!bR^;)H?B|SnHJkzq^!pS$&?W+(Lo!mCr6rnlO7%cjv|3$N?>TZ{?ZJ& zdgbyZOW>k6PtTv*I;Jo%Z6tsMkN^@u0!RP}AOR$BJP8;%MV>(FC(86|<#p<(Yk7+o zB!C2v01`j~NB{{S0VIF~kN^@u0!ZLLB*0=q?oJbO_7nsU-~X%s|8LI_@&?2}4?wp; z8=wHx1kHlp&l2()^a}JG)CJuF-2^p2-)0E;2>Jke0eT8bK?!IJvrD*KZ1EEZk58p4lG zXstj;N=OsByP_u++v8i;)fpO-XEaKf)+=7eWEA9U9_3lg)Q(4EW|*&dns0NzD-s^I zR#zym8^tnRuXR{70JqD_3Cns-J3f?2&f;eAMBDb%8Vv}l6%?+<{#y`D2;eFt>&PPh=2p0hv+uL%DnC8^k_=U~#k@(Xh z>)E2446=AXCLESV|LoBn?*3DX&giIeEIj@y$s)bN*@65D0_C#S7vSZ9^4VF>W8PRM zq})Fe&BsJqTC)9(!5(e*VNWnBPrO5m9t@jO@om9J?JRd2fNsM(rr{KPRr+>jsJOx_ iY+3cPcFX5%RCIBX&M(*kx*@dj!zdqSrrv0{_