sirv-image 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/sirv_client.rb +282 -0
  3. metadata +63 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d6c538246d67f50f35c56a61bd2ef2d14e28630d44c3253589e48a5cca03d776
4
+ data.tar.gz: f1c701e3610a77fc660161454647a2c382b640fa7d75da2b6dfe923fea146b9e
5
+ SHA512:
6
+ metadata.gz: 7cfc5e4b1bee19c6b8e958d7731f13fb9adb76b83113d3ecc755069667deda87085a4ca5e2900d2d62de9cd911e185e6bbe0a593af5c349d7049051c6aefcb78
7
+ data.tar.gz: 847db3c1eafd0ee31b1aa3d59ff238aa0baa720581fcf234f621d533789a47ff9877a15a84faeceae51b17d2e4bfb1e713bd8a89db24889f428dd05b8a5aac8f
@@ -0,0 +1,282 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+
5
+ module Sirv
6
+ # SDK for building Sirv URLs and HTML tags for images, spins, videos, 3D models, and galleries.
7
+ #
8
+ # @example
9
+ # sirv = Sirv::SirvClient.new(domain: 'demo.sirv.com', defaults: { q: 80 })
10
+ # url = sirv.url('/image.jpg', w: 300, format: 'webp')
11
+ # html = sirv.image('/photo.jpg', alt: 'A photo')
12
+ #
13
+ class SirvClient
14
+ # Create a SirvClient instance.
15
+ #
16
+ # @param domain [String] Sirv domain (e.g. 'demo.sirv.com')
17
+ # @param defaults [Hash] Default query parameters merged into every URL
18
+ # @raise [ArgumentError] if domain is not provided
19
+ def initialize(domain:, defaults: {})
20
+ raise ArgumentError, "domain is required" if domain.nil? || domain.empty?
21
+
22
+ @domain = domain.gsub(%r{/+\z}, "")
23
+ @defaults = defaults
24
+ end
25
+
26
+ # Build a full Sirv URL.
27
+ #
28
+ # @param path [String] Asset path (e.g. '/image.jpg')
29
+ # @param params [Hash] Transformation parameters (nested hashes are flattened to dot-notation)
30
+ # @return [String]
31
+ def url(path, params = {})
32
+ normalized = path.start_with?("/") ? path : "/#{path}"
33
+ "https://#{@domain}#{normalized}#{build_query(params)}"
34
+ end
35
+
36
+ # Generate a srcset string for responsive images.
37
+ #
38
+ # Supports three modes:
39
+ # - Explicit widths: `widths: [320, 640, 960]`
40
+ # - Auto-range: `min_width: 200, max_width: 2000, tolerance: 0.15`
41
+ # - Device pixel ratios: `device_pixel_ratios: [1, 2, 3]` with variable quality
42
+ #
43
+ # @param path [String] Image path
44
+ # @param params [Hash] Transformation parameters
45
+ # @param widths [Array<Integer>, nil] Explicit list of widths
46
+ # @param min_width [Integer, nil] Minimum width for auto-generation
47
+ # @param max_width [Integer, nil] Maximum width for auto-generation
48
+ # @param tolerance [Float] Tolerance for auto-generating widths (0-1)
49
+ # @param device_pixel_ratios [Array<Numeric>, nil] DPR values (e.g. [1, 2, 3])
50
+ # @return [String]
51
+ def src_set(path, params = {}, widths: nil, min_width: nil, max_width: nil, tolerance: 0.15, device_pixel_ratios: nil)
52
+ if widths
53
+ return widths
54
+ .map { |w| "#{url(path, params.merge(w: w))} #{w}w" }
55
+ .join(", ")
56
+ end
57
+
58
+ if min_width && max_width
59
+ generated = generate_widths(min_width, max_width, tolerance)
60
+ return generated
61
+ .map { |w| "#{url(path, params.merge(w: w))} #{w}w" }
62
+ .join(", ")
63
+ end
64
+
65
+ if device_pixel_ratios
66
+ base_q = params[:q] || params["q"] || @defaults[:q] || @defaults["q"] || 80
67
+ return device_pixel_ratios
68
+ .map do |dpr|
69
+ q = dpr_quality(base_q, dpr)
70
+ dpr_params = params.merge(q: q)
71
+ dpr_params[:w] = params[:w] * dpr if params[:w]
72
+ dpr_params[:h] = params[:h] * dpr if params[:h]
73
+ "#{url(path, dpr_params)} #{dpr}x"
74
+ end
75
+ .join(", ")
76
+ end
77
+
78
+ ""
79
+ end
80
+
81
+ # Generate an <img> tag for a Sirv image.
82
+ #
83
+ # @param path [String] Image path
84
+ # @param transform [Hash, nil] Transformation parameters for the URL
85
+ # @param viewer [Hash, nil] Viewer options for data-options attribute
86
+ # @param alt [String, nil] Alt text
87
+ # @param class_name [String, nil] Additional CSS class(es)
88
+ # @return [String]
89
+ def image(path, transform: nil, viewer: nil, alt: nil, class_name: nil)
90
+ src = url(path, transform || {})
91
+ cls = class_name ? "Sirv #{class_name}" : "Sirv"
92
+ html = "<img class=\"#{cls}\" data-src=\"#{escape_attr(src)}\""
93
+ html += " alt=\"#{escape_attr(alt)}\"" unless alt.nil?
94
+ html += " data-options=\"#{escape_attr(serialize_viewer_options(viewer))}\"" if viewer
95
+ html += ">"
96
+ html
97
+ end
98
+
99
+ # Generate a <div> tag for a Sirv zoom viewer.
100
+ #
101
+ # @param path [String] Image path
102
+ # @param transform [Hash, nil] Transformation parameters
103
+ # @param viewer [Hash, nil] Viewer options
104
+ # @param class_name [String, nil] Additional CSS class(es)
105
+ # @return [String]
106
+ def zoom(path, transform: nil, viewer: nil, class_name: nil)
107
+ viewer_div(path, "zoom", transform: transform, viewer: viewer, class_name: class_name)
108
+ end
109
+
110
+ # Generate a <div> tag for a Sirv spin viewer.
111
+ #
112
+ # @param path [String] Path to .spin file
113
+ # @param transform [Hash, nil] Transformation parameters
114
+ # @param viewer [Hash, nil] Viewer options
115
+ # @param class_name [String, nil] Additional CSS class(es)
116
+ # @return [String]
117
+ def spin(path, transform: nil, viewer: nil, class_name: nil)
118
+ viewer_div(path, nil, transform: transform, viewer: viewer, class_name: class_name)
119
+ end
120
+
121
+ # Generate a <div> tag for a Sirv video.
122
+ #
123
+ # @param path [String] Video path
124
+ # @param transform [Hash, nil] Transformation parameters
125
+ # @param viewer [Hash, nil] Viewer options
126
+ # @param class_name [String, nil] Additional CSS class(es)
127
+ # @return [String]
128
+ def video(path, transform: nil, viewer: nil, class_name: nil)
129
+ viewer_div(path, nil, transform: transform, viewer: viewer, class_name: class_name)
130
+ end
131
+
132
+ # Generate a <div> tag for a Sirv 3D model viewer.
133
+ #
134
+ # @param path [String] Path to .glb file
135
+ # @param transform [Hash, nil] Transformation parameters
136
+ # @param viewer [Hash, nil] Viewer options
137
+ # @param class_name [String, nil] Additional CSS class(es)
138
+ # @return [String]
139
+ def model(path, transform: nil, viewer: nil, class_name: nil)
140
+ viewer_div(path, nil, transform: transform, viewer: viewer, class_name: class_name)
141
+ end
142
+
143
+ # Generate a gallery container with multiple assets.
144
+ #
145
+ # @param items [Array<Hash>] Gallery items, each with:
146
+ # - :src [String] Asset path
147
+ # - :type [String, nil] Asset type override (e.g. 'zoom', 'spin')
148
+ # - :transform [Hash, nil] Per-item transformation params
149
+ # - :viewer [Hash, nil] Per-item viewer options
150
+ # @param viewer [Hash, nil] Gallery-level viewer options
151
+ # @param class_name [String, nil] Additional CSS class(es) for the gallery container
152
+ # @return [String]
153
+ def gallery(items, viewer: nil, class_name: nil)
154
+ cls = class_name ? "Sirv #{class_name}" : "Sirv"
155
+ html = "<div class=\"#{cls}\""
156
+ html += " data-options=\"#{escape_attr(serialize_viewer_options(viewer))}\"" if viewer
157
+ html += ">"
158
+
159
+ items.each do |item|
160
+ src = url(item[:src], item[:transform] || {})
161
+ child = "<div data-src=\"#{escape_attr(src)}\""
162
+ child += " data-type=\"#{item[:type]}\"" if item[:type]
163
+ child += " data-options=\"#{escape_attr(serialize_viewer_options(item[:viewer]))}\"" if item[:viewer]
164
+ child += "></div>"
165
+ html += child
166
+ end
167
+
168
+ html += "</div>"
169
+ html
170
+ end
171
+
172
+ # Generate a <script> tag to load Sirv JS.
173
+ #
174
+ # @param modules [Array<String>, nil] Specific modules to load (e.g. ['spin', 'zoom'])
175
+ # @param async [Boolean] Whether to add async attribute (default: true)
176
+ # @return [String]
177
+ def script_tag(modules: nil, async: true)
178
+ filename = "sirv"
179
+ if modules && !modules.empty?
180
+ filename = "sirv.#{modules.join(".")}"
181
+ end
182
+ html = "<script src=\"https://scripts.sirv.com/sirvjs/v3/#{filename}.js\""
183
+ html += " async" if async
184
+ html += "></script>"
185
+ html
186
+ end
187
+
188
+ private
189
+
190
+ # Flatten a nested hash into dot-notation key-value pairs.
191
+ #
192
+ # @param obj [Hash]
193
+ # @param prefix [String]
194
+ # @return [Array<Array(String, String)>]
195
+ def flatten_params(obj, prefix = "")
196
+ entries = []
197
+ obj.each do |key, value|
198
+ full_key = prefix.empty? ? key.to_s : "#{prefix}.#{key}"
199
+ if value.is_a?(Hash)
200
+ entries.concat(flatten_params(value, full_key))
201
+ elsif !value.nil?
202
+ entries << [full_key, value.to_s]
203
+ end
204
+ end
205
+ entries
206
+ end
207
+
208
+ # Build a query string from merged defaults + params.
209
+ #
210
+ # @param params [Hash]
211
+ # @return [String]
212
+ def build_query(params = {})
213
+ merged = @defaults.merge(params)
214
+ entries = flatten_params(merged)
215
+ return "" if entries.empty?
216
+
217
+ "?" + entries.map { |k, v| "#{CGI.escape(k)}=#{CGI.escape(v)}" }.join("&")
218
+ end
219
+
220
+ # Calculate quality for a given DPR.
221
+ #
222
+ # @param base_q [Numeric] Base quality at 1x
223
+ # @param dpr [Numeric] Device pixel ratio
224
+ # @return [Integer]
225
+ def dpr_quality(base_q, dpr)
226
+ return base_q if dpr <= 1
227
+
228
+ (base_q * (0.75**(dpr - 1))).round
229
+ end
230
+
231
+ # Generate widths between min and max using a tolerance step.
232
+ #
233
+ # @param min [Numeric]
234
+ # @param max [Numeric]
235
+ # @param tolerance [Float]
236
+ # @return [Array<Integer>]
237
+ def generate_widths(min, max, tolerance)
238
+ widths = []
239
+ current = min.to_f
240
+ while current < max
241
+ widths << current.round
242
+ current *= 1 + tolerance * 2
243
+ end
244
+ widths << max.round
245
+ widths
246
+ end
247
+
248
+ # Serialize viewer options to semicolon-separated format.
249
+ #
250
+ # @param opts [Hash]
251
+ # @return [String]
252
+ def serialize_viewer_options(opts)
253
+ opts.map { |k, v| "#{k}:#{v}" }.join(";")
254
+ end
255
+
256
+ # Escape HTML attribute values.
257
+ #
258
+ # @param str [String]
259
+ # @return [String]
260
+ def escape_attr(str)
261
+ str.gsub("&", "&amp;").gsub('"', "&quot;").gsub("<", "&lt;").gsub(">", "&gt;")
262
+ end
263
+
264
+ # Internal helper to generate viewer div tags.
265
+ #
266
+ # @param path [String]
267
+ # @param type [String, nil] data-type value (e.g. 'zoom'), or nil to omit
268
+ # @param transform [Hash, nil]
269
+ # @param viewer [Hash, nil]
270
+ # @param class_name [String, nil]
271
+ # @return [String]
272
+ def viewer_div(path, type, transform: nil, viewer: nil, class_name: nil)
273
+ src = url(path, transform || {})
274
+ cls = class_name ? "Sirv #{class_name}" : "Sirv"
275
+ html = "<div class=\"#{cls}\" data-src=\"#{escape_attr(src)}\""
276
+ html += " data-type=\"#{type}\"" if type
277
+ html += " data-options=\"#{escape_attr(serialize_viewer_options(viewer))}\"" if viewer
278
+ html += "></div>"
279
+ html
280
+ end
281
+ end
282
+ end
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sirv-image
3
+ version: !ruby/object:Gem::Version
4
+ version: 2.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Sirv
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-03-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.0'
27
+ description: Official Ruby SDK for the Sirv dynamic imaging API. This SDK provides
28
+ a simple way to request any modified image (dimensions, format, quality, sharpen,
29
+ crop, watermark etc.) using the 100+ image transformation options in Sirv's image
30
+ optimization service.
31
+ email:
32
+ - support@sirv.com
33
+ executables: []
34
+ extensions: []
35
+ extra_rdoc_files: []
36
+ files:
37
+ - lib/sirv_client.rb
38
+ homepage: https://sirv.github.io/sirv-image-ruby/
39
+ licenses:
40
+ - MIT
41
+ metadata:
42
+ source_code_uri: https://github.com/sirv/sirv-image-ruby
43
+ bug_tracker_uri: https://github.com/sirv/sirv-image-ruby/issues
44
+ post_install_message:
45
+ rdoc_options: []
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 2.6.0
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ requirements: []
59
+ rubygems_version: 3.0.3.1
60
+ signing_key:
61
+ specification_version: 4
62
+ summary: Sirv image transformation Ruby SDK
63
+ test_files: []