rmagick_tidy 0.1.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 +7 -0
- data/CHANGELOG.md +19 -0
- data/LICENSE +21 -0
- data/README.ja.md +129 -0
- data/README.md +132 -0
- data/lib/rmagick_tidy/configuration.rb +12 -0
- data/lib/rmagick_tidy/hook.rb +68 -0
- data/lib/rmagick_tidy/railtie.rb +18 -0
- data/lib/rmagick_tidy/registry.rb +73 -0
- data/lib/rmagick_tidy/tracker.rb +57 -0
- data/lib/rmagick_tidy/version.rb +3 -0
- data/lib/rmagick_tidy.rb +32 -0
- metadata +148 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 8f147dcd4088210662a53b4a66911e721b5b5a506ff4b1bead65db5a94ac873b
|
|
4
|
+
data.tar.gz: c80d6229d5ef1e1272097f3d30b939629c04ddb29bed8f185c2a01dc4f7f4d05
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: e60b3a6a5812a68936e71e73f073ce9f1d1f57032df67ff05c99c5279acba8fc7f935f8435c263388f95a31ac784ba09caa7e10d63954810f155ef3fadf5f592
|
|
7
|
+
data.tar.gz: 63f5780ab884f412049b3fc7290e3f0a050ce8859a66fd87952417156e978ec6f0623efbadb2b98d88711289158e03130efecce5adedea1aa92351bda61b3d73
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
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.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-05-21
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Initial release: `RmagickTidy.scope` block that tracks every `Magick::Image` / `Magick::ImageList` created inside it and calls `destroy!` on them when the block exits.
|
|
14
|
+
- `LICENSE` file (MIT).
|
|
15
|
+
- Gem metadata (`source_code_uri`, `changelog_uri`, `bug_tracker_uri`, `rubygems_mfa_required`).
|
|
16
|
+
- RuboCop and SimpleCov for style and coverage; both run in CI.
|
|
17
|
+
- Codecov integration: CI emits `coverage/lcov.info` (via `simplecov-lcov`) and uploads it through `codecov/codecov-action@v4`. README displays the live coverage badge.
|
|
18
|
+
- RSpec coverage for nested `Hash`/`Array` return values, `ImageList` iteration, missing `destroyed?`, `nil` / empty returns, and `destroy!` exception paths.
|
|
19
|
+
- README note about `Configuration` thread-safety expectations.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Junichiro Kasuya
|
|
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.ja.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# rmagick_tidy
|
|
2
|
+
|
|
3
|
+
[](https://github.com/jksy/rmagick_tidy/actions/workflows/ci.yml)
|
|
4
|
+
[](https://codecov.io/gh/jksy/rmagick_tidy)
|
|
5
|
+
|
|
6
|
+
`rmagick_tidy` は RMagick (`Magick::Image`) のメモリ管理を**スコープベース**で自動化する Ruby gem です。
|
|
7
|
+
|
|
8
|
+
RMagick が確保するメモリの実体は ImageMagick の C レイヤー側にあります。Ruby の GC は Ruby が確保したメモリしか把握しないため、ImageMagick 側の使用量を過小評価し、GC が適時に発火しません。Ruby ラッパが回収されるタイミングで C 側のメモリも最終的には解放されますが、それまではプロセスの RSS が膨らみ続けます。これを避けるため、`ensure + destroy!` を各所に手書きして即時解放しているプロジェクトが多いのが現状です。本 gem はその定型処理を 1 ブロックに集約します。
|
|
9
|
+
|
|
10
|
+
English: [README.md](README.md)
|
|
11
|
+
|
|
12
|
+
## インストール
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
# Gemfile
|
|
16
|
+
gem "rmagick_tidy"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
require "rmagick_tidy"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`require` した時点で `Magick::Image` / `Magick::ImageList` のフックがインストールされます。
|
|
24
|
+
|
|
25
|
+
## 基本的な使い方
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
RmagickTidy.scope do
|
|
29
|
+
img = Magick::Image.read("input.jpg").first
|
|
30
|
+
resized = img.resize(800, 600)
|
|
31
|
+
resized.write("output.jpg")
|
|
32
|
+
end
|
|
33
|
+
# ブロックを抜けたタイミングで img, resized が destroy! 済み
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### ブロックの戻り値は解放されない(keep)
|
|
37
|
+
|
|
38
|
+
呼び出し元に画像を返したい場合は、その画像をブロックの戻り値にします。
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
result = RmagickTidy.scope do
|
|
42
|
+
img = Magick::Image.read("input.jpg").first
|
|
43
|
+
img.resize(800, 600) # ← この戻り値だけは keep
|
|
44
|
+
end
|
|
45
|
+
# result は生きている。元の img は destroy! 済み
|
|
46
|
+
|
|
47
|
+
result.write("out.jpg")
|
|
48
|
+
result.destroy!
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
戻り値は `Magick::Image` 単体だけでなく、`Array<Image>`、`Hash` の値、`Magick::ImageList` 内の要素も再帰的に keep されます。
|
|
52
|
+
|
|
53
|
+
### ネスト
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
RmagickTidy.scope do # 外側
|
|
57
|
+
outer = Magick::Image.read("a.jpg").first
|
|
58
|
+
RmagickTidy.scope do # 内側
|
|
59
|
+
inner = outer.resize(100, 100)
|
|
60
|
+
# inner はここで destroy!
|
|
61
|
+
end
|
|
62
|
+
# outer はまだ生きている
|
|
63
|
+
end
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 例外時も解放される
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
RmagickTidy.scope do
|
|
70
|
+
img = Magick::Image.read("x.jpg").first
|
|
71
|
+
raise "boom"
|
|
72
|
+
end
|
|
73
|
+
# img は destroy! されてから例外が再送出される
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### bang メソッドは二重登録されない
|
|
77
|
+
|
|
78
|
+
`resize!` などの bang メソッドは `self` を返すため、`equal?` で判定して再登録しません。
|
|
79
|
+
|
|
80
|
+
## Rails で使う
|
|
81
|
+
|
|
82
|
+
`require "rmagick_tidy"` を済ませると Railtie が `ActionController::Base` に `within_rmagick_tidy_scope` を mixin します。
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
class ImagesController < ApplicationController
|
|
86
|
+
around_action :within_rmagick_tidy_scope
|
|
87
|
+
|
|
88
|
+
def show
|
|
89
|
+
img = Magick::Image.read(@source).first
|
|
90
|
+
@blob = img.resize(800, 600).to_blob { |info| info.format = "JPEG" }
|
|
91
|
+
send_data @blob, type: "image/jpeg"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`to_blob` は `String` を返すのでスコープの解放対象になりません。Image オブジェクトのみがクリーンアップされます。
|
|
97
|
+
|
|
98
|
+
## strict モード
|
|
99
|
+
|
|
100
|
+
開発・テスト環境で「スコープ外で作られた Image」を検知したいときに使います。
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
RmagickTidy.configure do |c|
|
|
104
|
+
c.strict_mode = :warn # or :raise / :off (default)
|
|
105
|
+
end
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
- `:off` — 何もしない(本番デフォルト)
|
|
109
|
+
- `:warn` — `warn` で標準エラー出力に通知
|
|
110
|
+
- `:raise` — `RmagickTidy::OutOfScopeError` を発生
|
|
111
|
+
|
|
112
|
+
> **Configuration はスレッドセーフではありません。** `strict_mode`(および将来追加されるオプション)は **起動時に 1 回だけ**設定してください(例: Rails の initializer)。ワーカースレッドが `Magick::Image` を使い始めたあとに別スレッドから書き換える挙動は未定義です。
|
|
113
|
+
|
|
114
|
+
## 仕組み
|
|
115
|
+
|
|
116
|
+
- `Magick::Image` / `Magick::ImageList` に対し、`Module#prepend` で全ての public instance method(および `new`, `read`, `from_blob`, ...のクラスメソッド)をラップ
|
|
117
|
+
- **戻り値の型をチェック**して `Magick::Image` であれば現在のスコープに登録(ホワイトリストを持たないので RMagick のバージョン差異を吸収)
|
|
118
|
+
- 戻り値が `self` と `equal?` なら bang メソッドとみなして登録しない
|
|
119
|
+
- スコープスタックは `Thread.current` 配下なのでマルチスレッド環境でも安全
|
|
120
|
+
- 二重 `destroy!` は `destroyed?` 判定 + rescue で防止
|
|
121
|
+
|
|
122
|
+
## 対応バージョン
|
|
123
|
+
|
|
124
|
+
- Ruby 3.2 以上
|
|
125
|
+
- RMagick 2.x 〜 6.x(戻り値チェック方式のため幅広く動作)
|
|
126
|
+
|
|
127
|
+
## ライセンス
|
|
128
|
+
|
|
129
|
+
MIT
|
data/README.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# rmagick_tidy
|
|
2
|
+
|
|
3
|
+
[](https://github.com/jksy/rmagick_tidy/actions/workflows/ci.yml)
|
|
4
|
+
[](https://codecov.io/gh/jksy/rmagick_tidy)
|
|
5
|
+
|
|
6
|
+
`rmagick_tidy` is a Ruby gem that automates **scope-based memory management** for RMagick (`Magick::Image`).
|
|
7
|
+
|
|
8
|
+
The bulk of the memory an `Magick::Image` represents is allocated by ImageMagick in C, outside Ruby's heap. Ruby's GC only accounts for memory it allocated itself, so it underestimates the real footprint and fires far less often than the actual memory pressure warrants. The C-side memory is eventually freed when the Ruby wrapper is collected, but until then the process's RSS keeps climbing. To avoid that, many projects scatter `ensure` / `destroy!` pairs through their code to release the memory immediately. This gem replaces that boilerplate with a single block.
|
|
9
|
+
|
|
10
|
+
日本語版: [README.ja.md](README.ja.md)
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
# Gemfile
|
|
16
|
+
gem "rmagick_tidy"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
require "rmagick_tidy"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Hooks for `Magick::Image` and `Magick::ImageList` are installed when the gem is required.
|
|
24
|
+
|
|
25
|
+
## Basic usage
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
RmagickTidy.scope do
|
|
29
|
+
img = Magick::Image.read("input.jpg").first
|
|
30
|
+
resized = img.resize(800, 600)
|
|
31
|
+
resized.write("output.jpg")
|
|
32
|
+
end
|
|
33
|
+
# Both img and resized have been released by the time the block exits.
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### The block return value is preserved (keep set)
|
|
37
|
+
|
|
38
|
+
When you need to return an image to the caller, make it the block's return value.
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
result = RmagickTidy.scope do
|
|
42
|
+
img = Magick::Image.read("input.jpg").first
|
|
43
|
+
img.resize(800, 600) # returned from the block, so it is kept
|
|
44
|
+
end
|
|
45
|
+
# result is still alive; the original img has been released.
|
|
46
|
+
|
|
47
|
+
result.write("out.jpg")
|
|
48
|
+
result.destroy!
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The keep set walks the return value recursively: a single `Magick::Image`, an `Array<Image>`, the values of a `Hash`, and the elements of a `Magick::ImageList` are all preserved.
|
|
52
|
+
|
|
53
|
+
### Nesting
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
RmagickTidy.scope do # outer scope
|
|
57
|
+
outer = Magick::Image.read("a.jpg").first
|
|
58
|
+
RmagickTidy.scope do # inner scope
|
|
59
|
+
inner = outer.resize(100, 100)
|
|
60
|
+
# inner is released here.
|
|
61
|
+
end
|
|
62
|
+
# outer is still alive.
|
|
63
|
+
end
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Cleanup runs on exceptions too
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
RmagickTidy.scope do
|
|
70
|
+
img = Magick::Image.read("x.jpg").first
|
|
71
|
+
raise "boom"
|
|
72
|
+
end
|
|
73
|
+
# img is released before the exception propagates.
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Bang methods are not registered twice
|
|
77
|
+
|
|
78
|
+
Bang methods such as `resize!` return `self`. `rmagick_tidy` detects this with an `equal?` check and skips re-registration, so the same image is never released twice.
|
|
79
|
+
|
|
80
|
+
## Rails integration
|
|
81
|
+
|
|
82
|
+
After `require "rmagick_tidy"`, a Railtie mixes `within_rmagick_tidy_scope` into `ActionController::Base`.
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
class ImagesController < ApplicationController
|
|
86
|
+
around_action :within_rmagick_tidy_scope
|
|
87
|
+
|
|
88
|
+
def show
|
|
89
|
+
img = Magick::Image.read(@source).first
|
|
90
|
+
@blob = img.resize(800, 600).to_blob { |info| info.format = "JPEG" }
|
|
91
|
+
send_data @blob, type: "image/jpeg"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`to_blob` returns a `String`, so it is never treated as a cleanup target — only the image objects are released.
|
|
97
|
+
|
|
98
|
+
## Strict mode
|
|
99
|
+
|
|
100
|
+
Use strict mode in development or test environments to catch images created outside of a scope.
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
RmagickTidy.configure do |c|
|
|
104
|
+
c.strict_mode = :warn # or :raise / :off (default)
|
|
105
|
+
end
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
- `:off` — do nothing (the production default)
|
|
109
|
+
- `:warn` — print a warning to stderr
|
|
110
|
+
- `:raise` — raise `RmagickTidy::OutOfScopeError`
|
|
111
|
+
|
|
112
|
+
> **Configuration is not thread-safe.** Set `strict_mode` (and any future
|
|
113
|
+
> options) **once at boot** — for example from a Rails initializer — before any
|
|
114
|
+
> worker thread starts using `Magick::Image`. Reading and writing the value
|
|
115
|
+
> from multiple threads concurrently is undefined.
|
|
116
|
+
|
|
117
|
+
## How it works
|
|
118
|
+
|
|
119
|
+
- `Magick::Image` and `Magick::ImageList` are hooked with `Module#prepend`, wrapping every public instance method as well as class methods such as `new`, `read`, and `from_blob`.
|
|
120
|
+
- Each wrapped method **inspects its return value**: if it is a `Magick::Image`, the image is registered with the current scope. Because the gem keeps no method whitelist, it works across a wide range of RMagick versions.
|
|
121
|
+
- If the return value is `equal?` to `self`, it is treated as a bang method and not registered.
|
|
122
|
+
- The scope stack lives in `Thread.current`, so it is safe to use under multi-threaded servers such as Puma.
|
|
123
|
+
- Calling `destroy!` twice is guarded against with a `destroyed?` check and a defensive `rescue`.
|
|
124
|
+
|
|
125
|
+
## Compatibility
|
|
126
|
+
|
|
127
|
+
- Ruby 3.2 or newer
|
|
128
|
+
- RMagick 2.x through 6.x (the return-value approach keeps the gem broadly compatible)
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
module RmagickTidy
|
|
2
|
+
module Hook
|
|
3
|
+
# Methods we never want to wrap. Most of them either return self, return
|
|
4
|
+
# primitives, or are introspection / lifecycle helpers where wrapping would
|
|
5
|
+
# be wasteful or risk recursion.
|
|
6
|
+
SKIP_INSTANCE_METHODS = %i[
|
|
7
|
+
destroy! destroyed? inspect to_s == eql? hash freeze frozen?
|
|
8
|
+
tainted? untrusted? object_id class itself
|
|
9
|
+
].freeze
|
|
10
|
+
|
|
11
|
+
# Class-level methods on Magick::Image that produce new images.
|
|
12
|
+
CLASS_METHODS = %i[new read ping from_blob read_inline capture constitute combine].freeze
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def install!
|
|
17
|
+
return if @installed
|
|
18
|
+
return unless defined?(::Magick::Image)
|
|
19
|
+
|
|
20
|
+
install_instance_hook(::Magick::Image)
|
|
21
|
+
install_class_hook(::Magick::Image)
|
|
22
|
+
|
|
23
|
+
if defined?(::Magick::ImageList)
|
|
24
|
+
install_instance_hook(::Magick::ImageList)
|
|
25
|
+
install_class_hook(::Magick::ImageList)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
@installed = true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def install_instance_hook(klass)
|
|
32
|
+
mod = Module.new do
|
|
33
|
+
def self.inspect = "#<RmagickTidy::InstanceHook>"
|
|
34
|
+
end
|
|
35
|
+
method_names = klass.public_instance_methods(false) - SKIP_INSTANCE_METHODS
|
|
36
|
+
method_names.each do |name|
|
|
37
|
+
next if name.to_s.end_with?("=")
|
|
38
|
+
|
|
39
|
+
define_wrapper(mod, name)
|
|
40
|
+
end
|
|
41
|
+
klass.prepend(mod)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def install_class_hook(klass)
|
|
45
|
+
mod = Module.new do
|
|
46
|
+
def self.inspect = "#<RmagickTidy::ClassHook>"
|
|
47
|
+
end
|
|
48
|
+
CLASS_METHODS.each do |name|
|
|
49
|
+
next unless klass.respond_to?(name)
|
|
50
|
+
|
|
51
|
+
define_wrapper(mod, name)
|
|
52
|
+
end
|
|
53
|
+
klass.singleton_class.prepend(mod)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def define_wrapper(mod, name)
|
|
57
|
+
mod.send(:define_method, name) do |*args, **kwargs, &block|
|
|
58
|
+
result = if kwargs.empty?
|
|
59
|
+
super(*args, &block)
|
|
60
|
+
else
|
|
61
|
+
super(*args, **kwargs, &block)
|
|
62
|
+
end
|
|
63
|
+
::RmagickTidy::Tracker.track(result, self)
|
|
64
|
+
result
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module RmagickTidy
|
|
2
|
+
module ControllerHelper
|
|
3
|
+
# Use as `around_action :within_rmagick_tidy_scope`.
|
|
4
|
+
def within_rmagick_tidy_scope(&)
|
|
5
|
+
RmagickTidy.scope(&)
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
if defined?(::Rails::Railtie)
|
|
10
|
+
class Railtie < ::Rails::Railtie
|
|
11
|
+
initializer "rmagick_tidy.controller" do
|
|
12
|
+
ActiveSupport.on_load(:action_controller) do
|
|
13
|
+
include ::RmagickTidy::ControllerHelper
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
module RmagickTidy
|
|
2
|
+
class Scope
|
|
3
|
+
attr_reader :images, :keeps
|
|
4
|
+
|
|
5
|
+
def initialize
|
|
6
|
+
@images = []
|
|
7
|
+
@keeps = {}.compare_by_identity
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def register(image)
|
|
11
|
+
@images << image
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def keep(image)
|
|
15
|
+
@keeps[image] = true
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def keep?(image)
|
|
19
|
+
@keeps.key?(image)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
module Registry
|
|
24
|
+
STACK_KEY = :rmagick_tidy_stack
|
|
25
|
+
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
def stack
|
|
29
|
+
Thread.current[STACK_KEY] ||= []
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def current
|
|
33
|
+
stack.last
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def in_scope?
|
|
37
|
+
!stack.empty?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def push
|
|
41
|
+
stack.push(Scope.new)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def pop_and_destroy
|
|
45
|
+
scope = stack.pop
|
|
46
|
+
return unless scope
|
|
47
|
+
|
|
48
|
+
seen = {}.compare_by_identity
|
|
49
|
+
scope.images.each do |img|
|
|
50
|
+
next if seen[img]
|
|
51
|
+
|
|
52
|
+
seen[img] = true
|
|
53
|
+
next if scope.keep?(img)
|
|
54
|
+
|
|
55
|
+
destroy_safely(img)
|
|
56
|
+
end
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def destroy_safely(img)
|
|
61
|
+
return unless img
|
|
62
|
+
return if img.respond_to?(:destroyed?) && img.destroyed?
|
|
63
|
+
|
|
64
|
+
img.destroy!
|
|
65
|
+
rescue ::Magick::ImageMagickError
|
|
66
|
+
# Swallow Magick-side destroy errors (e.g. DestroyedImageError on races);
|
|
67
|
+
# the goal is to free what we can without raising from `ensure`. Other
|
|
68
|
+
# exceptions (NoMethodError, ArgumentError, etc.) are intentionally not
|
|
69
|
+
# caught here so unrelated bugs surface early.
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module RmagickTidy
|
|
2
|
+
module Tracker
|
|
3
|
+
module_function
|
|
4
|
+
|
|
5
|
+
def track(result, receiver = nil)
|
|
6
|
+
return result if receiver && result.equal?(receiver)
|
|
7
|
+
|
|
8
|
+
each_image(result) do |img|
|
|
9
|
+
register(img)
|
|
10
|
+
end
|
|
11
|
+
result
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def keep(value)
|
|
15
|
+
scope = Registry.current
|
|
16
|
+
return unless scope
|
|
17
|
+
|
|
18
|
+
each_image(value) do |img|
|
|
19
|
+
scope.keep(img)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def each_image(value, &block)
|
|
24
|
+
case value
|
|
25
|
+
when nil
|
|
26
|
+
# nothing
|
|
27
|
+
when ->(v) { defined?(Magick::Image) && v.is_a?(Magick::Image) }
|
|
28
|
+
yield value
|
|
29
|
+
when ->(v) { defined?(Magick::ImageList) && v.is_a?(Magick::ImageList) }
|
|
30
|
+
value.each { |i| yield i if defined?(Magick::Image) && i.is_a?(Magick::Image) }
|
|
31
|
+
when Array
|
|
32
|
+
value.each { |v| each_image(v, &block) }
|
|
33
|
+
when Hash
|
|
34
|
+
value.each_value { |v| each_image(v, &block) }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def register(img)
|
|
39
|
+
scope = Registry.current
|
|
40
|
+
if scope
|
|
41
|
+
scope.register(img)
|
|
42
|
+
else
|
|
43
|
+
handle_out_of_scope(img)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def handle_out_of_scope(img)
|
|
48
|
+
case RmagickTidy.configuration.strict_mode
|
|
49
|
+
when :raise
|
|
50
|
+
raise OutOfScopeError,
|
|
51
|
+
"Magick::Image #{img.inspect} was created outside of RmagickTidy.scope"
|
|
52
|
+
when :warn
|
|
53
|
+
warn "[rmagick_tidy] Magick::Image created outside of RmagickTidy.scope (#{img.class})"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/rmagick_tidy.rb
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require "rmagick"
|
|
2
|
+
|
|
3
|
+
require "rmagick_tidy/version"
|
|
4
|
+
require "rmagick_tidy/configuration"
|
|
5
|
+
require "rmagick_tidy/registry"
|
|
6
|
+
require "rmagick_tidy/tracker"
|
|
7
|
+
require "rmagick_tidy/hook"
|
|
8
|
+
|
|
9
|
+
module RmagickTidy
|
|
10
|
+
class << self
|
|
11
|
+
def configuration
|
|
12
|
+
@configuration ||= Configuration.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def configure
|
|
16
|
+
yield configuration
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def scope
|
|
20
|
+
Registry.push
|
|
21
|
+
result = yield
|
|
22
|
+
Tracker.keep(result)
|
|
23
|
+
result
|
|
24
|
+
ensure
|
|
25
|
+
Registry.pop_and_destroy
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
RmagickTidy::Hook.install!
|
|
31
|
+
|
|
32
|
+
require "rmagick_tidy/railtie" if defined?(Rails::Railtie)
|
metadata
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rmagick_tidy
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Junichiro Kasuya
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 2026-05-21 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rmagick
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '5.0'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '8'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '5.0'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '8'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: rake
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - "~>"
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '13.0'
|
|
39
|
+
type: :development
|
|
40
|
+
prerelease: false
|
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - "~>"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '13.0'
|
|
46
|
+
- !ruby/object:Gem::Dependency
|
|
47
|
+
name: rspec
|
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - "~>"
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '3.12'
|
|
53
|
+
type: :development
|
|
54
|
+
prerelease: false
|
|
55
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - "~>"
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '3.12'
|
|
60
|
+
- !ruby/object:Gem::Dependency
|
|
61
|
+
name: rubocop
|
|
62
|
+
requirement: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - "~>"
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '1.60'
|
|
67
|
+
type: :development
|
|
68
|
+
prerelease: false
|
|
69
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - "~>"
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: '1.60'
|
|
74
|
+
- !ruby/object:Gem::Dependency
|
|
75
|
+
name: simplecov
|
|
76
|
+
requirement: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - "~>"
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '0.22'
|
|
81
|
+
type: :development
|
|
82
|
+
prerelease: false
|
|
83
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - "~>"
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '0.22'
|
|
88
|
+
- !ruby/object:Gem::Dependency
|
|
89
|
+
name: simplecov-lcov
|
|
90
|
+
requirement: !ruby/object:Gem::Requirement
|
|
91
|
+
requirements:
|
|
92
|
+
- - "~>"
|
|
93
|
+
- !ruby/object:Gem::Version
|
|
94
|
+
version: '0.8'
|
|
95
|
+
type: :development
|
|
96
|
+
prerelease: false
|
|
97
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
98
|
+
requirements:
|
|
99
|
+
- - "~>"
|
|
100
|
+
- !ruby/object:Gem::Version
|
|
101
|
+
version: '0.8'
|
|
102
|
+
description: rmagick_tidy provides a scope block that automatically tracks every Magick::Image
|
|
103
|
+
/ Magick::ImageList produced inside it and calls destroy! on them when the block
|
|
104
|
+
exits, so callers no longer need to litter their code with ensure + destroy! pairs.
|
|
105
|
+
email:
|
|
106
|
+
- junichiro.kasuya@gmail.com
|
|
107
|
+
executables: []
|
|
108
|
+
extensions: []
|
|
109
|
+
extra_rdoc_files: []
|
|
110
|
+
files:
|
|
111
|
+
- CHANGELOG.md
|
|
112
|
+
- LICENSE
|
|
113
|
+
- README.ja.md
|
|
114
|
+
- README.md
|
|
115
|
+
- lib/rmagick_tidy.rb
|
|
116
|
+
- lib/rmagick_tidy/configuration.rb
|
|
117
|
+
- lib/rmagick_tidy/hook.rb
|
|
118
|
+
- lib/rmagick_tidy/railtie.rb
|
|
119
|
+
- lib/rmagick_tidy/registry.rb
|
|
120
|
+
- lib/rmagick_tidy/tracker.rb
|
|
121
|
+
- lib/rmagick_tidy/version.rb
|
|
122
|
+
homepage: https://github.com/jksy/rmagick_tidy
|
|
123
|
+
licenses:
|
|
124
|
+
- MIT
|
|
125
|
+
metadata:
|
|
126
|
+
homepage_uri: https://github.com/jksy/rmagick_tidy
|
|
127
|
+
source_code_uri: https://github.com/jksy/rmagick_tidy/tree/main
|
|
128
|
+
changelog_uri: https://github.com/jksy/rmagick_tidy/blob/main/CHANGELOG.md
|
|
129
|
+
bug_tracker_uri: https://github.com/jksy/rmagick_tidy/issues
|
|
130
|
+
rubygems_mfa_required: 'true'
|
|
131
|
+
rdoc_options: []
|
|
132
|
+
require_paths:
|
|
133
|
+
- lib
|
|
134
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
135
|
+
requirements:
|
|
136
|
+
- - ">="
|
|
137
|
+
- !ruby/object:Gem::Version
|
|
138
|
+
version: 3.2.0
|
|
139
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
140
|
+
requirements:
|
|
141
|
+
- - ">="
|
|
142
|
+
- !ruby/object:Gem::Version
|
|
143
|
+
version: '0'
|
|
144
|
+
requirements: []
|
|
145
|
+
rubygems_version: 3.6.2
|
|
146
|
+
specification_version: 4
|
|
147
|
+
summary: Automatic scope-based memory management for RMagick (Magick::Image).
|
|
148
|
+
test_files: []
|