windraw 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 +24 -0
- data/LICENSE.txt +21 -0
- data/README.md +107 -0
- data/ext/windraw/extconf.rb +35 -0
- data/ext/windraw/windraw.cpp +635 -0
- data/lib/windraw/color.rb +41 -0
- data/lib/windraw/surface.rb +136 -0
- data/lib/windraw/version.rb +5 -0
- data/lib/windraw.rb +30 -0
- metadata +119 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: db764078067f634ec0cedfab09263f585de8ddf731e47296e3c01471dcaf488f
|
|
4
|
+
data.tar.gz: 9ebc3074ddbbf78b48b3e80689616fa002475962d1fbf73580dae387393a86e0
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: e8ff7ea96f7c957ac95ec973480e82f3cf1d0b2a0ead223a4fc5b5746ba7b4ac1c16b58fad2a1bf9b5637202cdf942bdf0b5ae2cccd08ac72abb90178861e36c
|
|
7
|
+
data.tar.gz: fb1f3b325e7b1934ffd0d7479f7297264e907b48128f4475c81aba4c3ef9b0b0835aad35bb209400d02eb6a8669dd868805ea82874b95ba0b63a136984eb2c8c
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format is based on
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
|
|
5
|
+
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] - 2026-05-30
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `Windraw.surface(width, height) { |c| ... }` — create an off-screen Direct2D
|
|
13
|
+
surface, draw, and (with a block) auto-finish.
|
|
14
|
+
- `Surface#clear`, `#rectangle`/`#rect` (with `radius:` for rounded corners),
|
|
15
|
+
`#ellipse`, `#circle`, `#line`, `#polygon`, `#polyline`, and `#text`, all with
|
|
16
|
+
hex-color (`#rgb`/`#rgba`/`#rrggbb`/`#rrggbbaa`) fill/stroke keywords.
|
|
17
|
+
- `Surface#save(path)` — write a PNG via WIC.
|
|
18
|
+
- `Surface#to_png` — return the PNG bytes as a binary String.
|
|
19
|
+
- `Surface#close`/`#dispose` — release COM resources deterministically.
|
|
20
|
+
- `Windraw::Color.parse` — CSS-style hex color to straight RGBA floats.
|
|
21
|
+
- DirectWrite text with font family, size, bold, and italic.
|
|
22
|
+
|
|
23
|
+
[Unreleased]: https://github.com/main-path/windraw/compare/v0.1.0...HEAD
|
|
24
|
+
[0.1.0]: https://github.com/main-path/windraw/releases/tag/v0.1.0
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ned
|
|
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
|
+
# windraw
|
|
2
|
+
|
|
3
|
+
**Headless 2D drawing for Windows — Direct2D + DirectWrite + WIC, straight to PNG.**
|
|
4
|
+
|
|
5
|
+
`windraw` is a small Cairo-style 2D canvas built entirely on the graphics stack
|
|
6
|
+
that already ships with Windows: **Direct2D** for vector shapes, **DirectWrite**
|
|
7
|
+
for text, and **WIC** for PNG encoding. It renders into an off-screen bitmap —
|
|
8
|
+
**no window, no message loop** — so it's perfect for generating images from
|
|
9
|
+
scripts, jobs, and tests.
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
require "windraw"
|
|
13
|
+
|
|
14
|
+
Windraw.surface(800, 600) do |c|
|
|
15
|
+
c.clear("#1e1e2e")
|
|
16
|
+
c.rectangle(50, 50, 200, 120, fill: "#89b4fa", stroke: "#ffffff", width: 3)
|
|
17
|
+
c.ellipse(400, 300, 120, 80, fill: "#a6e3a1aa")
|
|
18
|
+
c.circle(650, 150, 60, fill: "#f38ba8")
|
|
19
|
+
c.line(50, 550, 750, 450, color: "#fab387", width: 6)
|
|
20
|
+
c.text("Hello, Direct2D!", 60, 220, font: "Segoe UI", size: 40, color: "#cdd6f4", bold: true)
|
|
21
|
+
end.save("hello.png")
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Requirements
|
|
25
|
+
|
|
26
|
+
- **Windows** with a native **MSVC (mswin)** Ruby (`x64-mswin64`). On a
|
|
27
|
+
MinGW/UCRT Ruby this gem is not supported — its `extconf.rb` will say so.
|
|
28
|
+
- Visual Studio 2017+ or the Build Tools with the **Desktop development with
|
|
29
|
+
C++** workload (for `cl.exe` + the Windows SDK headers/libs).
|
|
30
|
+
|
|
31
|
+
> Building uses [`vcvars`](https://rubygems.org/gems/vcvars) to load the MSVC
|
|
32
|
+
> toolchain automatically — no "Developer Command Prompt" needed.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
gem install windraw
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## API
|
|
41
|
+
|
|
42
|
+
### `Windraw.surface(width, height) { |c| ... } → Surface`
|
|
43
|
+
|
|
44
|
+
Creates an off-screen surface (pixels; origin top-left). With a block, it yields
|
|
45
|
+
the surface, finishes drawing when the block returns, and returns the surface so
|
|
46
|
+
you can chain `#save` / `#to_png`. All drawing methods return `self`.
|
|
47
|
+
|
|
48
|
+
### Drawing
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
c.clear("#1e1e2e") # fill the whole canvas
|
|
52
|
+
c.rectangle(x, y, w, h, fill:, stroke:, width: 1.0, radius: nil) # alias: c.rect; radius rounds corners
|
|
53
|
+
c.ellipse(cx, cy, rx, ry, fill:, stroke:, width: 1.0)
|
|
54
|
+
c.circle(cx, cy, radius, fill:, stroke:, width: 1.0)
|
|
55
|
+
c.line(x1, y1, x2, y2, color: "#000000", width: 1.0)
|
|
56
|
+
c.polygon([[x, y], ...], fill:, stroke:, width: 1.0) # closed
|
|
57
|
+
c.polyline([[x, y], ...], color: "#000000", width: 1.0) # open
|
|
58
|
+
c.text(str, x, y, color:, font: "Segoe UI", size: 16, bold: false, italic: false)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`radius:` takes a single Numeric (`rx == ry`) or an `[rx, ry]` pair.
|
|
62
|
+
|
|
63
|
+
### Output & lifecycle
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
surface.save("out.png") # => "out.png"
|
|
67
|
+
surface.to_png # => String (PNG bytes, ASCII-8BIT)
|
|
68
|
+
surface.size # => [width, height]
|
|
69
|
+
surface.close # release COM resources now (alias: dispose; GC also frees)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
> **Roadmap (0.2):** gradient brushes, arcs/bezier paths, and image compositing
|
|
73
|
+
> (load + draw existing PNGs).
|
|
74
|
+
|
|
75
|
+
`fill:` and `stroke:` take hex colors and are optional; passing both draws the
|
|
76
|
+
fill first, then the stroke.
|
|
77
|
+
|
|
78
|
+
### Colors
|
|
79
|
+
|
|
80
|
+
Any CSS-style hex string: `#rgb`, `#rgba`, `#rrggbb`, or `#rrggbbaa` (the `#` is
|
|
81
|
+
optional). Alpha is supported everywhere.
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
Windraw::Color.parse("#ff8800") # => [1.0, 0.533..., 0.0, 1.0]
|
|
85
|
+
Windraw::Color.parse("#0af5") # => [0.0, 0.666..., 1.0, 0.333...]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Output
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
surface.save("out.png") # => "out.png" (writes a PNG via WIC)
|
|
92
|
+
surface.to_png # => String (PNG bytes, ASCII-8BIT)
|
|
93
|
+
surface.size # => [width, height]
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## How it works
|
|
97
|
+
|
|
98
|
+
A `Surface` creates a WIC bitmap and a Direct2D **WIC bitmap render target** at
|
|
99
|
+
96 DPI (so 1 unit == 1 pixel). Drawing calls go straight to Direct2D /
|
|
100
|
+
DirectWrite; `save`/`to_png` flush the render target (`EndDraw`) and encode the
|
|
101
|
+
bitmap to PNG with WIC — to a file stream or an in-memory stream. COM is
|
|
102
|
+
initialized per surface; drawing is intended to happen on the thread that
|
|
103
|
+
created the surface.
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
[MIT](LICENSE.txt).
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
#
|
|
3
|
+
# extconf.rb for the windraw C++ extension (Direct2D + DirectWrite + WIC).
|
|
4
|
+
#
|
|
5
|
+
# windraw is Windows-MSVC-only: it links the Direct2D/DirectWrite/WIC system
|
|
6
|
+
# import libraries and uses the MSVC C++ compiler. mkmf auto-discovers the .cpp
|
|
7
|
+
# source and compiles it as C++ (its CXX_EXT list includes "cpp").
|
|
8
|
+
|
|
9
|
+
require "mkmf"
|
|
10
|
+
|
|
11
|
+
unless RbConfig::CONFIG["target_os"] =~ /mswin/
|
|
12
|
+
abort <<~MSG
|
|
13
|
+
windraw requires a native Windows MSVC (mswin) Ruby — it builds against
|
|
14
|
+
Direct2D, DirectWrite, and WIC with cl.exe. Your Ruby is
|
|
15
|
+
"#{RbConfig::CONFIG['arch']}". On a MinGW/UCRT Ruby this gem is not supported.
|
|
16
|
+
MSG
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# C++ exceptions are not enabled by the stock mswin CXXFLAGS; the extension uses
|
|
20
|
+
# std::wstring (and C++ COM patterns), so turn them on.
|
|
21
|
+
$CXXFLAGS << " -EHsc"
|
|
22
|
+
|
|
23
|
+
# System import libraries. On mswin, mkmf expects bare "NAME.lib" tokens in
|
|
24
|
+
# $libs (no -l prefix); they land on the link line after the Ruby import lib.
|
|
25
|
+
# d2d1 - Direct2D (D2D1CreateFactory, render target, brushes, shapes)
|
|
26
|
+
# dwrite - DirectWrite (text formats + DrawText)
|
|
27
|
+
# windowscodecs - WIC (imaging factory, bitmap, PNG encoder, WIC CLSIDs)
|
|
28
|
+
# ole32 - COM (CoInitializeEx / CoCreateInstance / CoUninitialize)
|
|
29
|
+
# uuid - additional CLSID/IID definitions
|
|
30
|
+
# shlwapi - SHCreateMemStream (in-memory PNG for #to_png)
|
|
31
|
+
$libs = [$libs, "d2d1.lib", "dwrite.lib", "windowscodecs.lib",
|
|
32
|
+
"ole32.lib", "uuid.lib", "shlwapi.lib"].join(" ")
|
|
33
|
+
|
|
34
|
+
# Produces lib/windraw/windraw.so, required as "windraw/windraw".
|
|
35
|
+
create_makefile("windraw/windraw")
|
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* windraw — headless 2D drawing for Windows via Direct2D + DirectWrite + WIC.
|
|
3
|
+
*
|
|
4
|
+
* Renders shapes and text into an off-screen WIC bitmap through a Direct2D
|
|
5
|
+
* render target, and encodes the result to PNG (to a file or an in-memory
|
|
6
|
+
* Ruby string). No window / message pump required.
|
|
7
|
+
*
|
|
8
|
+
* Build notes (verified on Ruby 3.4 x64-mswin64_140, MSVC cl 19.5):
|
|
9
|
+
* - <ruby.h> MUST come before <windows.h> (winsock2 ordering / macro hygiene).
|
|
10
|
+
* - <windows.h> #defines DrawText as DrawTextW (a GDI macro) and OUT/IN as SAL
|
|
11
|
+
* markers; we #undef DrawText so rt->DrawText resolves to the D2D method, and
|
|
12
|
+
* we never use OUT/IN as identifiers.
|
|
13
|
+
* - All COM calls are checked; failures are released-then-raised so an rb_raise
|
|
14
|
+
* longjmp never skips a Release. Numeric/string args are converted up front,
|
|
15
|
+
* before any COM object exists, for the same reason.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
#include <ruby.h>
|
|
19
|
+
#include <ruby/encoding.h>
|
|
20
|
+
|
|
21
|
+
#define WIN32_LEAN_AND_MEAN
|
|
22
|
+
#include <windows.h>
|
|
23
|
+
#include <d2d1.h>
|
|
24
|
+
#include <d2d1helper.h>
|
|
25
|
+
#include <dwrite.h>
|
|
26
|
+
#include <wincodec.h>
|
|
27
|
+
#include <shlwapi.h>
|
|
28
|
+
|
|
29
|
+
#include <string>
|
|
30
|
+
#include <cstdlib> /* malloc / free for the longjmp-safe PNG buffer */
|
|
31
|
+
#include <climits> /* LONG_MAX */
|
|
32
|
+
|
|
33
|
+
/* NOTE on DrawText: <windows.h> defines `DrawText` as the GDI macro `DrawTextW`.
|
|
34
|
+
* Because that macro is active when <d2d1.h> is parsed, ID2D1RenderTarget's
|
|
35
|
+
* method is actually declared as `DrawTextW`, and our call site is rewritten the
|
|
36
|
+
* same way — so they match. Do NOT #undef DrawText here: that would desync the
|
|
37
|
+
* call (DrawText) from the real member (DrawTextW) -> C2039. */
|
|
38
|
+
|
|
39
|
+
template <class T>
|
|
40
|
+
static void SafeRelease(T** pp) {
|
|
41
|
+
if (*pp) { (*pp)->Release(); *pp = nullptr; }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
typedef struct WindrawSurface {
|
|
45
|
+
UINT width;
|
|
46
|
+
UINT height;
|
|
47
|
+
DWORD thread_id; /* thread that ran CoInitializeEx (teardown must match) */
|
|
48
|
+
bool com_owned; /* did our CoInitializeEx succeed (so we must CoUninitialize)? */
|
|
49
|
+
bool draw_open; /* BeginDraw issued, EndDraw not yet */
|
|
50
|
+
bool finished; /* EndDraw done; drawing no longer allowed */
|
|
51
|
+
bool closed; /* COM objects released + CoUninitialize done */
|
|
52
|
+
ID2D1Factory* d2d;
|
|
53
|
+
IWICImagingFactory* wic;
|
|
54
|
+
IWICBitmap* bitmap;
|
|
55
|
+
ID2D1RenderTarget* rt;
|
|
56
|
+
IDWriteFactory* dwrite; /* created lazily on first text() */
|
|
57
|
+
} WindrawSurface;
|
|
58
|
+
|
|
59
|
+
/* ---- TypedData plumbing -------------------------------------------------- */
|
|
60
|
+
|
|
61
|
+
/* Release every COM object and balance CoInitializeEx. MUST run on the thread
|
|
62
|
+
* that created the surface (COM init is per-thread; the D2D factory is
|
|
63
|
+
* single-threaded). */
|
|
64
|
+
static void surface_teardown(WindrawSurface* s) {
|
|
65
|
+
if (s->draw_open && s->rt) { s->rt->EndDraw(); s->draw_open = false; }
|
|
66
|
+
SafeRelease(&s->dwrite);
|
|
67
|
+
SafeRelease(&s->rt);
|
|
68
|
+
SafeRelease(&s->bitmap);
|
|
69
|
+
SafeRelease(&s->wic);
|
|
70
|
+
SafeRelease(&s->d2d);
|
|
71
|
+
if (s->com_owned) { CoUninitialize(); s->com_owned = false; }
|
|
72
|
+
s->closed = true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static void surface_free(void* p) {
|
|
76
|
+
WindrawSurface* s = static_cast<WindrawSurface*>(p);
|
|
77
|
+
if (!s) return;
|
|
78
|
+
if (!s->closed) {
|
|
79
|
+
/* COM init/uninit is per-thread and the D2D factory is single-threaded,
|
|
80
|
+
* so we can only tear down safely on the creating thread. GC may run
|
|
81
|
+
* this free on another thread; if so, skip COM teardown (a small leak)
|
|
82
|
+
* rather than corrupt COM state. Use #close on the creating thread for
|
|
83
|
+
* deterministic cleanup in multi-threaded programs. */
|
|
84
|
+
if (GetCurrentThreadId() == s->thread_id) surface_teardown(s);
|
|
85
|
+
}
|
|
86
|
+
xfree(s);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
static size_t surface_memsize(const void* p) {
|
|
90
|
+
(void)p;
|
|
91
|
+
return sizeof(WindrawSurface);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
static const rb_data_type_t surface_type = {
|
|
95
|
+
"Windraw::Surface",
|
|
96
|
+
{ 0, surface_free, surface_memsize },
|
|
97
|
+
0, 0, RUBY_TYPED_FREE_IMMEDIATELY
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
static VALUE surface_alloc(VALUE klass) {
|
|
101
|
+
WindrawSurface* s;
|
|
102
|
+
return TypedData_Make_Struct(klass, WindrawSurface, &surface_type, s);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
static WindrawSurface* get_surface(VALUE self) {
|
|
106
|
+
WindrawSurface* s;
|
|
107
|
+
TypedData_Get_Struct(self, WindrawSurface, &surface_type, s);
|
|
108
|
+
return s;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* ---- helpers ------------------------------------------------------------- */
|
|
112
|
+
|
|
113
|
+
static void raise_hr(const char* what, HRESULT hr) {
|
|
114
|
+
rb_raise(rb_eRuntimeError, "windraw: %s failed (hr=0x%08lX)", what,
|
|
115
|
+
static_cast<unsigned long>(hr));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
static void ensure_drawable(WindrawSurface* s) {
|
|
119
|
+
if (!s->rt) rb_raise(rb_eRuntimeError, "windraw: surface is not initialized");
|
|
120
|
+
if (s->finished || !s->draw_open)
|
|
121
|
+
rb_raise(rb_eRuntimeError, "windraw: surface is finished; cannot draw on it");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* Convert a Ruby String (any encoding) to a UTF-16 std::wstring. */
|
|
125
|
+
static std::wstring to_utf16(VALUE str) {
|
|
126
|
+
str = rb_str_export_to_enc(StringValue(str), rb_utf8_encoding());
|
|
127
|
+
long bytelen = RSTRING_LEN(str);
|
|
128
|
+
if (bytelen <= 0) return std::wstring();
|
|
129
|
+
const char* p = RSTRING_PTR(str);
|
|
130
|
+
int need = MultiByteToWideChar(CP_UTF8, 0, p, static_cast<int>(bytelen), nullptr, 0);
|
|
131
|
+
if (need <= 0) return std::wstring();
|
|
132
|
+
std::wstring w(static_cast<size_t>(need), L'\0');
|
|
133
|
+
MultiByteToWideChar(CP_UTF8, 0, p, static_cast<int>(bytelen), &w[0], need);
|
|
134
|
+
return w;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* WIC-encode the surface's bitmap to a PNG written into `out`. Releases its own
|
|
138
|
+
* locals and returns the HRESULT; never raises (caller raises after cleanup). */
|
|
139
|
+
static HRESULT encode_png(WindrawSurface* s, IStream* out) {
|
|
140
|
+
IWICBitmapEncoder* encoder = nullptr;
|
|
141
|
+
IWICBitmapFrameEncode* frame = nullptr;
|
|
142
|
+
|
|
143
|
+
HRESULT hr = s->wic->CreateEncoder(GUID_ContainerFormatPng, nullptr, &encoder);
|
|
144
|
+
if (SUCCEEDED(hr)) hr = encoder->Initialize(out, WICBitmapEncoderNoCache);
|
|
145
|
+
if (SUCCEEDED(hr)) hr = encoder->CreateNewFrame(&frame, nullptr);
|
|
146
|
+
if (SUCCEEDED(hr)) hr = frame->Initialize(nullptr);
|
|
147
|
+
if (SUCCEEDED(hr)) hr = frame->SetSize(s->width, s->height);
|
|
148
|
+
if (SUCCEEDED(hr)) {
|
|
149
|
+
WICPixelFormatGUID pf = GUID_WICPixelFormat32bppPBGRA;
|
|
150
|
+
hr = frame->SetPixelFormat(&pf);
|
|
151
|
+
}
|
|
152
|
+
if (SUCCEEDED(hr)) hr = frame->WriteSource(s->bitmap, nullptr);
|
|
153
|
+
if (SUCCEEDED(hr)) hr = frame->Commit();
|
|
154
|
+
if (SUCCEEDED(hr)) hr = encoder->Commit();
|
|
155
|
+
|
|
156
|
+
SafeRelease(&frame);
|
|
157
|
+
SafeRelease(&encoder);
|
|
158
|
+
return hr;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* ---- methods ------------------------------------------------------------- */
|
|
162
|
+
|
|
163
|
+
static VALUE surface_initialize(VALUE self, VALUE vw, VALUE vh) {
|
|
164
|
+
WindrawSurface* s = get_surface(self);
|
|
165
|
+
int w = NUM2INT(vw);
|
|
166
|
+
int h = NUM2INT(vh);
|
|
167
|
+
if (w <= 0 || h <= 0) rb_raise(rb_eArgError, "width and height must be positive");
|
|
168
|
+
s->width = static_cast<UINT>(w);
|
|
169
|
+
s->height = static_cast<UINT>(h);
|
|
170
|
+
s->thread_id = GetCurrentThreadId();
|
|
171
|
+
|
|
172
|
+
HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
|
173
|
+
if (hr == S_OK || hr == S_FALSE) {
|
|
174
|
+
s->com_owned = true; /* we incremented the init count; balance it */
|
|
175
|
+
} else if (hr == RPC_E_CHANGED_MODE) {
|
|
176
|
+
s->com_owned = false; /* thread already in another apartment; usable, not ours */
|
|
177
|
+
} else {
|
|
178
|
+
raise_hr("CoInitializeEx", hr);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
hr = D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, __uuidof(ID2D1Factory),
|
|
182
|
+
reinterpret_cast<void**>(&s->d2d));
|
|
183
|
+
if (FAILED(hr)) raise_hr("D2D1CreateFactory", hr);
|
|
184
|
+
|
|
185
|
+
hr = CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER,
|
|
186
|
+
IID_PPV_ARGS(&s->wic));
|
|
187
|
+
if (FAILED(hr)) raise_hr("CoCreateInstance(WICImagingFactory)", hr);
|
|
188
|
+
|
|
189
|
+
hr = s->wic->CreateBitmap(s->width, s->height, GUID_WICPixelFormat32bppPBGRA,
|
|
190
|
+
WICBitmapCacheOnLoad, &s->bitmap);
|
|
191
|
+
if (FAILED(hr)) raise_hr("CreateBitmap", hr);
|
|
192
|
+
|
|
193
|
+
/* DPI 96 => 1 DIP == 1 pixel, so all coordinates are exact pixels. */
|
|
194
|
+
D2D1_RENDER_TARGET_PROPERTIES props = D2D1::RenderTargetProperties(
|
|
195
|
+
D2D1_RENDER_TARGET_TYPE_DEFAULT,
|
|
196
|
+
D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED),
|
|
197
|
+
96.0f, 96.0f);
|
|
198
|
+
hr = s->d2d->CreateWicBitmapRenderTarget(s->bitmap, props, &s->rt);
|
|
199
|
+
if (FAILED(hr)) raise_hr("CreateWicBitmapRenderTarget", hr);
|
|
200
|
+
|
|
201
|
+
s->rt->BeginDraw();
|
|
202
|
+
s->draw_open = true;
|
|
203
|
+
s->rt->Clear(D2D1::ColorF(0.0f, 0.0f, 0.0f, 0.0f)); /* transparent canvas */
|
|
204
|
+
return self;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
static VALUE surface_clear(VALUE self, VALUE vr, VALUE vg, VALUE vb, VALUE va) {
|
|
208
|
+
WindrawSurface* s = get_surface(self);
|
|
209
|
+
double r = NUM2DBL(vr), g = NUM2DBL(vg), b = NUM2DBL(vb), a = NUM2DBL(va);
|
|
210
|
+
ensure_drawable(s);
|
|
211
|
+
s->rt->Clear(D2D1::ColorF(static_cast<float>(r), static_cast<float>(g),
|
|
212
|
+
static_cast<float>(b), static_cast<float>(a)));
|
|
213
|
+
return self;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
static VALUE surface_fill_rect(VALUE self, VALUE vx, VALUE vy, VALUE vw, VALUE vh,
|
|
217
|
+
VALUE vr, VALUE vg, VALUE vb, VALUE va) {
|
|
218
|
+
WindrawSurface* s = get_surface(self);
|
|
219
|
+
double x = NUM2DBL(vx), y = NUM2DBL(vy), w = NUM2DBL(vw), h = NUM2DBL(vh);
|
|
220
|
+
double r = NUM2DBL(vr), g = NUM2DBL(vg), b = NUM2DBL(vb), a = NUM2DBL(va);
|
|
221
|
+
ensure_drawable(s);
|
|
222
|
+
|
|
223
|
+
ID2D1SolidColorBrush* brush = nullptr;
|
|
224
|
+
HRESULT hr = s->rt->CreateSolidColorBrush(
|
|
225
|
+
D2D1::ColorF(static_cast<float>(r), static_cast<float>(g),
|
|
226
|
+
static_cast<float>(b), static_cast<float>(a)), &brush);
|
|
227
|
+
if (SUCCEEDED(hr)) {
|
|
228
|
+
s->rt->FillRectangle(
|
|
229
|
+
D2D1::RectF(static_cast<float>(x), static_cast<float>(y),
|
|
230
|
+
static_cast<float>(x + w), static_cast<float>(y + h)), brush);
|
|
231
|
+
}
|
|
232
|
+
SafeRelease(&brush);
|
|
233
|
+
if (FAILED(hr)) raise_hr("CreateSolidColorBrush", hr);
|
|
234
|
+
return self;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
static VALUE surface_stroke_rect(VALUE self, VALUE vx, VALUE vy, VALUE vw, VALUE vh,
|
|
238
|
+
VALUE vr, VALUE vg, VALUE vb, VALUE va, VALUE vwidth) {
|
|
239
|
+
WindrawSurface* s = get_surface(self);
|
|
240
|
+
double x = NUM2DBL(vx), y = NUM2DBL(vy), w = NUM2DBL(vw), h = NUM2DBL(vh);
|
|
241
|
+
double r = NUM2DBL(vr), g = NUM2DBL(vg), b = NUM2DBL(vb), a = NUM2DBL(va);
|
|
242
|
+
double sw = NUM2DBL(vwidth);
|
|
243
|
+
ensure_drawable(s);
|
|
244
|
+
|
|
245
|
+
ID2D1SolidColorBrush* brush = nullptr;
|
|
246
|
+
HRESULT hr = s->rt->CreateSolidColorBrush(
|
|
247
|
+
D2D1::ColorF(static_cast<float>(r), static_cast<float>(g),
|
|
248
|
+
static_cast<float>(b), static_cast<float>(a)), &brush);
|
|
249
|
+
if (SUCCEEDED(hr)) {
|
|
250
|
+
s->rt->DrawRectangle(
|
|
251
|
+
D2D1::RectF(static_cast<float>(x), static_cast<float>(y),
|
|
252
|
+
static_cast<float>(x + w), static_cast<float>(y + h)),
|
|
253
|
+
brush, static_cast<float>(sw));
|
|
254
|
+
}
|
|
255
|
+
SafeRelease(&brush);
|
|
256
|
+
if (FAILED(hr)) raise_hr("CreateSolidColorBrush", hr);
|
|
257
|
+
return self;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
static VALUE surface_fill_ellipse(VALUE self, VALUE vcx, VALUE vcy, VALUE vrx, VALUE vry,
|
|
261
|
+
VALUE vr, VALUE vg, VALUE vb, VALUE va) {
|
|
262
|
+
WindrawSurface* s = get_surface(self);
|
|
263
|
+
double cx = NUM2DBL(vcx), cy = NUM2DBL(vcy), rx = NUM2DBL(vrx), ry = NUM2DBL(vry);
|
|
264
|
+
double r = NUM2DBL(vr), g = NUM2DBL(vg), b = NUM2DBL(vb), a = NUM2DBL(va);
|
|
265
|
+
ensure_drawable(s);
|
|
266
|
+
|
|
267
|
+
ID2D1SolidColorBrush* brush = nullptr;
|
|
268
|
+
HRESULT hr = s->rt->CreateSolidColorBrush(
|
|
269
|
+
D2D1::ColorF(static_cast<float>(r), static_cast<float>(g),
|
|
270
|
+
static_cast<float>(b), static_cast<float>(a)), &brush);
|
|
271
|
+
if (SUCCEEDED(hr)) {
|
|
272
|
+
s->rt->FillEllipse(
|
|
273
|
+
D2D1::Ellipse(D2D1::Point2F(static_cast<float>(cx), static_cast<float>(cy)),
|
|
274
|
+
static_cast<float>(rx), static_cast<float>(ry)), brush);
|
|
275
|
+
}
|
|
276
|
+
SafeRelease(&brush);
|
|
277
|
+
if (FAILED(hr)) raise_hr("CreateSolidColorBrush", hr);
|
|
278
|
+
return self;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
static VALUE surface_stroke_ellipse(VALUE self, VALUE vcx, VALUE vcy, VALUE vrx, VALUE vry,
|
|
282
|
+
VALUE vr, VALUE vg, VALUE vb, VALUE va, VALUE vwidth) {
|
|
283
|
+
WindrawSurface* s = get_surface(self);
|
|
284
|
+
double cx = NUM2DBL(vcx), cy = NUM2DBL(vcy), rx = NUM2DBL(vrx), ry = NUM2DBL(vry);
|
|
285
|
+
double r = NUM2DBL(vr), g = NUM2DBL(vg), b = NUM2DBL(vb), a = NUM2DBL(va);
|
|
286
|
+
double sw = NUM2DBL(vwidth);
|
|
287
|
+
ensure_drawable(s);
|
|
288
|
+
|
|
289
|
+
ID2D1SolidColorBrush* brush = nullptr;
|
|
290
|
+
HRESULT hr = s->rt->CreateSolidColorBrush(
|
|
291
|
+
D2D1::ColorF(static_cast<float>(r), static_cast<float>(g),
|
|
292
|
+
static_cast<float>(b), static_cast<float>(a)), &brush);
|
|
293
|
+
if (SUCCEEDED(hr)) {
|
|
294
|
+
s->rt->DrawEllipse(
|
|
295
|
+
D2D1::Ellipse(D2D1::Point2F(static_cast<float>(cx), static_cast<float>(cy)),
|
|
296
|
+
static_cast<float>(rx), static_cast<float>(ry)),
|
|
297
|
+
brush, static_cast<float>(sw));
|
|
298
|
+
}
|
|
299
|
+
SafeRelease(&brush);
|
|
300
|
+
if (FAILED(hr)) raise_hr("CreateSolidColorBrush", hr);
|
|
301
|
+
return self;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
static VALUE surface_line(VALUE self, VALUE vx1, VALUE vy1, VALUE vx2, VALUE vy2,
|
|
305
|
+
VALUE vr, VALUE vg, VALUE vb, VALUE va, VALUE vwidth) {
|
|
306
|
+
WindrawSurface* s = get_surface(self);
|
|
307
|
+
double x1 = NUM2DBL(vx1), y1 = NUM2DBL(vy1), x2 = NUM2DBL(vx2), y2 = NUM2DBL(vy2);
|
|
308
|
+
double r = NUM2DBL(vr), g = NUM2DBL(vg), b = NUM2DBL(vb), a = NUM2DBL(va);
|
|
309
|
+
double sw = NUM2DBL(vwidth);
|
|
310
|
+
ensure_drawable(s);
|
|
311
|
+
|
|
312
|
+
ID2D1SolidColorBrush* brush = nullptr;
|
|
313
|
+
HRESULT hr = s->rt->CreateSolidColorBrush(
|
|
314
|
+
D2D1::ColorF(static_cast<float>(r), static_cast<float>(g),
|
|
315
|
+
static_cast<float>(b), static_cast<float>(a)), &brush);
|
|
316
|
+
if (SUCCEEDED(hr)) {
|
|
317
|
+
s->rt->DrawLine(
|
|
318
|
+
D2D1::Point2F(static_cast<float>(x1), static_cast<float>(y1)),
|
|
319
|
+
D2D1::Point2F(static_cast<float>(x2), static_cast<float>(y2)),
|
|
320
|
+
brush, static_cast<float>(sw));
|
|
321
|
+
}
|
|
322
|
+
SafeRelease(&brush);
|
|
323
|
+
if (FAILED(hr)) raise_hr("CreateSolidColorBrush", hr);
|
|
324
|
+
return self;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
static VALUE surface_fill_round_rect(VALUE self, VALUE vx, VALUE vy, VALUE vw, VALUE vh,
|
|
328
|
+
VALUE vrx, VALUE vry,
|
|
329
|
+
VALUE vr, VALUE vg, VALUE vb, VALUE va) {
|
|
330
|
+
WindrawSurface* s = get_surface(self);
|
|
331
|
+
double x = NUM2DBL(vx), y = NUM2DBL(vy), w = NUM2DBL(vw), h = NUM2DBL(vh);
|
|
332
|
+
double rx = NUM2DBL(vrx), ry = NUM2DBL(vry);
|
|
333
|
+
double r = NUM2DBL(vr), g = NUM2DBL(vg), b = NUM2DBL(vb), a = NUM2DBL(va);
|
|
334
|
+
ensure_drawable(s);
|
|
335
|
+
|
|
336
|
+
ID2D1SolidColorBrush* brush = nullptr;
|
|
337
|
+
HRESULT hr = s->rt->CreateSolidColorBrush(
|
|
338
|
+
D2D1::ColorF(static_cast<float>(r), static_cast<float>(g),
|
|
339
|
+
static_cast<float>(b), static_cast<float>(a)), &brush);
|
|
340
|
+
if (SUCCEEDED(hr)) {
|
|
341
|
+
D2D1_ROUNDED_RECT rr = D2D1::RoundedRect(
|
|
342
|
+
D2D1::RectF(static_cast<float>(x), static_cast<float>(y),
|
|
343
|
+
static_cast<float>(x + w), static_cast<float>(y + h)),
|
|
344
|
+
static_cast<float>(rx), static_cast<float>(ry));
|
|
345
|
+
s->rt->FillRoundedRectangle(rr, brush);
|
|
346
|
+
}
|
|
347
|
+
SafeRelease(&brush);
|
|
348
|
+
if (FAILED(hr)) raise_hr("CreateSolidColorBrush", hr);
|
|
349
|
+
return self;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
static VALUE surface_stroke_round_rect(VALUE self, VALUE vx, VALUE vy, VALUE vw, VALUE vh,
|
|
353
|
+
VALUE vrx, VALUE vry,
|
|
354
|
+
VALUE vr, VALUE vg, VALUE vb, VALUE va, VALUE vwidth) {
|
|
355
|
+
WindrawSurface* s = get_surface(self);
|
|
356
|
+
double x = NUM2DBL(vx), y = NUM2DBL(vy), w = NUM2DBL(vw), h = NUM2DBL(vh);
|
|
357
|
+
double rx = NUM2DBL(vrx), ry = NUM2DBL(vry);
|
|
358
|
+
double r = NUM2DBL(vr), g = NUM2DBL(vg), b = NUM2DBL(vb), a = NUM2DBL(va);
|
|
359
|
+
double sw = NUM2DBL(vwidth);
|
|
360
|
+
ensure_drawable(s);
|
|
361
|
+
|
|
362
|
+
ID2D1SolidColorBrush* brush = nullptr;
|
|
363
|
+
HRESULT hr = s->rt->CreateSolidColorBrush(
|
|
364
|
+
D2D1::ColorF(static_cast<float>(r), static_cast<float>(g),
|
|
365
|
+
static_cast<float>(b), static_cast<float>(a)), &brush);
|
|
366
|
+
if (SUCCEEDED(hr)) {
|
|
367
|
+
D2D1_ROUNDED_RECT rr = D2D1::RoundedRect(
|
|
368
|
+
D2D1::RectF(static_cast<float>(x), static_cast<float>(y),
|
|
369
|
+
static_cast<float>(x + w), static_cast<float>(y + h)),
|
|
370
|
+
static_cast<float>(rx), static_cast<float>(ry));
|
|
371
|
+
s->rt->DrawRoundedRectangle(rr, brush, static_cast<float>(sw));
|
|
372
|
+
}
|
|
373
|
+
SafeRelease(&brush);
|
|
374
|
+
if (FAILED(hr)) raise_hr("CreateSolidColorBrush", hr);
|
|
375
|
+
return self;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/* Build a closed/open path geometry from a flat [x0,y0,x1,y1,...] Ruby array.
|
|
379
|
+
* Returns the geometry (caller Releases) or raises. All point values are
|
|
380
|
+
* validated and extracted into a malloc'd buffer up front, so no rb_raise can
|
|
381
|
+
* longjmp while a COM object or the buffer is held un-freed. */
|
|
382
|
+
static ID2D1PathGeometry* build_polygon_geometry(WindrawSurface* s, VALUE arr,
|
|
383
|
+
bool closed, bool filled) {
|
|
384
|
+
Check_Type(arr, T_ARRAY);
|
|
385
|
+
long n = RARRAY_LEN(arr);
|
|
386
|
+
if (n < 4 || (n % 2) != 0) {
|
|
387
|
+
rb_raise(rb_eArgError,
|
|
388
|
+
"windraw: polygon/polyline needs an even, >= 2 list of x,y coordinates");
|
|
389
|
+
}
|
|
390
|
+
for (long i = 0; i < n; i++) {
|
|
391
|
+
VALUE e = rb_ary_entry(arr, i);
|
|
392
|
+
if (!RTEST(rb_obj_is_kind_of(e, rb_cNumeric)))
|
|
393
|
+
rb_raise(rb_eTypeError, "windraw: polygon points must be numbers");
|
|
394
|
+
/* Force the float conversion now: a Numeric that can't become a Float
|
|
395
|
+
* (e.g. a Complex with imaginary part) raises HERE, before malloc, so
|
|
396
|
+
* the longjmp can't skip free(pts). */
|
|
397
|
+
(void)NUM2DBL(e);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
long npoints = n / 2;
|
|
401
|
+
D2D1_POINT_2F* pts =
|
|
402
|
+
static_cast<D2D1_POINT_2F*>(malloc(sizeof(D2D1_POINT_2F) * static_cast<size_t>(npoints)));
|
|
403
|
+
if (!pts) rb_raise(rb_eNoMemError, "windraw: out of memory building polygon");
|
|
404
|
+
for (long i = 0; i < npoints; i++) {
|
|
405
|
+
pts[i].x = static_cast<float>(NUM2DBL(rb_ary_entry(arr, 2 * i)));
|
|
406
|
+
pts[i].y = static_cast<float>(NUM2DBL(rb_ary_entry(arr, 2 * i + 1)));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
ID2D1PathGeometry* geo = nullptr;
|
|
410
|
+
ID2D1GeometrySink* sink = nullptr;
|
|
411
|
+
HRESULT hr = s->d2d->CreatePathGeometry(&geo);
|
|
412
|
+
if (SUCCEEDED(hr)) hr = geo->Open(&sink);
|
|
413
|
+
if (SUCCEEDED(hr)) {
|
|
414
|
+
sink->BeginFigure(pts[0],
|
|
415
|
+
filled ? D2D1_FIGURE_BEGIN_FILLED : D2D1_FIGURE_BEGIN_HOLLOW);
|
|
416
|
+
if (npoints > 1) sink->AddLines(pts + 1, static_cast<UINT32>(npoints - 1));
|
|
417
|
+
sink->EndFigure(closed ? D2D1_FIGURE_END_CLOSED : D2D1_FIGURE_END_OPEN);
|
|
418
|
+
hr = sink->Close();
|
|
419
|
+
}
|
|
420
|
+
SafeRelease(&sink);
|
|
421
|
+
free(pts);
|
|
422
|
+
if (FAILED(hr)) { SafeRelease(&geo); raise_hr("path geometry", hr); }
|
|
423
|
+
return geo;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
static VALUE surface_fill_polygon(VALUE self, VALUE arr,
|
|
427
|
+
VALUE vr, VALUE vg, VALUE vb, VALUE va) {
|
|
428
|
+
WindrawSurface* s = get_surface(self);
|
|
429
|
+
double r = NUM2DBL(vr), g = NUM2DBL(vg), b = NUM2DBL(vb), a = NUM2DBL(va);
|
|
430
|
+
ensure_drawable(s);
|
|
431
|
+
|
|
432
|
+
ID2D1PathGeometry* geo = build_polygon_geometry(s, arr, /*closed*/ true, /*filled*/ true);
|
|
433
|
+
ID2D1SolidColorBrush* brush = nullptr;
|
|
434
|
+
HRESULT hr = s->rt->CreateSolidColorBrush(
|
|
435
|
+
D2D1::ColorF(static_cast<float>(r), static_cast<float>(g),
|
|
436
|
+
static_cast<float>(b), static_cast<float>(a)), &brush);
|
|
437
|
+
if (SUCCEEDED(hr)) s->rt->FillGeometry(geo, brush);
|
|
438
|
+
SafeRelease(&brush);
|
|
439
|
+
SafeRelease(&geo);
|
|
440
|
+
if (FAILED(hr)) raise_hr("CreateSolidColorBrush", hr);
|
|
441
|
+
return self;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
static VALUE surface_stroke_polygon(VALUE self, VALUE arr,
|
|
445
|
+
VALUE vr, VALUE vg, VALUE vb, VALUE va,
|
|
446
|
+
VALUE vwidth, VALUE vclosed) {
|
|
447
|
+
WindrawSurface* s = get_surface(self);
|
|
448
|
+
double r = NUM2DBL(vr), g = NUM2DBL(vg), b = NUM2DBL(vb), a = NUM2DBL(va);
|
|
449
|
+
double sw = NUM2DBL(vwidth);
|
|
450
|
+
bool closed = RTEST(vclosed);
|
|
451
|
+
ensure_drawable(s);
|
|
452
|
+
|
|
453
|
+
ID2D1PathGeometry* geo = build_polygon_geometry(s, arr, closed, /*filled*/ false);
|
|
454
|
+
ID2D1SolidColorBrush* brush = nullptr;
|
|
455
|
+
HRESULT hr = s->rt->CreateSolidColorBrush(
|
|
456
|
+
D2D1::ColorF(static_cast<float>(r), static_cast<float>(g),
|
|
457
|
+
static_cast<float>(b), static_cast<float>(a)), &brush);
|
|
458
|
+
if (SUCCEEDED(hr)) s->rt->DrawGeometry(geo, brush, static_cast<float>(sw));
|
|
459
|
+
SafeRelease(&brush);
|
|
460
|
+
SafeRelease(&geo);
|
|
461
|
+
if (FAILED(hr)) raise_hr("CreateSolidColorBrush", hr);
|
|
462
|
+
return self;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
static VALUE surface_text(VALUE self, VALUE vstr, VALUE vx, VALUE vy,
|
|
466
|
+
VALUE vr, VALUE vg, VALUE vb, VALUE va,
|
|
467
|
+
VALUE vfont, VALUE vsize, VALUE vbold, VALUE vitalic) {
|
|
468
|
+
WindrawSurface* s = get_surface(self);
|
|
469
|
+
double x = NUM2DBL(vx), y = NUM2DBL(vy), size = NUM2DBL(vsize);
|
|
470
|
+
double r = NUM2DBL(vr), g = NUM2DBL(vg), b = NUM2DBL(vb), a = NUM2DBL(va);
|
|
471
|
+
DWRITE_FONT_WEIGHT weight = RTEST(vbold) ? DWRITE_FONT_WEIGHT_BOLD
|
|
472
|
+
: DWRITE_FONT_WEIGHT_NORMAL;
|
|
473
|
+
DWRITE_FONT_STYLE style = RTEST(vitalic) ? DWRITE_FONT_STYLE_ITALIC
|
|
474
|
+
: DWRITE_FONT_STYLE_NORMAL;
|
|
475
|
+
/* Coerce both strings up front: if either is not String-convertible, the
|
|
476
|
+
* TypeError must raise before any std::wstring exists (rb_raise longjmps
|
|
477
|
+
* past C++ destructors, which would otherwise leak the first wstring). */
|
|
478
|
+
StringValue(vfont);
|
|
479
|
+
StringValue(vstr);
|
|
480
|
+
ensure_drawable(s);
|
|
481
|
+
|
|
482
|
+
/* Lazily create the DirectWrite factory (shared). */
|
|
483
|
+
if (!s->dwrite) {
|
|
484
|
+
HRESULT hr = DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(IDWriteFactory),
|
|
485
|
+
reinterpret_cast<IUnknown**>(&s->dwrite));
|
|
486
|
+
if (FAILED(hr)) raise_hr("DWriteCreateFactory", hr);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
HRESULT hr;
|
|
490
|
+
IDWriteTextFormat* format = nullptr;
|
|
491
|
+
ID2D1SolidColorBrush* brush = nullptr;
|
|
492
|
+
{
|
|
493
|
+
std::wstring wfont = to_utf16(vfont);
|
|
494
|
+
std::wstring wtext = to_utf16(vstr);
|
|
495
|
+
hr = s->dwrite->CreateTextFormat(wfont.c_str(), nullptr, weight, style,
|
|
496
|
+
DWRITE_FONT_STRETCH_NORMAL, static_cast<float>(size), L"", &format);
|
|
497
|
+
if (SUCCEEDED(hr)) {
|
|
498
|
+
hr = s->rt->CreateSolidColorBrush(
|
|
499
|
+
D2D1::ColorF(static_cast<float>(r), static_cast<float>(g),
|
|
500
|
+
static_cast<float>(b), static_cast<float>(a)), &brush);
|
|
501
|
+
}
|
|
502
|
+
if (SUCCEEDED(hr)) {
|
|
503
|
+
D2D1_RECT_F rect = D2D1::RectF(static_cast<float>(x), static_cast<float>(y),
|
|
504
|
+
static_cast<float>(s->width),
|
|
505
|
+
static_cast<float>(s->height));
|
|
506
|
+
s->rt->DrawText(wtext.c_str(), static_cast<UINT32>(wtext.length()),
|
|
507
|
+
format, rect, brush);
|
|
508
|
+
}
|
|
509
|
+
} /* wfont/wtext destroyed here, before any possible raise */
|
|
510
|
+
|
|
511
|
+
SafeRelease(&brush);
|
|
512
|
+
SafeRelease(&format);
|
|
513
|
+
if (FAILED(hr)) raise_hr("CreateTextFormat/DrawText", hr);
|
|
514
|
+
return self;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
static void ensure_encodable(WindrawSurface* s) {
|
|
518
|
+
if (s->closed || !s->wic) rb_raise(rb_eRuntimeError, "windraw: surface is closed");
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
static VALUE surface_finish(VALUE self) {
|
|
522
|
+
WindrawSurface* s = get_surface(self);
|
|
523
|
+
if (s->draw_open && s->rt) {
|
|
524
|
+
HRESULT hr = s->rt->EndDraw();
|
|
525
|
+
s->draw_open = false;
|
|
526
|
+
s->finished = true;
|
|
527
|
+
if (FAILED(hr)) raise_hr("EndDraw", hr);
|
|
528
|
+
}
|
|
529
|
+
s->finished = true;
|
|
530
|
+
return self;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/* Release all COM resources now, on the calling (creating) thread. Idempotent.
|
|
534
|
+
* After #close the surface can no longer draw or encode. */
|
|
535
|
+
static VALUE surface_close(VALUE self) {
|
|
536
|
+
WindrawSurface* s = get_surface(self);
|
|
537
|
+
if (!s->closed) surface_teardown(s);
|
|
538
|
+
return self;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
static VALUE surface_save(VALUE self, VALUE vpath) {
|
|
542
|
+
WindrawSurface* s = get_surface(self);
|
|
543
|
+
ensure_encodable(s);
|
|
544
|
+
surface_finish(self); /* flush drawing before encoding */
|
|
545
|
+
|
|
546
|
+
IWICStream* stream = nullptr;
|
|
547
|
+
HRESULT hr;
|
|
548
|
+
{
|
|
549
|
+
std::wstring wpath = to_utf16(vpath);
|
|
550
|
+
hr = s->wic->CreateStream(&stream);
|
|
551
|
+
if (SUCCEEDED(hr)) hr = stream->InitializeFromFilename(wpath.c_str(), GENERIC_WRITE);
|
|
552
|
+
} /* wpath destroyed before any raise */
|
|
553
|
+
if (SUCCEEDED(hr)) hr = encode_png(s, stream);
|
|
554
|
+
SafeRelease(&stream);
|
|
555
|
+
if (FAILED(hr)) raise_hr("save", hr);
|
|
556
|
+
return vpath;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
static VALUE surface_to_png(VALUE self) {
|
|
560
|
+
WindrawSurface* s = get_surface(self);
|
|
561
|
+
ensure_encodable(s);
|
|
562
|
+
surface_finish(self);
|
|
563
|
+
|
|
564
|
+
IStream* mem = SHCreateMemStream(nullptr, 0);
|
|
565
|
+
if (!mem) rb_raise(rb_eRuntimeError, "windraw: SHCreateMemStream failed");
|
|
566
|
+
|
|
567
|
+
/* Drain the encoded PNG into a plain C buffer while the stream is alive,
|
|
568
|
+
* then Release the stream BEFORE building the Ruby string. rb_str_new can
|
|
569
|
+
* raise (NoMemError) via longjmp, which would otherwise skip mem->Release()
|
|
570
|
+
* and leak the COM stream. We also reject >LONG_MAX sizes up front (on
|
|
571
|
+
* LLP64 a cast to `long` would otherwise go negative). */
|
|
572
|
+
char* buf = nullptr;
|
|
573
|
+
long buflen = 0;
|
|
574
|
+
HRESULT hr = encode_png(s, mem);
|
|
575
|
+
if (SUCCEEDED(hr)) {
|
|
576
|
+
STATSTG st;
|
|
577
|
+
hr = mem->Stat(&st, STATFLAG_NONAME);
|
|
578
|
+
if (SUCCEEDED(hr)) {
|
|
579
|
+
ULONGLONG full = st.cbSize.QuadPart;
|
|
580
|
+
if (full > static_cast<ULONGLONG>(LONG_MAX)) {
|
|
581
|
+
hr = E_OUTOFMEMORY; /* PNG too large to hand to Ruby */
|
|
582
|
+
} else if (full > 0) {
|
|
583
|
+
LARGE_INTEGER zero;
|
|
584
|
+
zero.QuadPart = 0;
|
|
585
|
+
hr = mem->Seek(zero, STREAM_SEEK_SET, nullptr);
|
|
586
|
+
if (SUCCEEDED(hr)) {
|
|
587
|
+
buf = static_cast<char*>(malloc(static_cast<size_t>(full)));
|
|
588
|
+
if (!buf) {
|
|
589
|
+
hr = E_OUTOFMEMORY;
|
|
590
|
+
} else {
|
|
591
|
+
ULONG got = 0;
|
|
592
|
+
hr = mem->Read(buf, static_cast<ULONG>(full), &got);
|
|
593
|
+
if (SUCCEEDED(hr)) buflen = static_cast<long>(got);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
mem->Release();
|
|
600
|
+
if (FAILED(hr)) { free(buf); raise_hr("to_png", hr); }
|
|
601
|
+
|
|
602
|
+
VALUE result = rb_str_new(buf, buflen); /* copies; buf may be null when 0 */
|
|
603
|
+
free(buf);
|
|
604
|
+
rb_enc_associate(result, rb_ascii8bit_encoding());
|
|
605
|
+
return result;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
static VALUE surface_width(VALUE self) { return UINT2NUM(get_surface(self)->width); }
|
|
609
|
+
static VALUE surface_height(VALUE self) { return UINT2NUM(get_surface(self)->height); }
|
|
610
|
+
|
|
611
|
+
extern "C" void Init_windraw(void) {
|
|
612
|
+
VALUE mWindraw = rb_define_module("Windraw");
|
|
613
|
+
VALUE cSurface = rb_define_class_under(mWindraw, "Surface", rb_cObject);
|
|
614
|
+
|
|
615
|
+
rb_define_alloc_func(cSurface, surface_alloc);
|
|
616
|
+
rb_define_method(cSurface, "initialize", RUBY_METHOD_FUNC(surface_initialize), 2);
|
|
617
|
+
rb_define_method(cSurface, "_clear", RUBY_METHOD_FUNC(surface_clear), 4);
|
|
618
|
+
rb_define_method(cSurface, "_fill_rect", RUBY_METHOD_FUNC(surface_fill_rect), 8);
|
|
619
|
+
rb_define_method(cSurface, "_stroke_rect", RUBY_METHOD_FUNC(surface_stroke_rect), 9);
|
|
620
|
+
rb_define_method(cSurface, "_fill_ellipse", RUBY_METHOD_FUNC(surface_fill_ellipse), 8);
|
|
621
|
+
rb_define_method(cSurface, "_stroke_ellipse", RUBY_METHOD_FUNC(surface_stroke_ellipse), 9);
|
|
622
|
+
rb_define_method(cSurface, "_line", RUBY_METHOD_FUNC(surface_line), 9);
|
|
623
|
+
rb_define_method(cSurface, "_fill_round_rect", RUBY_METHOD_FUNC(surface_fill_round_rect), 10);
|
|
624
|
+
rb_define_method(cSurface, "_stroke_round_rect", RUBY_METHOD_FUNC(surface_stroke_round_rect), 11);
|
|
625
|
+
rb_define_method(cSurface, "_fill_polygon", RUBY_METHOD_FUNC(surface_fill_polygon), 5);
|
|
626
|
+
rb_define_method(cSurface, "_stroke_polygon", RUBY_METHOD_FUNC(surface_stroke_polygon), 7);
|
|
627
|
+
rb_define_method(cSurface, "_text", RUBY_METHOD_FUNC(surface_text), 11);
|
|
628
|
+
rb_define_method(cSurface, "finish", RUBY_METHOD_FUNC(surface_finish), 0);
|
|
629
|
+
rb_define_method(cSurface, "close", RUBY_METHOD_FUNC(surface_close), 0);
|
|
630
|
+
rb_define_method(cSurface, "save", RUBY_METHOD_FUNC(surface_save), 1);
|
|
631
|
+
rb_define_method(cSurface, "to_png", RUBY_METHOD_FUNC(surface_to_png), 0);
|
|
632
|
+
rb_define_method(cSurface, "width", RUBY_METHOD_FUNC(surface_width), 0);
|
|
633
|
+
rb_define_method(cSurface, "height", RUBY_METHOD_FUNC(surface_height), 0);
|
|
634
|
+
rb_define_alias(cSurface, "dispose", "close");
|
|
635
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Windraw
|
|
4
|
+
# Parses CSS-style hex colors into straight (non-premultiplied) RGBA floats in
|
|
5
|
+
# 0.0..1.0, the form Direct2D's D2D1::ColorF expects.
|
|
6
|
+
module Color
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# Accepts "#rgb", "#rgba", "#rrggbb", "#rrggbbaa" (leading "#" optional).
|
|
10
|
+
# Returns [r, g, b, a] as floats in 0.0..1.0. Raises ArgumentError otherwise.
|
|
11
|
+
def parse(spec, default_alpha: 1.0)
|
|
12
|
+
unless spec.is_a?(String) || spec.respond_to?(:to_str)
|
|
13
|
+
raise ArgumentError, "invalid hex color: #{spec.inspect} (expected a String)"
|
|
14
|
+
end
|
|
15
|
+
s = spec.to_s.strip
|
|
16
|
+
s = s[1..] if s.start_with?("#")
|
|
17
|
+
|
|
18
|
+
unless s.match?(/\A\h+\z/)
|
|
19
|
+
raise ArgumentError, "invalid hex color: #{spec.inspect} (non-hex characters)"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
case s.length
|
|
23
|
+
when 3 # rgb
|
|
24
|
+
r, g, b = s.chars.map { |c| (c * 2).to_i(16) }
|
|
25
|
+
a = (default_alpha * 255).round
|
|
26
|
+
when 4 # rgba
|
|
27
|
+
r, g, b, a = s.chars.map { |c| (c * 2).to_i(16) }
|
|
28
|
+
when 6 # rrggbb
|
|
29
|
+
r, g, b = s.scan(/../).map { |h| h.to_i(16) }
|
|
30
|
+
a = (default_alpha * 255).round
|
|
31
|
+
when 8 # rrggbbaa
|
|
32
|
+
r, g, b, a = s.scan(/../).map { |h| h.to_i(16) }
|
|
33
|
+
else
|
|
34
|
+
raise ArgumentError,
|
|
35
|
+
"invalid hex color: #{spec.inspect} (use #rgb, #rgba, #rrggbb, or #rrggbbaa)"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
[r / 255.0, g / 255.0, b / 255.0, a / 255.0]
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "windraw/color"
|
|
4
|
+
|
|
5
|
+
module Windraw
|
|
6
|
+
# Ergonomic, hex-color-aware drawing API layered over the native Surface
|
|
7
|
+
# primitives (defined in the C++ extension). Coordinates are in pixels; the
|
|
8
|
+
# origin (0, 0) is the top-left corner.
|
|
9
|
+
#
|
|
10
|
+
# Every drawing method returns self, so calls can be chained.
|
|
11
|
+
class Surface
|
|
12
|
+
# Fill the whole surface with a single color (default fully transparent).
|
|
13
|
+
def clear(color = "#00000000")
|
|
14
|
+
r, g, b, a = Color.parse(color)
|
|
15
|
+
_clear(r, g, b, a)
|
|
16
|
+
self
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Axis-aligned rectangle from (x, y) with the given width/height.
|
|
20
|
+
# Pass fill: and/or stroke: hex colors; with both, fill is drawn first.
|
|
21
|
+
# radius: rounds the corners — a single Numeric (rx == ry) or a [rx, ry] pair.
|
|
22
|
+
def rectangle(x, y, w, h, fill: nil, stroke: nil, width: 1.0, radius: nil)
|
|
23
|
+
if radius
|
|
24
|
+
rx, ry =
|
|
25
|
+
if radius.is_a?(Array)
|
|
26
|
+
unless radius.length == 2
|
|
27
|
+
raise ArgumentError, "radius: must be a Numeric or an [rx, ry] pair, got #{radius.inspect}"
|
|
28
|
+
end
|
|
29
|
+
radius
|
|
30
|
+
else
|
|
31
|
+
[radius, radius]
|
|
32
|
+
end
|
|
33
|
+
if fill
|
|
34
|
+
r, g, b, a = Color.parse(fill)
|
|
35
|
+
_fill_round_rect(x, y, w, h, rx, ry, r, g, b, a)
|
|
36
|
+
end
|
|
37
|
+
if stroke
|
|
38
|
+
r, g, b, a = Color.parse(stroke)
|
|
39
|
+
_stroke_round_rect(x, y, w, h, rx, ry, r, g, b, a, width)
|
|
40
|
+
end
|
|
41
|
+
else
|
|
42
|
+
if fill
|
|
43
|
+
r, g, b, a = Color.parse(fill)
|
|
44
|
+
_fill_rect(x, y, w, h, r, g, b, a)
|
|
45
|
+
end
|
|
46
|
+
if stroke
|
|
47
|
+
r, g, b, a = Color.parse(stroke)
|
|
48
|
+
_stroke_rect(x, y, w, h, r, g, b, a, width)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
self
|
|
52
|
+
end
|
|
53
|
+
alias rect rectangle
|
|
54
|
+
|
|
55
|
+
# Ellipse centered at (cx, cy) with radii rx, ry.
|
|
56
|
+
def ellipse(cx, cy, rx, ry, fill: nil, stroke: nil, width: 1.0)
|
|
57
|
+
if fill
|
|
58
|
+
r, g, b, a = Color.parse(fill)
|
|
59
|
+
_fill_ellipse(cx, cy, rx, ry, r, g, b, a)
|
|
60
|
+
end
|
|
61
|
+
if stroke
|
|
62
|
+
r, g, b, a = Color.parse(stroke)
|
|
63
|
+
_stroke_ellipse(cx, cy, rx, ry, r, g, b, a, width)
|
|
64
|
+
end
|
|
65
|
+
self
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Circle centered at (cx, cy) — convenience for an ellipse with equal radii.
|
|
69
|
+
def circle(cx, cy, radius, fill: nil, stroke: nil, width: 1.0)
|
|
70
|
+
ellipse(cx, cy, radius, radius, fill: fill, stroke: stroke, width: width)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Straight line from (x1, y1) to (x2, y2).
|
|
74
|
+
def line(x1, y1, x2, y2, color: "#000000", width: 1.0)
|
|
75
|
+
r, g, b, a = Color.parse(color)
|
|
76
|
+
_line(x1, y1, x2, y2, r, g, b, a, width)
|
|
77
|
+
self
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Closed polygon through the given points (an Array of [x, y] pairs).
|
|
81
|
+
# fill: and/or stroke: hex colors; with both, fill is drawn first.
|
|
82
|
+
def polygon(points, fill: nil, stroke: nil, width: 1.0)
|
|
83
|
+
flat = flatten_points(points)
|
|
84
|
+
if fill
|
|
85
|
+
r, g, b, a = Color.parse(fill)
|
|
86
|
+
_fill_polygon(flat, r, g, b, a)
|
|
87
|
+
end
|
|
88
|
+
if stroke
|
|
89
|
+
r, g, b, a = Color.parse(stroke)
|
|
90
|
+
_stroke_polygon(flat, r, g, b, a, width, true)
|
|
91
|
+
end
|
|
92
|
+
self
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Open connected line segments through the given points (Array of [x, y]).
|
|
96
|
+
def polyline(points, color: "#000000", width: 1.0)
|
|
97
|
+
r, g, b, a = Color.parse(color)
|
|
98
|
+
_stroke_polygon(flatten_points(points), r, g, b, a, width, false)
|
|
99
|
+
self
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Draw a string with its top-left at (x, y).
|
|
103
|
+
def text(string, x, y, color: "#000000", font: "Segoe UI", size: 16.0,
|
|
104
|
+
bold: false, italic: false)
|
|
105
|
+
r, g, b, a = Color.parse(color)
|
|
106
|
+
_text(string.to_s, x, y, r, g, b, a, font.to_s, size, bold, italic)
|
|
107
|
+
self
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# [width, height] in pixels.
|
|
111
|
+
def size
|
|
112
|
+
[width, height]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def inspect
|
|
116
|
+
"#<Windraw::Surface #{width}x#{height}>"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
# Convert an Array of [x, y] pairs into a flat [x0, y0, x1, y1, ...] Array of
|
|
122
|
+
# Floats. Raises ArgumentError on non-numeric coords or fewer than 2 points.
|
|
123
|
+
def flatten_points(points)
|
|
124
|
+
flat = Array(points).flat_map do |p|
|
|
125
|
+
unless p.is_a?(Array) && p.length == 2
|
|
126
|
+
raise ArgumentError, "each point must be an [x, y] pair, got #{p.inspect}"
|
|
127
|
+
end
|
|
128
|
+
[Float(p[0]), Float(p[1])]
|
|
129
|
+
end
|
|
130
|
+
if flat.length < 4
|
|
131
|
+
raise ArgumentError, "need at least 2 [x, y] points, got #{points.inspect}"
|
|
132
|
+
end
|
|
133
|
+
flat
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
data/lib/windraw.rb
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "windraw/version"
|
|
4
|
+
require "windraw/windraw" # native C++ extension: defines Windraw::Surface
|
|
5
|
+
require "windraw/color"
|
|
6
|
+
require "windraw/surface" # reopens Windraw::Surface with the ergonomic API
|
|
7
|
+
|
|
8
|
+
# windraw — headless 2D drawing for Windows using Direct2D + DirectWrite + WIC.
|
|
9
|
+
#
|
|
10
|
+
# Windraw.surface(800, 600) do |c|
|
|
11
|
+
# c.clear("#1e1e2e")
|
|
12
|
+
# c.rectangle(50, 50, 200, 120, fill: "#89b4fa")
|
|
13
|
+
# c.ellipse(400, 300, 80, 50, fill: "#a6e3a1")
|
|
14
|
+
# c.text("Hello, Direct2D!", 60, 200, font: "Segoe UI", size: 32, color: "#cdd6f4")
|
|
15
|
+
# end.save("out.png")
|
|
16
|
+
module Windraw
|
|
17
|
+
# Create a drawing surface of the given pixel size. With a block, yields the
|
|
18
|
+
# surface, finishes drawing when the block returns, and returns the surface
|
|
19
|
+
# (so you can chain #save / #to_png). Without a block, returns an open surface.
|
|
20
|
+
#
|
|
21
|
+
# width/height are whole pixels; a Float is truncated toward zero (NUM2INT).
|
|
22
|
+
def self.surface(width, height)
|
|
23
|
+
surface = Surface.new(width, height)
|
|
24
|
+
if block_given?
|
|
25
|
+
yield surface
|
|
26
|
+
surface.finish
|
|
27
|
+
end
|
|
28
|
+
surface
|
|
29
|
+
end
|
|
30
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: windraw
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- ned
|
|
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: rake
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '13.0'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '13.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rake-compiler
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.2'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.2'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: minitest
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '5.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '5.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: vcvars
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0.1'
|
|
61
|
+
- - ">="
|
|
62
|
+
- !ruby/object:Gem::Version
|
|
63
|
+
version: 0.1.1
|
|
64
|
+
type: :development
|
|
65
|
+
prerelease: false
|
|
66
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
67
|
+
requirements:
|
|
68
|
+
- - "~>"
|
|
69
|
+
- !ruby/object:Gem::Version
|
|
70
|
+
version: '0.1'
|
|
71
|
+
- - ">="
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: 0.1.1
|
|
74
|
+
description: |
|
|
75
|
+
windraw renders shapes and text into an off-screen bitmap using the graphics
|
|
76
|
+
stack that already ships with Windows — Direct2D for vector drawing,
|
|
77
|
+
DirectWrite for text, and WIC for PNG encoding — then saves a PNG to a file
|
|
78
|
+
or returns the bytes as a string. A Cairo-style 2D canvas with no window or
|
|
79
|
+
message loop required. Windows MSVC (mswin) Ruby only.
|
|
80
|
+
executables: []
|
|
81
|
+
extensions:
|
|
82
|
+
- ext/windraw/extconf.rb
|
|
83
|
+
extra_rdoc_files: []
|
|
84
|
+
files:
|
|
85
|
+
- CHANGELOG.md
|
|
86
|
+
- LICENSE.txt
|
|
87
|
+
- README.md
|
|
88
|
+
- ext/windraw/extconf.rb
|
|
89
|
+
- ext/windraw/windraw.cpp
|
|
90
|
+
- lib/windraw.rb
|
|
91
|
+
- lib/windraw/color.rb
|
|
92
|
+
- lib/windraw/surface.rb
|
|
93
|
+
- lib/windraw/version.rb
|
|
94
|
+
homepage: https://github.com/main-path/windraw
|
|
95
|
+
licenses:
|
|
96
|
+
- MIT
|
|
97
|
+
metadata:
|
|
98
|
+
homepage_uri: https://github.com/main-path/windraw
|
|
99
|
+
changelog_uri: https://github.com/main-path/windraw/blob/main/CHANGELOG.md
|
|
100
|
+
bug_tracker_uri: https://github.com/main-path/windraw/issues
|
|
101
|
+
rubygems_mfa_required: 'true'
|
|
102
|
+
rdoc_options: []
|
|
103
|
+
require_paths:
|
|
104
|
+
- lib
|
|
105
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - ">="
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '3.0'
|
|
110
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
111
|
+
requirements:
|
|
112
|
+
- - ">="
|
|
113
|
+
- !ruby/object:Gem::Version
|
|
114
|
+
version: '0'
|
|
115
|
+
requirements: []
|
|
116
|
+
rubygems_version: 3.6.9
|
|
117
|
+
specification_version: 4
|
|
118
|
+
summary: Headless 2D drawing for Windows (Direct2D + DirectWrite + WIC) to PNG.
|
|
119
|
+
test_files: []
|