AbehoM's avatar

How to handle image gallery to have the same height and width

I'm creating a website and I will allow the users to upload multiple images. In the first page of the website has a list of images organized in Bootstrap rows. Bootstrap rows are responsive, meaning that if I put an image there it will resize and fit nicely, the problem is the height. As each user will have an image with different height I have a problem where the users might upload different images with different heights and something like this happens (with Bootstrap):

I want to keep the images with the same height - but not by using CSS or JS.

I was wondering: what is the best way to handle this kind of situation?

1 - Use some kind of Vue component to crop the image and make it on the same aspect ratio for every single image and on the server side (Laravel) resize the image to multiple sizes but keeping the aspect ratio? 2 - Crop and resize on the server side using Laravel - this one I'm not sure cause i might crop something important from the picture. Or maybe only resizing by the height and keeping the aspect ratio (the image might be small on the width in some cases right?)

The first option is valid, the problem is that MOST or all of the Vue plugins I found for croping is for single files online, I can't find one that allows me to create multiple and upload them later using Laravel. (I also need to create a kind of checkbox so the user can select which image is the default (to appear on the front of the website - the rest will be in the gallery)

Any tips on how I can do this? Thank you very much

0 likes
4 replies
AbehoM's avatar

@artcore Can you show me how you did the multiple cropping and upload with cropperjs and Laravel? Thank you.

artcore's avatar

I've only done a single but multiple croppers just work on different elements. You would have to add a listener to the parent containing all cropper elements and take it from there.

Upon cropping I let cropperjs put the base64 encoded image on a hidden input and simply decode it on the server side.

I'll copy some here for you (still in dev, unpolished) The image name and file location are also inputs to allow me to store a reference to the file as well as clean up the filename. See the job handler at the end

@extends('catalog::layout.app')
@section('title', 'Catalog Image Form')
@section('stylesheets')
  <link href="{{ asset('public/assets/collection/collection.css') }}" rel="stylesheet" type="text/css" media="all">
  <link href="{{ asset('public/assets/catalog/css/custom.css') }}" rel="stylesheet" type="text/css" media="all">
  <link href="{{ asset('public/assets/core/cropper.min.css') }}" rel="stylesheet" type="text/css" media="all">
@endsection
@section('content')
  <div class="container">

    <div class="card mt-5">
      <div class="card-header">
        <h5>
          <i class="fa fa-pencil position-left"></i>
          <span class="text-semibold">{{ is_object($data) ? 'Edit' : 'Add' }}</span> Image:
        </h5>
        <div class="heading-elements">
          <a class="btn btn-warning btn-xs" role="button" title="Cancel" href="{{ route('image.index') }}">
            <i class="fa fa-reply"></i>
          </a>
        </div>
      </div>
    </div>

    <div class="row justify-content-center mt-5">
      <div class="col-12">
        <form id="form" method="POST"
              action="{{ is_object($data) ? route('image.update', $data->id) : route('image.store') }}"
              enctype="multipart/form-data"
              class="steps-validation">

          @if(is_object($data))
            {{ method_field('PUT') }}
          @endif

          {{ csrf_field() }}

          <div class="row align-items-center">
            <fieldset class="col-md-5 col-xs-12 card">

              <div class="btn-group btn-block">
                <a class="btn btn-danger" role="button" title="Cancel" href="{{ route('image.index') }}">Cancel</a>
                <button form="form" type="submit" class="btn btn-success" role="button" title="Save">Save</button>
              </div>

              <div class="form-group mt-3">
                <div class="custom-file">
                  <label class="custom-file-label" for="location">{{ $data->location ?? old('image.location') ?? 'Choose file' }}</label>
                  <input type="file"
                         name="image[file]"
                         class="custom-file-input form-control"
                         id="location">
                  <input type="hidden"
                         name="image[location]"
                         value="{{ $data->location ?? old('image.location') }}">
                  <input type="hidden"
                         name="image[crop]"
                         value="">
                </div>
              </div>

              <div class="form-group" data-select="link" data-collection="lookup">
                <input type="text"
                       placeholder="add link"
                       class="form-control">
              </div>
              <div class="form-group mt-5 mb-5">
                <ul id="link-list" class="list-group list-group-flush">
                  <li class="list-group-item active">Linked To</li>
                  @if(is_object($data))
                    @foreach($data->links as $link)
                      <li class="list-group-item d-flex justify-content-between align-items-center deletable">
                        <span class="badge badge-{{ $link->item === 'product' ? 'primary' : 'dark' }}">{{ $link->item }}</span>&nbsp;
                        <span class="btn-block">{{ $link->title }}</span>
                        <span class="btn btn-sm btn-danger delete"><i class="fa fa-trash"></i></span>
                        <input type="hidden" name="links[{{ $link->item }}][]" value="{{ $link->id }}">
                      </li>
                    @endforeach
                  @endif
                </ul>
              </div>

              <div class="form-group">
                <label class="col-form-label" for="status">Status</label>
                <input id="status" type="number"
                       name="image[status]"
                       value="{{ $data->status ?? old('image.status') }}"
                       class="form-control">
              </div>
            </fieldset>

            <div class="col-md-7 col-xs-12">

              <div id="controls" class="btn-group btn-group-justified mb-5">
                {{--<button class="btn btn-outline-danger" data-method="disable" title="Disable"><span class="fa fa-lock"></span></button>--}}
                {{--<button class="btn btn-outline-success" data-method="enable" title="Enable"><span class="fa fa-unlock"></span></button>--}}
                <button class="btn btn-outline-primary" data-method="reset" title="Reset"><span class="fa fa-refresh"></span></button>
              </div>

              <div id="preview">
                <img src="{{ isset($data->location)
              ? asset('public/assets/catalog/img/item/'.$data->location)
              : 'https://via.placeholder.com/400x600.png?text=Empty' }}" alt="">
              </div>
            </div>
          </div>
        </form>
      </div>
    </div>
  </div>
@endsection
@section('scripts')
  <script src="{{ asset('public/assets/core/cropper.min.js')}}"></script>
  <script type="module">
    const fileInput = document.getElementById('location'),
          preview   = document.getElementById('preview'),
          input     = document.querySelector('input[name="image[crop]"]'),
          controls  = document.getElementById('controls');

    let cropper;

    const crop = () =>
    {
      const set = () =>
      {
        input.value = cropper.getCroppedCanvas(
          {
            width:                 400,
            height:                600,
            // fillColor: '#fff',
            imageSmoothingEnabled: true,
            imageSmoothingQuality: 'high'
          }).toDataURL("image/png", 100);
      };

      const image = preview.querySelector('img');
      cropper = new Cropper(image,
        {
          minContainerWidth:  404,
          minContainerHeight: 606,
          minCropBoxWidth:    404,
          minCropBoxHeight:   606,
          wheelZoomRatio:     0.1,
          autoCropArea:       1,
          ready:              event =>
                              {
                                set();
                              }
        });

      image.addEventListener('cropend', (event) =>
      {
        set();
      });
      image.addEventListener('cropmove', (event) =>
      {
        set();
      });
      image.addEventListener('zoom', (event) =>
      {
        set();
      });
    };

    crop();

    fileInput.addEventListener('change', event =>
    {
      const fileName = event.target.files[0].name;

      event.target.previousElementSibling.innerText = fileName;
      event.target.nextElementSibling.value = fileName;

      preview.innerHTML = `<img alt="" src="${URL.createObjectURL(event.target.files[0])}">`;

      // cropper.destroy();
      setTimeout(() =>
      {
        crop();
      }, 300);
    });

    controls.addEventListener('click', e =>
    {
      e.preventDefault();
      const method = e.target.getAttribute('data-method');

      switch (method)
      {
        case 'reset':
          cropper.reset();
          break;
        case 'enable':
          cropper.enable();
          break;
        case 'disable':
          cropper.disable();
          break;
      }

    }, false);
  </script>
  <script type="module">
    import List    from "{{ asset('public/assets/collection/markup/item.js')}}";
    import suggest from "{{ asset('public/assets/collection/templates/link.js')}}";
    import action  from "{{ asset('public/assets/collection/events/action-link.js')}}";

    (new List('{{ route('catalog-items') }}'))
      .renderWhen('link', 5)
      .template(suggest,
        {
          action: action,
          title:  ['title', 'item']
        });
  </script>
@endsection

and the backend which is a job

<?php namespace Extend\Catalog\Domains\Image;

use Illuminate\Filesystem\Filesystem;
use Illuminate\Http\UploadedFile;
use Lucid\Foundation\Job;

class MoveImageJob extends Job
{

  /**
   * @var UploadedFile $file
   */
  private $file, $crop, $location;


  public function __construct($file, $crop, $location)
  {
    $this->file     = $file;
    $this->crop     = $crop;
    $this->location = $location;
  }


  public function handle(Filesystem $filesystem)
  {
    try
    {
      if ($this->file || $this->crop)
      {
        $file = $this->crop ? base64_decode(str_replace('data:image/png;base64,', '', $this->crop)) : null;

        if (!$file && $this->file instanceof UploadedFile)
          $file = $this->file->get();

        if (!$file)
          return false;

        $fileName = strtolower(str_replace(' ', '-', $this->location));

        $filesystem->put(
          config('catalog.disks.archive.folder') . $fileName, $file);

        return $fileName;
      }
    }
    catch (\Exception $e)
    {
      logger($e->getTraceAsString());
    }
  }
}
AbehoM's avatar

@ARTCORE - Thank you very much for this, I will be trying that for sure.

Please or to participate in this conversation.