croppable 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: bc06a6b34a2f7a532d24d39e64720bac364ccf40d176a2c47d64e0a93814c779
4
+ data.tar.gz: 6045bfc5c350d93eae70dd4692194327e38f47b3d8008ee87610c21a5160a2fd
5
+ SHA512:
6
+ metadata.gz: 4b9055c91ee55c59e79f1f2c46abee67d3b99dda35fbb90cb4d5abf24f7df39587ecb4e7d07bc08554831db71ab631a3e645629a181299eb760ad4bf406d1b65
7
+ data.tar.gz: 1904b7435d75b0c19ed2db1c1887d722e07e2991374a53e051403c417738c3310518254cfb13a60cc13cc3ad40253c23a7417bf7262fb68d2266c99821f9d002
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2023 Steven Barragan
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # Croppable
2
+ Easily crop images in Ruby on Rails with [Cropper.js](https://fengyuanchen.github.io/cropperjs/v2/) integration.
3
+
4
+ ## Installation
5
+ Add this line to your application's Gemfile:
6
+
7
+ ```ruby
8
+ gem "croppable"
9
+ ```
10
+
11
+ ## Setup
12
+ ```bash
13
+ bin/rails croppable:install
14
+ ```
15
+
16
+ The asset_host config needs to be set on every environment
17
+ ```ruby
18
+ config.asset_host = "http://localhost:3000"
19
+ ```
20
+
21
+ ## Manual setup
22
+ Install cropperjs JavaScript dependency
23
+ ```
24
+ yarn add cropperjs@next
25
+ // or
26
+ bin/importmap pin cropperjs@next
27
+ ```
28
+
29
+ If you're using importmap add croppable pin to importmap.rb
30
+ ```ruby
31
+ pin "croppable"
32
+ ```
33
+
34
+ Import the croppable JavaScript module in your application entrypoint
35
+ ```js
36
+ import "croppable"
37
+ ```
38
+
39
+ Import croppable styles in your base stylesheet
40
+ ```
41
+ *= require croppable
42
+ ```
43
+
44
+ Install croppable migrations
45
+ ```
46
+ bin/rails croppable:install:migrations
47
+ ```
48
+
49
+ ## Install [libvips](https://www.libvips.org/install.html)
50
+ ```bash
51
+ brew install vips
52
+ ```
53
+
54
+ ## Usage
55
+ Add has_croppable into your model
56
+ ```ruby
57
+ has_croppable :logo, width: 300, height: 300
58
+ ```
59
+
60
+ Add croppable_field to your form
61
+ ```ruby
62
+ form.croppable_field :logo
63
+ ```
64
+
65
+ Update controller strong paramenters to permit each croppable parameter
66
+ ```ruby
67
+ params.require(:model).permit(..., :logo)
68
+ ```
69
+
70
+ Display cropped image in your view
71
+ ```ruby
72
+ image_tag model.logo if model.logo.present?
73
+ ```
74
+
75
+ Original image can be accessed in \<croppable\>_original
76
+ ```ruby
77
+ model.logo_original
78
+ ```
79
+
80
+ NOTE: Images are cropped in a background job after model gets saved so they might not be immediately available
81
+
82
+ ## Contributing
83
+
84
+ Run all test
85
+ ```bash
86
+ rails test
87
+ rails app:test:system
88
+ ```
89
+
90
+ ## License
91
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
8
+ require "bundler/gem_tasks"
@@ -0,0 +1,2 @@
1
+ //= link_directory ../stylesheets/croppable .css
2
+ //= link_directory ../javascripts/ .js
@@ -0,0 +1,182 @@
1
+ import Cropper from 'cropperjs';
2
+
3
+ function isTurbolinksEnabled() {
4
+ try {
5
+ return Turbolinks.supported;
6
+ } catch(_) {
7
+ return false;
8
+ }
9
+ }
10
+
11
+ if(isTurbolinksEnabled()) {
12
+ document.addEventListener('turbo:load', start)
13
+ } else {
14
+ document.addEventListener('DOMContentLoaded', start)
15
+ }
16
+
17
+ function start() {
18
+ const dropAreas = document.getElementsByClassName('croppable-droparea');
19
+
20
+ Array.from(dropAreas).forEach((dropArea) => {
21
+ const wrapper = dropArea.closest(".croppable-wrapper");
22
+ const input = wrapper.querySelector(".croppable-input");
23
+
24
+ input.addEventListener('change', (event) => {
25
+ const file = input.files[0];
26
+
27
+ const image = document.createElement('img');
28
+ image.src = URL.createObjectURL(file);
29
+
30
+ updateImageDisplay(image, wrapper, true, input)
31
+ });
32
+
33
+ dropArea.onclick = () => input.click()
34
+
35
+ dropArea.addEventListener("dragover", (event)=>{
36
+ event.preventDefault();
37
+ dropArea.classList.add("active");
38
+ });
39
+
40
+ dropArea.addEventListener("dragleave", ()=>{
41
+ dropArea.classList.remove("active");
42
+ });
43
+
44
+ dropArea.addEventListener("drop", (event)=>{
45
+ event.preventDefault();
46
+
47
+ const file = event.dataTransfer.files[0];
48
+
49
+ if (file.type.match(/image.*/)) {
50
+ input.files = event.dataTransfer.files;
51
+
52
+ const image = document.createElement('img');
53
+ image.src = URL.createObjectURL(file);
54
+
55
+ updateImageDisplay(image, wrapper, true, input)
56
+ }
57
+
58
+ dropArea.classList.remove("active");
59
+ });
60
+ });
61
+
62
+ const images = document.getElementsByClassName('croppable-image');
63
+
64
+ Array.from(images).forEach((image) => {
65
+ const wrapper = image.closest(".croppable-wrapper");
66
+
67
+ updateImageDisplay(image, wrapper, false, false)
68
+ });
69
+ }
70
+
71
+ function updateImageDisplay(image, wrapper, isNewImage, input) {
72
+ const controls = wrapper.querySelector(".croppable-controls");
73
+ const container = wrapper.querySelector(".croppable-container");
74
+ const centerBtn = wrapper.querySelector(".croppable-center");
75
+ const fitBtn = wrapper.querySelector(".croppable-fit");
76
+ const deleteBtn = wrapper.querySelector(".croppable-delete");
77
+ const bgColorBtn = wrapper.querySelector(".croppable-bgcolor");
78
+ const xInput = wrapper.querySelector(".croppable-x");
79
+ const yInput = wrapper.querySelector(".croppable-y");
80
+ const scaleInput = wrapper.querySelector(".croppable-scale");
81
+ const deleteInput = wrapper.querySelector(".croppable-input-delete");
82
+ const dropArea = wrapper.querySelector(".croppable-droparea");
83
+ const width = wrapper.dataset.width;
84
+ const height = wrapper.dataset.height;
85
+
86
+ dropArea.classList.add("inactive");
87
+ container.classList.add("active");
88
+ deleteInput.checked = false;
89
+
90
+ cleanContainer()
91
+
92
+ const cropper = new Cropper(image, {container, template: template(width, height)});
93
+
94
+ const cropperImage = cropper.getCropperImage();
95
+ const cropperCanvas = cropper.getCropperCanvas();
96
+
97
+ controls.style.display = "flex";
98
+
99
+ var saveTransform = false;
100
+
101
+ cropperImage.$ready(() => {
102
+ if (xInput.value != "" && !isNewImage) {
103
+ var waitForTranform = null;
104
+
105
+ // Turbolinks hack to actually apply initial transformation
106
+ if(isTurbolinksEnabled) {
107
+ waitForTranform = 10;
108
+ } else {
109
+ waitForTranform = 0;
110
+ }
111
+
112
+ setTimeout(() => {
113
+ cropperImage.$setTransform(+scaleInput.value, 0, 0, +scaleInput.value, +xInput.value, +yInput.value);
114
+
115
+ saveTransform = true;
116
+ }, waitForTranform)
117
+ } else {
118
+ const matrix = cropperImage.$getTransform();
119
+ xInput.value = matrix[4];
120
+ yInput.value = matrix[5];
121
+ scaleInput.value = matrix[0];
122
+
123
+ saveTransform = true;
124
+ }
125
+ })
126
+
127
+ cropperImage.addEventListener('transform', (event) => {
128
+ if(saveTransform) {
129
+ const matrix = event.detail.matrix;
130
+ xInput.value = matrix[4];
131
+ yInput.value = matrix[5];
132
+ scaleInput.value = matrix[0];
133
+ }
134
+ });
135
+
136
+ cropperCanvas.style.backgroundColor = bgColorBtn.value;
137
+
138
+ bgColorBtn.addEventListener("change", (event) => {
139
+ event.preventDefault();
140
+ cropperCanvas.style.backgroundColor = event.target.value;
141
+ })
142
+
143
+ centerBtn.addEventListener("click", (event) => {
144
+ event.preventDefault();
145
+ cropperImage.$center('cover')
146
+ })
147
+
148
+ fitBtn.addEventListener("click", (event) => {
149
+ event.preventDefault();
150
+ cropperImage.$center('contain')
151
+ })
152
+
153
+ deleteBtn.addEventListener("click", (event) => {
154
+ event.preventDefault();
155
+
156
+ deleteInput.checked = true;
157
+
158
+ if (input) { input.value = ""; }
159
+
160
+ cleanContainer()
161
+
162
+ dropArea.classList.remove("inactive");
163
+ container.classList.remove("active");
164
+
165
+ controls.style.display = "none";
166
+ })
167
+
168
+ function cleanContainer() {
169
+ while(container.firstChild) {
170
+ container.removeChild(container.lastChild);
171
+ }
172
+ }
173
+ }
174
+
175
+ function template(width, height) {
176
+ return `
177
+ <cropper-canvas style="height: ${height}px; width: ${width}px;">
178
+ <cropper-image slottable></cropper-image>
179
+ <cropper-handle action="move" plain></cropper-handle>
180
+ </cropper-canvas>
181
+ `
182
+ }
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,71 @@
1
+ .croppable-preview {
2
+ display: flex;
3
+ }
4
+
5
+ .croppable-wrapper {
6
+ margin-bottom: 1em;
7
+ }
8
+
9
+ .croppable-controls {
10
+ display: none;
11
+ flex-direction: column;
12
+ padding: 0 0.7em;
13
+ }
14
+
15
+ .croppable-input-delete, .croppable-input {
16
+ display: none;
17
+ }
18
+
19
+ .croppable-controls button {
20
+ background-position: center;
21
+ background-repeat: no-repeat;
22
+ background-size: contain;
23
+ border-radius: 4px;
24
+ border: 1px solid #3a4855;
25
+ cursor: pointer;
26
+ margin-bottom: 3px;
27
+ padding: 0;
28
+ height: 30px;
29
+ width: 35px;
30
+ }
31
+
32
+ .croppable-controls .croppable-bgcolor {
33
+ cursor: pointer;
34
+ margin-bottom: 3px;
35
+ padding: 4px;
36
+ height: 30px;
37
+ width: 35px;
38
+ }
39
+
40
+ .croppable-delete img {
41
+ height: 28px;
42
+ }
43
+
44
+ .croppable-center {
45
+ background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA4klEQVR4nO2XUQ6CMAyGewkWd/+b6HYBefA49XWWFlkk+m/+X7KQQCF8dO2GCCGEEEJe0EHHhulERkEpAoYyI7NnpAbd5NYRWxBENBhXJ7b0tNFfijxEJB+4ZxGRO6rI2hzTTnwysXAiS/OVo8zYmAuiiLyR8SQESaSYwrZTJwXn2vu9DgexINoXP1o/kCu77UztdBpeJJ/4fOXUCqimWHuLvaJtUfZarHcto7VfDRZEr7A9GTiRdZYtina0WOhNo37wPwIhUk74H4HdonwDpQgYyoyMlhEdbGyYRoQQQgj5W57BIckAMCn5cwAAAABJRU5ErkJggg==');
46
+ }
47
+
48
+ .croppable-fit {
49
+ background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAACXBIWXMAAAsTAAALEwEAmpwYAAABJklEQVR4nO2XbQ6DMAiGuYRGD9ZL7hh+XGD+2HHYH026plRBW7HjScgSi8LrgFYAwzAMwzB+wIil1kraRmqtXiHctZygJCcTkhG8uko0lpaIRwlxq3EfOBPTZGT4ToK4jspXWo/UyBwivuOBEXvb1PKT+QBAD/s0APDWKmTxftuEiDbwVSek8d4y9c+EPp1GIbAjJiYCNAkZg8YOS6clrlH3n87ptRrrJoIw8aP9w4mbyvfSjSmcTH455YxbREhfIC5UU1oobPY5OI5wm30WHlGw5Pj1e4Iazeo3xC7iGxOjTshSyxEFGSNW9aERT3yPXCrECT+sJiKxgeE7CuK6RL7ldti74uKThKCwVnOCJZs9J2hCID4S8WbbODS2qxFiGIZhGH/LF6hxCMsktMvWAAAAAElFTkSuQmCC');
50
+ }
51
+
52
+ .croppable-delete {
53
+ background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA4UlEQVR4nO2aSw7CIBRFz6iLUren1W3JwLiPttvASDAhJi9tlRbS3pO8CaGfm8eZACC2SwPcgAHwI9UDbXymOq4TAnzXhQrp488dJsw9Jp2pDh9rqfmrUXUQ98O6X6rcVoLcc3Sm1Hr2ub+rIH/i1RGDXS2tJ/DIMF48iDVn7niKglioI2hpBeSIhRxBjgTkiIUcQY4E5IiFHEGOBOSIhRxBjgTkiIUcQY6Uc8QZZxhzx1O0iW2xq03sJfAKMnIZ4H3Qvxan+M0u50vbgqe555xBmhjm05k1qoshqrx4IxjhBYDU2PkqqpaWAAAAAElFTkSuQmCC');
54
+ background-size: 23px !important;
55
+ }
56
+
57
+ .croppable-droparea {
58
+ background: #5256ad;
59
+ border-radius: 5px;
60
+ border: 2px dashed #fff;
61
+ box-sizing: content-box;
62
+ cursor: pointer;
63
+ }
64
+
65
+ .croppable-droparea.active, .croppable-container.active {
66
+ border: 2px solid #4D37AD;
67
+ }
68
+
69
+ .croppable-droparea.inactive {
70
+ display: none;
71
+ }
@@ -0,0 +1,24 @@
1
+ require "active_support/concern"
2
+ require "croppable/param"
3
+
4
+ ActionController::Parameters::PERMITTED_SCALAR_TYPES << Croppable::Param
5
+
6
+ module CleanCroppableParams
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ before_action :setup_croppable_params, only: [:create, :update]
11
+ end
12
+
13
+ private
14
+
15
+ def setup_croppable_params
16
+ if params[:croppables]
17
+ params[:croppables].each do |(key, croppable)|
18
+ params[croppable[:base]] ||= {}
19
+ delete = croppable[:delete] == "1"
20
+ params[croppable[:base]][key] = Croppable::Param.new(croppable[:image], croppable[:data], delete: delete)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,4 @@
1
+ module Croppable
2
+ class ApplicationController < ActionController::Base
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Croppable
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,46 @@
1
+ module Croppable
2
+ module TagHelper
3
+ def croppable_field_tag(name, method, value, object, options = {})
4
+ width = options["width"] || object.send("#{ method }_croppable_setup")[:width]
5
+ height = options["height"] || object.send("#{ method }_croppable_setup")[:height]
6
+
7
+ original = object.send(:"#{ method }_original")
8
+ data = object.send(:"#{ method }_croppable_data")
9
+
10
+ render "croppable/tag", width: width, height: height, method: method, name: name,
11
+ original: original, data: data
12
+ end
13
+ end
14
+ end
15
+
16
+ module ActionView::Helpers
17
+ class Tags::CroppableImage < Tags::Base
18
+ delegate :dom_id, to: ActionView::RecordIdentifier
19
+
20
+ def render
21
+ options = @options.stringify_keys
22
+
23
+ add_default_name_and_id(options)
24
+
25
+ options["input"] ||= dom_id(object, [options["id"], :croppable].compact.join("_")) if object
26
+
27
+ html_tag = @template_object.croppable_field_tag(@object_name, @method_name, options.fetch("value") { value }, object, options.except("value"))
28
+
29
+ error_wrapping(html_tag)
30
+ end
31
+ end
32
+
33
+ module FormHelper
34
+ def croppable_field(object_name, method, options = {})
35
+ Tags::CroppableImage.new(object_name, method, self, options).render
36
+ end
37
+ end
38
+
39
+ class FormBuilder
40
+ def croppable_field(method, options = {})
41
+ self.multipart = true
42
+
43
+ @template.croppable_field(@object_name, method, objectify_options(options))
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,4 @@
1
+ module Croppable
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,11 @@
1
+ require "croppable/crop"
2
+
3
+ module Croppable
4
+ class CropImageJob < ApplicationJob
5
+ queue_as :default
6
+
7
+ def perform(model, croppable_name)
8
+ Croppable::Crop.new(model, croppable_name).perform()
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,6 @@
1
+ module Croppable
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: "from@example.com"
4
+ layout "mailer"
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module Croppable
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module Croppable
2
+ class Datum < ApplicationRecord
3
+ belongs_to :croppable, polymorphic: true
4
+ end
5
+ end
@@ -0,0 +1,31 @@
1
+ <%= content_tag :div, class: "croppable-wrapper", data: { width: width, height: height } do %>
2
+ <div class="croppable-droparea" style="width: <%= width %>px; height: <%= height %>px;">
3
+ <%= render 'croppable/uploader' %>
4
+ </div>
5
+ <%= fields_for :croppables do |croppables| %>
6
+ <%= croppables.fields_for method do |croppable| %>
7
+ <%= croppable.hidden_field :base, value: name %>
8
+ <%= croppable.file_field :image, class: "croppable-input",
9
+ accept: ActiveStorage.web_image_content_types.join(",") %>
10
+ <%= croppable.check_box :delete, class: "croppable-input-delete" %>
11
+ <div class="croppable-preview">
12
+ <div class="croppable-container">
13
+ <% if original.present? %>
14
+ <%= image_tag original, width: width, height: height, class: "croppable-image" %>
15
+ <% end %>
16
+ </div>
17
+ <div class="croppable-controls">
18
+ <%= croppable.fields_for :data, model: data do |data_form| %>
19
+ <%= data_form.hidden_field :x, class: "croppable-x" %>
20
+ <%= data_form.hidden_field :y, class: "croppable-y" %>
21
+ <%= data_form.hidden_field :scale, class: "croppable-scale" %>
22
+ <%= data_form.color_field :background_color, class: "croppable-bgcolor", value: data&.background_color || "#FFFFFF" %>
23
+ <% end %>
24
+ <button class="croppable-center"></button>
25
+ <button class="croppable-fit"></button>
26
+ <button class="croppable-delete"></button>
27
+ </div>
28
+ </div>
29
+ <% end %>
30
+ <% end %>
31
+ <% end %>
@@ -0,0 +1,60 @@
1
+ <svg height="100%" stroke-miterlimit="10" style="fill-rule:nonzero;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;" version="1.1" viewBox="0 0 500 500" width="100%" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
2
+ <defs>
3
+ <path d="M103.307 330.437L396.418 330.437C429.637 330.437 456.566 354.366 456.566 383.885L456.566 383.992C456.566 413.51 429.637 437.439 396.418 437.439L103.307 437.439C70.088 437.439 43.1588 413.51 43.1588 383.992L43.1588 383.885C43.1588 354.366 70.088 330.437 103.307 330.437Z" id="Fill"/>
4
+ <filter color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse" height="136.002" id="Filter" width="442.408" x="28.6588" y="325.937">
5
+ <feDropShadow dx="-3.67321e-05" dy="10" flood-color="#000000" flood-opacity="0.333" in="SourceGraphic" result="Shadow" stdDeviation="5"/>
6
+ </filter>
7
+ </defs>
8
+ <g id="Layer-1">
9
+ <path d="M0 0L500 0L500 500L0 500L0 0Z" fill="#5256ad" fill-rule="nonzero" opacity="1" stroke="none"/>
10
+ </g>
11
+ <g id="Layer-2">
12
+ <g opacity="1">
13
+ <path d="M245.72 58.7781C246.818 57.6776 248.308 57.0591 249.863 57.0591C251.417 57.0591 252.908 57.6776 254.005 58.7781L277.41 82.1831C279.698 84.471 279.698 88.1805 277.41 90.4684C275.122 92.7563 271.413 92.7563 269.125 90.4684L255.714 77.0457L255.714 121.433C255.714 124.665 253.094 127.284 249.863 127.284C246.631 127.284 244.011 124.665 244.011 121.433L244.011 77.0457L230.6 90.4684C228.312 92.7563 224.603 92.7563 222.315 90.4684C220.027 88.1805 220.027 84.471 222.315 82.1831L245.72 58.7781Z" fill="#ffffff" fill-rule="evenodd" opacity="1" stroke="none"/>
14
+ <path d="M207.804 37.6669C219.5 27.581 234.418 22.0106 249.863 21.9622C281.342 21.9622 307.474 45.3671 310.318 75.5478C328.948 78.1808 343.482 93.7802 343.482 112.925C343.482 133.943 325.952 150.689 304.712 150.689L200.49 150.689C176.231 150.689 156.243 131.567 156.243 107.601C156.243 86.9694 171.058 69.8838 190.672 65.5539C192.345 55.4546 198.84 45.3905 207.804 37.6669ZM215.446 46.5257C206.587 54.1674 201.953 63.3772 201.953 70.5859L201.953 75.8286L196.745 76.402C180.397 78.1925 167.945 91.6152 167.945 107.601C167.945 124.768 182.339 138.987 200.49 138.987L304.712 138.987C319.843 138.987 331.78 127.144 331.78 112.925C331.78 98.6952 319.843 86.8523 304.712 86.8523L298.861 86.8523L298.861 81.0011C298.873 55.0217 277.106 33.6647 249.863 33.6647C237.223 33.7152 225.016 38.2805 215.446 46.5374L215.446 46.5257Z" fill="#ffffff" fill-rule="nonzero" opacity="1" stroke="none"/>
15
+ </g>
16
+ <g fill="#ffffff" opacity="1" stroke="none">
17
+ <path d="M88.7326 235.073L88.7326 193.073L102.623 193.073C105.523 193.073 108.243 193.618 110.783 194.708C113.323 195.798 115.553 197.308 117.473 199.238C119.393 201.168 120.898 203.398 121.988 205.928C123.078 208.458 123.623 211.173 123.623 214.073C123.623 216.973 123.078 219.688 121.988 222.218C120.898 224.748 119.393 226.978 117.473 228.908C115.553 230.838 113.323 232.348 110.783 233.438C108.243 234.528 105.523 235.073 102.623 235.073L88.7326 235.073ZM92.9326 230.873L102.623 230.873C104.943 230.873 107.118 230.438 109.148 229.568C111.178 228.698 112.963 227.493 114.503 225.953C116.043 224.413 117.248 222.628 118.118 220.598C118.988 218.568 119.423 216.393 119.423 214.073C119.423 211.753 118.988 209.578 118.118 207.548C117.248 205.518 116.038 203.733 114.488 202.193C112.938 200.653 111.153 199.448 109.133 198.578C107.113 197.708 104.943 197.273 102.623 197.273L92.9326 197.273L92.9326 230.873Z"/>
18
+ <path d="M128.933 235.073L128.933 205.073L133.133 205.073L133.133 209.213C134.193 207.693 135.573 206.483 137.273 205.583C138.973 204.683 140.823 204.233 142.823 204.233C143.983 204.233 145.103 204.383 146.183 204.683L144.473 208.853C143.633 208.593 142.813 208.463 142.013 208.463C140.393 208.463 138.908 208.863 137.558 209.663C136.208 210.463 135.133 211.533 134.333 212.873C133.533 214.213 133.133 215.703 133.133 217.343L133.133 235.073L128.933 235.073Z"/>
19
+ <path d="M173.033 205.073L177.233 205.073L177.233 235.073L173.033 235.073L172.883 229.493C171.903 231.393 170.533 232.928 168.773 234.098C167.013 235.268 164.913 235.853 162.473 235.853C160.273 235.853 158.213 235.438 156.293 234.608C154.373 233.778 152.683 232.633 151.223 231.173C149.763 229.713 148.623 228.023 147.803 226.103C146.983 224.183 146.573 222.123 146.573 219.923C146.573 217.023 147.278 214.388 148.688 212.018C150.098 209.648 151.983 207.758 154.343 206.348C156.703 204.938 159.323 204.233 162.203 204.233C164.703 204.233 166.883 204.828 168.743 206.018C170.603 207.208 172.083 208.763 173.183 210.683L173.033 205.073ZM162.383 231.773C164.523 231.773 166.363 231.248 167.903 230.198C169.443 229.148 170.628 227.733 171.458 225.953C172.288 224.173 172.703 222.213 172.703 220.073C172.703 217.873 172.283 215.888 171.443 214.118C170.603 212.348 169.413 210.943 167.873 209.903C166.333 208.863 164.503 208.343 162.383 208.343C160.263 208.343 158.328 208.868 156.578 209.918C154.828 210.968 153.438 212.383 152.408 214.163C151.378 215.943 150.863 217.913 150.863 220.073C150.863 222.253 151.393 224.228 152.453 225.998C153.513 227.768 154.918 229.173 156.668 230.213C158.418 231.253 160.323 231.773 162.383 231.773Z"/>
20
+ <path d="M206.483 205.073L210.683 205.073L210.683 236.183C210.683 238.103 210.268 239.903 209.438 241.583C208.608 243.263 207.488 244.738 206.078 246.008C204.668 247.278 203.063 248.273 201.263 248.993C199.463 249.713 197.593 250.073 195.653 250.073C193.573 250.073 191.563 249.598 189.623 248.648C187.683 247.698 185.998 246.433 184.568 244.853C183.138 243.273 182.123 241.523 181.523 239.603L185.393 237.833C185.773 239.333 186.488 240.688 187.538 241.898C188.588 243.108 189.828 244.068 191.258 244.778C192.688 245.488 194.153 245.843 195.653 245.843C197.493 245.843 199.238 245.403 200.888 244.523C202.538 243.643 203.883 242.473 204.923 241.013C205.963 239.553 206.483 237.943 206.483 236.183L206.483 229.553C205.443 231.433 204.038 232.953 202.268 234.113C200.498 235.273 198.443 235.853 196.103 235.853C193.303 235.853 190.743 235.143 188.423 233.723C186.103 232.303 184.258 230.398 182.888 228.008C181.518 225.618 180.833 222.973 180.833 220.073C180.833 217.153 181.518 214.498 182.888 212.108C184.258 209.718 186.103 207.813 188.423 206.393C190.743 204.973 193.303 204.263 196.103 204.263C198.443 204.263 200.498 204.838 202.268 205.988C204.038 207.138 205.443 208.663 206.483 210.563L206.483 205.073ZM196.133 231.773C198.213 231.773 200.008 231.233 201.518 230.153C203.028 229.073 204.193 227.643 205.013 225.863C205.833 224.083 206.243 222.153 206.243 220.073C206.243 217.933 205.828 215.978 204.998 214.208C204.168 212.438 202.993 211.018 201.473 209.948C199.953 208.878 198.173 208.343 196.133 208.343C194.093 208.343 192.238 208.868 190.568 209.918C188.898 210.968 187.573 212.383 186.593 214.163C185.613 215.943 185.123 217.913 185.123 220.073C185.123 222.233 185.623 224.198 186.623 225.968C187.623 227.738 188.958 229.148 190.628 230.198C192.298 231.248 194.133 231.773 196.133 231.773Z"/>
21
+ <path d="M243.233 235.853C240.853 235.853 238.748 235.278 236.918 234.128C235.088 232.978 233.658 231.473 232.628 229.613C231.598 227.753 231.083 225.763 231.083 223.643C231.083 222.343 231.218 221.213 231.488 220.253C231.758 219.293 232.228 218.343 232.898 217.403C233.568 216.463 234.503 215.373 235.703 214.133C236.903 212.893 238.433 211.343 240.293 209.483C238.833 208.003 237.708 206.813 236.918 205.913C236.128 205.013 235.583 204.173 235.283 203.393C234.983 202.613 234.833 201.663 234.833 200.543C234.833 198.963 235.218 197.533 235.988 196.253C236.758 194.973 237.783 193.958 239.063 193.208C240.343 192.458 241.733 192.083 243.233 192.083C244.793 192.083 246.213 192.463 247.493 193.223C248.773 193.983 249.793 194.998 250.553 196.268C251.313 197.538 251.693 198.963 251.693 200.543C251.693 201.703 251.533 202.683 251.213 203.483C250.893 204.283 250.338 205.128 249.548 206.018C248.758 206.908 247.653 208.063 246.233 209.483L257.453 220.703L264.053 214.103L264.053 220.043L260.363 223.733L268.793 232.103L265.823 235.073L257.453 226.733C255.573 228.613 254.008 230.148 252.758 231.338C251.508 232.528 250.413 233.453 249.473 234.113C248.533 234.773 247.588 235.228 246.638 235.478C245.688 235.728 244.553 235.853 243.233 235.853ZM243.233 206.993C244.533 205.713 245.513 204.743 246.173 204.083C246.833 203.423 247.283 202.848 247.523 202.358C247.763 201.868 247.883 201.243 247.883 200.483C247.883 199.163 247.418 198.063 246.488 197.183C245.558 196.303 244.473 195.863 243.233 195.863C241.953 195.863 240.868 196.323 239.978 197.243C239.088 198.163 238.643 199.243 238.643 200.483C238.643 201.243 238.773 201.878 239.033 202.388C239.293 202.898 239.753 203.483 240.413 204.143C241.073 204.803 242.013 205.753 243.233 206.993ZM243.233 231.653C244.233 231.653 245.083 231.543 245.783 231.323C246.483 231.103 247.198 230.708 247.928 230.138C248.658 229.568 249.528 228.763 250.538 227.723C251.548 226.683 252.863 225.343 254.483 223.703L243.233 212.453C241.573 214.133 240.223 215.493 239.183 216.533C238.143 217.573 237.343 218.448 236.783 219.158C236.223 219.868 235.838 220.568 235.628 221.258C235.418 221.948 235.313 222.773 235.313 223.733C235.313 225.213 235.673 226.553 236.393 227.753C237.113 228.953 238.073 229.903 239.273 230.603C240.473 231.303 241.793 231.653 243.233 231.653Z"/>
22
+ <path d="M289.193 235.073L289.193 193.073L303.083 193.073C305.983 193.073 308.703 193.618 311.243 194.708C313.783 195.798 316.013 197.308 317.933 199.238C319.853 201.168 321.358 203.398 322.448 205.928C323.538 208.458 324.083 211.173 324.083 214.073C324.083 216.973 323.538 219.688 322.448 222.218C321.358 224.748 319.853 226.978 317.933 228.908C316.013 230.838 313.783 232.348 311.243 233.438C308.703 234.528 305.983 235.073 303.083 235.073L289.193 235.073ZM293.393 230.873L303.083 230.873C305.403 230.873 307.578 230.438 309.608 229.568C311.638 228.698 313.423 227.493 314.963 225.953C316.503 224.413 317.708 222.628 318.578 220.598C319.448 218.568 319.883 216.393 319.883 214.073C319.883 211.753 319.448 209.578 318.578 207.548C317.708 205.518 316.498 203.733 314.948 202.193C313.398 200.653 311.613 199.448 309.593 198.578C307.573 197.708 305.403 197.273 303.083 197.273L293.393 197.273L293.393 230.873Z"/>
23
+ <path d="M329.393 235.073L329.393 205.073L333.593 205.073L333.593 209.213C334.653 207.693 336.033 206.483 337.733 205.583C339.433 204.683 341.283 204.233 343.283 204.233C344.443 204.233 345.563 204.383 346.643 204.683L344.933 208.853C344.093 208.593 343.273 208.463 342.473 208.463C340.853 208.463 339.368 208.863 338.018 209.663C336.668 210.463 335.593 211.533 334.793 212.873C333.993 214.213 333.593 215.703 333.593 217.343L333.593 235.073L329.393 235.073Z"/>
24
+ <path d="M362.033 235.853C359.273 235.853 356.758 235.143 354.488 233.723C352.218 232.303 350.408 230.398 349.058 228.008C347.708 225.618 347.033 222.973 347.033 220.073C347.033 217.153 347.708 214.493 349.058 212.093C350.408 209.693 352.218 207.783 354.488 206.363C356.758 204.943 359.273 204.233 362.033 204.233C364.793 204.233 367.308 204.943 369.578 206.363C371.848 207.783 373.658 209.693 375.008 212.093C376.358 214.493 377.033 217.153 377.033 220.073C377.033 222.973 376.358 225.618 375.008 228.008C373.658 230.398 371.848 232.303 369.578 233.723C367.308 235.143 364.793 235.853 362.033 235.853ZM362.033 231.653C364.073 231.653 365.908 231.118 367.538 230.048C369.168 228.978 370.458 227.563 371.408 225.803C372.358 224.043 372.833 222.133 372.833 220.073C372.833 217.973 372.353 216.038 371.393 214.268C370.433 212.498 369.138 211.083 367.508 210.023C365.878 208.963 364.053 208.433 362.033 208.433C359.993 208.433 358.158 208.968 356.528 210.038C354.898 211.108 353.608 212.523 352.658 214.283C351.708 216.043 351.233 217.973 351.233 220.073C351.233 222.233 351.723 224.188 352.703 225.938C353.683 227.688 354.993 229.078 356.633 230.108C358.273 231.138 360.073 231.653 362.033 231.653Z"/>
25
+ <path d="M386.333 250.073L382.133 250.073L382.133 205.073L386.333 205.073L386.333 210.683C387.373 208.783 388.793 207.243 390.593 206.063C392.393 204.883 394.533 204.293 397.013 204.293C399.933 204.293 402.583 205.003 404.963 206.423C407.343 207.843 409.243 209.748 410.663 212.138C412.083 214.528 412.793 217.173 412.793 220.073C412.793 222.993 412.083 225.653 410.663 228.053C409.243 230.453 407.343 232.363 404.963 233.783C402.583 235.203 399.933 235.913 397.013 235.913C394.533 235.913 392.393 235.323 390.593 234.143C388.793 232.963 387.373 231.423 386.333 229.523L386.333 250.073ZM396.983 208.373C394.883 208.373 393.058 208.898 391.508 209.948C389.958 210.998 388.763 212.403 387.923 214.163C387.083 215.923 386.663 217.893 386.663 220.073C386.663 222.213 387.078 224.173 387.908 225.953C388.738 227.733 389.928 229.153 391.478 230.213C393.028 231.273 394.863 231.803 396.983 231.803C399.063 231.803 400.978 231.283 402.728 230.243C404.478 229.203 405.883 227.798 406.943 226.028C408.003 224.258 408.533 222.273 408.533 220.073C408.533 217.933 408.013 215.978 406.973 214.208C405.933 212.438 404.543 211.023 402.803 209.963C401.063 208.903 399.123 208.373 396.983 208.373Z"/>
26
+ </g>
27
+ <g fill="#ffffff" opacity="1" stroke="none">
28
+ <path d="M163.559 296.697C161.351 296.697 159.339 296.129 157.523 294.993C155.707 293.857 154.259 292.333 153.179 290.421C152.099 288.509 151.559 286.393 151.559 284.073C151.559 281.737 152.099 279.609 153.179 277.689C154.259 275.769 155.707 274.241 157.523 273.105C159.339 271.969 161.351 271.401 163.559 271.401C165.767 271.401 167.779 271.969 169.595 273.105C171.411 274.241 172.859 275.769 173.939 277.689C175.019 279.609 175.559 281.737 175.559 284.073C175.559 286.393 175.019 288.509 173.939 290.421C172.859 292.333 171.411 293.857 169.595 294.993C167.779 296.129 165.767 296.697 163.559 296.697ZM163.559 293.337C165.191 293.337 166.659 292.909 167.963 292.053C169.267 291.197 170.299 290.065 171.059 288.657C171.819 287.249 172.199 285.721 172.199 284.073C172.199 282.393 171.815 280.845 171.047 279.429C170.279 278.013 169.243 276.881 167.939 276.033C166.635 275.185 165.175 274.761 163.559 274.761C161.927 274.761 160.459 275.189 159.155 276.045C157.851 276.901 156.819 278.033 156.059 279.441C155.299 280.849 154.919 282.393 154.919 284.073C154.919 285.801 155.311 287.365 156.095 288.765C156.879 290.165 157.927 291.277 159.239 292.101C160.551 292.925 161.991 293.337 163.559 293.337Z"/>
29
+ <path d="M179.639 296.073L179.639 272.073L182.999 272.073L182.999 275.385C183.847 274.169 184.951 273.201 186.311 272.481C187.671 271.761 189.151 271.401 190.751 271.401C191.679 271.401 192.575 271.521 193.439 271.761L192.071 275.097C191.399 274.889 190.743 274.785 190.103 274.785C188.807 274.785 187.619 275.105 186.539 275.745C185.459 276.385 184.599 277.241 183.959 278.313C183.319 279.385 182.999 280.577 182.999 281.889L182.999 296.073L179.639 296.073Z"/>
30
+ <path d="M225.455 290.049L228.455 291.681C227.367 293.201 225.987 294.417 224.315 295.329C222.643 296.241 220.831 296.697 218.879 296.697C216.671 296.697 214.659 296.129 212.843 294.993C211.027 293.857 209.579 292.333 208.499 290.421C207.419 288.509 206.879 286.393 206.879 284.073C206.879 281.737 207.419 279.609 208.499 277.689C209.579 275.769 211.027 274.241 212.843 273.105C214.659 271.969 216.671 271.401 218.879 271.401C220.831 271.401 222.643 271.857 224.315 272.769C225.987 273.681 227.367 274.905 228.455 276.441L225.455 278.049C224.623 276.993 223.623 276.181 222.455 275.613C221.287 275.045 220.095 274.761 218.879 274.761C217.263 274.761 215.799 275.189 214.487 276.045C213.175 276.901 212.139 278.033 211.379 279.441C210.619 280.849 210.239 282.393 210.239 284.073C210.239 285.753 210.627 287.297 211.403 288.705C212.179 290.113 213.223 291.237 214.535 292.077C215.847 292.917 217.295 293.337 218.879 293.337C220.191 293.337 221.423 293.033 222.575 292.425C223.727 291.817 224.687 291.025 225.455 290.049Z"/>
31
+ <path d="M232.463 296.073L232.463 260.073L235.823 260.073L235.823 296.073L232.463 296.073Z"/>
32
+ <path d="M241.583 272.073L244.943 272.073L244.943 296.073L241.583 296.073L241.583 272.073ZM243.287 267.873C242.743 267.873 242.283 267.693 241.907 267.333C241.531 266.973 241.343 266.521 241.343 265.977C241.343 265.433 241.531 264.981 241.907 264.621C242.283 264.261 242.743 264.081 243.287 264.081C243.815 264.081 244.267 264.261 244.643 264.621C245.019 264.981 245.207 265.433 245.207 265.977C245.207 266.521 245.023 266.973 244.655 267.333C244.287 267.693 243.831 267.873 243.287 267.873Z"/>
33
+ <path d="M267.599 290.049L270.599 291.681C269.511 293.201 268.131 294.417 266.459 295.329C264.787 296.241 262.975 296.697 261.023 296.697C258.815 296.697 256.803 296.129 254.987 294.993C253.171 293.857 251.723 292.333 250.643 290.421C249.563 288.509 249.023 286.393 249.023 284.073C249.023 281.737 249.563 279.609 250.643 277.689C251.723 275.769 253.171 274.241 254.987 273.105C256.803 271.969 258.815 271.401 261.023 271.401C262.975 271.401 264.787 271.857 266.459 272.769C268.131 273.681 269.511 274.905 270.599 276.441L267.599 278.049C266.767 276.993 265.767 276.181 264.599 275.613C263.431 275.045 262.239 274.761 261.023 274.761C259.407 274.761 257.943 275.189 256.631 276.045C255.319 276.901 254.283 278.033 253.523 279.441C252.763 280.849 252.383 282.393 252.383 284.073C252.383 285.753 252.771 287.297 253.547 288.705C254.323 290.113 255.367 291.237 256.679 292.077C257.991 292.917 259.439 293.337 261.023 293.337C262.335 293.337 263.567 293.033 264.719 292.425C265.871 291.817 266.831 291.025 267.599 290.049Z"/>
34
+ <path d="M293.351 296.073L289.487 296.073L282.767 284.481L277.967 290.217L277.967 296.073L274.607 296.073L274.607 260.073L277.967 260.073L277.967 284.985L288.791 272.073L293.183 272.073L285.047 281.745L293.351 296.073Z"/>
35
+ <path d="M321.959 275.433L316.223 275.433L316.199 296.073L312.839 296.073L312.863 275.433L308.519 275.433L308.519 272.073L312.863 272.073L312.839 264.537L316.199 264.537L316.223 272.073L321.959 272.073L321.959 275.433Z"/>
36
+ <path d="M336.167 296.697C333.959 296.697 331.947 296.129 330.131 294.993C328.315 293.857 326.867 292.333 325.787 290.421C324.707 288.509 324.167 286.393 324.167 284.073C324.167 281.737 324.707 279.609 325.787 277.689C326.867 275.769 328.315 274.241 330.131 273.105C331.947 271.969 333.959 271.401 336.167 271.401C338.375 271.401 340.387 271.969 342.203 273.105C344.019 274.241 345.467 275.769 346.547 277.689C347.627 279.609 348.167 281.737 348.167 284.073C348.167 286.393 347.627 288.509 346.547 290.421C345.467 292.333 344.019 293.857 342.203 294.993C340.387 296.129 338.375 296.697 336.167 296.697ZM336.167 293.337C337.799 293.337 339.267 292.909 340.571 292.053C341.875 291.197 342.907 290.065 343.667 288.657C344.427 287.249 344.807 285.721 344.807 284.073C344.807 282.393 344.423 280.845 343.655 279.429C342.887 278.013 341.851 276.881 340.547 276.033C339.243 275.185 337.783 274.761 336.167 274.761C334.535 274.761 333.067 275.189 331.763 276.045C330.459 276.901 329.427 278.033 328.667 279.441C327.907 280.849 327.527 282.393 327.527 284.073C327.527 285.801 327.919 287.365 328.703 288.765C329.487 290.165 330.535 291.277 331.847 292.101C333.159 292.925 334.599 293.337 336.167 293.337Z"/>
37
+ </g>
38
+ <g filter="url(#Filter)">
39
+ <use fill="#ffffff" fill-rule="nonzero" stroke="none" xlink:href="#Fill"/>
40
+ <mask height="111.002" id="StrokeMask" maskUnits="userSpaceOnUse" width="417.408" x="41.1588" y="328.437">
41
+ <rect fill="#ffffff" height="111.002" stroke="none" width="417.408" x="41.1588" y="328.437"/>
42
+ <use fill="#000000" fill-rule="evenodd" stroke="none" xlink:href="#Fill"/>
43
+ </mask>
44
+ <use fill="none" mask="url(#StrokeMask)" stroke="#4d37ad" stroke-linecap="butt" stroke-linejoin="round" stroke-width="4" xlink:href="#Fill"/>
45
+ </g>
46
+ <g fill="#5256ad" opacity="1" stroke="none">
47
+ <path d="M62.5876 404.938L62.5876 362.938L77.4976 362.938C79.6576 362.938 81.6126 363.383 83.3626 364.273C85.1126 365.163 86.5026 366.428 87.5326 368.068C88.5626 369.708 89.0776 371.678 89.0776 373.978C89.0776 375.638 88.6826 377.238 87.8926 378.778C87.1026 380.318 86.0076 381.348 84.6076 381.868C86.3276 382.308 87.6626 383.168 88.6126 384.448C89.5626 385.728 90.2326 387.168 90.6226 388.768C91.0126 390.368 91.2076 391.868 91.2076 393.268C91.2076 395.528 90.6676 397.538 89.5876 399.298C88.5076 401.058 87.0626 402.438 85.2526 403.438C83.4426 404.438 81.4476 404.938 79.2676 404.938L62.5876 404.938ZM68.8276 379.558L76.8376 379.558C78.4776 379.558 79.8876 379.058 81.0676 378.058C82.2476 377.058 82.8376 375.688 82.8376 373.948C82.8376 372.088 82.2476 370.693 81.0676 369.763C79.8876 368.833 78.4776 368.368 76.8376 368.368L68.8276 368.368L68.8276 379.558ZM68.8276 399.538L78.6676 399.538C80.5876 399.538 82.2226 398.833 83.5726 397.423C84.9226 396.013 85.5976 394.238 85.5976 392.098C85.5976 390.858 85.2876 389.688 84.6676 388.588C84.0476 387.488 83.2126 386.593 82.1626 385.903C81.1126 385.213 79.9476 384.868 78.6676 384.868L68.8276 384.868L68.8276 399.538Z"/>
48
+ <path d="M97.2076 404.938L97.2076 374.938L103.448 374.938L103.448 378.118C104.488 376.878 105.768 375.898 107.288 375.178C108.808 374.458 110.448 374.098 112.208 374.098C113.328 374.098 114.438 374.248 115.538 374.548L113.048 380.848C112.268 380.548 111.488 380.398 110.708 380.398C109.388 380.398 108.178 380.723 107.078 381.373C105.978 382.023 105.098 382.898 104.438 383.998C103.778 385.098 103.448 386.318 103.448 387.658L103.448 404.938L97.2076 404.938Z"/>
49
+ <path d="M130.748 405.718C127.988 405.718 125.473 405.008 123.203 403.588C120.933 402.168 119.123 400.263 117.773 397.873C116.423 395.483 115.748 392.838 115.748 389.938C115.748 387.018 116.423 384.358 117.773 381.958C119.123 379.558 120.933 377.648 123.203 376.228C125.473 374.808 127.988 374.098 130.748 374.098C133.508 374.098 136.023 374.808 138.293 376.228C140.563 377.648 142.373 379.558 143.723 381.958C145.073 384.358 145.748 387.018 145.748 389.938C145.748 392.838 145.073 395.483 143.723 397.873C142.373 400.263 140.563 402.168 138.293 403.588C136.023 405.008 133.508 405.718 130.748 405.718ZM130.748 399.478C132.408 399.478 133.898 399.038 135.218 398.158C136.538 397.278 137.583 396.113 138.353 394.663C139.123 393.213 139.508 391.638 139.508 389.938C139.508 388.198 139.113 386.598 138.323 385.138C137.533 383.678 136.478 382.513 135.158 381.643C133.838 380.773 132.368 380.338 130.748 380.338C129.108 380.338 127.623 380.778 126.293 381.658C124.963 382.538 123.913 383.708 123.143 385.168C122.373 386.628 121.988 388.218 121.988 389.938C121.988 391.718 122.388 393.328 123.188 394.768C123.988 396.208 125.053 397.353 126.383 398.203C127.713 399.053 129.168 399.478 130.748 399.478Z"/>
50
+ <path d="M163.688 404.938L157.478 404.938L147.428 374.938L153.548 374.938L160.628 395.998L167.648 374.938L173.948 374.938L180.968 395.998L188.018 374.938L194.138 374.938L184.118 404.938L177.848 404.938L170.768 383.728L163.688 404.938Z"/>
51
+ <path d="M207.608 405.508C205.208 405.368 202.968 404.728 200.888 403.588C198.808 402.448 197.348 400.938 196.508 399.058L201.818 396.778C202.218 397.598 203.028 398.363 204.248 399.073C205.468 399.783 206.798 400.138 208.238 400.138C209.618 400.138 210.863 399.828 211.973 399.208C213.083 398.588 213.638 397.678 213.638 396.478C213.638 395.598 213.343 394.918 212.753 394.438C212.163 393.958 211.428 393.583 210.548 393.313C209.668 393.043 208.788 392.788 207.908 392.548C205.928 392.108 204.128 391.503 202.508 390.733C200.888 389.963 199.598 388.973 198.638 387.763C197.678 386.553 197.198 385.068 197.198 383.308C197.198 381.388 197.718 379.723 198.758 378.313C199.798 376.903 201.158 375.818 202.838 375.058C204.518 374.298 206.328 373.918 208.268 373.918C210.668 373.918 212.868 374.428 214.868 375.448C216.868 376.468 218.398 377.868 219.458 379.648L214.508 382.588C214.048 381.688 213.278 380.933 212.198 380.323C211.118 379.713 209.978 379.378 208.778 379.318C207.218 379.258 205.873 379.543 204.743 380.173C203.613 380.803 203.048 381.798 203.048 383.158C203.048 384.038 203.358 384.698 203.978 385.138C204.598 385.578 205.383 385.923 206.333 386.173C207.283 386.423 208.258 386.708 209.258 387.028C211.038 387.608 212.708 388.298 214.268 389.098C215.828 389.898 217.088 390.888 218.048 392.068C219.008 393.248 219.468 394.688 219.428 396.388C219.428 398.268 218.863 399.913 217.733 401.323C216.603 402.733 215.138 403.813 213.338 404.563C211.538 405.313 209.628 405.628 207.608 405.508Z"/>
52
+ <path d="M237.398 405.718C234.638 405.718 232.123 405.008 229.853 403.588C227.583 402.168 225.773 400.263 224.423 397.873C223.073 395.483 222.398 392.838 222.398 389.938C222.398 387.018 223.073 384.358 224.423 381.958C225.773 379.558 227.583 377.648 229.853 376.228C232.123 374.808 234.638 374.098 237.398 374.098C239.698 374.098 241.808 374.573 243.728 375.523C245.648 376.473 247.288 377.783 248.648 379.453C250.008 381.123 251.008 383.043 251.648 385.213C252.288 387.383 252.478 389.688 252.218 392.128L229.088 392.128C229.468 394.208 230.403 395.948 231.893 397.348C233.383 398.748 235.218 399.458 237.398 399.478C238.918 399.478 240.303 399.098 241.553 398.338C242.803 397.578 243.828 396.528 244.628 395.188L250.958 396.658C249.758 399.318 247.948 401.493 245.528 403.183C243.108 404.873 240.398 405.718 237.398 405.718ZM228.878 387.358L245.918 387.358C245.718 385.938 245.213 384.638 244.403 383.458C243.593 382.278 242.583 381.343 241.373 380.653C240.163 379.963 238.838 379.618 237.398 379.618C235.238 379.618 233.368 380.363 231.788 381.853C230.208 383.343 229.238 385.178 228.878 387.358Z"/>
53
+ <path d="M273.038 404.938L273.038 362.938L279.278 362.938L279.278 404.938L273.038 404.938Z"/>
54
+ <path d="M287.378 404.938L287.378 374.938L293.618 374.938L293.618 378.118C294.658 376.878 295.933 375.898 297.443 375.178C298.953 374.458 300.588 374.098 302.348 374.098C304.488 374.098 306.458 374.613 308.258 375.643C310.058 376.673 311.488 378.038 312.548 379.738C313.588 378.038 314.998 376.673 316.778 375.643C318.558 374.613 320.528 374.098 322.688 374.098C324.888 374.098 326.898 374.638 328.718 375.718C330.538 376.798 331.988 378.248 333.068 380.068C334.148 381.888 334.688 383.898 334.688 386.098L334.688 404.938L328.448 404.938L328.448 387.718C328.448 386.398 328.128 385.183 327.488 384.073C326.848 382.963 325.988 382.073 324.908 381.403C323.828 380.733 322.628 380.398 321.308 380.398C319.328 380.398 317.643 381.098 316.253 382.498C314.863 383.898 314.168 385.638 314.168 387.718L314.168 404.938L307.928 404.938L307.928 387.718C307.928 385.638 307.228 383.898 305.828 382.498C304.428 381.098 302.738 380.398 300.758 380.398C299.458 380.398 298.263 380.733 297.173 381.403C296.083 382.073 295.218 382.963 294.578 384.073C293.938 385.183 293.618 386.398 293.618 387.718L293.618 404.938L287.378 404.938Z"/>
55
+ <path d="M363.428 374.938L369.668 374.938L369.668 404.938L363.398 404.938L363.158 400.588C362.278 402.128 361.093 403.368 359.603 404.308C358.113 405.248 356.338 405.718 354.278 405.718C352.078 405.718 350.008 405.303 348.068 404.473C346.128 403.643 344.428 402.493 342.968 401.023C341.508 399.553 340.363 397.848 339.533 395.908C338.703 393.968 338.288 391.888 338.288 389.668C338.288 386.808 338.983 384.198 340.373 381.838C341.763 379.478 343.628 377.598 345.968 376.198C348.308 374.798 350.908 374.098 353.768 374.098C355.948 374.098 357.873 374.593 359.543 375.583C361.213 376.573 362.598 377.838 363.698 379.378L363.428 374.938ZM354.128 399.688C355.888 399.688 357.428 399.253 358.748 398.383C360.068 397.513 361.093 396.338 361.823 394.858C362.553 393.378 362.918 391.738 362.918 389.938C362.918 388.118 362.548 386.468 361.808 384.988C361.068 383.508 360.038 382.333 358.718 381.463C357.398 380.593 355.868 380.158 354.128 380.158C352.368 380.158 350.778 380.598 349.358 381.478C347.938 382.358 346.803 383.538 345.953 385.018C345.103 386.498 344.678 388.138 344.678 389.938C344.678 391.758 345.113 393.408 345.983 394.888C346.853 396.368 348.003 397.538 349.433 398.398C350.863 399.258 352.428 399.688 354.128 399.688Z"/>
56
+ <path d="M397.898 374.938L404.138 374.938L404.138 405.718C404.138 408.478 403.393 410.928 401.903 413.068C400.413 415.208 398.458 416.888 396.038 418.108C393.618 419.328 391.018 419.938 388.238 419.938C386.138 419.938 384.123 419.493 382.193 418.603C380.263 417.713 378.558 416.508 377.078 414.988C375.598 413.468 374.498 411.748 373.778 409.828L379.508 407.248C380.088 409.108 381.213 410.643 382.883 411.853C384.553 413.063 386.338 413.668 388.238 413.668C389.878 413.668 391.433 413.328 392.903 412.648C394.373 411.968 395.573 411.033 396.503 409.843C397.433 408.653 397.898 407.278 397.898 405.718L397.898 400.648C396.918 402.148 395.653 403.368 394.103 404.308C392.553 405.248 390.768 405.718 388.748 405.718C385.908 405.718 383.313 405.008 380.963 403.588C378.613 402.168 376.743 400.263 375.353 397.873C373.963 395.483 373.268 392.838 373.268 389.938C373.268 387.038 373.963 384.393 375.353 382.003C376.743 379.613 378.613 377.703 380.963 376.273C383.313 374.843 385.908 374.128 388.748 374.128C390.768 374.128 392.553 374.598 394.103 375.538C395.653 376.478 396.918 377.708 397.898 379.228L397.898 374.938ZM388.838 399.688C390.538 399.688 392.043 399.243 393.353 398.353C394.663 397.463 395.678 396.278 396.398 394.798C397.118 393.318 397.478 391.698 397.478 389.938C397.478 388.158 397.113 386.528 396.383 385.048C395.653 383.568 394.638 382.383 393.338 381.493C392.038 380.603 390.538 380.158 388.838 380.158C387.138 380.158 385.593 380.598 384.203 381.478C382.813 382.358 381.708 383.538 380.888 385.018C380.068 386.498 379.658 388.138 379.658 389.938C379.658 391.738 380.073 393.378 380.903 394.858C381.733 396.338 382.843 397.513 384.233 398.383C385.623 399.253 387.158 399.688 388.838 399.688Z"/>
57
+ <path d="M423.998 405.718C421.238 405.718 418.723 405.008 416.453 403.588C414.183 402.168 412.373 400.263 411.023 397.873C409.673 395.483 408.998 392.838 408.998 389.938C408.998 387.018 409.673 384.358 411.023 381.958C412.373 379.558 414.183 377.648 416.453 376.228C418.723 374.808 421.238 374.098 423.998 374.098C426.298 374.098 428.408 374.573 430.328 375.523C432.248 376.473 433.888 377.783 435.248 379.453C436.608 381.123 437.608 383.043 438.248 385.213C438.888 387.383 439.078 389.688 438.818 392.128L415.688 392.128C416.068 394.208 417.003 395.948 418.493 397.348C419.983 398.748 421.818 399.458 423.998 399.478C425.518 399.478 426.903 399.098 428.153 398.338C429.403 397.578 430.428 396.528 431.228 395.188L437.558 396.658C436.358 399.318 434.548 401.493 432.128 403.183C429.708 404.873 426.998 405.718 423.998 405.718ZM415.478 387.358L432.518 387.358C432.318 385.938 431.813 384.638 431.003 383.458C430.193 382.278 429.183 381.343 427.973 380.653C426.763 379.963 425.438 379.618 423.998 379.618C421.838 379.618 419.968 380.363 418.388 381.853C416.808 383.343 415.838 385.178 415.478 387.358Z"/>
58
+ </g>
59
+ </g>
60
+ </svg>
@@ -0,0 +1,15 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Croppable</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+
8
+ <%= stylesheet_link_tag "croppable/application", media: "all" %>
9
+ </head>
10
+ <body>
11
+
12
+ <%= yield %>
13
+
14
+ </body>
15
+ </html>
data/config/routes.rb ADDED
@@ -0,0 +1,2 @@
1
+ Croppable::Engine.routes.draw do
2
+ end
@@ -0,0 +1,14 @@
1
+ class CreateCroppableData < ActiveRecord::Migration[7.0]
2
+ def change
3
+ create_table :croppable_data do |t|
4
+ t.references :croppable, polymorphic: true
5
+ t.string :name
6
+ t.float :scale
7
+ t.integer :x
8
+ t.integer :y
9
+ t.string :background_color
10
+
11
+ t.timestamps
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,46 @@
1
+ require 'open-uri'
2
+ require 'vips'
3
+
4
+ module Croppable
5
+
6
+ class Crop
7
+ def initialize(model, attr_name)
8
+ @model = model
9
+ @attr_name = attr_name
10
+ @data = model.send("#{attr_name}_croppable_data")
11
+ @setup = model.send("#{attr_name}_croppable_setup")
12
+ original = model.send("#{attr_name}_original")
13
+ @url = Rails.application.routes.url_helpers.rails_blob_url(original, host: Rails.application.config.asset_host)
14
+ end
15
+
16
+ def perform()
17
+ file = URI(@url).open
18
+ vips_img = Vips::Image.new_from_file(file.path)
19
+
20
+ height = vips_img.height
21
+ width = vips_img.width
22
+
23
+ x = (width - (width * @data.scale)) / 2 + @data.x
24
+ y = (height - (height * @data.scale)) / 2 + @data.y
25
+
26
+ background = @data.background_color.remove("#").scan(/\w{2}/).map {|color| color.to_i(16) }
27
+ background_embed = background.dup
28
+ background_embed << 255 if vips_img.bands == 4
29
+
30
+ vips_img = vips_img.resize(@data.scale * @setup[:resolution])
31
+ vips_img = vips_img.embed(
32
+ x * @setup[:resolution],
33
+ y * @setup[:resolution],
34
+ @setup[:width] * @setup[:resolution],
35
+ @setup[:height] * @setup[:resolution],
36
+ background: background_embed
37
+ )
38
+
39
+ path = Tempfile.new('cropped').path + ".jpg"
40
+
41
+ vips_img.write_to_file(path, background: background, Q: 100)
42
+
43
+ @model.send("#{ @attr_name }_cropped").attach(io: File.open(path), filename: "cropped")
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,27 @@
1
+ require "croppable/model"
2
+ require "croppable/param"
3
+
4
+ module Croppable
5
+ class Engine < ::Rails::Engine
6
+ isolate_namespace Croppable
7
+
8
+ ActiveSupport.on_load(:active_record) do
9
+ include Croppable::Model
10
+ end
11
+
12
+ ActiveSupport.on_load(:action_controller_base) do
13
+ helper Croppable::Engine.helpers
14
+
15
+ include CleanCroppableParams
16
+ end
17
+
18
+ initializer "croppable.assets.precompile" do
19
+ config.after_initialize do |app|
20
+ if app.config.respond_to?(:assets)
21
+ app.config.assets.precompile += %w( croppable.js croppable.css )
22
+ end
23
+ end
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,57 @@
1
+ module Croppable
2
+ module Model
3
+ extend ActiveSupport::Concern
4
+
5
+ # High-resolution displays, which are a large part of the market, need twice
6
+ # the pixels to look professional.
7
+ DEFAULT_RESOLUTION = 2
8
+
9
+ class_methods do
10
+ def has_croppable(name, width:, height:, resolution: DEFAULT_RESOLUTION)
11
+ has_one_attached :"#{ name }_cropped"
12
+ has_one_attached :"#{ name }_original"
13
+
14
+ has_one :"#{ name }_croppable_data", -> { where(name: name) },
15
+ as: :croppable, inverse_of: :croppable, dependent: :destroy, class_name: "Croppable::Datum"
16
+
17
+ after_commit if: -> { to_crop_croppable[name] } do
18
+ Croppable::CropImageJob.perform_later(self, name)
19
+ end
20
+
21
+ generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1
22
+ def #{ name }_croppable_setup
23
+ {width: #{ width }, height: #{ height }, resolution: #{ resolution }}
24
+ end
25
+
26
+ def to_crop_croppable
27
+ @to_crop_croppable ||= Hash.new
28
+ end
29
+
30
+ def #{ name }
31
+ self.#{ name }_cropped
32
+ end
33
+
34
+ def #{ name }=(croppable_param)
35
+ if croppable_param.delete
36
+ self.#{ name }_original = nil
37
+ self.#{ name }_cropped = nil
38
+ self.#{ name }_croppable_data = nil
39
+ else
40
+ self.#{ name }_original = croppable_param.image if croppable_param.image
41
+
42
+ if self.#{ name }_original.present?
43
+ if self.#{ name }_croppable_data
44
+ self.#{ name }_croppable_data.update(croppable_param.data)
45
+ else
46
+ self.#{ name }_croppable_data = Croppable::Datum.new(croppable_param.data.merge(name: "#{ name }"))
47
+ end
48
+
49
+ to_crop_croppable[:#{ name }] = self.#{ name }_croppable_data.updated_at_previously_changed? || self.#{ name }_croppable_data.new_record?
50
+ end
51
+ end
52
+ end
53
+ CODE
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,16 @@
1
+ module Croppable
2
+ class Param
3
+ attr_accessor :image, :data, :delete
4
+
5
+ def initialize(image, data, delete: false)
6
+ @image = image
7
+ @delete = delete
8
+ @data = {
9
+ x: data[:x],
10
+ y: data[:y],
11
+ scale: data[:scale],
12
+ background_color: data[:background_color]
13
+ }
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,3 @@
1
+ module Croppable
2
+ VERSION = "0.1.0"
3
+ end
data/lib/croppable.rb ADDED
@@ -0,0 +1,6 @@
1
+ require "croppable/version"
2
+ require "croppable/engine"
3
+
4
+ module Croppable
5
+ # Your code goes here...
6
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "json"
5
+
6
+ module Croppable
7
+ module Generators
8
+ class InstallGenerator < ::Rails::Generators::Base
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ def install_javascript_dependencies
12
+ destination = Pathname(destination_root)
13
+
14
+ if Pathname(destination_root).join("package.json").exist?
15
+ say "Installing JavaScript dependencies", :green
16
+ run "yarn add cropperjs@next"
17
+ end
18
+
19
+ if (importmap_path = destination.join("config/importmap.rb")).exist?
20
+ say "Installing JavaScript dependencies", :green
21
+ run "bin/importmap pin cropperjs@next"
22
+ end
23
+ end
24
+
25
+ def append_javascript_dependencies
26
+ destination = Pathname(destination_root)
27
+
28
+ if (application_javascript_path = destination.join("app/javascript/application.js")).exist?
29
+ insert_into_file application_javascript_path.to_s, %(\nimport "croppable"\n)
30
+ else
31
+ say <<~INSTRUCTIONS, :green
32
+ You must import the croppable JavaScript module in your application entrypoint.
33
+ INSTRUCTIONS
34
+ end
35
+
36
+ if (importmap_path = destination.join("config/importmap.rb")).exist?
37
+ append_to_file importmap_path.to_s, %(pin "croppable"\n)
38
+ end
39
+ end
40
+
41
+ def append_css_dependencies
42
+ destination = Pathname(destination_root)
43
+
44
+ if (stylesheet = destination.join("app/assets/stylesheets/application.css")).exist?
45
+ insert_into_file stylesheet, %( *= require croppable\n), before: " *= require_self"
46
+ else
47
+ say <<~INSTRUCTIONS, :green
48
+ To use the Croppable gem, you must import 'croppable' in your base stylesheet.
49
+ INSTRUCTIONS
50
+ end
51
+ end
52
+
53
+ def install_active_storage
54
+ rails_command "active_storage:install", inline: true
55
+ end
56
+
57
+ def create_migrations
58
+ rails_command "croppable:install:migrations", inline: true
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,4 @@
1
+ desc "Setup croppable"
2
+ task "croppable:install" do
3
+ Rails::Command.invoke :generate, ["croppable:install"]
4
+ end
metadata ADDED
@@ -0,0 +1,102 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: croppable
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Steven Barragán Naranjo
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-03-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '7.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '7.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: image_processing
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.2'
41
+ description:
42
+ email:
43
+ - me@steven.mx
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - MIT-LICENSE
49
+ - README.md
50
+ - Rakefile
51
+ - app/assets/config/croppable_manifest.js
52
+ - app/assets/javascripts/croppable.js
53
+ - app/assets/stylesheets/croppable.css
54
+ - app/assets/stylesheets/croppable/application.css
55
+ - app/controllers/concerns/clean_croppable_params.rb
56
+ - app/controllers/croppable/application_controller.rb
57
+ - app/helpers/croppable/application_helper.rb
58
+ - app/helpers/croppable/tag_helper.rb
59
+ - app/jobs/croppable/application_job.rb
60
+ - app/jobs/croppable/crop_image_job.rb
61
+ - app/mailers/croppable/application_mailer.rb
62
+ - app/models/croppable/application_record.rb
63
+ - app/models/croppable/datum.rb
64
+ - app/views/croppable/_tag.erb
65
+ - app/views/croppable/_uploader.erb
66
+ - app/views/layouts/croppable/application.html.erb
67
+ - config/routes.rb
68
+ - db/migrate/20230303170333_create_croppable_data.rb
69
+ - lib/croppable.rb
70
+ - lib/croppable/crop.rb
71
+ - lib/croppable/engine.rb
72
+ - lib/croppable/model.rb
73
+ - lib/croppable/param.rb
74
+ - lib/croppable/version.rb
75
+ - lib/generators/croppable/install/install_generator.rb
76
+ - lib/tasks/croppable_tasks.rake
77
+ homepage: https://github.com/stevenbarragan/croppable
78
+ licenses:
79
+ - MIT
80
+ metadata:
81
+ homepage_uri: https://github.com/stevenbarragan/croppable
82
+ source_code_uri: https://github.com/stevenbarragan/croppable
83
+ post_install_message:
84
+ rdoc_options: []
85
+ require_paths:
86
+ - lib
87
+ required_ruby_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ required_rubygems_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ requirements: []
98
+ rubygems_version: 3.4.1
99
+ signing_key:
100
+ specification_version: 4
101
+ summary: Easily crop images in Ruby on Rails with Cropper.js integration
102
+ test_files: []