Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

eddieace's avatar

Livewire 3 and Cropper.js to crop and store cropped image

Hello, I'm rewriting an application from Vue to Livewire 3. In one case, I'm using a plugin to crop images called Cropperjs, and the logic I used to write in Vue is now written in Alpine instead. I'm struggling to sync the cropped image with the Livewire and store it on the server without having to send the image blob in a post request, which of course could be done, but would be awesome if not needed. In the example, I'v attempted to bind it to a wire:model without success.

Steps

  1. Initilize cropper method on the original image
  2. Crop the image
  3. Save the cropped image blob to a key in the Alpine component
  4. Sync the cropped image to Livewire
  5. Store the Cropped image on the server

Any ideas?

Javascript method for Alpine

function data() {
  return {
    image: null,
    image_1x1_ratio: null,
    imageIsSelected: false,
    imageIsCropped: false,
    cropper: null,
    select() {
      this.image = document.getElementById('image');
      if(this.cropper != null) {
        this.cropper.destroy()
      }
      setTimeout(() => {
        this.cropper = new Cropper(this.image, {
          aspectRatio: 1 / 1,
          zoomable: false,
          crop(event) {

          },
        });
      }, 500);
      this.imageIsSelected = true;
    },
    crop() {
      this.cropper.getCroppedCanvas();

      this.cropper.getCroppedCanvas({
        width: 160,
        height: 90,
        minWidth: 256,
        minHeight: 256,
        maxWidth: 4096,
        maxHeight: 4096,
        fillColor: '#fff',
        imageSmoothingEnabled: false,
        imageSmoothingQuality: 'high',
      });

      this.cropper.getCroppedCanvas().toBlob((blob) => {
        this.image_1x1_ratio = URL.createObjectURL(blob)
      }/*, 'image/png' */);
        this.imageIsCropped = true
    }
  }
}

Blade

<div x-data="data">
  <input type="hidden" wire:model="croppedImage" :value="image_1x1_ratio">
  <div class="w-full h-64 bg-gray-900 flex items-center justify-center">
    @if($image)
      <img src="{{ $image }}" id="image" class="h-full">
    @else
      <span class="text-white">No image selected</span>
    @endif
  </div>
  <div class="w-full bg-gray-200 flex p-5 justify-end">
     <button x-show="!imageIsSelected" type="button" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded" @click="select">Select</button>
     <button x-show="imageIsSelected" type="button" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded" @click="crop">Crop</button>
     <button x-show="imageIsCropped" type="button" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded" wire:click="save">Save</button>
  </div>
</div>

Component

<?php

namespace App\Livewire;

use Livewire\Attributes\On;
use Livewire\Component;
use Livewire\WithFileUploads;

class CropImage extends Component
{
    use WithFileUploads;
 
    public $image;

    public $croppedImage;

    public function save() {
        dd($this->croppedImage);
        $this->croppedImage->store('croppedphotos');
    }

    public function render()
    {
        return view('livewire.crop-image');
    }
}
0 likes
2 replies
LaryAI's avatar
Level 58

To sync the cropped image with Livewire and store it on the server, you can follow these steps:

  1. Initialize the Cropper.js method on the original image using Alpine.js.
  2. Crop the image using the Cropper.js library.
  3. Save the cropped image blob to a key in the Alpine component.
  4. Sync the cropped image to Livewire using wire:model.
  5. Store the cropped image on the server in the Livewire component.

Here's an updated version of the code with the necessary changes:

function data() {
  return {
    image: null,
    image_1x1_ratio: null,
    imageIsSelected: false,
    imageIsCropped: false,
    cropper: null,
    select() {
      this.image = document.getElementById('image');
      if (this.cropper != null) {
        this.cropper.destroy();
      }
      setTimeout(() => {
        this.cropper = new Cropper(this.image, {
          aspectRatio: 1 / 1,
          zoomable: false,
          crop(event) {
            // Sync the cropped image to Livewire
            this.image_1x1_ratio = this.cropper.getCroppedCanvas().toDataURL();
          },
        });
      }, 500);
      this.imageIsSelected = true;
    },
    crop() {
      this.cropper.getCroppedCanvas();

      this.cropper.getCroppedCanvas({
        width: 160,
        height: 90,
        minWidth: 256,
        minHeight: 256,
        maxWidth: 4096,
        maxHeight: 4096,
        fillColor: '#fff',
        imageSmoothingEnabled: false,
        imageSmoothingQuality: 'high',
      });

      this.cropper.getCroppedCanvas().toBlob((blob) => {
        this.image_1x1_ratio = URL.createObjectURL(blob);
      }/*, 'image/png' */);
      this.imageIsCropped = true;
    }
  };
}
<div x-data="data">
  <input type="hidden" wire:model="croppedImage" :value="image_1x1_ratio">
  <div class="w-full h-64 bg-gray-900 flex items-center justify-center">
    @if($image)
      <img src="{{ $image }}" id="image" class="h-full">
    @else
      <span class="text-white">No image selected</span>
    @endif
  </div>
  <div class="w-full bg-gray-200 flex p-5 justify-end">
     <button x-show="!imageIsSelected" type="button" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded" @click="select">Select</button>
     <button x-show="imageIsSelected" type="button" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded" @click="crop">Crop</button>
     <button x-show="imageIsCropped" type="button" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded" wire:click="save">Save</button>
  </div>
</div>
<?php

namespace App\Livewire;

use Livewire\Component;
use Livewire\WithFileUploads;

class CropImage extends Component
{
    use WithFileUploads;

    public $image;
    public $croppedImage;

    public function save()
    {
        // Store the cropped image on the server
        $this->croppedImage->store('croppedphotos');

        // Additional logic if needed

        // Redirect or show success message
    }

    public function render()
    {
        return view('livewire.crop-image');
    }
}

Make sure to update the Livewire component class namespace and the view file path according to your project structure.

This solution should allow you to crop the image using Cropper.js, sync the cropped image with Livewire, and store it on the server without sending the image blob in a post request.

dbleall's avatar

I Am trying all day to set the canvas size to match the container and failed. can someone help me ?

                    <div class="cropper-container-wrapper relative mb-4" style="height: 500px !important; width: 100%; overflow: hidden; background-color: #f8f8f8;" wire:ignore>
                        <img id="coverImage" src="{{ $tempImageUrl }}" alt="Imagem para recorte" style="max-width: 100%; min-height: 500px; height: auto; display: block;">
                    </div>

                    <script>
                        // Inicializar o cropper diretamente quando a imagem for carregada
                        document.addEventListener('DOMContentLoaded', function() {
                            setTimeout(function() {
                                const image = document.getElementById('coverImage');
                                if (image && typeof Cropper !== 'undefined') {
                                    console.log('Inicializando cropper diretamente...');
                                    new Cropper(image, {
                                        viewMode: 0, // Sem restrições
                                        dragMode: 'move',
                                        aspectRatio: 16 / 5,
                                        autoCropArea: 1, // Usar toda a área
                                        responsive: false, // Desativar responsividade
                                        restore: false,
                                        guides: true,
                                        center: true,
                                        highlight: true,
                                        cropBoxMovable: true,
                                        cropBoxResizable: true,
                                        toggleDragModeOnDblclick: false,
                                        minContainerWidth: 800,
                                        minContainerHeight: 500,
                                        minCropBoxWidth: 800,
                                        minCropBoxHeight: 500,
                                        minCanvasWidth: 800,
                                        minCanvasHeight: 500,
                                        ready: function() {
                                            console.log('Cropper direto está pronto');

                                            // FORÇAR altura do canvas para exatamente 500px
                                            console.log('Forçando altura do canvas para 500px');

                                            // Obter dados do container
                                            const containerData = this.cropper.getContainerData();

                                            // Definir altura fixa de 500px
                                            const fixedHeight = 500;

                                            // Calcular largura mantendo proporção 16:5
                                            const fixedWidth = fixedHeight * (16/5);

                                            // Centralizar o canvas horizontalmente
                                            const left = (containerData.width - fixedWidth) / 2;
                                            const top = 0; // Alinhar ao topo

                                            console.log('Dimensões do canvas:', {
                                                width: fixedWidth,
                                                height: fixedHeight,
                                                left: left,
                                                top: top
                                            });

                                            // Aplicar o tamanho fixo ao canvas
                                            this.cropper.setCanvasData({
                                                left: left,
                                                top: top,
                                                width: fixedWidth,
                                                height: fixedHeight
                                            });

                                            // Ajustar a caixa de recorte para ter o mesmo tamanho
                                            this.cropper.setCropBoxData({
                                                left: left,
                                                top: top,
                                                width: fixedWidth,
                                                height: fixedHeight
                                            });
                                        },
                                        crop: function(event) {
                                            // Enviar dados do recorte para o componente Livewire
                                            const data = {
                                                x: Math.round(event.detail.x),
                                                y: Math.round(event.detail.y),
                                                width: Math.round(event.detail.width),
                                                height: Math.round(event.detail.height)
                                            };

                                            // Encontrar o componente Livewire
                                            const livewireEl = document.querySelector('[wire\\:id]');
                                            if (livewireEl && livewireEl.__livewire) {
                                                Livewire.find(livewireEl.__livewire.$id).setCropData(data);
                                            }
                                        }
                                    });
                                }
                            }, 500);
                        });
                    </script>

Please or to participate in this conversation.