Cropper won't work, response.uploadURL is null

Hi all,

I have been using Shrine inside Rails to successfully post images directly to S3 using Uppy for the frontend, and doing background jobs for derivatives, and using a derivation_endpoint for on-the-fly processing when a derivative isn’t yet available.

I have multiple uploaders working this way, and then tried to add image cropping to one of them using Cropper.JS, and the instructions on Shrine’s Wiki (Image Cropping · shrinerb/shrine Wiki · GitHub), but I can’t seem to get past this hiccup…

WHAT GOES WRONG
When I’m inside my form and choose an image to upload, I see Uppy places a thumbnail for it on the page, and Uppy’s upload progress bar fills up. But when the Progress Bar reaches the end, the image thumbnail disappears, and a broken file icon is shown in its place. The broken file icon has a NULL url (http://localhost:3000/company_profiles/1/null)

When I go to my fileUpload.js file, to the ‘uppy.on(‘upload-success’) function call, I discovered that the ‘response.uploadURL’ being passed to Cropper is null, so I suppose that’s why Cropper can’t do anything!

But I’m not sure how to troubleshoot ‘response.uploadURL’ being null….

Any thoughts? THANKS VERY MUCH!!!

CODE SAMPLES

fileUpload.js
const headshotUpload = (fileInput) => {
const hiddenInput = document.querySelector(’.headshot-upload-data’),
imagePreview = document.querySelector(’.headshot-preview img’),
formGroup = fileInput.parentNode
// remove our file input in favour of Uppy’s
formGroup.removeChild(fileInput)
const uppy = Core({
autoProceed: true,
restrictions: {
allowedFileTypes: [‘image/jpg’, ‘image/jpeg’, ‘image/heic’]
},
})
.use(FileInput, {
target: formGroup,
locale: { strings: { chooseFiles: ‘Choose file’ } },
})
.use(Informer, {
target: formGroup,
})
.use(ProgressBar, {
target: imagePreview.parentNode,
})
.use(ThumbnailGenerator, {
thumbnailWidth: 320,
thumbnailType: ‘image/jpeg’,
})
.use(AwsS3, {
companionUrl: ‘/’, // will call the presign endpoint on /s3/params
})
uppy.on(‘thumbnail:generated’, (file, preview) => {
// show preview of the image using URL from thumbnail generator
imagePreview.src = preview
})
uppy.on(‘upload-success’, (file, response) => {
// construct uploaded file data in the format that Shrine expects
const uploadedFileData = response.body[‘data’]
hiddenInput.value = JSON.stringify(uploadedFileData)

cropbox(imagePreview, response.uploadURL, {

  onCrop(detail) {
    let fileData = JSON.parse(hiddenInput.value)
    fileData['metadata']['crop'] = detail
    hiddenInput.value = JSON.stringify(fileData)
    
  }
})

})
}

headshot_uploader.rb
require ‘image_processing/vips’

class HeadshotUploader < Shrine
ALLOWED_TYPES = %w[image/jpeg image/heic].freeze
MAX_SIZE = 3 * 1024 * 1024
MAX_DIMENSIONS = [3000, 3000].freeze # 5000x5000
THUMBNAILS = {
small: [320, 320],
medium: [600, 600],
large: [800, 800]
}.freeze

plugin :remove_attachment
plugin :pretty_location # creates a certain file structure on s3. Need to look it up.
plugin :validation_helpers
plugin :store_dimensions, log_subscriber: nil
plugin :derivation_endpoint, prefix: ‘derivations/headshot’

Attacher.derivatives do |original|
vips = ImageProcessing::Vips.source(original)
vips = vips.crop(*file.crop_points) # apply cropping
THUMBNAILS.transform_values do |(width, height)|
vips.resize_to_limit!(width, height)
end
end

begin crop code

Attacher.default_url do |derivative: nil, **|
next unless derivative && file

file.derivation_url :transform, shrine_class.urlsafe_serialize(
  crop: file.crop_points,
  resize_to_limit: THUMBNAILS.fetch(derivative)
)

end

derivation :transform do |file, transformations|
transformations = shrine_class.urlsafe_deserialize(transformations)
vips = ImageProcessing::Vips.source(file)
vips.apply!(transformations)
end

class UploadedFile
# convenience method for fetching crop points from metadata
def crop_points
metadata.fetch(‘crop’).fetch_values(‘x’, ‘y’, ‘width’, ‘height’)
end
end
end

Hi @k2director have you solved your problem ?
I am using Cropper too on one of my projects. I can share my code if you are still facing the issue …

Hi Maxence, thanks much for the reply! Sorry for the delay, we hit the road on vacation as soon as school was out. :wink:

I am indeed still stuck where I left off in my last message. If you have any code to share that has a successful Cropper.js instance, I’d love to see it!

As I said in my earlier message, my particular situation calls for using Cropper.js on images that are uploaded to S3, and then background processing of variants is done as a Sidekiq job…kind of the typical scenario that’s covered in a lot of Shrine How-tos.

Thanks very much!

I don’t think that will work like that because: (i) you are uploading directly to the s3 bucket instead of uploading directly to your app (which the cropping Wiki instructions presume) - You’ll have to properly parse that response body to extract the uploadURL - this is my best guess – but i haven’t studied what the s3 response would be in any great detail.

if your files are being successfully uploaded and reflected in active record - that’s very positive. Accordingly, how could the response body that you have posted be completely empty? It must have something in there, because the s3 bucket keys are being successfully saved to your database, if i’m understanding you correctly.

You would need to interrogate the response body carefully:

const uploadedFileData = response.body["data"]
let jsonResponse =  JSON.stringify(uploadedFileData)
hiddenInput.value = jsonResponse;

// add debugger statement here:
console.log(jsonResponse);
debugger;

// interogate the jsonResponse here
// surely the upload URL should be in there somewhere
// if it's not in there, then an easy solution is to upload using s3 multlipart
// i'm sure that has an upload url response in there
// which would probably be ok unless people are uploading images
// with a reasonable file size

cropbox(imagePreview, uploadURL, {

  onCrop(detail) {
    let fileData = JSON.parse(hiddenInput.value)
    fileData['metadata']['crop'] = detail
    hiddenInput.value = JSON.stringify(fileData)
    
  }
})

But I wouldn’t bother with all of that if you can’t get it to work too quickly.

People are uploading directly from their filesystem, right? Consequently, use the local file api to throw the image onto the DOM, and extract the crop points from there.

See here: javascript - How to display an image from a file input? - Stack Overflow

// at least that's my brief understanding of cropper js
  new Cropper(imageDomElement, {
    aspectRatio: 1,
    viewMode: 1,
    guides: false,
    autoCropArea: 1.0,
    background: false,
    zoomable: false,
    crop: event => onCrop(event.detail)
  })

(also as a side note, it would be of immense benefit to people reading your code snippets if it was better formatted):