smallpict 0.0.6 → 0.0.7
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -0
- data/README.md +44 -1
- data/lib/smallpict/client.rb +17 -3
- data/lib/smallpict/errors.rb +10 -1
- data/lib/smallpict/folder.rb +18 -0
- data/lib/smallpict/mode.rb +13 -0
- data/lib/smallpict/models/capacity.rb +48 -0
- data/lib/smallpict/models/job_status_result.rb +5 -1
- data/lib/smallpict/models/optimize_options.rb +28 -4
- data/lib/smallpict/models/optimize_result.rb +3 -1
- data/lib/smallpict/models/quota_response.rb +3 -1
- data/lib/smallpict/version.rb +1 -1
- data/lib/smallpict.rb +5 -2
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4c99d1d9f8d685017396f2c3e94e7715fbed0aa6a2e2c2315bf7dca11a1ec336
|
|
4
|
+
data.tar.gz: d87ea5708336c3600cc10d2e7bd11c219f0f355014eedeab0041accb30fc57fa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0ae9beee1528a5242fa7bede52a15186be036bce30f85456c03a6210bf5a6eaa7e533d73f54d3bd0fe54d1259e87286080cf1c79f04fdc489f8d72dd8b1b6bec
|
|
7
|
+
data.tar.gz: b7166c544f39637266ee9633a7cc7cdc4bf6912e89657da7fd3707744c3186411707499614a3c79047ec66e4a7c07c6cb9857c52a1a6d1fe171ce4af0dd6ddf6
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,31 @@ All notable changes to the `smallpict` gem will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.0.7] - 2026-10-31
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `SmallPict::Mode::AUTO` (`"auto"`, also in `Mode::ALL`): the server picks the quality per image from a visual score (CMP-07). Plans: WP Pro and Business, Velocity, Momentum and sandbox; a server without auto enabled answers `400 invalid_mode` (`ValidationError`).
|
|
14
|
+
- `mode:` option on `optimize` (`"lossy"` default, `"extreme"`, `"lossless"`), with constants `SmallPict::Mode::LOSSY`, `EXTREME`, `LOSSLESS` and the frozen list `SmallPict::Mode::ALL`. The server checks the mode against the plan: `403 mode_not_in_plan` raises `PermissionDeniedError` with `details["mode"]` and `details["allowed_modes"]`; `400 invalid_mode` raises `ValidationError`.
|
|
15
|
+
- `JobStatusResult#mode_applied` and `#quality_applied`, as reported by the server (`nil` for jobs created before modes existed).
|
|
16
|
+
- `folder:` option on `optimize`, sent as `options.folder` and omitted when unset. The server stores the file under that folder of your account and lower-cases the value; `OptimizeResult#folder` and `JobStatusResult#folder` return it as stored (`nil` without one). The client rejects empty segments (`a//b`, leading or trailing `/`), `..` segments and more than 3 segments with `ValidationError` before any request; the server checks the full format (`400 invalid_folder` raises `ValidationError` with the server's message).
|
|
17
|
+
- `purge_cdn(purge_type: "folder", folder: "...")` purges one upload folder. `folder:` is required for that type (`ValidationError` before any request when missing or malformed) and is sent only with it; `url` and `all` purges are unchanged.
|
|
18
|
+
- `QuotaResponse#capacity`: the read-only capacity of `GET /v1/quota` as `SmallPict::Models::Capacity` (`period_end`, `storage`, `cdn`, `booster` with `next_expiry`, `lapse`, `manage_url`). Members that do not apply to the plan are `nil`; `capacity` is `nil` when the server could not read it and on older servers that do not send it. Existing quota fields are unchanged. Capacity is bought only in the dashboard; the SDK exposes `manage_url` as a string and never opens or fetches it.
|
|
19
|
+
- `SmallPict::Error#upgrade_url` on every error: `details["upgrade_url"]` when it is a string (sent with `402` quota, storage and read-only errors), otherwise `nil`. `PermissionDeniedError#allowed_modes` returns `details["allowed_modes"]` of `403 mode_not_in_plan`, otherwise `nil`. Status-to-class mapping is unchanged.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- README: example of `mode: SmallPict::Mode::EXTREME` without `quality:`.
|
|
23
|
+
- `429 purge_rate_limited` (hourly limit: 10 `folder` and 2 `all` purges per hour) raises `RateLimitError` without automatic retries, since the limit does not reset within seconds. Other 429 responses are still retried as before.
|
|
24
|
+
- Errors raised as the base `SmallPict::Error` (statuses without a dedicated class, such as `409`) now carry the server's `error.code` in `#code` instead of `"INTERNAL_ERROR"`; `"INTERNAL_ERROR"` remains when the body has no code. Typed errors keep their fixed codes.
|
|
25
|
+
|
|
26
|
+
### Deprecated
|
|
27
|
+
- `lossless:` option on `optimize`. Use `mode: SmallPict::Mode::LOSSLESS` instead. `lossless: true` without `mode:` is sent as `mode: "lossless"`; `lossless: true` with a different `mode:` raises `ValidationError` before any request. Using it emits a `:deprecated` category warning (shown when `Warning[:deprecated]` is enabled). It will not be removed before the next major version.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
- `quality:` is no longer sent when unset (its default is now `nil` instead of `80`), so the server picks the default per mode: lossy stays at 80, and `extreme` now gets its per-codec default instead of a lossy-sized Q80 result. A quality you set is still sent. `OptimizeOptions#quality` returns `nil` when unset.
|
|
31
|
+
- `lossless: true` used to be ignored by the server; it is now honoured (sent as `mode: "lossless"`). The SDK no longer sends `lossless` on the wire.
|
|
32
|
+
|
|
8
33
|
## [0.0.2] - 2026-08-22
|
|
9
34
|
|
|
10
35
|
### Added
|
data/README.md
CHANGED
|
@@ -55,13 +55,27 @@ result = SmallPict.optimize(
|
|
|
55
55
|
image_data,
|
|
56
56
|
format: "avif",
|
|
57
57
|
quality: 80,
|
|
58
|
-
max_width: 1920
|
|
58
|
+
max_width: 1920,
|
|
59
|
+
mode: SmallPict::Mode::LOSSY, # LOSSY (default) | EXTREME | LOSSLESS | AUTO; replaces the deprecated `lossless:`
|
|
60
|
+
folder: "products/shoes" # optional, 1-3 segments; stored lower case, returned as result.folder
|
|
59
61
|
)
|
|
60
62
|
|
|
61
63
|
puts "Optimized CDN URL: #{result.url}"
|
|
62
64
|
puts "Saved: #{result.savings_percentage}% (#{result.bytes_saved} bytes)"
|
|
63
65
|
```
|
|
64
66
|
|
|
67
|
+
`AUTO` picks, per image, the lowest quality that keeps the visual score (SSIMULACRA 2) at or above 70, and ignores `quality`. It is on WP Pro and Business, Velocity, Momentum and sandbox keys (5 auto jobs per minute per sandbox key); where it cannot search (PNG or GIF output, animation, time limit) the job is lossy and `mode_applied` says so.
|
|
68
|
+
|
|
69
|
+
`EXTREME` gives the smallest files on every plan. Leave `quality` out so the server uses its default for the output format; a quality you set is kept or raised to the format floor. Mode comparison: https://smallpict.app/en/docs/image-settings
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
result = SmallPict.optimize(
|
|
73
|
+
File.binread("hero-banner.png"),
|
|
74
|
+
format: "webp",
|
|
75
|
+
mode: SmallPict::Mode::EXTREME # no quality: the server picks it per format
|
|
76
|
+
)
|
|
77
|
+
```
|
|
78
|
+
|
|
65
79
|
### 2. Rails Initializer (`config/initializers/smallpict.rb`)
|
|
66
80
|
|
|
67
81
|
```ruby
|
|
@@ -106,8 +120,37 @@ puts "Plan: #{quota.plan}, Quota Used: #{quota.quota_percentage}%"
|
|
|
106
120
|
# 2. Invalidate CDN cache for updated images
|
|
107
121
|
purge = SmallPict.purge_cdn(["https://cdn.smallpict.app/opt/hero-banner.avif"])
|
|
108
122
|
puts purge.message
|
|
123
|
+
|
|
124
|
+
# 3. Purge one upload folder, or all of your files
|
|
125
|
+
SmallPict.purge_cdn(purge_type: "folder", folder: "products/shoes")
|
|
126
|
+
SmallPict.purge_cdn(purge_type: "all")
|
|
127
|
+
# Folder purges are limited to 10 per hour and "all" to 2 per hour: one more raises
|
|
128
|
+
# SmallPict::RateLimitError (429 purge_rate_limited), which the client does not retry.
|
|
109
129
|
```
|
|
110
130
|
|
|
131
|
+
### Capacity and upgrade links
|
|
132
|
+
|
|
133
|
+
`get_quota` also returns the read-only `capacity` of the key's product. Members that do not apply to your plan are `nil`, and `capacity` itself is `nil` when the server could not read it or does not send it.
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
cap = SmallPict.get_quota.capacity
|
|
137
|
+
if cap
|
|
138
|
+
puts "Storage: #{cap.storage.used_bytes}/#{cap.storage.limit_bytes}" if cap.storage
|
|
139
|
+
puts "CDN status: #{cap.cdn.status}" if cap.cdn
|
|
140
|
+
puts "Account lapsed (#{cap.lapse.stage}), manage it at #{cap.manage_url}" if cap.lapse
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
begin
|
|
144
|
+
SmallPict.optimize("photo.jpg")
|
|
145
|
+
rescue SmallPict::QuotaExceededError => e
|
|
146
|
+
puts "Add capacity at #{e.upgrade_url}" if e.upgrade_url
|
|
147
|
+
rescue SmallPict::PermissionDeniedError => e
|
|
148
|
+
puts "Allowed modes: #{e.allowed_modes.join(', ')}" if e.allowed_modes
|
|
149
|
+
end
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Capacity is bought only in the dashboard. `manage_url` and `upgrade_url` are plain strings to show your users; the SDK never opens or fetches them.
|
|
153
|
+
|
|
111
154
|
---
|
|
112
155
|
|
|
113
156
|
## 🧪 Testing
|
data/lib/smallpict/client.rb
CHANGED
|
@@ -68,12 +68,18 @@ module SmallPict
|
|
|
68
68
|
Models::QuotaResponse.new(res)
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
# purge_type: "url" (default), "folder" (with `folder:`) or "all".
|
|
72
|
+
def purge_cdn(urls = [], purge_type: "url", folder: nil)
|
|
72
73
|
url_list = urls.is_a?(Array) ? urls : [urls]
|
|
73
74
|
payload = {
|
|
74
75
|
purge_type: purge_type.to_s,
|
|
75
76
|
urls: url_list
|
|
76
77
|
}
|
|
78
|
+
if payload[:purge_type] == "folder"
|
|
79
|
+
raise ValidationError, "folder is required for purge_type \"folder\"" if folder.nil? || folder.to_s.empty?
|
|
80
|
+
|
|
81
|
+
payload[:folder] = Folder.validate!(folder)
|
|
82
|
+
end
|
|
77
83
|
|
|
78
84
|
res = request(:post, "/v1/purge", payload)
|
|
79
85
|
Models::PurgeResponse.new(res)
|
|
@@ -145,7 +151,7 @@ module SmallPict
|
|
|
145
151
|
request_id = response.headers["x-request-id"]
|
|
146
152
|
retry_after = response.headers["retry-after"]&.to_i
|
|
147
153
|
|
|
148
|
-
if status == 429 || (status >= 500 && status <= 504)
|
|
154
|
+
if (status == 429 && !hourly_purge_limit?(response.body)) || (status >= 500 && status <= 504)
|
|
149
155
|
if attempt <= @max_retries
|
|
150
156
|
delay = base_delay * (2**(attempt - 1))
|
|
151
157
|
delay = retry_after if retry_after && retry_after.positive?
|
|
@@ -184,6 +190,12 @@ module SmallPict
|
|
|
184
190
|
end
|
|
185
191
|
end
|
|
186
192
|
|
|
193
|
+
# 429 purge_rate_limited is an hourly limit: retrying within seconds cannot succeed.
|
|
194
|
+
def hourly_purge_limit?(body_str)
|
|
195
|
+
body = parse_body(body_str)
|
|
196
|
+
body.is_a?(Hash) && body["error"].is_a?(Hash) && body["error"]["code"] == "purge_rate_limited"
|
|
197
|
+
end
|
|
198
|
+
|
|
187
199
|
def parse_body(body_str)
|
|
188
200
|
return {} if body_str.nil? || body_str.empty?
|
|
189
201
|
|
|
@@ -195,11 +207,13 @@ module SmallPict
|
|
|
195
207
|
def handle_error_response(status, body, request_id, retry_after)
|
|
196
208
|
message = "API request failed with HTTP #{status}"
|
|
197
209
|
details = nil
|
|
210
|
+
code = "INTERNAL_ERROR"
|
|
198
211
|
|
|
199
212
|
if body.is_a?(Hash)
|
|
200
213
|
if body["error"].is_a?(Hash)
|
|
201
214
|
message = body["error"]["message"] || message
|
|
202
215
|
details = body["error"]["details"]
|
|
216
|
+
code = body["error"]["code"] if body["error"]["code"].is_a?(String)
|
|
203
217
|
elsif body["error"].is_a?(String)
|
|
204
218
|
message = body["error"]
|
|
205
219
|
elsif body["message"].is_a?(String)
|
|
@@ -219,7 +233,7 @@ module SmallPict
|
|
|
219
233
|
raise ServerError.new(message, status_code: status, request_id: request_id, details: details)
|
|
220
234
|
end
|
|
221
235
|
|
|
222
|
-
raise Error.new(message, status_code: status, request_id: request_id, details: details)
|
|
236
|
+
raise Error.new(message, code: code, status_code: status, request_id: request_id, details: details)
|
|
223
237
|
end
|
|
224
238
|
end
|
|
225
239
|
|
data/lib/smallpict/errors.rb
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module SmallPict
|
|
4
4
|
class Error < StandardError
|
|
5
|
-
|
|
5
|
+
# upgrade_url: dashboard page where the missing capacity is added
|
|
6
|
+
# (details.upgrade_url), or nil. The SDK never opens or fetches it.
|
|
7
|
+
attr_reader :code, :status_code, :request_id, :details, :upgrade_url
|
|
6
8
|
|
|
7
9
|
def initialize(message = "", code: "INTERNAL_ERROR", status_code: nil, request_id: nil, details: nil)
|
|
8
10
|
@raw_message = self.class.sanitize(message)
|
|
@@ -10,6 +12,8 @@ module SmallPict
|
|
|
10
12
|
@status_code = status_code
|
|
11
13
|
@request_id = request_id
|
|
12
14
|
@details = details
|
|
15
|
+
url = details.is_a?(Hash) ? (details["upgrade_url"] || details[:upgrade_url]) : nil
|
|
16
|
+
@upgrade_url = url.is_a?(String) ? url : nil
|
|
13
17
|
super(@raw_message)
|
|
14
18
|
end
|
|
15
19
|
|
|
@@ -58,8 +62,13 @@ module SmallPict
|
|
|
58
62
|
end
|
|
59
63
|
|
|
60
64
|
class PermissionDeniedError < Error
|
|
65
|
+
# Modes the plan allows, from details.allowed_modes of 403 mode_not_in_plan; otherwise nil.
|
|
66
|
+
attr_reader :allowed_modes
|
|
67
|
+
|
|
61
68
|
def initialize(message = "Permission denied for this resource", request_id: nil, details: nil)
|
|
62
69
|
super(message, code: "FORBIDDEN", status_code: 403, request_id: request_id, details: details)
|
|
70
|
+
modes = details.is_a?(Hash) ? (details["allowed_modes"] || details[:allowed_modes]) : nil
|
|
71
|
+
@allowed_modes = modes.is_a?(Array) ? modes.grep(String) : nil
|
|
63
72
|
end
|
|
64
73
|
end
|
|
65
74
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SmallPict
|
|
4
|
+
# Light client-side check of an upload folder. The server is the authority
|
|
5
|
+
# (400 invalid_folder): it lower-cases the value and enforces the full format.
|
|
6
|
+
module Folder
|
|
7
|
+
MAX_SEGMENTS = 3
|
|
8
|
+
|
|
9
|
+
def self.validate!(folder)
|
|
10
|
+
segments = folder.to_s.split("/", -1)
|
|
11
|
+
if segments.empty? || segments.any?(&:empty?) || segments.include?("..") || segments.size > MAX_SEGMENTS
|
|
12
|
+
raise ValidationError, "Invalid folder #{folder.inspect}: use 1-#{MAX_SEGMENTS} non-empty segments separated by '/', without '..'."
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
folder.to_s
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SmallPict
|
|
4
|
+
# Compression modes for `optimize(mode: ...)`. The server checks the mode
|
|
5
|
+
# against the plan (403 mode_not_in_plan); the SDK does not.
|
|
6
|
+
module Mode
|
|
7
|
+
LOSSY = "lossy"
|
|
8
|
+
EXTREME = "extreme"
|
|
9
|
+
LOSSLESS = "lossless"
|
|
10
|
+
AUTO = "auto" # the server picks the quality per image from a visual score (CMP-07)
|
|
11
|
+
ALL = [LOSSY, EXTREME, LOSSLESS, AUTO].freeze
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SmallPict
|
|
4
|
+
module Models
|
|
5
|
+
CapacityStorage = Data.define(:limit_bytes, :used_bytes, :base_bytes, :addon_blocks)
|
|
6
|
+
CapacityCdn = Data.define(:limit_bytes, :used_bytes, :status)
|
|
7
|
+
CapacityBooster = Data.define(:active, :wp_quota_bytes, :api_transforms, :cdn_bytes, :next_expiry)
|
|
8
|
+
CapacityExpiry = Data.define(:kind, :units, :expires_at)
|
|
9
|
+
CapacityLapse = Data.define(:stage, :delete_after)
|
|
10
|
+
|
|
11
|
+
# Read-only capacity of the key's product from GET /v1/quota. A member that
|
|
12
|
+
# does not apply to the product or plan is nil. manage_url is a plain string:
|
|
13
|
+
# the SDK never opens or fetches it; capacity is bought only in the dashboard.
|
|
14
|
+
Capacity = Data.define(:period_end, :storage, :cdn, :booster, :lapse, :manage_url) do
|
|
15
|
+
def self.from_hash(data)
|
|
16
|
+
return nil unless data.is_a?(Hash)
|
|
17
|
+
|
|
18
|
+
s = get(data, :storage)
|
|
19
|
+
cd = get(data, :cdn)
|
|
20
|
+
b = get(data, :booster) || {}
|
|
21
|
+
e = get(b, :next_expiry)
|
|
22
|
+
l = get(data, :lapse)
|
|
23
|
+
new(
|
|
24
|
+
period_end: get(data, :period_end),
|
|
25
|
+
storage: s && CapacityStorage.new(limit_bytes: get(s, :limit_bytes), used_bytes: get(s, :used_bytes),
|
|
26
|
+
base_bytes: get(s, :base_bytes), addon_blocks: get(s, :addon_blocks)),
|
|
27
|
+
cdn: cd && CapacityCdn.new(limit_bytes: get(cd, :limit_bytes), used_bytes: get(cd, :used_bytes),
|
|
28
|
+
status: get(cd, :status)),
|
|
29
|
+
booster: CapacityBooster.new(
|
|
30
|
+
active: get(b, :active) == true,
|
|
31
|
+
wp_quota_bytes: get(b, :wp_quota_bytes),
|
|
32
|
+
api_transforms: get(b, :api_transforms),
|
|
33
|
+
cdn_bytes: get(b, :cdn_bytes),
|
|
34
|
+
next_expiry: e && CapacityExpiry.new(kind: get(e, :kind), units: get(e, :units),
|
|
35
|
+
expires_at: get(e, :expires_at))
|
|
36
|
+
),
|
|
37
|
+
lapse: l && CapacityLapse.new(stage: get(l, :stage), delete_after: get(l, :delete_after)),
|
|
38
|
+
manage_url: get(data, :manage_url)
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# String keys from JSON, symbol keys from callers; keeps false and nil as sent.
|
|
43
|
+
def self.get(hash, key)
|
|
44
|
+
hash.is_a?(Hash) ? hash.fetch(key.to_s) { hash[key] } : nil
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -4,7 +4,7 @@ module SmallPict
|
|
|
4
4
|
module Models
|
|
5
5
|
class JobStatusResult
|
|
6
6
|
attr_reader :job_id, :status, :url, :format, :bytes_saved,
|
|
7
|
-
:error, :created_at, :updated_at
|
|
7
|
+
:error, :created_at, :updated_at, :mode_applied, :quality_applied, :folder
|
|
8
8
|
|
|
9
9
|
def initialize(attributes = {})
|
|
10
10
|
@job_id = attributes[:job_id] || attributes["job_id"]
|
|
@@ -15,6 +15,10 @@ module SmallPict
|
|
|
15
15
|
@error = attributes[:error] || attributes["error"]
|
|
16
16
|
@created_at = attributes[:created_at] || attributes["created_at"]
|
|
17
17
|
@updated_at = attributes[:updated_at] || attributes["updated_at"]
|
|
18
|
+
# Reported by the server; nil for jobs created before modes existed.
|
|
19
|
+
@mode_applied = attributes[:mode_applied] || attributes["mode_applied"]
|
|
20
|
+
@quality_applied = attributes[:quality_applied] || attributes["quality_applied"]
|
|
21
|
+
@folder = attributes[:folder] || attributes["folder"]
|
|
18
22
|
end
|
|
19
23
|
end
|
|
20
24
|
end
|
|
@@ -4,16 +4,22 @@ module SmallPict
|
|
|
4
4
|
module Models
|
|
5
5
|
class OptimizeOptions
|
|
6
6
|
attr_reader :format, :quality, :max_width, :max_height, :max_dimension, :fit,
|
|
7
|
-
:lossless, :strip_metadata, :filename, :mime_type, :content_type, :idempotency_key
|
|
7
|
+
:mode, :lossless, :folder, :strip_metadata, :filename, :mime_type, :content_type, :idempotency_key
|
|
8
8
|
|
|
9
|
+
# mode: SmallPict::Mode::LOSSY (server default when omitted), EXTREME or LOSSLESS.
|
|
10
|
+
# quality: optional 1-100; omitted when nil so the server picks the default per mode.
|
|
11
|
+
# folder: optional upload folder, e.g. "products/shoes"; the server lower-cases it.
|
|
12
|
+
# lossless: deprecated, use `mode: SmallPict::Mode::LOSSLESS`; removal not before the next major.
|
|
9
13
|
def initialize(
|
|
10
14
|
format: "auto",
|
|
11
|
-
quality:
|
|
15
|
+
quality: nil,
|
|
12
16
|
max_width: nil,
|
|
13
17
|
max_height: nil,
|
|
14
18
|
max_dimension: nil,
|
|
15
19
|
fit: "cover",
|
|
16
|
-
|
|
20
|
+
mode: nil,
|
|
21
|
+
lossless: nil,
|
|
22
|
+
folder: nil,
|
|
17
23
|
strip_metadata: true,
|
|
18
24
|
filename: nil,
|
|
19
25
|
mime_type: nil,
|
|
@@ -27,6 +33,8 @@ module SmallPict
|
|
|
27
33
|
@max_dimension = max_dimension
|
|
28
34
|
@fit = fit.to_s
|
|
29
35
|
@lossless = lossless
|
|
36
|
+
@mode = resolve_mode(mode, lossless)
|
|
37
|
+
@folder = folder.nil? ? nil : Folder.validate!(folder)
|
|
30
38
|
@strip_metadata = strip_metadata
|
|
31
39
|
@filename = filename
|
|
32
40
|
@mime_type = mime_type || content_type
|
|
@@ -42,10 +50,26 @@ module SmallPict
|
|
|
42
50
|
max_height: @max_height,
|
|
43
51
|
max_dimension: @max_dimension,
|
|
44
52
|
fit: @fit,
|
|
45
|
-
|
|
53
|
+
mode: @mode,
|
|
54
|
+
folder: @folder,
|
|
46
55
|
strip_metadata: @strip_metadata
|
|
47
56
|
}.compact
|
|
48
57
|
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
# `lossless:` is deprecated: map it to `mode:` and never send it on the wire.
|
|
62
|
+
def resolve_mode(mode, lossless)
|
|
63
|
+
mode = mode&.to_s
|
|
64
|
+
unless lossless.nil?
|
|
65
|
+
Kernel.warn("SmallPict: `lossless:` is deprecated, use `mode: SmallPict::Mode::LOSSLESS`.",
|
|
66
|
+
category: :deprecated)
|
|
67
|
+
end
|
|
68
|
+
return mode unless lossless == true
|
|
69
|
+
return Mode::LOSSLESS if mode.nil? || mode == Mode::LOSSLESS
|
|
70
|
+
|
|
71
|
+
raise ValidationError, "`lossless: true` contradicts `mode: #{mode.inspect}`; set only `mode:`."
|
|
72
|
+
end
|
|
49
73
|
end
|
|
50
74
|
end
|
|
51
75
|
end
|
|
@@ -5,7 +5,7 @@ module SmallPict
|
|
|
5
5
|
class OptimizeResult
|
|
6
6
|
attr_reader :job_id, :status, :url, :format, :original_size,
|
|
7
7
|
:compressed_size, :bytes_saved, :savings_percentage,
|
|
8
|
-
:upload_url, :data
|
|
8
|
+
:upload_url, :data, :folder
|
|
9
9
|
|
|
10
10
|
def initialize(attributes = {})
|
|
11
11
|
@job_id = attributes[:job_id] || attributes["job_id"] || "sync"
|
|
@@ -18,6 +18,8 @@ module SmallPict
|
|
|
18
18
|
@savings_percentage = (attributes[:savings_percentage] || attributes["savings_percentage"] || calculate_savings_percentage).to_f.round(2)
|
|
19
19
|
@upload_url = attributes[:upload_url] || attributes["upload_url"]
|
|
20
20
|
@data = attributes[:data]
|
|
21
|
+
# Normalized (lower case) by the server; nil without a folder.
|
|
22
|
+
@folder = attributes[:folder] || attributes["folder"]
|
|
21
23
|
end
|
|
22
24
|
|
|
23
25
|
private
|
|
@@ -5,7 +5,7 @@ module SmallPict
|
|
|
5
5
|
class QuotaResponse
|
|
6
6
|
attr_reader :plan, :bytes_used, :quota_limit, :quota_percentage,
|
|
7
7
|
:cdn_egress_used_bytes, :cdn_egress_quota_bytes,
|
|
8
|
-
:active_keys_count, :active_sites_count
|
|
8
|
+
:active_keys_count, :active_sites_count, :capacity
|
|
9
9
|
|
|
10
10
|
def initialize(attributes = {})
|
|
11
11
|
@plan = attributes[:plan] || attributes["plan"] || "free"
|
|
@@ -16,6 +16,8 @@ module SmallPict
|
|
|
16
16
|
@cdn_egress_quota_bytes = attributes[:cdn_egress_quota_bytes] || attributes["cdn_egress_quota_bytes"]
|
|
17
17
|
@active_keys_count = attributes[:active_keys_count] || attributes["active_keys_count"]
|
|
18
18
|
@active_sites_count = attributes[:active_sites_count] || attributes["active_sites_count"]
|
|
19
|
+
# nil when the server could not read capacity, and from older servers that omit it.
|
|
20
|
+
@capacity = Capacity.from_hash(attributes[:capacity] || attributes["capacity"])
|
|
19
21
|
end
|
|
20
22
|
|
|
21
23
|
private
|
data/lib/smallpict/version.rb
CHANGED
data/lib/smallpict.rb
CHANGED
|
@@ -4,8 +4,11 @@ require_relative "smallpict/version"
|
|
|
4
4
|
require_relative "smallpict/configuration"
|
|
5
5
|
require_relative "smallpict/crypto"
|
|
6
6
|
require_relative "smallpict/errors"
|
|
7
|
+
require_relative "smallpict/mode"
|
|
8
|
+
require_relative "smallpict/folder"
|
|
7
9
|
require_relative "smallpict/models/optimize_options"
|
|
8
10
|
require_relative "smallpict/models/optimize_result"
|
|
11
|
+
require_relative "smallpict/models/capacity"
|
|
9
12
|
require_relative "smallpict/models/quota_response"
|
|
10
13
|
require_relative "smallpict/models/purge_response"
|
|
11
14
|
require_relative "smallpict/models/job_status_result"
|
|
@@ -39,8 +42,8 @@ module SmallPict
|
|
|
39
42
|
client.get_quota
|
|
40
43
|
end
|
|
41
44
|
|
|
42
|
-
def purge_cdn(urls = [], purge_type: "url")
|
|
43
|
-
client.purge_cdn(urls, purge_type: purge_type)
|
|
45
|
+
def purge_cdn(urls = [], purge_type: "url", folder: nil)
|
|
46
|
+
client.purge_cdn(urls, purge_type: purge_type, folder: folder)
|
|
44
47
|
end
|
|
45
48
|
|
|
46
49
|
def validate_key
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: smallpict
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- SmallPict Engineering
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-11-02 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -98,6 +98,9 @@ files:
|
|
|
98
98
|
- lib/smallpict/configuration.rb
|
|
99
99
|
- lib/smallpict/crypto.rb
|
|
100
100
|
- lib/smallpict/errors.rb
|
|
101
|
+
- lib/smallpict/folder.rb
|
|
102
|
+
- lib/smallpict/mode.rb
|
|
103
|
+
- lib/smallpict/models/capacity.rb
|
|
101
104
|
- lib/smallpict/models/job_status_result.rb
|
|
102
105
|
- lib/smallpict/models/optimize_options.rb
|
|
103
106
|
- lib/smallpict/models/optimize_result.rb
|