diff --git a/.gitattributes b/.gitattributes index 5280247..d9d63a6 100644 --- a/.gitattributes +++ b/.gitattributes @@ -20,9 +20,8 @@ /phpstan-baseline.neon export-ignore # Merge ours -# *.css merge=ours -# *.js merge=ours -# package.json merge=ours -# package-lock.json merge=ours -# composer.json merge=ours -# composer.lock merge=ours +*.css merge=ours +*.js merge=ours + +# Merge theirs +CHANGELOG.md merge=theirs diff --git a/.php-cs-fixer.cache b/.php-cs-fixer.cache new file mode 100644 index 0000000..6cc4447 --- /dev/null +++ b/.php-cs-fixer.cache @@ -0,0 +1 @@ +{"php":"8.3.14","version":"3.65.0","indent":" ","lineEnding":"\n","rules":{"blank_line_after_namespace":true,"braces_position":true,"class_definition":true,"constant_case":true,"control_structure_braces":true,"control_structure_continuation_position":true,"elseif":true,"function_declaration":true,"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"method_argument_space":{"on_multiline":"ensure_fully_multiline","keep_multiple_spaces_after_comma":true},"no_break_comment":true,"no_closing_tag":true,"no_multiple_statements_per_line":true,"no_space_around_double_colon":true,"no_spaces_after_function_name":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_class_element_per_statement":{"elements":["property"]},"single_import_per_statement":true,"single_line_after_imports":true,"single_space_around_construct":{"constructs_followed_by_a_single_space":["abstract","as","case","catch","class","do","else","elseif","final","for","foreach","function","if","interface","namespace","private","protected","public","static","switch","trait","try","use_lambda","while"],"constructs_preceded_by_a_single_space":["as","else","elseif","use_lambda"]},"spaces_inside_parentheses":true,"statement_indentation":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"visibility_required":{"elements":["method","property"]},"encoding":true,"full_opening_tag":true,"array_syntax":{"syntax":"short"},"ordered_imports":{"sort_algorithm":"alpha"},"no_unused_imports":true,"not_operator_with_successor_space":true,"trailing_comma_in_multiline":true,"phpdoc_scalar":true,"unary_operator_spaces":true,"binary_operator_spaces":true,"blank_line_before_statement":{"statements":["break","continue","declare","return","throw","try"]},"phpdoc_single_line_var_spacing":true,"phpdoc_var_without_name":true,"class_attributes_separation":{"elements":{"method":"one"}},"single_trait_insert_per_statement":true},"hashes":{"src\/AppInitServiceProvider.php":"66c62559d6588bba8ac864307459072d","src\/Facades\/AppInit.php":"679a4aa07ef5dc2f0a3d9ccfe901d455","src\/Commands\/InstallTailwindCssCommand.php":"fac59dc698a1bb6fa268191b83aff2c2","src\/Commands\/InstallCommand.php":"99949ea2172e7078edbf70b674585ee7","src\/Commands\/InstallGitDotFilesCommand.php":"74163d4bec8a5e68f189487eece9b2e8","src\/Commands\/InstallComposerPackagesCommand.php":"e3abc6bcbcc2b04c513758e8061a06a3","src\/Commands\/ProcessImagesCommand.php":"f4b32ec96702e9c111e8078d418fa290","src\/Commands\/ConfigureEnvCommand.php":"bff4a2e5d2a7cb78fce4ef647d175931","src\/Commands\/InstallPrettierCommand.php":"11b614fe35e88ca4ac8daa5e1709f0c8","src\/Commands\/InstallViteCommand.php":"19b748e17c54ece6d5bdd55ef1541344","src\/Commands\/InstallChangeLogCommand.php":"7127ba50ce9fb50f71f2da8375410286","src\/AppInit.php":"520161f0ae6f23aed9cacf0de7396c93","tests\/Pest.php":"5b2122d0e0474fb9f568a8cb9fb479d7","tests\/ArchTest.php":"3ee5b69122c072917aa2cac591256b77","tests\/TestCase.php":"f707c59c644ad648e719c8eb6c3615c7"}} \ No newline at end of file diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..d96e7b2 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,43 @@ +notPath('bootstrap/*') + ->notPath('storage/*') + ->notPath('resources/view/mail/*') + ->in([ + __DIR__ . '/src', + __DIR__ . '/tests', + ]) + ->name('*.php') + ->notName('*.blade.php') + ->ignoreDotFiles(true) + ->ignoreVCS(true); + +return (new PhpCsFixer\Config()) + ->setRules([ + '@PSR2' => true, + 'array_syntax' => ['syntax' => 'short'], + 'ordered_imports' => ['sort_algorithm' => 'alpha'], + 'no_unused_imports' => true, + 'not_operator_with_successor_space' => true, + 'trailing_comma_in_multiline' => true, + 'phpdoc_scalar' => true, + 'unary_operator_spaces' => true, + 'binary_operator_spaces' => true, + 'blank_line_before_statement' => [ + 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], + ], + 'phpdoc_single_line_var_spacing' => true, + 'phpdoc_var_without_name' => true, + 'class_attributes_separation' => [ + 'elements' => [ + 'method' => 'one', + ], + ], + 'method_argument_space' => [ + 'on_multiline' => 'ensure_fully_multiline', + 'keep_multiple_spaces_after_comma' => true, + ], + 'single_trait_insert_per_statement' => true, + ]) + ->setFinder($finder); diff --git a/README.md b/README.md index 203f006..d62856f 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,31 @@ You can force initialize the package using the installation command: php artisan app-init:install --force ``` +You can process images in the ```public/images/``` folder using the installation command: + +```bash +php artisan app-init:images +``` + +You can force process images in the ```public/images/``` folder using the installation command: + +```bash +php artisan app-init:images --force +``` + + +Optional: Install the image manipulation binaries on MacOS (using Homebrew): + +```bash +brew install jpegoptim +brew install optipng +brew install pngquant +npm install -g svgo +brew install gifsicle +brew install webp +brew install libavif +``` + ## Testing ```bash diff --git a/composer.json b/composer.json index 69f0924..99ee4bd 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,8 @@ ], "require": { "php": "^8.2", + "intervention/image": "^3.9", + "spatie/image-optimizer": "^1.8", "spatie/laravel-package-tools": "^1.14.0" }, "require-dev": { @@ -70,6 +72,9 @@ "providers": [ "Fuelviews\\AppInit\\AppInitServiceProvider" ] + }, + "aliases": { + "RedirectIfNotFound": "Fuelviews\\AppInit\\Facades\\AppInit" } }, "minimum-stability": "dev", diff --git a/composer.lock b/composer.lock index 56b9fb1..cca8303 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7006ffa5c1ce624f5b91365c0fdb71e2", + "content-hash": "2ffcf43eb4aaee706444888d9d05dde0", "packages": [ { "name": "brick/math", @@ -729,6 +729,150 @@ ], "time": "2023-12-03T19:50:20+00:00" }, + { + "name": "intervention/gif", + "version": "4.2.0", + "source": { + "type": "git", + "url": "https://github.com/Intervention/gif.git", + "reference": "42c131a31b93c440ad49061b599fa218f06f93be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Intervention/gif/zipball/42c131a31b93c440ad49061b599fa218f06f93be", + "reference": "42c131a31b93c440ad49061b599fa218f06f93be", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "phpstan/phpstan": "^1", + "phpunit/phpunit": "^10.0", + "slevomat/coding-standard": "~8.0", + "squizlabs/php_codesniffer": "^3.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Intervention\\Gif\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oliver Vogel", + "email": "oliver@intervention.io", + "homepage": "https://intervention.io/" + } + ], + "description": "Native PHP GIF Encoder/Decoder", + "homepage": "https://github.com/intervention/gif", + "keywords": [ + "animation", + "gd", + "gif", + "image" + ], + "support": { + "issues": "https://github.com/Intervention/gif/issues", + "source": "https://github.com/Intervention/gif/tree/4.2.0" + }, + "funding": [ + { + "url": "https://paypal.me/interventionio", + "type": "custom" + }, + { + "url": "https://github.com/Intervention", + "type": "github" + }, + { + "url": "https://ko-fi.com/interventionphp", + "type": "ko_fi" + } + ], + "time": "2024-09-20T13:35:02+00:00" + }, + { + "name": "intervention/image", + "version": "3.9.1", + "source": { + "type": "git", + "url": "https://github.com/Intervention/image.git", + "reference": "b496d1f6b9f812f96166623358dfcafb8c3b1683" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Intervention/image/zipball/b496d1f6b9f812f96166623358dfcafb8c3b1683", + "reference": "b496d1f6b9f812f96166623358dfcafb8c3b1683", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "intervention/gif": "^4.2", + "php": "^8.1" + }, + "require-dev": { + "mockery/mockery": "^1.6", + "phpstan/phpstan": "^1", + "phpunit/phpunit": "^10.0", + "slevomat/coding-standard": "~8.0", + "squizlabs/php_codesniffer": "^3.8" + }, + "suggest": { + "ext-exif": "Recommended to be able to read EXIF data properly." + }, + "type": "library", + "autoload": { + "psr-4": { + "Intervention\\Image\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oliver Vogel", + "email": "oliver@intervention.io", + "homepage": "https://intervention.io/" + } + ], + "description": "PHP image manipulation", + "homepage": "https://image.intervention.io/", + "keywords": [ + "gd", + "image", + "imagick", + "resize", + "thumbnail", + "watermark" + ], + "support": { + "issues": "https://github.com/Intervention/image/issues", + "source": "https://github.com/Intervention/image/tree/3.9.1" + }, + "funding": [ + { + "url": "https://paypal.me/interventionio", + "type": "custom" + }, + { + "url": "https://github.com/Intervention", + "type": "github" + }, + { + "url": "https://ko-fi.com/interventionphp", + "type": "ko_fi" + } + ], + "time": "2024-10-27T10:15:54+00:00" + }, { "name": "laravel/framework", "version": "v10.48.23", @@ -2383,6 +2527,61 @@ ], "time": "2024-04-27T21:32:50+00:00" }, + { + "name": "spatie/image-optimizer", + "version": "1.8.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/image-optimizer.git", + "reference": "4fd22035e81d98fffced65a8c20d9ec4daa9671c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/image-optimizer/zipball/4fd22035e81d98fffced65a8c20d9ec4daa9671c", + "reference": "4fd22035e81d98fffced65a8c20d9ec4daa9671c", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": "^7.3|^8.0", + "psr/log": "^1.0 | ^2.0 | ^3.0", + "symfony/process": "^4.2|^5.0|^6.0|^7.0" + }, + "require-dev": { + "pestphp/pest": "^1.21", + "phpunit/phpunit": "^8.5.21|^9.4.4", + "symfony/var-dumper": "^4.2|^5.0|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\ImageOptimizer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "Easily optimize images using PHP", + "homepage": "https://github.com/spatie/image-optimizer", + "keywords": [ + "image-optimizer", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/image-optimizer/issues", + "source": "https://github.com/spatie/image-optimizer/tree/1.8.0" + }, + "time": "2024-11-04T08:24:54+00:00" + }, { "name": "spatie/laravel-package-tools", "version": "1.16.5", @@ -9607,12 +9806,12 @@ ], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { "php": "^8.2" }, - "platform-dev": [], + "platform-dev": {}, "plugin-api-version": "2.6.0" } diff --git a/src/AppInitServiceProvider.php b/src/AppInitServiceProvider.php index 1194521..11d434f 100644 --- a/src/AppInitServiceProvider.php +++ b/src/AppInitServiceProvider.php @@ -10,6 +10,7 @@ use Fuelviews\AppInit\Commands\InstallPrettierCommand; use Fuelviews\AppInit\Commands\InstallTailwindCssCommand; use Fuelviews\AppInit\Commands\InstallViteCommand; +use Fuelviews\AppInit\Commands\ProcessImagesCommand; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; @@ -28,6 +29,7 @@ public function configurePackage(Package $package): void InstallViteCommand::class, InstallComposerPackagesCommand::class, ConfigureEnvCommand::class, + ProcessImagesCommand::class, ]); } diff --git a/src/Commands/ProcessImagesCommand.php b/src/Commands/ProcessImagesCommand.php new file mode 100644 index 0000000..22899b9 --- /dev/null +++ b/src/Commands/ProcessImagesCommand.php @@ -0,0 +1,172 @@ +info('Processing images in the public/images/ folder...'); + + $imagePath = public_path('images'); + + if (! File::exists($imagePath)) { + $this->error('The public/images/ directory does not exist.'); + + return; + } + + $files = File::files($imagePath); + + if (empty($files)) { + $this->info('No images found to process.'); + + return; + } + + foreach ($files as $file) { + if ($file->getExtension() === 'svg') { + $this->warn("Skipping {$file->getFilename()}: SVG files are not processed."); + + continue; + } + + $this->processImage($file); + } + + $this->info('Image processing and optimization completed.'); + $this->info('Total space saved: '.$this->formatSize($this->totalSavedSpace)); + } + + private function processImage($file): void + { + $originalFilename = $file->getFilename(); + $filePath = $file->getPathname(); + + $this->info("Processing file: {$filePath}"); + + try { + // Get original file size + $originalSize = filesize($filePath); + + // Determine the correct file extension + $image = Image::make($filePath); + $mime = $image->mime(); // Get MIME type + $correctExtension = $this->getCorrectExtension($mime); + + if (! $correctExtension) { + $this->warn("Skipping {$originalFilename}: Unknown MIME type."); + + return; + } + + // Check if renaming is required + $newFilename = $this->generateNewName($originalFilename, $correctExtension); + $newFilePath = $file->getPath().DIRECTORY_SEPARATOR.$newFilename; + + if ($originalFilename !== $newFilename) { + if (File::exists($newFilePath) && ! $this->option('force')) { + $this->warn("Skipping renaming {$originalFilename}: {$newFilename} already exists."); + } else { + if (! File::move($filePath, $newFilePath)) { + $this->error("Failed to rename {$filePath} to {$newFilePath}"); + + return; + } + $this->info("Renamed {$originalFilename} -> {$newFilename}"); + $filePath = $newFilePath; // Update file path for optimization + } + } + + // Optimize the file (only if savings > 5 KB) + if (File::exists($filePath)) { + $this->optimizeImage($filePath, $originalSize); + } else { + $this->error("File {$filePath} not found after renaming."); + } + } catch (\Exception $e) { + $this->error("Failed to process {$originalFilename}: {$e->getMessage()}"); + } + } + + private function getCorrectExtension(string $mime): ?string + { + $extensions = [ + 'image/jpeg' => 'jpg', + 'image/png' => 'png', + 'image/gif' => 'gif', + 'image/webp' => 'webp', + ]; + + return $extensions[$mime] ?? null; + } + + private function generateNewName(string $filename, string $correctExtension): string + { + $fileInfo = pathinfo($filename); + + // Convert base name to lowercase and apply correct extension + return strtolower($fileInfo['filename']).'.'.$correctExtension; + } + + private function optimizeImage(string $filePath, int $originalSize): void + { + $optimizerChain = OptimizerChainFactory::create(); + + if (! File::exists($filePath)) { + $this->error("File {$filePath} does not exist for optimization."); + + return; + } + + // Optimize the image + try { + $optimizerChain->optimize($filePath); + $optimizedSize = filesize($filePath); + + // Calculate space saved + $spaceSaved = $originalSize - $optimizedSize; + + if ($spaceSaved >= $this->minSavings) { + $this->totalSavedSpace += $spaceSaved; + $this->info("Optimized {$filePath}: Saved ".$this->formatSize($spaceSaved)); + } else { + $this->warn("Skipped optimization for {$filePath}: Savings less than 5 KB."); + // Revert to original size if savings are insufficient + File::put($filePath, File::get($filePath)); + } + } catch (\Exception $e) { + $this->warn("Optimization failed for {$filePath}: {$e->getMessage()}"); + } + } + + private function formatSize(int $size): string + { + if ($size >= 1 << 30) { + return number_format($size / (1 << 30), 2).' GB'; + } + + if ($size >= 1 << 20) { + return number_format($size / (1 << 20), 2).' MB'; + } + + if ($size >= 1 << 10) { + return number_format($size / (1 << 10), 2).' KB'; + } + + return $size.' bytes'; + } +} diff --git a/stubs/.gitattributes.stub b/stubs/.gitattributes.stub index 103179d..04c10b8 100644 --- a/stubs/.gitattributes.stub +++ b/stubs/.gitattributes.stub @@ -29,3 +29,6 @@ CHANGELOG.md export-ignore # Merge ours *.css merge=ours *.js merge=ours + +# Merge theirs +CHANGELOG.md merge=theirs