placeholder-image 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: 1b9fc489b1812d0f1f82cbc9bfa765d6e01cdba612da2697a416bbcbc460ae0e
4
+ data.tar.gz: 1e0d6fe2e8fd81ca37bad24219771fd4125190e92d1f0a9cdc0b216b526ef7d5
5
+ SHA512:
6
+ metadata.gz: 28682fa1cfa37908f42663ba841ff50de20dcb578d7cbeabd8eb09b46e32bd67b92d1d2cabcb8010d1a1cba1469f3de68b43b690b0858fbcb5f65797a749d236
7
+ data.tar.gz: ddd2662d54dbdf2bd81125763a3ccf9cf4c927e57e32a2952cab2e47f76f446f13ffa3c959b9ceab41d04a19443a552f1c6a8a4e38868fc9ec8c4e9d1337a877
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented here. This project follows
4
+ [Semantic Versioning](https://semver.org/).
5
+
6
+ ## v1.0.0 - 2026-08-05
7
+
8
+ - Initial release. Includes PNG-generating Rack middleware, stand-alone Docker service, and complete examples of integrating with Sinatra, Rails, and generic Rack applications.
9
+
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rodney Waldhoff and Placeholder Image contributors
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,107 @@
1
+ # Placeholder Image
2
+
3
+ A dependency-light Rack middleware that generates simple placeholder images in pure Ruby (stdlib zlib only, no image gem dependencies).
4
+
5
+ [![CI](https://github.com/rodw/placeholder-image-rb/actions/workflows/ci.yml/badge.svg)](https://github.com/rodw/placeholder-image-rb/actions/workflows/ci.yml)
6
+ [![Gem Version](https://img.shields.io/gem/v/placeholder-image)](https://rubygems.org/gems/placeholder-image)
7
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.2-CC342D)](https://www.ruby-lang.org)
8
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE.txt)
9
+
10
+ <p>
11
+ <img
12
+ src="https://raw.githubusercontent.com/rodw/placeholder-image-rb/main/examples/images/example-100x160.png"
13
+ alt="A 100 by 160 pixel placeholder using the default colors"
14
+ width="100"
15
+ align="middle"
16
+ title="/placeholder/100x160.png">
17
+ <img
18
+ src="https://raw.githubusercontent.com/rodw/placeholder-image-rb/main/examples/images/example-640x360-bg1D3557-fgF1FAEE.png"
19
+ alt="A 640 by 360 pixel placeholder with custom colors"
20
+ width="320"
21
+ align="middle"
22
+ title="/placeholder/640x360.png?bg=1d3557&fg=f1faee">
23
+ <img
24
+ src="https://raw.githubusercontent.com/rodw/placeholder-image-rb/main/examples/images/example-120-bg000-fgF90.png"
25
+ alt="A 120-pixel square placeholder with custom colors"
26
+ width="120"
27
+ align="middle"
28
+ title="/placeholder/120.png?bg=000&fg=f90">
29
+ </p>
30
+
31
+ ## Installation
32
+
33
+ Placeholder-Image requires Ruby 3.2+ and Rack 3.
34
+
35
+ Add it to your bundle:
36
+
37
+ ```ruby
38
+ gem "placeholder-image"
39
+ ```
40
+
41
+ Then install:
42
+
43
+ ```sh
44
+ bundle install
45
+ ```
46
+
47
+ ## Using
48
+
49
+ Placeholder-Image runs as standard Rack middleware:
50
+
51
+ ```ruby
52
+ require "placeholder_image"
53
+
54
+ use PlaceholderImage::Middleware, path_prefix: "/placeholder"
55
+ run MyApp
56
+ ```
57
+
58
+ The URL path specifies a square or rectangular image, with optional background and foreground colors as 3- or 6-digit hex codes:
59
+
60
+ ```text
61
+ /placeholder/300.png
62
+ /placeholder/640x480.png
63
+ /placeholder/640x480.png?bg=eee&fg=1d3557
64
+ ```
65
+
66
+ See [the examples](https://github.com/rodw/placeholder-image-rb/tree/main/examples) for complete demonstrations of integrating placeholder-image with Sinatra, Rails, and vanilla Rack applications.
67
+
68
+ ## Configuration
69
+
70
+ Pass configuration options as keyword arguments when adding the middleware:
71
+
72
+ ```ruby
73
+ use PlaceholderImage::Middleware,
74
+ path_prefix: "/placeholder",
75
+ http_header_cache_control: "public, max-age=31536000, immutable",
76
+ image_max_dim_px: 4_000,
77
+ image_max_total_px: 4_000 * 4_000,
78
+ image_default_bg: "#eeeeee",
79
+ image_default_fg: "#909",
80
+ cache_max_entries: 128
81
+ ```
82
+
83
+ | Option | Default | Description |
84
+ | --- | --- | --- |
85
+ | `path_prefix` | `/placeholder` | URL path prefix under which generated images are served. For example, the default serves `/placeholder/300.png`. A trailing slash is optional. |
86
+ | `http_header_cache_control` | `public, max-age=31536000, immutable` | Value of the `Cache-Control` response header. The default allows public caches to retain an image for one year. |
87
+ | `image_max_dim_px` | `4000` | Maximum permitted width or height, in pixels. Requests exceeding this limit return `400 Bad Request`. |
88
+ | `image_max_total_px` | `16000000` | Maximum permitted total pixel count (`width * height`). This limits the CPU time and memory consumed by a single image. Requests exceeding this limit return `400 Bad Request`. |
89
+ | `image_default_bg` | `[0xEE,0xEE,0xEE]` | Default background color as an RGB byte array or a 3- or 6-digit hex string, optionally beginning with `#`. The request's `bg` parameter overrides it. |
90
+ | `image_default_fg` | `[0x99,0x99,0x99]`| Default foreground color used for the border and dimension label. It accepts the same array and hex-string formats as `image_default_bg`; the request's `fg` parameter overrides it. |
91
+ | `cache_max_entries` | `128` | Maximum number of generated images retained in each middleware instance's in-memory FIFO cache. Set to `0` to disable caching. |
92
+
93
+
94
+ ## Running as a Stand-Alone Container
95
+
96
+ Placeholder-Image can be run as a stand-alone, containerized service.
97
+
98
+ See the [Docker server documentation](https://github.com/rodw/placeholder-image-rb/blob/main/docker/README.md) for details.
99
+
100
+ ## Development
101
+
102
+ For development setup and contribution guidelines, see
103
+ [CONTRIBUTING.md](https://github.com/rodw/placeholder-image-rb/blob/main/CONTRIBUTING.md).
104
+
105
+ ## License
106
+
107
+ Placeholder-Image is available under the [MIT License](https://github.com/rodw/placeholder-image-rb/blob/main/LICENSE.txt).
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require "digest"
5
+
6
+ # Rack middleware that serves generated placeholder PNGs.
7
+ #
8
+ # use PlaceholderImage::Middleware, path_prefix: "/placeholder"
9
+ #
10
+ # Examples:
11
+ # GET /placeholder/300.png -> 300x300, default colors
12
+ # GET /placeholder/640x480.png -> 640x480, default colors
13
+ # GET /placeholder/300x200.png?bg=eee&fg=999 -> 300x200, specified colors
14
+ #
15
+ # No image gems required: PNGs are encoded with stdlib zlib.
16
+ module PlaceholderImage
17
+ class BadRequest < StandardError; end
18
+
19
+ # ------------------------------------------------------------- Middleware --
20
+ class Middleware
21
+ DEFAULTS = {
22
+ path_prefix: "/placeholder",
23
+ image_max_dim_px: 4_000,
24
+ image_max_total_px: 4_000 * 4_000,
25
+ http_header_cache_control: "public, max-age=31536000, immutable",
26
+ cache_max_entries: 128,
27
+ image_default_bg: [0xEE, 0xEE, 0xEE],
28
+ image_default_fg: [0x99, 0x99, 0x99]
29
+ }.freeze
30
+
31
+ DIMENSION = /[1-9]\d*/
32
+
33
+ # @param app [#call] the downstream Rack application.
34
+ # @param options [Hash] configuration overrides merged over {DEFAULTS}.
35
+ # @option options [String] :path_prefix ("/placeholder") URL prefix the middleware serves.
36
+ # @option options [Integer] :image_max_dim_px (4000) maximum width or height, in pixels.
37
+ # @option options [Integer] :image_max_total_px (16_000_000) maximum total pixel count.
38
+ # @option options [String] :http_header_cache_control the +Cache-Control+ response header value.
39
+ # @option options [Integer] :cache_max_entries (128) in-memory FIFO cache size; +0+ disables caching.
40
+ # @option options [Array(Integer, Integer, Integer), String] :image_default_bg default background color.
41
+ # @option options [Array(Integer, Integer, Integer), String] :image_default_fg default foreground color.
42
+ def initialize(app, **options)
43
+ @app = app
44
+ @options = DEFAULTS.merge(options)
45
+ @cache = {}
46
+ @mutex = Mutex.new
47
+
48
+ path_prefix = Regexp.escape(@options[:path_prefix].chomp("/"))
49
+ @owned = %r{\A#{path_prefix}(?:[./]|\z)}
50
+ @route = %r{\A#{path_prefix}/(#{DIMENSION})(?:x(#{DIMENSION}))?\.png\z}
51
+ end
52
+
53
+ # Rack entry point. Serves a PNG for owned paths and delegates everything else downstream.
54
+ #
55
+ # @param env [Hash] the Rack environment.
56
+ # @return [Array(Integer, Hash, Array)] a Rack response tuple.
57
+ def call(env)
58
+ return @app.call(env) unless @owned.match?(env["PATH_INFO"])
59
+ return error(env["REQUEST_METHOD"], 405, "method not allowed") unless %w[GET HEAD].include?(env["REQUEST_METHOD"])
60
+
61
+ spec = parse(env["PATH_INFO"], env["QUERY_STRING"].to_s)
62
+ etag = etag_for(spec)
63
+ return [304, cache_headers(etag), []] if fresh?(env, etag)
64
+
65
+ img = fetch(spec)
66
+ body = env["REQUEST_METHOD"] == "HEAD" ? [] : [img]
67
+
68
+ [200, cache_headers(etag).merge(
69
+ "content-type" => "image/png",
70
+ "content-length" => img.bytesize.to_s
71
+ ), body]
72
+ rescue BadRequest => e
73
+ error(env["REQUEST_METHOD"], 400, e.message)
74
+ end
75
+
76
+ private
77
+
78
+ def cache_headers(etag)
79
+ { "etag" => etag,
80
+ "cache-control" => @options[:http_header_cache_control] }
81
+ end
82
+
83
+ def fresh?(env, etag)
84
+ env["HTTP_IF_NONE_MATCH"].to_s.split(",").map(&:strip).include?(etag)
85
+ end
86
+
87
+ def etag_for(spec)
88
+ %("#{Digest::SHA256.hexdigest(spec.inspect)[0, 16]}")
89
+ end
90
+
91
+ def error(req_method, status, message)
92
+ body = req_method == "HEAD" ? "" : "#{message}\n"
93
+
94
+ [
95
+ status,
96
+ { "content-type" => "text/plain; charset=utf-8",
97
+ "content-length" => body.bytesize.to_s },
98
+ [body]
99
+ ]
100
+ end
101
+
102
+ def fetch(spec)
103
+ @mutex.synchronize { return @cache[spec] if @cache.key?(spec) }
104
+
105
+ img = Renderer.call(**spec)
106
+
107
+ @mutex.synchronize do
108
+ @cache[spec] = img if @options[:cache_max_entries].positive?
109
+ @cache.shift while @cache.size > @options[:cache_max_entries] # FIFO eviction
110
+ end
111
+
112
+ img
113
+ end
114
+
115
+ def parse(path, query)
116
+ width, height = parse_dimensions(path)
117
+ params = parse_query_string(query)
118
+
119
+ { width: width,
120
+ height: height,
121
+ bg: parse_color(params["bg"], @options[:image_default_bg]),
122
+ fg: parse_color(params["fg"], @options[:image_default_fg]),
123
+ text: "#{width}x#{height}" }
124
+ end
125
+
126
+ def parse_dimensions(path)
127
+ match = @route.match(path) or
128
+ raise BadRequest, "no such image; expected #{@options[:path_prefix]}/<SIZE>.png or " \
129
+ "#{@options[:path_prefix]}/<W>x<H>.png"
130
+
131
+ width = match[1].to_i
132
+ height = (match[2] || match[1]).to_i
133
+ max = @options[:image_max_dim_px]
134
+
135
+ raise BadRequest, "dimensions must be between 1 and #{max}" if width > max || height > max
136
+ raise BadRequest, "image exceeds #{@options[:image_max_total_px]} pixels" if
137
+ width * height > @options[:image_max_total_px]
138
+
139
+ [width, height]
140
+ end
141
+
142
+ def parse_query_string(query)
143
+ return {} if query.empty?
144
+
145
+ URI.decode_www_form(query).to_h
146
+ rescue ArgumentError
147
+ raise BadRequest, "malformed query string"
148
+ end
149
+
150
+ def parse_color(value, default)
151
+ value = default if value.nil? || value.empty?
152
+ return value if value.is_a?(Array)
153
+
154
+ hex = value.delete_prefix("#")
155
+ hex = hex.chars.map { |c| c * 2 }.join if hex.length == 3
156
+ raise BadRequest, "invalid color: #{value}" unless hex.match?(/\A\h{6}\z/)
157
+
158
+ [hex[0, 2], hex[2, 2], hex[4, 2]].map { |pair| pair.to_i(16) }
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zlib"
4
+
5
+ module PlaceholderImage
6
+ # Generates a placeholder PNG from dimensions, colors, and label text.
7
+ module Renderer
8
+ LABEL_WIDTH_PCT = 0.80
9
+ LABEL_HEIGHT_PCT = 0.50
10
+
11
+ # Minimal 8-bit truecolor PNG encoder.
12
+ class Canvas
13
+ SIGNATURE = "\x89PNG\r\n\x1A\n".b
14
+
15
+ attr_reader :width, :height
16
+
17
+ def initialize(width, height, rgb)
18
+ @width = width
19
+ @height = height
20
+ @row_bytes = width * 3
21
+ @data = (rgb.pack("C3") * (width * height)).b
22
+ end
23
+
24
+ def []=(x, y, rgb)
25
+ return unless x.between?(0, @width - 1) && y.between?(0, @height - 1)
26
+
27
+ i = ((y * @width) + x) * 3
28
+ @data.setbyte(i, rgb[0])
29
+ @data.setbyte(i + 1, rgb[1])
30
+ @data.setbyte(i + 2, rgb[2])
31
+ end
32
+
33
+ def fill_rect(x, y, width, height, rgb)
34
+ y.upto(y + height - 1) do |py|
35
+ x.upto(x + width - 1) { |px| self[px, py] = rgb }
36
+ end
37
+ end
38
+
39
+ def to_png
40
+ raw = String.new(capacity: (@row_bytes + 1) * @height, encoding: Encoding::BINARY)
41
+ @height.times { |y| raw << 0.chr << @data.byteslice(y * @row_bytes, @row_bytes) }
42
+ ihdr = [@width, @height, 8, 2, 0, 0, 0].pack("NNC5") # 8-bit, color type 2 (RGB)
43
+
44
+ SIGNATURE +
45
+ chunk("IHDR", ihdr) +
46
+ chunk("IDAT", Zlib::Deflate.deflate(raw, Zlib::BEST_SPEED)) +
47
+ chunk("IEND", "".b)
48
+ end
49
+
50
+ private
51
+
52
+ def chunk(type, data)
53
+ type = type.b
54
+ [data.bytesize].pack("N") + type + data + [Zlib.crc32(type + data)].pack("N")
55
+ end
56
+ end
57
+
58
+ # 5x7 bitmap font, just enough to render dimension labels such as "1024x768".
59
+ module Font
60
+ FONT_WIDTH = 5
61
+ FONT_HEIGHT = 7
62
+
63
+ GLYPHS = {
64
+ "0" => %w[01110 10001 10011 10101 11001 10001 01110],
65
+ "1" => %w[00100 01100 00100 00100 00100 00100 01110],
66
+ "2" => %w[01110 10001 00001 00010 00100 01000 11111],
67
+ "3" => %w[11111 00010 00100 00010 00001 10001 01110],
68
+ "4" => %w[00010 00110 01010 10010 11111 00010 00010],
69
+ "5" => %w[11111 10000 11110 00001 00001 10001 01110],
70
+ "6" => %w[00110 01000 10000 11110 10001 10001 01110],
71
+ "7" => %w[11111 00001 00010 00100 01000 01000 01000],
72
+ "8" => %w[01110 10001 10001 01110 10001 10001 01110],
73
+ "9" => %w[01110 10001 10001 01111 00001 00010 01100],
74
+ "x" => %w[00000 00000 10001 01010 00100 01010 10001]
75
+ }.freeze
76
+
77
+ def self.advance(scale) = (FONT_WIDTH + 1) * scale
78
+ def self.text_width(text, scale) = text.empty? ? 0 : (advance(scale) * text.length) - scale
79
+
80
+ def self.draw(canvas, text, x, y, scale, color)
81
+ text.each_char.with_index do |char, i|
82
+ glyph = GLYPHS[char] or next
83
+ ox = x + (advance(scale) * i)
84
+ glyph.each_with_index do |row, ry|
85
+ row.each_char.with_index do |bit, rx|
86
+ next if bit == "0"
87
+
88
+ canvas.fill_rect(ox + (rx * scale), y + (ry * scale), scale, scale, color)
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ module_function
96
+
97
+ # Render a placeholder PNG.
98
+ #
99
+ # @param width [Integer] image width in pixels.
100
+ # @param height [Integer] image height in pixels.
101
+ # @param bg [Array(Integer, Integer, Integer)] background color as RGB bytes.
102
+ # @param fg [Array(Integer, Integer, Integer)] foreground color, used for the border and label.
103
+ # @param text [String] label drawn centered on the image (digits and "x" only).
104
+ # @return [String] binary PNG data.
105
+ # @example
106
+ # Renderer.call(width: 320, height: 240, bg: [238, 238, 238], fg: [153, 153, 153], text: "320x240")
107
+ def call(width:, height:, bg:, fg:, text:)
108
+ canvas = Canvas.new(width, height, bg)
109
+ draw_border(canvas, fg)
110
+ draw_label(canvas, text, fg)
111
+ canvas.to_png
112
+ end
113
+
114
+ def draw_border(canvas, color)
115
+ width = canvas.width
116
+ height = canvas.height
117
+ thickness = ([width, height].min / 40).clamp(1, 6)
118
+ return if width < 4 * thickness || height < 4 * thickness
119
+
120
+ canvas.fill_rect(0, 0, width, thickness, color)
121
+ canvas.fill_rect(0, height - thickness, width, thickness, color)
122
+ canvas.fill_rect(0, 0, thickness, height, color)
123
+ canvas.fill_rect(width - thickness, 0, thickness, height, color)
124
+ end
125
+
126
+ def draw_label(canvas, text, color)
127
+ return if text.empty?
128
+
129
+ natural_width = Font.text_width(text, 1)
130
+ scale = [(canvas.width * LABEL_WIDTH_PCT / natural_width).floor,
131
+ (canvas.height * LABEL_HEIGHT_PCT / Font::FONT_HEIGHT).floor].min
132
+ return if scale < 1
133
+
134
+ Font.draw(canvas,
135
+ text,
136
+ (canvas.width - Font.text_width(text, scale)) / 2,
137
+ (canvas.height - (Font::FONT_HEIGHT * scale)) / 2,
138
+ scale,
139
+ color)
140
+ end
141
+
142
+ private_class_method :draw_border, :draw_label
143
+
144
+ private_constant :Canvas, :Font
145
+ end
146
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlaceholderImage
4
+ VERSION = "1.0.0"
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "placeholder_image/version"
4
+ require_relative "placeholder_image/renderer"
5
+ require_relative "placeholder_image/middleware"
@@ -0,0 +1,44 @@
1
+ module PlaceholderImage
2
+ # Raised for any client error; converted to an HTTP 400 by the middleware.
3
+ class BadRequest < StandardError
4
+ end
5
+
6
+ # Rack middleware that serves generated placeholder PNGs.
7
+ class Middleware
8
+ type rack_response = [ Integer, Hash[String, String], Array[String] ]
9
+
10
+ DEFAULTS: Hash[Symbol, untyped]
11
+ DIMENSION: Regexp
12
+
13
+ @app: untyped
14
+ @options: Hash[Symbol, untyped]
15
+ @cache: Hash[untyped, untyped]
16
+ @mutex: Thread::Mutex
17
+ @owned: Regexp
18
+ @route: Regexp
19
+
20
+ def initialize: (untyped app, **untyped options) -> void
21
+
22
+ def call: (Hash[String, untyped] env) -> rack_response
23
+
24
+ private
25
+
26
+ def cache_headers: (String etag) -> Hash[String, String]
27
+
28
+ def fresh?: (Hash[String, untyped] env, String etag) -> bool
29
+
30
+ def etag_for: (untyped spec) -> String
31
+
32
+ def error: (untyped req_method, Integer status, String message) -> rack_response
33
+
34
+ def fetch: (untyped spec) -> String
35
+
36
+ def parse: (untyped path, untyped query) -> Hash[Symbol, untyped]
37
+
38
+ def parse_dimensions: (untyped path) -> [ Integer, Integer ]
39
+
40
+ def parse_query_string: (untyped query) -> Hash[String, String]
41
+
42
+ def parse_color: (untyped value, untyped default) -> Array[Integer]
43
+ end
44
+ end
@@ -0,0 +1,53 @@
1
+ module PlaceholderImage
2
+ # Generates a placeholder PNG from dimensions, colors, and label text.
3
+ module Renderer
4
+ type rgb = Array[Integer]
5
+
6
+ LABEL_WIDTH_PCT: Float
7
+ LABEL_HEIGHT_PCT: Float
8
+
9
+ # Minimal 8-bit truecolor PNG encoder.
10
+ class Canvas
11
+ SIGNATURE: String
12
+
13
+ attr_reader width: Integer
14
+ attr_reader height: Integer
15
+
16
+ @width: Integer
17
+ @height: Integer
18
+ @row_bytes: Integer
19
+ @data: String
20
+
21
+ def initialize: (Integer width, Integer height, PlaceholderImage::Renderer::rgb rgb) -> void
22
+
23
+ def []=: (Integer x, Integer y, PlaceholderImage::Renderer::rgb rgb) -> void
24
+
25
+ def fill_rect: (Integer x, Integer y, Integer width, Integer height, PlaceholderImage::Renderer::rgb rgb) -> void
26
+
27
+ def to_png: () -> String
28
+
29
+ private
30
+
31
+ def chunk: (String type, String data) -> String
32
+ end
33
+
34
+ # 5x7 bitmap font, just enough to render dimension labels such as "1024x768".
35
+ module Font
36
+ FONT_WIDTH: Integer
37
+ FONT_HEIGHT: Integer
38
+ GLYPHS: Hash[String, Array[String]]
39
+
40
+ def self.advance: (Integer scale) -> Integer
41
+
42
+ def self.text_width: (String text, Integer scale) -> Integer
43
+
44
+ def self.draw: (PlaceholderImage::Renderer::Canvas canvas, String text, Integer x, Integer y, Integer scale, PlaceholderImage::Renderer::rgb color) -> void
45
+ end
46
+
47
+ def self?.call: (width: Integer, height: Integer, bg: PlaceholderImage::Renderer::rgb, fg: PlaceholderImage::Renderer::rgb, text: String) -> String
48
+
49
+ def self?.draw_border: (PlaceholderImage::Renderer::Canvas canvas, PlaceholderImage::Renderer::rgb color) -> void
50
+
51
+ def self?.draw_label: (PlaceholderImage::Renderer::Canvas canvas, String text, PlaceholderImage::Renderer::rgb color) -> void
52
+ end
53
+ end
@@ -0,0 +1,3 @@
1
+ module PlaceholderImage
2
+ VERSION: String
3
+ end
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: placeholder-image
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Rodney Waldhoff
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rack
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '3.0'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '4.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '3.0'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '4.0'
32
+ description: A small, dependency-light Rack middleware that generates simple placeholder
33
+ images.
34
+ executables: []
35
+ extensions: []
36
+ extra_rdoc_files: []
37
+ files:
38
+ - CHANGELOG.md
39
+ - LICENSE.txt
40
+ - README.md
41
+ - lib/placeholder_image.rb
42
+ - lib/placeholder_image/middleware.rb
43
+ - lib/placeholder_image/renderer.rb
44
+ - lib/placeholder_image/version.rb
45
+ - sig/placeholder_image/middleware.rbs
46
+ - sig/placeholder_image/renderer.rbs
47
+ - sig/placeholder_image/version.rbs
48
+ homepage: https://github.com/rodw/placeholder-image-rb
49
+ licenses:
50
+ - MIT
51
+ metadata:
52
+ bug_tracker_uri: https://github.com/rodw/placeholder-image-rb/issues
53
+ changelog_uri: https://github.com/rodw/placeholder-image-rb/blob/main/CHANGELOG.md
54
+ documentation_uri: https://rubydoc.info/gems/placeholder-image
55
+ homepage_uri: https://github.com/rodw/placeholder-image-rb
56
+ source_code_uri: https://github.com/rodw/placeholder-image-rb
57
+ rubygems_mfa_required: 'true'
58
+ rdoc_options: []
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '3.2'
66
+ - - "<"
67
+ - !ruby/object:Gem::Version
68
+ version: '5.0'
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubygems_version: 3.6.9
76
+ specification_version: 4
77
+ summary: Rack middleware for generating placeholder images
78
+ test_files: []