Andreas Ludvigsson

Bridging Code and Commerce

Creating a TinyPNG Clone using Laravel, Vue.js, and pngquant


In the fast-paced world of web development, the speed and performance of a website are crucial for a great user experience. This is where image optimization comes into play. It’s all about reducing the file size of your images without losing quality, so your pages load faster. Imagine waiting forever for a webpage to load because of heavy images. Frustrating, right? That’s exactly what we want to avoid.

TinyPNG is a name you might have heard in this context. It’s a fantastic tool that shrinks the size of PNG files, one of the most common image formats used on the web. By cleverly reducing the number of colors in the image and applying compression, TinyPNG makes images much lighter and web-friendly, all while keeping them looking good. It’s like magic but for images.

Now, imagine you could create your own version of this tool. That’s precisely what we’re diving into today. We’re going to walk through building a TinyPNG clone, but we’ll use Laravel for the backend, Vue.js for the front end, and pngquant for the heavy lifting of compressing PNG files. Whether you’re managing a blog, running an e-commerce site, or developing web applications, this guide is for you. We’ll keep things simple and straightforward, so by the end, you’ll have your own image compression tool ready to speed up your website. Let’s get started!

Tools and Technologies

Let’s talk about the tools we’re going to use for our project. We’ve picked Laravel, Vue.js, and pngquant. Each of these has a special role in making sure our image compression tool works smoothly and efficiently.

Laravel might seem like a big gun for something as straightforward as compressing images. It’s a powerful tool for building web applications, handling everything from the database to web pages and even security. Yes, it’s a bit more than what we need for just squishing images into smaller sizes. But, it’s my go-to tool, and it’s great for setting up the backbone of our project, handling all the behind-the-scenes work, and making sure everything runs smoothly on the server side. Plus, it makes dealing with web requests and responses a breeze.

Vue.js is all about the front end. It’s what users interact with. Vue.js helps us create a nice, responsive interface where you can upload images, see the magic happen, and then download the compressed versions. It’s like the friendly face of our project, making everything look good and work smoothly for anyone using our tool.

pngquant is the specialist in the group. It’s a tool specifically designed for compressing PNG images. Think of it as a master at squeezing images into smaller sizes without making them look bad. It works in the background, taking the images you’ve uploaded and shrinking them down so they load faster on websites while still looking sharp.

In summary, Laravel handles the heavy lifting on the server side, even though it’s a bit more than we need (but hey, it’s my favorite). Vue.js makes our tool easy and enjoyable to use with a slick interface. And pngquant does the actual image compressing, making sure your pictures are web-ready. Together, they make a great team for our project.

Setting Up the Project Environment

Getting our project up and running involves a few steps, but don’t worry, I’ll guide you through each one. We’re setting up Laravel and Vue.js to work together in harmony, and we’ll also get our hands on some tools to help with managing our website’s look and feel.

Step 1: Creating the Laravel Project

First things first, let’s start with setting up Laravel. Open up your terminal or command prompt. We’re going to create a new Laravel project. If you’ve got Laravel installed globally, you can simply run:

laravel new my-image-compressor

If you don’t have Laravel installed globally, no worries. You can use Composer, PHP’s package manager, to create the project:

composer create-project --prefer-dist laravel/laravel my-image-compressor

This command sets up a new Laravel project in a directory called my-image-compressor. It might take a minute, as it needs to download all the necessary Laravel components.

Step 2: Adding Vue.js

Now, let’s bring Vue.js into the mix. Navigate to your project’s root directory in the terminal, if you’re not already there. We’re going to install Vue along with Vite, which Laravel uses to handle asset compilation (like your JavaScript and CSS files). Vite makes everything super fast, which is always nice. Run these commands:

npm install vue@next vue-loader@next npm install @vitejs/plugin-vue

This adds Vue.js to your project, ready for you to create a dynamic front-end.

Step 3: Installing Tailwind CSS (Optional but Recommended)

I always go with Tailwind CSS, so let’s add that too. It’s perfect for designing your UI without sweating the small stuff. Run:

npm install -D tailwindcss@latest postcss@latest autoprefixer@latest npx tailwindcss init

This sets up Tailwind CSS with its default configuration. You can customize tailwind.config.js as needed for your project.

Step 4: Setting Up Laravel Vite

Laravel Vite is a wrapper around Vite for Laravel applications, making it easier to work with Vue.js and Tailwind. If it’s not already included in your Laravel setup, you can add it like so:

npm install laravel-vite-plugin

Then, you’ll need to configure it. Open the vite.config.js file in your project root (or create it if it doesn’t exist) and make sure it’s set up to handle Vue files:

import { defineConfig } from 'vite'; import laravel from 'laravel-vite-plugin'; import vue from '@vitejs/plugin-vue'; export default defineConfig({ plugins: [ laravel({ input: ['resources/css/app.css', 'resources/js/app.js'], refresh: true, }), vue(), ], });

This tells Vite where your CSS and JavaScript files are and ensures that Vue works nicely within your Laravel project.

Step 5: Running Your Project

With all the pieces in place, you’re almost ready to go. Start by compiling your assets:

npm run dev

Then, serve your Laravel application:

php artisan serve

And that’s it! You’ve got a Laravel project with Vue.js and Tailwind CSS all set up and ready to build out your image compression tool. Next, we’ll dive into how to use these tools together to create something awesome.

Building the Front-End with Vue.js and Tailwind CSS

When we’re building a web application, especially something as cool as an image compression tool, we want it to not just work well but also look good and be easy to use. That’s where Vue.js and Tailwind CSS come into play for our project. Let’s break down how we use these tools to create a user-friendly interface for uploading and compressing PNG images.

  1. Create the Vue Component: Navigate to the resources/js directory in your Laravel project. Here, create a new file for your Vue component, say ImageCompressor.vue. This is where you’ll place the Vue.js template code provided earlier. It includes the form for image upload, the display of compression results, and the styling with Tailwind CSS.
  2. Edit app.js to Use Your Component: Inside the resources/js folder, you’ll find an app.js file. This is the main JavaScript file that Vue uses to load your components. Edit this file to import and use your ImageCompressor.vue component. For example
import {createApp} from 'vue'

import App from './App.vue'


Setting Up app.blade.php

Now, let’s ensure your Laravel blade template is ready to display your Vue application.

<!DOCTYPE html> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Application</title> @vite('resources/css/app.css') </head> <body class="bg-gray-900"> <div id="app"></div> @vite('resources/js/app.js') </body> </html>
  1. Create or Edit app.blade.php: This file should be located in the resources/views directory of your Laravel project. If it doesn’t exist, create it. This blade file acts as the entry point for your web application, loading the necessary CSS and JavaScript files.
  2. Include Your Vue Application: Use the provided HTML structure in app.blade.php. This file loads your compiled CSS and JavaScript, which now includes Vue and your component. The @vite directive is used to include the Vite-compiled assets. The <div id="app"></div> is where your Vue application will be rendered
  3. Run Your Development Server: Make sure to compile your assets and run your Laravel development server. You can compile your assets using the command npm run dev and start your Laravel server with php artisan serve.

By following these steps, you’ve integrated Vue.js into your Laravel application, set up a Vue component for image compression, and prepared your Laravel application to render the Vue component through the app.blade.php file. This setup provides a seamless integration of Laravel’s backend capabilities with Vue’s dynamic frontend, allowing users to upload images for compression and view the results, all within a modern, responsive design styled with Tailwind CSS.

Vue.js Component Structure

Our application revolves around a single Vue component that does all the heavy lifting. This component is responsible for uploading images, communicating with the backend to compress the images, and then displaying the results. It’s like the command center for our image compressor.

The Vue.js Template

    <div class="max-w-md mx-auto mt-10 bg-white dark:bg-gray-800 shadow-lg rounded-lg p-5">
        <form @submit.prevent="submit" class="flex flex-col space-y-4">
            <label for="image" class="block text-sm font-medium text-gray-900 dark:text-white mb-2">Upload Images</label>
                class="block w-full text-sm text-gray-900 border border-gray-300 rounded-lg cursor-pointer bg-gray-50 dark:text-gray-400 focus:outline-none dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 p-2"
            <div id="image_help" class="mt-1 text-sm text-gray-500 dark:text-gray-300">Please select one or more PNG images to compress.</div>
                class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
                Compress Images
        <div v-for="(result, index) in compressionResults" :key="index" class="mt-4 p-4 bg-blue-100 border-t border-blue-500 text-blue-700">
            <img :src="result.path" alt="Thumbnail" class="w-20 h-20 object-cover rounded-lg shadow-md">

            <p class="text-sm">Original Size: {{ formatBytes(result.originalSize) }}</p>
            <p class="text-sm">Compressed Size: {{ formatBytes(result.compressedSize) }}</p>
            <p class="text-sm">Compression Ratio: {{ calculateCompressionRatio(result.originalSize, result.compressedSize) }}%</p>
                class="block mt-4 text-blue-500  bg-blue-100 hover:text-white hover:bg-blue-500 font-semibold py-2 px-4 rounded-md text-center"
                Download Compressed Image

import axios from 'axios';

export default {
    data() {
        return {
            files: [],
            compressionResults: [],
    methods: {
        handleFileChange(e) {
            this.files = Array.from(;
        async submit() {
            const formData = new FormData();
            // Append each file to the formData object
            this.files.forEach((file, index) => {
                // Note: 'image[]' is used to denote that it is an array of files
                formData.append(`image[]`, file);

            try {
                const response = await'/compress', formData, {
                    headers: {
                        'Content-Type': 'multipart/form-data',
                // Assuming the backend sends back an array of results for each file
                this.compressionResults =;
            } catch (error) {
                console.error('Compression failed', error);
        formatBytes(bytes, decimals = 2) {
            if (bytes === 0) return '0 Bytes';
            const k = 1024;
            const dm = decimals < 0 ? 0 : decimals;
            const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
            const i = Math.floor(Math.log(bytes) / Math.log(k));
            return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
        calculateCompressionRatio(originalSize, compressedSize) {
            return (((originalSize - compressedSize) / originalSize) * 100).toFixed(2);

The heart of our Vue.js front-end is the template section. This is where we define how our application looks and feels. Let’s dive into the different parts of the template:

  • Image Upload Form: We start with a form that users can use to upload their PNG images. This form includes an input field where users can select one or multiple images for compression. It’s styled with Tailwind CSS to look clean and modern. The form is pretty straightforward but powerful, allowing for bulk image uploads.
  • Upload Button: A simple button labeled “Compress Images” triggers the upload and compression process. It stands out, thanks to Tailwind’s styling, ensuring users know exactly what to do next.
  • Compression Results Display: After the images are compressed, we show the results right on the same page. For each image, we display a thumbnail, the original size, the compressed size, and the compression ratio. This immediate feedback is great for users who want to see the benefits of compressing their images. Plus, there’s a download link for each compressed image, making it super easy to get the optimized files.

Styling with Tailwind CSS

Tailwind CSS plays a huge role in making our application look good. We use it to style our form, buttons, and result display with minimal effort. The beauty of Tailwind CSS is its utility-first approach, allowing us to apply styling directly in our template with class names. This means we can quickly adjust paddings, margins, colors, and more without leaving our HTML.

  • Responsive and Modern Design: The application is wrapped in a container (max-w-md mx-auto) that centers it on the page and gives it a maximum width, making sure it looks good on both mobile and desktop screens. The dark mode support (dark:bg-gray-800) ensures that users who prefer dark mode get a visually comfortable experience.
  • Interactive Elements: Elements like the file input and submit button are not only functional but also visually appealing. The button changes color on hover (hover:bg-blue-700), providing a clear indication that it’s clickable. The form and result cards are designed to be visually distinct, making it easy for users to follow the flow of the application.

In summary, our Vue.js component, combined with the power of Tailwind CSS, provides a simple yet effective user interface for our image compression tool. It’s all about making the user’s journey from uploading to downloading compressed images as smooth and pleasant as possible.

Implementing Image Compression in Laravel

To integrate image compression functionality into your Laravel application, we’ll focus on setting up the routes and creating the ImageCompressionController. This controller will handle the image compression logic, utilizing pngquant for efficient PNG compression.

Setting Up Routes in Laravel

First, let’s define the necessary routes for our application. Open or create the web.php file located in the routes directory of your Laravel project. Here, you’ll register two routes: one for displaying the application’s main page and another for handling the image compression request.

use App\Http\Controllers\ImageCompressionController; use Illuminate\Support\Facades\Route; Route::get('/', function () { return view('app'); })->name('application'); Route::post('/compress', [ImageCompressionController::class, 'compress']);

The first route returns the app view when visiting the root URL, which will load your Vue.js application. The second route listens for POST requests to /compress, which triggers the compress method in ImageCompressionController.

Installing pngquant on Mac

Before proceeding with the controller logic, ensure pngquant is installed on your system as it’s required for image compression. For Mac users, you can easily install it using Homebrew:

brew install pngquant

This command installs pngquant on your Mac, making it available for your Laravel application to use for compressing PNG images.

ImageCompressionController Logic

Now, let’s dive into the ImageCompressionController. This controller is responsible for receiving image upload requests, validating the images, compressing them using pngquant, and returning the compression results.


namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Process;

class ImageCompressionController extends Controller
    public function compress(Request $request)

        $request->validate(['image.*' => 'required|image|mimes:png']);

        $compressionResults = [];
        foreach ($request->file('image') as $file) {
            $imagePath = $file->getRealPath();
            $imgName = 'compressed_' . uniqid() . '.png';
            $outputPath = storage_path('app/public/'. $imgName);

            $command = ['/opt/homebrew/bin/pngquant', '--force', '--output', $outputPath, '--', $imagePath];
            $process = Process::run($command);

            if ($process->failed()) {
                continue; // Or handle the error appropriately

            $beforeSize = filesize($imagePath);
            $afterSize = filesize($outputPath);

            $compressionResults[] = [
                'message' => 'Image compressed successfully',
                'path' => $imgName,
                'originalSize' => $beforeSize,
                'compressedSize' => $afterSize,

        return response()->json($compressionResults);

    public function index() {


Key Points of the Controller:

  • Validation: It starts by validating the uploaded files to make sure they are images and specifically PNG files.
  • Compression Process: For each file, it constructs a command to run pngquant, specifying the input and output paths. Process::run() is then used to execute the command.
  • Error Handling: If pngquant fails to compress an image, the process is skipped. You might want to add more sophisticated error handling based on your application’s needs.
  • Result Compilation: After compression, it calculates the original and compressed file sizes to determine the effectiveness of the compression and returns these results as a JSON response.

By following these steps, you integrate a robust image compression feature into your Laravel application, offering a similar functionality to TinyPNG but with the flexibility and control of a custom-built solution.

Leave a Reply

Your email address will not be published. Required fields are marked *