Shrine with cropping - flow?

357 views
Skip to first unread message

Ondra Žádník

unread,
Aug 8, 2016, 3:47:44 AM8/8/16
to Shrine
Hello,

I'm trying to implement Shrine for image upload in our app. However our application is a little bit special. I will describe you our steps and problem which i'm currently facing.

Steps:
1. User will upload image directly to S3 via javascript(Shrine will store original image to my model)
2. Then i will redirect user to cropping action where i will show original image with cropper.js 
3. After crop i will send form with crop cords and here comes the problem described below...

Goal:
I want to have original image and crop from original image with 2 versions(400x400, 200x200) saved on S3. 

Problem:
My idea was that i will store original then crop and then i want to create 2 versions from cropped image. But I'm not able to start Shrine after crop. I think there is a problem because the form with cropped image will send the same image_data without any change. That means shrine is not "activated" and i cant create versions. 

Is there any solution how to create versions manually? Or is there a better solution for cropping  flow?

My Image model class:
class Image < ActiveRecord::Base
  include ImageUploader[:image]

  acts_as_paranoid

  belongs_to :imageable, polymorphic: true
end

My image_uploader mode class:
require "image_processing/mini_magick"

class ImageUploader < Shrine
  include ImageProcessing::MiniMagick

  process(:store) do |io, context|
    if cropping_params_present?(context)
      cropped = crop(io.download, context[:crop_width], context[:crop_height], context[:crop_x], context[:crop_y], gravity: "NorthWest")
      size_400 = resize_to_limit(cropped,    400, 400)
      size_200 = resize_to_limit(size_400,    200, 200)

      {cropped_original: cropped, medium: size_400, small: size_200}
    else
      {original: io}
    end
  end

  def after_upload(io, context)
    Pusher.trigger("processing_complete_signal", 'processing_complete', { message: 'ok' })
  end

  def cropping_params_present?(context)
    if context[:crop_x] && context[:crop_y] && context[:crop_width] && context[:crop_height]
      true
    else
      false
    end
  end
end

Form for original image save:
<div id="image-wrapper">
  <%= render 'users/image', user: @user %>
</div>
<%= form_for user, :url => update_avatar_user_path, :html => { :multipart => true, :class => "directUpload" }, remote: true do |f| %>
  <%= f.fields_for :image do |img| %>
    <%= img.hidden_field :image, value: img.object.cached_image_data %>
    <%= img.file_field :image, name: "file" %>
  <% end %>
  <div id="progress" class="progress">
    <div class="progress-bar progress-bar-success progress-bar-striped"></div>
  </div>
  <%= f.submit %>
<% end %>

Form for cropping:
<div id="image-wrapper">
  <%= render 'users/image', user: @user %>
</div>
<%= form_for user, :url => update_avatar_user_path, :html => { :multipart => true, :class => "directUpload" } do |f| %>
  <%= f.fields_for :image do |img| %>
    <%= img.hidden_field :crop_width, id: 'crop_width' %>
    <%= img.hidden_field :crop_height, id: 'crop_height' %>
    <%= img.hidden_field :crop_x, id: 'crop_x' %>
    <%= img.hidden_field :crop_y, id: 'crop_y' %>
    <%= img.hidden_field :image %>
  <% end %>
  <div id="progress" class="progress">
    <div class="progress-bar progress-bar-success progress-bar-striped"></div>
  </div>
  <%= f.submit %>
<% end %>


Js for direct upload to S3:
<script>

$(function() {
  var progress_bar = $('.progress-bar');
  var form = $('.directUpload');
    $('.directUpload input:file').fileupload({
      add: function(e, data) {
        var options = {
          extension: data.files[0].name.match(/(\.\w+)?$/)[0], // set extension
          _: Date.now(),                                       // prevent caching
        }
        $.getJSON('/images/cache/presign', options, function(result) {
          data.formData = result['fields'];
          data.url = result['url'];
          data.submit();
        });
      },
      progress: function(e, data) {
        var progress = parseInt(data.loaded / data.total * 100, 10);
        var percentage = progress.toString() + '%'
        progress_bar.css("width", percentage).html(percentage);
      },
      done: function(e, data) {
        progress_bar.text("Nahrávání hotovo");
        var image_attrs = {
          id: data.formData.key.match(/cache\/(.+)/)[1], // we have to remove the prefix part
          storage: 'cache',
          metadata: {
            size:      data.files[0].size,
            filename:  data.files[0].name.match(/[^\/\\]+$/)[0], // IE returns full path
            mime_type: data.files[0].type
          }
        }

        var image_data = $("<input />", { type:'hidden', name: 'user[image_attributes][image]', value: image_attrs })
        form.append(image_data);

        $("input[name='file']").remove();
        $(".directUpload").submit();
      }
    });
  });

</script>


Tell me please if you understand what im trying to achieve. Thank you for your help :)

Ondra






Janko Marohnić

unread,
Aug 8, 2016, 5:41:09 AM8/8/16
to Ondra Žádník, Shrine
Ahoj! :)

You can always trigger processing by calling `#promote` on the attacher:

user.image.image_attacher #=> #<Shrine::Attacher>
user.image.image_attacher.promote

Firstly, I recommend sending cropping parameters inside of `image_data` attributes, you can add them to the metadata hash via JavaScript:

var imageData = JSON.parse($("#your-image-column-id").val());
imageData.metadata["cropping"] = {
  "width": ...,
  "height": ...,
  "x": ...,
  "y": ..., 
};
$("#your-image-column-id").val(JSON.stringify(imageData));

Now when you submit cropping data, you can trigger promoting with the ":crop" action, and then setup processing for that action:

# in your controller
user.update(user_params)
user.image.image_attacher.promote(action: :crop)

# in your uploader
class ImageUploader < Shrine
  # ...
  process(:crop) do |io, context| # we set the :crop action
    cropping_args = io.metadata["cropping"].values_at("width", "height", "x", "y")

    cropped = crop(io.download, *cropping_args, gravity: "NorthWest")
    size_400 = resize_to_limit(cropped,  400, 400)
    size_200 = resize_to_limit(size_400, 200, 200)

    {cropped_original: cropped, medium: size_400, small: size_200}
  end
end

-------

However, I think that it's much better to let user crop immediately after direct upload to S3. That way you can submit the file together with cropping info, so you don't need any custom code since you can perform this same processing on ":store" action. And also it will correctly work with backgrounding, if you choose to add it (the first solution will not).

Cheers,
Janko

--
You received this message because you are subscribed to the Google Groups "Shrine" group.
To unsubscribe from this group and stop receiving emails from it, send an email to ruby-shrine...@googlegroups.com.
To post to this group, send email to ruby-...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/ruby-shrine/bb7c5d07-84be-494d-8199-c27cfca4048e%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Reply all
Reply to author
Forward
0 new messages