Skip to content

Commit a808053

Browse files
committed
Init
0 parents  commit a808053

10 files changed

Lines changed: 549 additions & 0 deletions

File tree

.github/workflows/test.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
13+
strategy:
14+
matrix:
15+
ruby-version: ["3.1", "3.2", "3.3", "4.0"]
16+
17+
steps:
18+
- uses: actions/checkout@v6
19+
20+
- name: Set up Ruby ${{ matrix.ruby-version }}
21+
uses: ruby/setup-ruby@v1
22+
with:
23+
ruby-version: ${{ matrix.ruby-version }}
24+
bundler-cache: true
25+
26+
- name: Run tests
27+
run: bundle exec rspec

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
*.gem
2+
*.gemspec.lock
3+
.bundle/
4+
Gemfile.lock
5+
pkg/
6+
spec/reports/
7+
tmp/

.mise.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[tools]
2+
ruby = "4.0.2"

Gemfile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# frozen_string_literal: true
2+
3+
source "https://rubygems.org"
4+
5+
gemspec
6+
7+
group :development, :test do
8+
gem "rspec", "~> 3.12"
9+
gem "rake", "~> 13.0"
10+
end

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Techulus
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.

README.md

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# Capture Ruby SDK
2+
3+
Official Ruby SDK for [Capture](https://capture.page) - Screenshot and content extraction API.
4+
5+
## Installation
6+
7+
Add to your Gemfile:
8+
9+
```ruby
10+
gem "capture_page"
11+
```
12+
13+
Or install directly:
14+
15+
```bash
16+
gem install capture_page
17+
```
18+
19+
## Quick Start
20+
21+
```ruby
22+
require "capture"
23+
24+
client = Capture.new("your-api-key", "your-api-secret")
25+
26+
image_url = client.build_image_url("https://example.com")
27+
puts image_url
28+
```
29+
30+
## Features
31+
32+
- **Screenshot Capture**: Capture full-page or viewport screenshots as PNG/JPG
33+
- **PDF Generation**: Convert web pages to PDF documents
34+
- **Content Extraction**: Extract HTML and text content from web pages
35+
- **Metadata Extraction**: Get page metadata (title, description, og tags, etc.)
36+
- **Animated GIFs**: Create animated GIFs of page interactions
37+
- **Zero Dependencies**: Uses only Ruby standard library
38+
39+
## Usage
40+
41+
### Initialize the Client
42+
43+
```ruby
44+
require "capture"
45+
46+
client = Capture.new("your-api-key", "your-api-secret")
47+
48+
# Use edge endpoint for faster response times
49+
client = Capture.new("your-api-key", "your-api-secret", use_edge: true)
50+
```
51+
52+
### Building URLs
53+
54+
#### Image Capture
55+
56+
```ruby
57+
image_url = client.build_image_url("https://example.com")
58+
59+
image_url = client.build_image_url("https://example.com", {
60+
"full" => true,
61+
"delay" => 2,
62+
"vw" => 1920,
63+
"vh" => 1080
64+
})
65+
```
66+
67+
#### PDF Capture
68+
69+
```ruby
70+
pdf_url = client.build_pdf_url("https://example.com")
71+
72+
pdf_url = client.build_pdf_url("https://example.com", {
73+
"format" => "A4",
74+
"landscape" => true
75+
})
76+
```
77+
78+
#### Content Extraction
79+
80+
```ruby
81+
content_url = client.build_content_url("https://example.com")
82+
```
83+
84+
#### Metadata Extraction
85+
86+
```ruby
87+
metadata_url = client.build_metadata_url("https://example.com")
88+
```
89+
90+
#### Animated GIF
91+
92+
```ruby
93+
animated_url = client.build_animated_url("https://example.com")
94+
```
95+
96+
### Fetching Data
97+
98+
#### Fetch Image
99+
100+
```ruby
101+
image_data = client.fetch_image("https://example.com")
102+
File.binwrite("screenshot.png", image_data)
103+
```
104+
105+
#### Fetch PDF
106+
107+
```ruby
108+
pdf_data = client.fetch_pdf("https://example.com", { "full" => true })
109+
File.binwrite("page.pdf", pdf_data)
110+
```
111+
112+
#### Fetch Content
113+
114+
```ruby
115+
content = client.fetch_content("https://example.com")
116+
puts content["html"]
117+
puts content["textContent"]
118+
puts content["markdown"]
119+
```
120+
121+
#### Fetch Metadata
122+
123+
```ruby
124+
metadata = client.fetch_metadata("https://example.com")
125+
puts metadata["metadata"]
126+
```
127+
128+
#### Fetch Animated GIF
129+
130+
```ruby
131+
gif_data = client.fetch_animated("https://example.com")
132+
File.binwrite("animation.gif", gif_data)
133+
```
134+
135+
## Configuration Options
136+
137+
### Constructor Options
138+
139+
- `use_edge` (boolean): Use edge.capture.page instead of cdn.capture.page for faster response times
140+
141+
## API Endpoints
142+
143+
The SDK supports two base URLs:
144+
145+
- **CDN**: `https://cdn.capture.page` (default)
146+
- **Edge**: `https://edge.capture.page` (when `use_edge: true`)
147+
148+
## License
149+
150+
MIT
151+
152+
## Links
153+
154+
- [Website](https://capture.page)
155+
- [Documentation](https://docs.capture.page)
156+
- [GitHub](https://github.com/techulus/capture-ruby)
157+
158+
## Support
159+
160+
For support, please visit [capture.page](https://capture.page) or open an issue on GitHub.

Rakefile

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# frozen_string_literal: true
2+
3+
require "rspec/core/rake_task"
4+
5+
RSpec::Core::RakeTask.new(:spec)
6+
7+
task default: :spec

capture_page.gemspec

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# frozen_string_literal: true
2+
3+
Gem::Specification.new do |spec|
4+
spec.name = "capture_page"
5+
spec.version = "1.0.0"
6+
spec.authors = ["Capture Team"]
7+
spec.email = ["support@capture.page"]
8+
9+
spec.summary = "Ruby SDK for Capture - Screenshot and content extraction API"
10+
spec.description = "Official Ruby SDK for Capture (capture.page). Capture screenshots, generate PDFs, extract content and metadata from web pages."
11+
spec.homepage = "https://capture.page"
12+
spec.license = "MIT"
13+
spec.required_ruby_version = ">= 3.1"
14+
15+
spec.metadata["homepage_uri"] = spec.homepage
16+
spec.metadata["source_code_uri"] = "https://github.com/techulus/capture-ruby"
17+
spec.metadata["documentation_uri"] = "https://docs.capture.page"
18+
19+
spec.files = Dir["lib/**/*.rb", "README.md", "LICENSE"]
20+
spec.require_paths = ["lib"]
21+
end

lib/capture.rb

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# frozen_string_literal: true
2+
3+
require "digest/md5"
4+
require "net/http"
5+
require "uri"
6+
require "json"
7+
8+
class Capture
9+
API_URL = "https://cdn.capture.page"
10+
EDGE_URL = "https://edge.capture.page"
11+
12+
attr_reader :key, :options
13+
14+
def initialize(key, secret, options = {})
15+
@key = key
16+
@secret = secret
17+
options = options.nil? ? {} : options
18+
raise TypeError, "options must be a Hash" unless options.is_a?(Hash)
19+
@options = options
20+
end
21+
22+
def build_image_url(url, options = {})
23+
build_url(url, "image", options)
24+
end
25+
26+
def build_pdf_url(url, options = {})
27+
build_url(url, "pdf", options)
28+
end
29+
30+
def build_content_url(url, options = {})
31+
build_url(url, "content", options)
32+
end
33+
34+
def build_metadata_url(url, options = {})
35+
build_url(url, "metadata", options)
36+
end
37+
38+
def build_animated_url(url, options = {})
39+
build_url(url, "animated", options)
40+
end
41+
42+
def fetch_image(url, options = {})
43+
fetch_binary(build_image_url(url, options))
44+
end
45+
46+
def fetch_pdf(url, options = {})
47+
fetch_binary(build_pdf_url(url, options))
48+
end
49+
50+
def fetch_content(url, options = {})
51+
fetch_json(build_content_url(url, options))
52+
end
53+
54+
def fetch_metadata(url, options = {})
55+
fetch_json(build_metadata_url(url, options))
56+
end
57+
58+
def fetch_animated(url, options = {})
59+
fetch_binary(build_animated_url(url, options))
60+
end
61+
62+
private
63+
64+
def build_url(url, request_type, options)
65+
raise TypeError, "key and secret must be strings" unless @key.is_a?(String) && @secret.is_a?(String)
66+
raise ArgumentError, "Key and Secret is required" if @key.empty? || @secret.empty?
67+
raise ArgumentError, "url is required" if url.nil? || (url.is_a?(String) && url.empty?)
68+
raise TypeError, "url should be of type string (something like www.google.com)" unless url.is_a?(String)
69+
70+
options = options.nil? ? {} : options
71+
raise TypeError, "options must be a Hash" unless options.is_a?(Hash)
72+
params = options.merge("url" => url)
73+
query_string = encode_query_string(params)
74+
token = generate_token(@secret, query_string)
75+
base_url = @options[:use_edge] || @options["useEdge"] ? EDGE_URL : API_URL
76+
77+
"#{base_url}/#{@key}/#{token}/#{request_type}?#{query_string}"
78+
end
79+
80+
def generate_token(secret, query_string)
81+
Digest::MD5.hexdigest("#{secret}#{query_string}")
82+
end
83+
84+
def encode_query_string(params)
85+
filtered = params.each_with_object({}) do |(k, v), hash|
86+
next if v.nil?
87+
88+
hash[k] = case v
89+
when true then "true"
90+
when false then "false"
91+
else v.to_s
92+
end
93+
end
94+
95+
URI.encode_www_form(filtered)
96+
end
97+
98+
def fetch_binary(url)
99+
uri = URI.parse(url)
100+
response = Net::HTTP.get_response(uri)
101+
raise "HTTP Error: #{response.code} #{response.message}" unless response.is_a?(Net::HTTPSuccess)
102+
103+
response.body.force_encoding("BINARY")
104+
end
105+
106+
def fetch_json(url)
107+
uri = URI.parse(url)
108+
response = Net::HTTP.get_response(uri)
109+
raise "HTTP Error: #{response.code} #{response.message}" unless response.is_a?(Net::HTTPSuccess)
110+
111+
JSON.parse(response.body)
112+
end
113+
end

0 commit comments

Comments
 (0)