pictify 1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 61ccf483d399d895aed8467dd41ba36464a70b237c370cd03eb4a14911602a17
4
+ data.tar.gz: 168e24019d0d4e03c56866316a1557e397ed3c643d570ba6a9a0df49ddd2f601
5
+ SHA512:
6
+ metadata.gz: 128158b9198f1a6388efad4a18152d0d563d9865b1ac306da1a280e8ad4e89882191cf912eb9a59d3ce4e1485e838861146de50a28ee55b2a52258dc1feb2c6e
7
+ data.tar.gz: f50e95a9dbb6e177021d512696be627a986f40e7b519712e2b994f2a920da6dae30832ccd6b8274dbb159e91308503c2de6d21ac4f94fd29a34535bfeb027d64
data/CHANGELOG.md ADDED
@@ -0,0 +1,59 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.0.0] - 2026-06-08
9
+
10
+ ### Changed (BREAKING)
11
+
12
+ - Re-pointed the entire SDK to the real Pictify API. The previous `/v1/render*`
13
+ endpoints did not exist.
14
+ - `render_html(html:, css:, width:, height:, selector:, format:)` now posts to
15
+ `POST /image` and returns an `ImageResult` (`url`, `id`, `created_at`). `css`
16
+ is injected as a `<style>` tag; `format` maps to `fileExtension`.
17
+ - `render_url(url:, ...)` added — screenshots a live URL via `POST /image`.
18
+ - `render(template_id:, variables:, format:, quality:, width:, height:, layout:, layouts:)`
19
+ posts to `POST /templates/:uid/render` and returns a `RenderResult` with a
20
+ `results` envelope; `result.url` returns the first result's URL.
21
+ - `render_layouts(template_id:, layouts:, ...)` now takes explicit layout names
22
+ (max 20); failed layouts are returned in `errors`.
23
+ - `render_gif(html:|url:|template_id:, variables:, width:, height:, quality:)`
24
+ posts to `POST /gif`, uses the `template` body key, accepts `quality`
25
+ (`:low`/`:medium`/`:high`), and flattens the `{ gif: {...} }` envelope into a
26
+ `GifRenderResult`.
27
+ - `render_batch(template_id:, variable_sets:, format:, quality:, concurrency:, layout:, layouts:)`
28
+ is now asynchronous — posts to `POST /templates/:uid/batch-render` and returns
29
+ `BatchRenderResult` (`batch_id`, `status`, `total_items`).
30
+ - `get_batch_results(batch_id)` added — polls `GET /templates/batch/:id/results`.
31
+ Per-item URLs are delivered via the `render.completed` webhook, not the poll.
32
+ - `get_template(template_id)` unwraps the `{ template }` envelope; templates are
33
+ keyed by `uid` with `variable_definitions`.
34
+ - `list_templates(page:, limit:, sort:)` returns `ListTemplatesResult`
35
+ (`templates`, `pagination`).
36
+ - `create_template(html:, name:, width:, height:, variable_definitions:, output_format:)`
37
+ added — `POST /templates`.
38
+ - Error mapping updated: 422 → `RenderError` (carries `errors`), 5xx →
39
+ `ServerError`, 429 with `quota_exceeded` → `QuotaExceededError`. Only 5xx and
40
+ network failures are retried; 4xx (including 429) are never retried.
41
+
42
+ ### Removed (BREAKING)
43
+
44
+ - `render_stream` — no endpoint streams raw image bytes.
45
+ - Legacy image fields (`image_url`, `render_id`, `device_scale_factor`,
46
+ `transparent`, `download`) and the frame-based GIF API.
47
+
48
+ ## [1.0.0] - 2026-01-23
49
+
50
+ ### Added
51
+
52
+ - Initial release
53
+ - `Pictify::Client` with render, render_html, render_gif, stream, batch, and template methods
54
+ - Comprehensive error handling with specific error types
55
+ - Automatic retry with exponential backoff
56
+ - Support for all image formats (PNG, JPG, JPEG, WebP, GIF, PDF)
57
+ - HTML render endpoint for rendering raw HTML directly
58
+ - GIF render endpoint for creating animated GIFs
59
+ - Rails and Sinatra integration examples
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pictify
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,284 @@
1
+ # Pictify Ruby SDK
2
+
3
+ Official Ruby SDK for [Pictify](https://pictify.io) — generate images, PDFs, and GIFs from raw HTML, live URLs, and reusable templates.
4
+
5
+ ## Installation
6
+
7
+ Add to your Gemfile:
8
+
9
+ ```ruby
10
+ gem "pictify"
11
+ ```
12
+
13
+ Then run:
14
+
15
+ ```bash
16
+ bundle install
17
+ ```
18
+
19
+ Or install directly:
20
+
21
+ ```bash
22
+ gem install pictify
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ ```ruby
28
+ require "pictify"
29
+
30
+ client = Pictify::Client.new(api_key: "your-api-key")
31
+
32
+ # Render raw HTML to a PNG
33
+ image = client.render_html(
34
+ html: "<div style='font-size:48px;padding:40px'>Hello World</div>",
35
+ width: 1200,
36
+ height: 630
37
+ )
38
+ puts image.url
39
+
40
+ # Render a template
41
+ result = client.render(
42
+ template_id: "your-template-uid",
43
+ variables: { name: "Ada", company: "Pictify" }
44
+ )
45
+ puts result.url
46
+ ```
47
+
48
+ ## Configuration
49
+
50
+ ```ruby
51
+ client = Pictify::Client.new(
52
+ api_key: "your-api-key",
53
+ base_url: "https://api.pictify.io", # Custom API URL
54
+ timeout: 30, # Request timeout in seconds
55
+ max_retries: 3 # Max retry attempts (5xx / network only)
56
+ )
57
+ ```
58
+
59
+ ## Rendering Images
60
+
61
+ ### From HTML — `POST /image`
62
+
63
+ ```ruby
64
+ image = client.render_html(
65
+ html: "<div>Hello</div>",
66
+ css: "div { color: blue; }", # injected as a <style> tag before rendering
67
+ width: 1200, # default 1280
68
+ height: 630, # default 720
69
+ selector: "#card", # crop to a specific element (optional)
70
+ format: :png # :png, :jpg, :jpeg, :webp, :pdf — mapped to fileExtension
71
+ )
72
+
73
+ puts image.url # CDN URL
74
+ puts image.id # image id
75
+ puts image.created_at # ISO timestamp
76
+ ```
77
+
78
+ ### From a live URL (screenshot) — `POST /image`
79
+
80
+ ```ruby
81
+ image = client.render_url(
82
+ url: "https://example.com",
83
+ width: 1280,
84
+ height: 720,
85
+ selector: "#main", # optional
86
+ format: :png # optional
87
+ )
88
+ puts image.url
89
+ ```
90
+
91
+ ## Rendering Templates
92
+
93
+ ### Single render — `POST /templates/:uid/render`
94
+
95
+ Returns a `results` array (one item per rendered layout). `result.url` is a
96
+ convenience accessor for the first result's URL.
97
+
98
+ ```ruby
99
+ result = client.render(
100
+ template_id: "your-template-uid",
101
+ variables: { title: "My Post", author: "Ada" },
102
+ format: :png, # :png, :jpg, :jpeg, :webp, :pdf
103
+ quality: 0.9, # raster quality 0.1–1.0
104
+ width: 1200, # optional override
105
+ height: 630 # optional override
106
+ )
107
+
108
+ puts result.url # results.first.url
109
+ puts result.template_uid
110
+ result.results.each do |item|
111
+ puts "#{item.layout}: #{item.url} (#{item.width}x#{item.height})"
112
+ end
113
+ ```
114
+
115
+ ### Multiple layout variants — `POST /templates/:uid/render`
116
+
117
+ Templates can have multiple layout variants (e.g. landscape, square, story)
118
+ created via AI Resize in the Pictify editor. Render several in one call (max 20);
119
+ layouts that fail land in `errors`.
120
+
121
+ ```ruby
122
+ result = client.render_layouts(
123
+ template_id: "your-template-uid",
124
+ variables: { title: "Hello World" },
125
+ layouts: ["default", "twitter-post", "instagram-story"]
126
+ )
127
+
128
+ result.results.each do |item|
129
+ puts "#{item.layout}: #{item.url} (#{item.width}x#{item.height})"
130
+ end
131
+ result.errors.each do |err|
132
+ puts "#{err.layout} failed: #{err.error}"
133
+ end
134
+
135
+ puts "rendered #{result.total_rendered} of #{result.total_layouts}"
136
+ ```
137
+
138
+ You can also render a single named variant via `render(..., layout: "square")`.
139
+
140
+ ## Rendering GIFs — `POST /gif`
141
+
142
+ Provide exactly one source: `html`, `url`, or `template_id` (+ optional
143
+ `variables`). The source must contain motion (CSS animation, etc.).
144
+
145
+ ```ruby
146
+ gif = client.render_gif(
147
+ html: "<style>@keyframes p{0%{opacity:.2}50%{opacity:1}100%{opacity:.2}}" \
148
+ "div{animation:p 2s infinite}</style><div>Hi</div>",
149
+ width: 400, # default 800
150
+ height: 200, # default 600
151
+ quality: :medium # :low, :medium, :high
152
+ )
153
+ puts gif.url
154
+ puts gif.uid
155
+ puts gif.animation_length
156
+
157
+ # From a template
158
+ gif = client.render_gif(template_id: "your-template-uid", variables: { name: "Ada" })
159
+
160
+ # From a live URL
161
+ gif = client.render_gif(url: "https://example.com/animated-page")
162
+ ```
163
+
164
+ ## Batch Rendering (async) — `POST /templates/:uid/batch-render`
165
+
166
+ Batch rendering is asynchronous. Submitting returns a `batch_id` immediately;
167
+ poll `get_batch_results` to track progress.
168
+
169
+ > Note: the poll endpoint reports per-item `index`, `success`, and `variables`
170
+ > but **not** rendered URLs — URLs are delivered via the `render.completed`
171
+ > webhook.
172
+
173
+ ```ruby
174
+ job = client.render_batch(
175
+ template_id: "your-template-uid",
176
+ variable_sets: [
177
+ { name: "Card 1", company: "X" },
178
+ { name: "Card 2", company: "Y" }
179
+ ],
180
+ format: :png,
181
+ quality: 0.9, # optional
182
+ concurrency: 5, # optional, 1–10
183
+ layouts: ["default", "twitter-post"] # optional
184
+ )
185
+
186
+ puts job.batch_id
187
+ puts job.status # "pending"
188
+ puts job.total_items
189
+
190
+ # Poll for progress
191
+ results = client.get_batch_results(job.batch_id)
192
+ puts "status: #{results.status} (#{results.progress}%)"
193
+ puts "completed: #{results.completed_items} / #{results.total_items}"
194
+ results.results.each do |item|
195
+ puts "Item #{item.index}: success=#{item.success?} vars=#{item.variables}"
196
+ end
197
+ ```
198
+
199
+ ## Template Management
200
+
201
+ ```ruby
202
+ # Get a template by UID — GET /templates/:uid
203
+ template = client.get_template("your-template-uid")
204
+ puts "Template: #{template.name} (#{template.uid})"
205
+ template.variable_definitions.each do |var|
206
+ puts " - #{var.name} (#{var.type})"
207
+ end
208
+
209
+ # List templates — GET /templates
210
+ result = client.list_templates(page: 1, limit: 20, sort: :newest)
211
+ result.templates.each { |t| puts "#{t.uid}: #{t.name}" }
212
+ puts "total: #{result.pagination.total}"
213
+
214
+ # Create a template from HTML — POST /templates
215
+ # Variables are auto-discovered from {{variableName}} tokens.
216
+ template = client.create_template(
217
+ html: "<div>Hi {{firstName}}</div>",
218
+ name: "Welcome Card",
219
+ width: 600,
220
+ height: 200,
221
+ output_format: "image" # "image" | "pdf"
222
+ )
223
+ puts template.uid
224
+ ```
225
+
226
+ ## Error Handling
227
+
228
+ ```ruby
229
+ begin
230
+ result = client.render(template_id: "your-template-uid", variables: { name: "Ada" })
231
+ rescue Pictify::AuthenticationError
232
+ puts "Invalid API key"
233
+ rescue Pictify::TemplateNotFoundError => e
234
+ puts "Template not found: #{e.message}"
235
+ rescue Pictify::QuotaExceededError
236
+ puts "Render quota exceeded"
237
+ rescue Pictify::RateLimitError => e
238
+ puts "Rate limited. Retry after: #{e.retry_after}s"
239
+ rescue Pictify::RenderError => e
240
+ puts "Render/validation failed: #{e.message}"
241
+ puts e.errors # field-level validation errors when present (422)
242
+ rescue Pictify::ServerError => e
243
+ puts "Server error: #{e.message}"
244
+ rescue Pictify::NetworkError => e
245
+ puts "Network error: #{e.message}"
246
+ rescue Pictify::TimeoutError
247
+ puts "Request timed out"
248
+ rescue Pictify::Error => e
249
+ puts "Error: #{e.message}"
250
+ end
251
+ ```
252
+
253
+ | Status | Error class |
254
+ |--------|-------------|
255
+ | 401 | `Pictify::AuthenticationError` |
256
+ | 402 | `Pictify::QuotaExceededError` |
257
+ | 404 | `Pictify::TemplateNotFoundError` |
258
+ | 422 | `Pictify::RenderError` (carries `errors`) |
259
+ | 429 (`quota_exceeded`) | `Pictify::QuotaExceededError` |
260
+ | 429 (other) | `Pictify::RateLimitError` |
261
+ | other 4xx | `Pictify::RenderError` |
262
+ | 5xx | `Pictify::ServerError` |
263
+
264
+ Only 5xx responses and network failures are retried (with exponential backoff);
265
+ 4xx responses (including 429) are never retried.
266
+
267
+ ## Framework Example — Rails
268
+
269
+ ```ruby
270
+ class OgImagesController < ApplicationController
271
+ def show
272
+ client = Pictify::Client.new(api_key: ENV["PICTIFY_API_KEY"])
273
+ image = client.render(
274
+ template_id: "og-image-template",
275
+ variables: { title: params[:title] }
276
+ )
277
+ redirect_to image.url, allow_other_host: true
278
+ end
279
+ end
280
+ ```
281
+
282
+ ## License
283
+
284
+ MIT
@@ -0,0 +1,367 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "faraday/retry"
5
+ require "json"
6
+ require "cgi"
7
+
8
+ module Pictify
9
+ # Client for the Pictify API.
10
+ #
11
+ # Generate images, PDFs, and GIFs from raw HTML, live URLs, and reusable
12
+ # templates.
13
+ #
14
+ # @example Render raw HTML to a PNG
15
+ # client = Pictify::Client.new(api_key: "your-api-key")
16
+ # image = client.render_html(html: "<div>Hello World</div>", width: 1200, height: 630)
17
+ # puts image.url
18
+ #
19
+ # @example Render a template
20
+ # result = client.render(template_id: "your-template-uid", variables: { name: "Ada" })
21
+ # puts result.url # results.first.url
22
+ #
23
+ class Client
24
+ DEFAULT_BASE_URL = "https://api.pictify.io"
25
+ DEFAULT_TIMEOUT = 30
26
+ DEFAULT_MAX_RETRIES = 3
27
+
28
+ # @param api_key [String] Your Pictify API key
29
+ # @param base_url [String] Custom API base URL
30
+ # @param timeout [Integer] Request timeout in seconds
31
+ # @param max_retries [Integer] Maximum retry attempts (5xx / network only)
32
+ def initialize(api_key:, base_url: DEFAULT_BASE_URL, timeout: DEFAULT_TIMEOUT, max_retries: DEFAULT_MAX_RETRIES)
33
+ raise AuthenticationError, "API key is required" if api_key.nil? || api_key.empty?
34
+
35
+ @api_key = api_key
36
+ @base_url = base_url.chomp("/")
37
+ @timeout = timeout
38
+ @max_retries = max_retries
39
+ end
40
+
41
+ # ------------------------------------------------------------------------
42
+ # Image rendering (POST /image)
43
+ # ------------------------------------------------------------------------
44
+
45
+ # Render an image (or PDF) directly from HTML.
46
+ #
47
+ # +POST /image+ — returns an {ImageResult} with +url+, +id+, +created_at+.
48
+ #
49
+ # @param html [String] Raw HTML content to render
50
+ # @param css [String, nil] Optional CSS, injected into the HTML inside a
51
+ # <style> tag before rendering (the +/image+ endpoint takes a single +html+
52
+ # field)
53
+ # @param width [Integer, nil] Output width in pixels (default 1280)
54
+ # @param height [Integer, nil] Output height in pixels (default 720)
55
+ # @param selector [String, nil] CSS selector to crop the screenshot to
56
+ # @param format [Symbol, String, nil] Output format, mapped to +fileExtension+
57
+ # (default png)
58
+ # @return [ImageResult]
59
+ def render_html(html:, css: nil, width: nil, height: nil, selector: nil, format: nil)
60
+ body = css ? "<style>#{css}</style>#{html}" : html
61
+
62
+ response = request(:post, "image", {
63
+ html: body,
64
+ width: width,
65
+ height: height,
66
+ selector: selector,
67
+ fileExtension: (format || :png).to_s
68
+ })
69
+ ImageResult.new(response)
70
+ end
71
+
72
+ # Screenshot a live URL.
73
+ #
74
+ # +POST /image+ with +url+ — returns an {ImageResult}.
75
+ #
76
+ # @param url [String] The URL to screenshot
77
+ # @param width [Integer, nil] Output width in pixels (default 1280)
78
+ # @param height [Integer, nil] Output height in pixels (default 720)
79
+ # @param selector [String, nil] CSS selector to crop the screenshot to
80
+ # @param format [Symbol, String, nil] Output format, mapped to +fileExtension+
81
+ # (default png)
82
+ # @return [ImageResult]
83
+ def render_url(url:, width: nil, height: nil, selector: nil, format: nil)
84
+ response = request(:post, "image", {
85
+ url: url,
86
+ width: width,
87
+ height: height,
88
+ selector: selector,
89
+ fileExtension: (format || :png).to_s
90
+ })
91
+ ImageResult.new(response)
92
+ end
93
+
94
+ # ------------------------------------------------------------------------
95
+ # Template rendering (POST /templates/:uid/render)
96
+ # ------------------------------------------------------------------------
97
+
98
+ # Render a single image (or PDF) from a template.
99
+ #
100
+ # +POST /templates/:uid/render+ — returns the {RenderResult} +results+
101
+ # envelope, with a convenience +url+ accessor (+results.first.url+).
102
+ #
103
+ # @param template_id [String] The UID of the template to render
104
+ # @param variables [Hash] Variables to inject into the template
105
+ # @param format [Symbol, String, nil] Output format (default png; +pdf+ supported)
106
+ # @param quality [Float, nil] Render quality for raster output (0.1–1.0, default 0.9)
107
+ # @param width [Integer, nil] Output width in pixels
108
+ # @param height [Integer, nil] Output height in pixels
109
+ # @param layout [String, nil] Render a single named layout variant
110
+ # @param layouts [Array<String>, nil] Render multiple named layout variants (max 20)
111
+ # @return [RenderResult]
112
+ def render(template_id:, variables: {}, format: nil, quality: nil, width: nil, height: nil,
113
+ layout: nil, layouts: nil)
114
+ response = request(:post, "templates/#{encode(template_id)}/render", {
115
+ variables: variables || {},
116
+ format: (format || :png).to_s,
117
+ quality: quality,
118
+ width: width,
119
+ height: height,
120
+ layout: layout,
121
+ layouts: layouts
122
+ })
123
+ RenderResult.new(response)
124
+ end
125
+
126
+ # Render multiple layout variants of a template in a single call.
127
+ #
128
+ # +POST /templates/:uid/render+ with +layouts+ — returns one +results+ item
129
+ # per successful layout; missing/invalid layouts appear in +errors+.
130
+ #
131
+ # @param template_id [String] The UID of the template to render
132
+ # @param layouts [Array<String>] Layout variant names to render (max 20).
133
+ # Use +"default"+ for the base layout.
134
+ # @param variables [Hash] Variables to inject into the template
135
+ # @param format [Symbol, String, nil] Output format (default png)
136
+ # @param quality [Float, nil] Render quality for raster output (0.1–1.0)
137
+ # @param width [Integer, nil] Output width in pixels
138
+ # @param height [Integer, nil] Output height in pixels
139
+ # @return [RenderResult]
140
+ def render_layouts(template_id:, layouts:, variables: {}, format: nil, quality: nil,
141
+ width: nil, height: nil)
142
+ render(
143
+ template_id: template_id,
144
+ variables: variables,
145
+ format: format,
146
+ quality: quality,
147
+ width: width,
148
+ height: height,
149
+ layouts: layouts
150
+ )
151
+ end
152
+
153
+ # ------------------------------------------------------------------------
154
+ # GIF rendering (POST /gif)
155
+ # ------------------------------------------------------------------------
156
+
157
+ # Render an animated GIF from raw HTML, a live URL, or a template.
158
+ #
159
+ # +POST /gif+ — the +{ gif: {...} }+ envelope is flattened to a
160
+ # {GifRenderResult} (+url+, +uid+, +width+, +height+, +animation_length+).
161
+ # Provide exactly one source: +html+, +url+, or +template_id+.
162
+ #
163
+ # @param html [String, nil] Raw HTML to render into a GIF (must contain motion)
164
+ # @param url [String, nil] A live URL to capture motion from
165
+ # @param template_id [String, nil] A template UID to render into a GIF
166
+ # @param variables [Hash, nil] Variables to inject when using +template_id+
167
+ # @param width [Integer, nil] Output width in pixels (default 800)
168
+ # @param height [Integer, nil] Output height in pixels (default 600)
169
+ # @param quality [Symbol, String, nil] Quality preset (:low, :medium, :high; default medium)
170
+ # @return [GifRenderResult]
171
+ def render_gif(html: nil, url: nil, template_id: nil, variables: nil, width: nil, height: nil,
172
+ quality: nil)
173
+ response = request(:post, "gif", {
174
+ html: html,
175
+ url: url,
176
+ template: template_id,
177
+ variables: variables,
178
+ width: width,
179
+ height: height,
180
+ quality: (quality || :medium).to_s
181
+ })
182
+ GifRenderResult.new(response["gif"] || {})
183
+ end
184
+
185
+ # ------------------------------------------------------------------------
186
+ # Batch rendering (async)
187
+ # ------------------------------------------------------------------------
188
+
189
+ # Submit an async batch render of a template across many variable sets.
190
+ #
191
+ # +POST /templates/:uid/batch-render+ — returns a {BatchRenderResult} with
192
+ # +batch_id+, +status+, +total_items+ immediately (HTTP 202). Poll
193
+ # {#get_batch_results} to track progress.
194
+ #
195
+ # Rendered URLs are NOT returned by the poll endpoint — they are delivered
196
+ # via the +render.completed+ webhook.
197
+ #
198
+ # @param template_id [String] The UID of the template to render
199
+ # @param variable_sets [Array<Hash>] Variable sets — one render per set (max 100)
200
+ # @param format [Symbol, String, nil] Output format (default png)
201
+ # @param quality [Float, nil] Render quality for raster output (0.1–1.0, default 0.9)
202
+ # @param concurrency [Integer, nil] Maximum parallel renders (1–10, default 5)
203
+ # @param layout [String, nil] Render a single named layout variant for every item
204
+ # @param layouts [Array<String>, nil] Render multiple named layout variants for every item
205
+ # @return [BatchRenderResult]
206
+ def render_batch(template_id:, variable_sets:, format: nil, quality: nil, concurrency: nil,
207
+ layout: nil, layouts: nil)
208
+ response = request(:post, "templates/#{encode(template_id)}/batch-render", {
209
+ variableSets: variable_sets,
210
+ format: (format || :png).to_s,
211
+ quality: quality,
212
+ concurrency: concurrency,
213
+ layout: layout,
214
+ layouts: layouts
215
+ })
216
+ BatchRenderResult.new(response)
217
+ end
218
+
219
+ # Get the status, progress, and per-item results of a batch job.
220
+ #
221
+ # +GET /templates/batch/:batch_id/results+. Results carry +index+, +success+,
222
+ # and +variables+ (plus +error+ on failures) but NOT rendered URLs.
223
+ #
224
+ # @param batch_id [String] The batch job ID returned by {#render_batch}
225
+ # @return [BatchResults]
226
+ def get_batch_results(batch_id)
227
+ response = request(:get, "templates/batch/#{encode(batch_id)}/results")
228
+ BatchResults.new(response)
229
+ end
230
+
231
+ # ------------------------------------------------------------------------
232
+ # Template CRUD
233
+ # ------------------------------------------------------------------------
234
+
235
+ # Get a single template by its UID.
236
+ #
237
+ # +GET /templates/:uid+ — unwraps the +{ template }+ envelope.
238
+ #
239
+ # @param template_id [String] The template UID
240
+ # @return [Template]
241
+ def get_template(template_id)
242
+ response = request(:get, "templates/#{encode(template_id)}")
243
+ Template.new(response["template"] || {})
244
+ end
245
+
246
+ # List templates in your account.
247
+ #
248
+ # +GET /templates+ — returns a {ListTemplatesResult} with +templates+ and
249
+ # +pagination+.
250
+ #
251
+ # @param page [Integer, nil] Page number (1-based, default 1)
252
+ # @param limit [Integer, nil] Items per page (max 100, default 12)
253
+ # @param sort [Symbol, String, nil] Sort order (:newest, :oldest, :name)
254
+ # @return [ListTemplatesResult]
255
+ def list_templates(page: nil, limit: nil, sort: nil)
256
+ params = {}
257
+ params[:page] = page unless page.nil?
258
+ params[:limit] = limit unless limit.nil?
259
+ params[:sort] = sort.to_s unless sort.nil?
260
+
261
+ response = request(:get, "templates", nil, params)
262
+ ListTemplatesResult.new(response)
263
+ end
264
+
265
+ # Create a template from HTML.
266
+ #
267
+ # +POST /templates+ — unwraps the +{ template }+ envelope. Variables are
268
+ # auto-discovered from +{{variableName}}+ tokens in the HTML body.
269
+ #
270
+ # @param html [String] Raw HTML body
271
+ # @param name [String, nil] Template name
272
+ # @param width [Integer, nil] Default width in pixels
273
+ # @param height [Integer, nil] Default height in pixels
274
+ # @param variable_definitions [Array<Hash>, nil] Explicit variable definitions
275
+ # (auto-extracted from HTML when omitted)
276
+ # @param output_format [Symbol, String, nil] Output format ("image" | "pdf", default image)
277
+ # @return [Template]
278
+ def create_template(html:, name: nil, width: nil, height: nil, variable_definitions: nil,
279
+ output_format: nil)
280
+ response = request(:post, "templates", {
281
+ html: html,
282
+ name: name,
283
+ width: width,
284
+ height: height,
285
+ variableDefinitions: variable_definitions,
286
+ outputFormat: output_format.nil? ? nil : output_format.to_s
287
+ })
288
+ Template.new(response["template"] || {})
289
+ end
290
+
291
+ private
292
+
293
+ def encode(value)
294
+ CGI.escape(value.to_s)
295
+ end
296
+
297
+ def connection
298
+ @connection ||= Faraday.new(url: @base_url) do |f|
299
+ f.request :retry,
300
+ max: @max_retries,
301
+ interval: 0.05,
302
+ backoff_factor: 2,
303
+ retry_statuses: [500, 502, 503, 504],
304
+ # Keep faraday-retry's defaults (which include RetriableResponse
305
+ # — required for retry_statuses to engage) and add network
306
+ # failures so 5xx and connection drops retry, but 4xx never do.
307
+ exceptions: Faraday::Retry::Middleware::DEFAULT_EXCEPTIONS + [Faraday::ConnectionFailed],
308
+ methods: %i[get post put delete patch]
309
+ f.options.timeout = @timeout
310
+ f.options.open_timeout = 10
311
+ f.headers["Authorization"] = "Bearer #{@api_key}"
312
+ f.headers["Content-Type"] = "application/json"
313
+ f.headers["User-Agent"] = "pictify-ruby/#{VERSION}"
314
+ end
315
+ end
316
+
317
+ # Make an authenticated JSON request. Nil body fields are stripped so the
318
+ # backend applies its own defaults. HTTP errors are mapped to typed errors.
319
+ def request(method, path, body = nil, params = nil)
320
+ response = connection.send(method) do |req|
321
+ req.url(path, params)
322
+ req.body = JSON.generate(strip_nils(body)) if body
323
+ end
324
+
325
+ handle_response(response)
326
+ rescue Faraday::RetriableResponse => e
327
+ # faraday-retry re-raises this once retries on a retriable (5xx) status are
328
+ # exhausted; map the carried response through the typed-error logic.
329
+ env = e.response
330
+ raise Pictify.error_from_response(env.status, error_body(env.body))
331
+ rescue Faraday::TimeoutError
332
+ raise TimeoutError.new("Request timed out", timeout: @timeout)
333
+ rescue Faraday::ConnectionFailed => e
334
+ if e.message.include?("timeout") || e.message.include?("timed out") || e.message.include?("execution expired")
335
+ raise TimeoutError.new("Request timed out", timeout: @timeout)
336
+ end
337
+
338
+ raise NetworkError.new(nil, original_error: e)
339
+ end
340
+
341
+ def strip_nils(body)
342
+ return body unless body.is_a?(Hash)
343
+
344
+ body.reject { |_k, v| v.nil? }
345
+ end
346
+
347
+ def handle_response(response)
348
+ return parse_body(response.body) if response.success?
349
+
350
+ raise Pictify.error_from_response(response.status, error_body(response.body))
351
+ end
352
+
353
+ def parse_body(raw)
354
+ return {} if raw.nil? || raw.to_s.empty?
355
+
356
+ JSON.parse(raw)
357
+ rescue JSON::ParserError
358
+ {}
359
+ end
360
+
361
+ def error_body(raw)
362
+ JSON.parse(raw)
363
+ rescue StandardError
364
+ { "message" => raw.to_s }
365
+ end
366
+ end
367
+ end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pictify
4
+ # Base error class for all Pictify errors.
5
+ class Error < StandardError
6
+ attr_reader :status_code, :response_body
7
+
8
+ def initialize(message = nil, status_code: nil, response_body: nil)
9
+ @status_code = status_code
10
+ @response_body = response_body
11
+ @raw_message = message || default_message
12
+ super(@raw_message)
13
+ end
14
+
15
+ def message
16
+ @raw_message
17
+ end
18
+
19
+ def to_s
20
+ return "[#{status_code}] #{@raw_message}" if status_code
21
+
22
+ @raw_message
23
+ end
24
+
25
+ private
26
+
27
+ def default_message
28
+ "An error occurred"
29
+ end
30
+ end
31
+
32
+ # Raised when the API key is invalid or missing (HTTP 401).
33
+ class AuthenticationError < Error
34
+ private
35
+
36
+ def default_message
37
+ "Invalid or missing API key"
38
+ end
39
+ end
40
+
41
+ # Raised when the specified template does not exist (HTTP 404).
42
+ class TemplateNotFoundError < Error
43
+ private
44
+
45
+ def default_message
46
+ "Template not found"
47
+ end
48
+ end
49
+
50
+ # Raised when the rate limit is exceeded (HTTP 429 without a quota code).
51
+ class RateLimitError < Error
52
+ attr_reader :retry_after
53
+
54
+ def initialize(message = nil, retry_after: nil, **kwargs)
55
+ @retry_after = retry_after
56
+ super(message, **kwargs)
57
+ end
58
+
59
+ def to_s
60
+ base_str = super
61
+ return "#{base_str} (retry after #{retry_after}s)" if retry_after
62
+
63
+ base_str
64
+ end
65
+
66
+ private
67
+
68
+ def default_message
69
+ "Rate limit exceeded"
70
+ end
71
+ end
72
+
73
+ # Raised when the render quota is exceeded (HTTP 402, or 429 with a quota code).
74
+ class QuotaExceededError < Error
75
+ private
76
+
77
+ def default_message
78
+ "Render quota exceeded"
79
+ end
80
+ end
81
+
82
+ # Raised when a render fails or input validation fails (HTTP 422, and other
83
+ # non-quota 4xx). Carries field-level +errors+ from the API when present.
84
+ class RenderError < Error
85
+ attr_reader :errors
86
+
87
+ def initialize(message = nil, errors: nil, **kwargs)
88
+ @errors = errors
89
+ super(message, **kwargs)
90
+ end
91
+
92
+ private
93
+
94
+ def default_message
95
+ "Render failed"
96
+ end
97
+ end
98
+
99
+ # Raised when the server fails (HTTP 5xx).
100
+ class ServerError < Error
101
+ private
102
+
103
+ def default_message
104
+ "Server error"
105
+ end
106
+ end
107
+
108
+ # Raised when a network error occurs.
109
+ class NetworkError < Error
110
+ attr_reader :original_error
111
+
112
+ def initialize(message = nil, original_error: nil)
113
+ @original_error = original_error
114
+ super(message || original_error&.message || "Network error occurred")
115
+ end
116
+ end
117
+
118
+ # Raised when a request times out.
119
+ class TimeoutError < Error
120
+ attr_reader :timeout
121
+
122
+ def initialize(message = nil, timeout: nil)
123
+ @timeout = timeout
124
+ super(message || "Request timed out")
125
+ end
126
+ end
127
+
128
+ # Map an HTTP error response to a typed Pictify error.
129
+ #
130
+ # The Pictify API has no unified error envelope: image/GIF endpoints return
131
+ # +{ error, code }+ while template/CRUD endpoints return +{ message }+ or
132
+ # +{ message, errors }+. Message precedence is +error+ then +message+ then a
133
+ # fallback.
134
+ #
135
+ # Status mapping:
136
+ # - 401 -> AuthenticationError
137
+ # - 402 -> QuotaExceededError
138
+ # - 404 -> TemplateNotFoundError
139
+ # - 422 -> RenderError (validation; includes +errors+)
140
+ # - 429 -> QuotaExceededError when code == "quota_exceeded", else RateLimitError
141
+ # - other 4xx -> RenderError
142
+ # - 5xx -> ServerError
143
+ def self.error_from_response(status_code, body)
144
+ body ||= {}
145
+ message = body["error"] || body["message"] || "An unexpected error occurred"
146
+
147
+ case status_code
148
+ when 401
149
+ AuthenticationError.new(message, status_code: status_code, response_body: body)
150
+ when 402
151
+ QuotaExceededError.new(message, status_code: status_code, response_body: body)
152
+ when 404
153
+ TemplateNotFoundError.new(message, status_code: status_code, response_body: body)
154
+ when 422
155
+ RenderError.new(message, status_code: status_code, response_body: body, errors: body["errors"])
156
+ when 429
157
+ if body["code"] == "quota_exceeded"
158
+ QuotaExceededError.new(message, status_code: status_code, response_body: body)
159
+ else
160
+ RateLimitError.new(message, status_code: status_code, response_body: body, retry_after: body["retry_after"])
161
+ end
162
+ else
163
+ if status_code >= 500
164
+ ServerError.new(message, status_code: status_code, response_body: body)
165
+ else
166
+ RenderError.new(message, status_code: status_code, response_body: body, errors: body["errors"])
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,256 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Pictify
6
+ # Image output formats supported by Pictify renders.
7
+ #
8
+ # The +/image+ endpoint accepts these via +format+ (mapped to +fileExtension+);
9
+ # template renders accept them via +format+ (including +pdf+).
10
+ FORMATS = %i[png jpg jpeg webp pdf].freeze
11
+
12
+ # GIF quality presets accepted by the +/gif+ endpoint.
13
+ GIF_QUALITIES = %i[low medium high].freeze
14
+
15
+ # Result of an +/image+ render (+render_html+ / +render_url+).
16
+ #
17
+ # Maps the API response +{ url, id, createdAt }+.
18
+ class ImageResult
19
+ attr_reader :url, :id, :created_at, :raw
20
+
21
+ def initialize(data)
22
+ @raw = data
23
+ @url = data["url"]
24
+ @id = data["id"]
25
+ @created_at = data["createdAt"]
26
+ end
27
+ end
28
+
29
+ # A single rendered layout item within a template render response.
30
+ #
31
+ # Maps +{ layout, url, width, height, format, name, id, createdAt }+.
32
+ class RenderResultItem
33
+ attr_reader :layout, :url, :width, :height, :format, :name, :id, :created_at, :raw
34
+
35
+ def initialize(data)
36
+ @raw = data
37
+ @layout = data["layout"]
38
+ @url = data["url"]
39
+ @width = data["width"]
40
+ @height = data["height"]
41
+ @format = data["format"]
42
+ @name = data["name"]
43
+ @id = data["id"]
44
+ @created_at = data["createdAt"]
45
+ end
46
+ end
47
+
48
+ # Error entry returned when a specific layout fails to render.
49
+ #
50
+ # Maps +{ layout, error }+.
51
+ class RenderErrorEntry
52
+ attr_reader :layout, :error
53
+
54
+ def initialize(data)
55
+ @layout = data["layout"]
56
+ @error = data["error"]
57
+ end
58
+ end
59
+
60
+ # Result of a template render (+render+ / +render_layouts+).
61
+ #
62
+ # The API returns a +results[]+ envelope. The convenience method +url+ returns
63
+ # the URL of the first rendered item, or +nil+ if nothing rendered.
64
+ class RenderResult
65
+ attr_reader :results, :errors, :total_layouts, :total_rendered, :total_errors,
66
+ :template_uid, :raw
67
+
68
+ def initialize(data)
69
+ @raw = data
70
+ @results = (data["results"] || []).map { |r| RenderResultItem.new(r) }
71
+ @errors = (data["errors"] || []).map { |e| RenderErrorEntry.new(e) }
72
+ @total_layouts = data["totalLayouts"]
73
+ @total_rendered = data["totalRendered"]
74
+ @total_errors = data["totalErrors"]
75
+ @template_uid = data["templateUid"]
76
+ end
77
+
78
+ # Convenience accessor: URL of the first rendered item (+results[0].url+).
79
+ def url
80
+ results.first&.url
81
+ end
82
+ end
83
+
84
+ # Result of a GIF render (+render_gif+). Flattened from the API's
85
+ # +{ gif: {...} }+ envelope.
86
+ #
87
+ # Maps +{ url, uid, width, height, animationLength }+.
88
+ class GifRenderResult
89
+ attr_reader :url, :uid, :width, :height, :animation_length, :raw
90
+
91
+ def initialize(data)
92
+ @raw = data
93
+ @url = data["url"]
94
+ @uid = data["uid"]
95
+ @width = data["width"]
96
+ @height = data["height"]
97
+ @animation_length = data["animationLength"]
98
+ end
99
+ end
100
+
101
+ # Result of submitting an async batch render (+render_batch+).
102
+ #
103
+ # The job runs asynchronously: poll {Client#get_batch_results} with the
104
+ # returned +batch_id+ to track progress. Maps +{ batchId, status, totalItems }+.
105
+ class BatchRenderResult
106
+ attr_reader :batch_id, :status, :total_items, :message, :raw
107
+
108
+ def initialize(data)
109
+ @raw = data
110
+ @batch_id = data["batchId"]
111
+ @status = data["status"]
112
+ @total_items = data["totalItems"]
113
+ @message = data["message"]
114
+ end
115
+ end
116
+
117
+ # Status of a single item within a batch job.
118
+ #
119
+ # NOTE: batch results do NOT include rendered URLs — those are delivered via
120
+ # the +render.completed+ webhook. Each item reports only its index, success,
121
+ # the variable names it used, and (on failure) an error message.
122
+ class BatchItemStatus
123
+ attr_reader :index, :success, :variables, :error, :raw
124
+
125
+ def initialize(data)
126
+ @raw = data
127
+ @index = data["index"]
128
+ @success = data["success"]
129
+ @variables = data["variables"]
130
+ @error = data["error"]
131
+ end
132
+
133
+ def success?
134
+ !!@success
135
+ end
136
+ end
137
+
138
+ # Full status and progress of a batch job (+get_batch_results+).
139
+ #
140
+ # Maps +{ batchId, status, progress, totalItems, completedItems, failedItems,
141
+ # results[], errors[], createdAt, startedAt, completedAt }+.
142
+ class BatchResults
143
+ attr_reader :batch_id, :status, :progress, :total_items, :completed_items,
144
+ :failed_items, :results, :errors, :created_at, :started_at,
145
+ :completed_at, :raw
146
+
147
+ def initialize(data)
148
+ @raw = data
149
+ @batch_id = data["batchId"]
150
+ @status = data["status"]
151
+ @progress = data["progress"]
152
+ @total_items = data["totalItems"]
153
+ @completed_items = data["completedItems"]
154
+ @failed_items = data["failedItems"]
155
+ @results = (data["results"] || []).map { |r| BatchItemStatus.new(r) }
156
+ @errors = (data["errors"] || []).map { |e| BatchItemStatus.new(e) }
157
+ @created_at = data["createdAt"]
158
+ @started_at = data["startedAt"]
159
+ @completed_at = data["completedAt"]
160
+ end
161
+ end
162
+
163
+ # A variable definition declared on a template.
164
+ #
165
+ # The API keys variables by +name+ and may include +type+, +defaultValue+,
166
+ # +description+, and +validation+. Unknown engine-specific fields are kept in
167
+ # +raw+.
168
+ class TemplateVariableDefinition
169
+ attr_reader :name, :type, :default_value, :description, :validation, :raw
170
+
171
+ def initialize(data)
172
+ @raw = data
173
+ @name = data["name"]
174
+ @type = data["type"]
175
+ @default_value = data["defaultValue"]
176
+ @description = data["description"]
177
+ @validation = data["validation"]
178
+ end
179
+ end
180
+
181
+ # Template information returned by +get_template+ / +create_template+ /
182
+ # +list_templates+.
183
+ #
184
+ # The API keys templates by +uid+ (not +id+) and declares variables in
185
+ # +variableDefinitions+ (with a legacy flat +variables+ list of names).
186
+ # Unknown fields are passed through via +raw+.
187
+ class Template
188
+ attr_reader :uid, :name, :html, :width, :height, :engine, :output_format,
189
+ :variables, :variable_definitions, :thumbnail, :created_at,
190
+ :updated_at, :raw
191
+
192
+ def initialize(data)
193
+ @raw = data
194
+ @uid = data["uid"]
195
+ @name = data["name"]
196
+ @html = data["html"]
197
+ @width = data["width"]
198
+ @height = data["height"]
199
+ @engine = data["engine"]
200
+ @output_format = data["outputFormat"]
201
+ @variables = data["variables"] || []
202
+ @variable_definitions = (data["variableDefinitions"] || []).map do |v|
203
+ TemplateVariableDefinition.new(v)
204
+ end
205
+ @thumbnail = data["thumbnail"]
206
+ @created_at = parse_time(data["createdAt"])
207
+ @updated_at = parse_time(data["updatedAt"])
208
+ end
209
+
210
+ private
211
+
212
+ def parse_time(value)
213
+ return nil unless value
214
+
215
+ Time.parse(value)
216
+ rescue ArgumentError, TypeError
217
+ nil
218
+ end
219
+ end
220
+
221
+ # Pagination metadata returned by +list_templates+.
222
+ class Pagination
223
+ attr_reader :page, :limit, :total, :total_pages, :has_next, :has_prev, :raw
224
+
225
+ def initialize(data)
226
+ @raw = data
227
+ @page = data["page"]
228
+ @limit = data["limit"]
229
+ @total = data["total"]
230
+ @total_pages = data["totalPages"]
231
+ @has_next = data["hasNext"]
232
+ @has_prev = data["hasPrev"]
233
+ end
234
+
235
+ def has_next?
236
+ !!@has_next
237
+ end
238
+
239
+ def has_prev?
240
+ !!@has_prev
241
+ end
242
+ end
243
+
244
+ # Result of listing templates (+list_templates+).
245
+ #
246
+ # Maps +{ templates: [...], pagination: {...} }+.
247
+ class ListTemplatesResult
248
+ attr_reader :templates, :pagination, :raw
249
+
250
+ def initialize(data)
251
+ @raw = data
252
+ @templates = (data["templates"] || []).map { |t| Template.new(t) }
253
+ @pagination = data["pagination"] ? Pagination.new(data["pagination"]) : nil
254
+ end
255
+ end
256
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pictify
4
+ VERSION = "1.0.0"
5
+ end
data/lib/pictify.rb ADDED
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "pictify/version"
4
+ require_relative "pictify/errors"
5
+ require_relative "pictify/types"
6
+ require_relative "pictify/client"
7
+
8
+ # Pictify Ruby SDK
9
+ #
10
+ # Official SDK for generating images, PDFs, and GIFs from raw HTML, live URLs,
11
+ # and reusable templates using the Pictify API.
12
+ #
13
+ # @example Render raw HTML to a PNG
14
+ # client = Pictify::Client.new(api_key: "your-api-key")
15
+ # image = client.render_html(html: "<div>Hello World</div>", width: 1200, height: 630)
16
+ # puts image.url
17
+ #
18
+ # @example Render a template
19
+ # result = client.render(
20
+ # template_id: "your-template-uid",
21
+ # variables: { name: "Ada", company: "Pictify" }
22
+ # )
23
+ # puts result.url
24
+ #
25
+ module Pictify
26
+ end
metadata ADDED
@@ -0,0 +1,147 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pictify
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Pictify
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-06-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '3.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '2.0'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '3.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: faraday-retry
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.0'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '2.0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: minitest
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '5.0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '5.0'
61
+ - !ruby/object:Gem::Dependency
62
+ name: rake
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '13.0'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '13.0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: rubocop
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '1.0'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '1.0'
89
+ - !ruby/object:Gem::Dependency
90
+ name: webmock
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '3.0'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '3.0'
103
+ description: Ruby client library for the Pictify API. Generate OG images, social cards,
104
+ and dynamic images from HTML templates.
105
+ email:
106
+ - support@pictify.io
107
+ executables: []
108
+ extensions: []
109
+ extra_rdoc_files: []
110
+ files:
111
+ - CHANGELOG.md
112
+ - LICENSE
113
+ - README.md
114
+ - lib/pictify.rb
115
+ - lib/pictify/client.rb
116
+ - lib/pictify/errors.rb
117
+ - lib/pictify/types.rb
118
+ - lib/pictify/version.rb
119
+ homepage: https://pictify.io
120
+ licenses:
121
+ - MIT
122
+ metadata:
123
+ homepage_uri: https://pictify.io
124
+ source_code_uri: https://github.com/pictify-io/pictify-ruby
125
+ changelog_uri: https://github.com/pictify-io/pictify-ruby/blob/main/CHANGELOG.md
126
+ documentation_uri: https://docs.pictify.io/sdks/ruby
127
+ rubygems_mfa_required: 'true'
128
+ post_install_message:
129
+ rdoc_options: []
130
+ require_paths:
131
+ - lib
132
+ required_ruby_version: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: 3.0.0
137
+ required_rubygems_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ requirements: []
143
+ rubygems_version: 3.4.6
144
+ signing_key:
145
+ specification_version: 4
146
+ summary: Official Ruby SDK for Pictify - Generate images from HTML templates
147
+ test_files: []