This commit is contained in:
Michael Zhang 2020-12-23 18:30:32 -06:00
parent 942d959b53
commit 3536c3f262
Signed by: michael
GPG key ID: BDA47A31A3C8EE6B
11 changed files with 442 additions and 1011 deletions

2
.gitignore vendored
View file

@ -1,3 +1,3 @@
/target
**/*.rs.bk
shiet.png
shiet.*

641
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -2,21 +2,16 @@
name = "leanshot"
description = "Screenshot capture for Linux."
version = "0.4.0"
repository = "https://git.sr.ht/~iptq/leanshot"
repository = "https://git.mzhang.io/michael/leanshot"
license-file = "LICENSE"
edition = "2018"
authors = ["Michael Zhang <iptq@protonmail.com>"]
[features]
default = ["x11"]
x11 = ["safex11"]
authors = ["Michael Zhang <mail@mzhang.io>"]
[dependencies]
png = "0.14"
structopt = "0.2"
anyhow = "1.0.31"
image = "0.23.5"
anyhow = "1.0.36"
image = "0.23.12"
log = "0.4.8"
stderrlog = "0.4.3"
safex11 = { version = "0.0", path = "../safex11", optional = true, features = ["xlib", "xinerama"] }
structopt = "0.2"
xcb-util = { version = "0.3.0", features = ["image", "cursor"] }
xcb = { version = "0.9" }

18
LICENSE
View file

@ -1,7 +1,19 @@
Copyright 2018-2020 Michael Zhang
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,15 +1,13 @@
leanshot
========
[![](https://api.travis-ci.org/iptq/leanshot.svg?branch=develop)](https://travis-ci.org/iptq/leanshot)
[![dependency status](https://deps.rs/repo/github/iptq/leanshot/status.svg)](https://deps.rs/repo/github/iptq/leanshot)
Screenshot-capturing utility.
Requirements
------------
You must have imlib2 and OpenGL installed. Fortunately, these are relatively common libraries.
You must have imlib2 and OpenGL installed. Fortunately, these are relatively
common libraries.
Installation
------------

View file

@ -1,67 +1,141 @@
use std::path::Path;
use std::ptr;
use anyhow::Result;
use image::RgbaImage;
use x11::{
xlib::{Display, Window},
Rectangle,
};
use image::ColorType;
use xcb::{base::Connection, xproto, Screen};
use xcb_util::{ffi::image::*, image as xcb_image};
use crate::platform::{x11::X11, Platform};
use crate::{Options, Region};
pub struct GUI<P: Platform> {
inner: P,
pub struct Gui {
conn: Connection,
default_screen: i32,
}
impl GUI<X11> {
impl Gui {
pub fn new() -> Result<Self> {
let x11 = X11::new()?;
Ok(GUI { inner: x11 })
let (conn, default_screen) = Connection::connect(None)?;
Ok(Gui {
conn,
default_screen,
})
}
}
impl<P: Platform> GUI<P> {
/// Captures the window and produces an Image.
pub fn capture_window(&self, opt: &Options, window: Window) -> Result<RgbaImage> {
let attr = window.get_attributes()?;
let width = attr.get_width();
let height = attr.get_height();
let root = attr.get_root();
fn get_default_screen(&self) -> Screen {
// TODO: multiple screens
let setup = self.conn.get_setup();
let mut iter = setup.roots();
iter.next().unwrap()
}
// let (x, y, _) = self.display.translate_coordinates(window, 0, 0, root)?;
pub fn capture_entire_screen(&self) -> Result<ScreenCapture> {
// get the dimensions of the screen
let screen = self.get_default_screen();
let (width, height) = (screen.width_in_pixels(), screen.height_in_pixels());
println!("width, height = {:?}", (width, height));
// imlib2::context_set_display(&self.display);
// let visual = Visual::default(&self.display, 0);
// imlib2::context_set_visual(&visual);
let image = xcb_image::get(
&self.conn,
screen.root(),
0,
0,
width,
height,
u32::MAX,
xcb::IMAGE_FORMAT_Z_PIXMAP,
)
.unwrap();
if let Region::Selection = opt.region {
let image = self.inner.capture_full_screen()?;
let rect = self.inner.show_interactive_selection(image)?;
unimplemented!("rect: {:?}", rect);
// let capture =
// Image2::create_from_drawable(window, 0, x, y, width as i32, height as i32, true)?;
// let region = self.interactive_select(&capture)?;
// imlib2::context_set_image(&capture);
// return imlib2::create_scaled_cropped_image(
// region.x,
// region.y,
// region.width,
// region.height,
// )
// .map_err(|err| err.into());
Ok(ScreenCapture { image })
}
pub fn interactive_select(&self, image: &ScreenCapture) -> Result<()> {
let id = self.conn.generate_id();
let screen = self.get_default_screen();
let root_window = screen.root();
let (width, height) = (screen.width_in_pixels(), screen.height_in_pixels());
let window = xproto::create_window(
&self.conn,
xcb::COPY_FROM_PARENT as u8,
id,
root_window,
0,
0,
width,
height,
0,
xcb::WINDOW_CLASS_INPUT_OUTPUT as u16,
screen.root_visual(),
&[
// (xcb::CW_OVERRIDE_REDIRECT, 1),
(xcb::CW_BACK_PIXEL, screen.white_pixel()),
(xcb::CW_EVENT_MASK, u32::MAX),
],
).request_check()?;
xproto::map_window(&self.conn, id).request_check()?;
self.conn.flush();
xproto::set_input_focus(
&self.conn,
xcb::INPUT_FOCUS_POINTER_ROOT as u8,
id,
xcb::CURRENT_TIME,
);
xproto::grab_pointer(
&self.conn,
true,
id,
u16::MAX,
xcb::GRAB_MODE_ASYNC as u8,
xcb::GRAB_MODE_ASYNC as u8,
xcb::NONE,
xcb::NONE,
xcb::CURRENT_TIME,
);
let gc = self.conn.generate_id();
xproto::create_gc(&self.conn, gc, id, &[]);
// drag start
let mut dragging = false;
let mut dx = -1.0f64;
let mut dy = -1.0f64;
while let Some(evt) = self.conn.wait_for_event() {
xcb_image::put(&self.conn, id, gc, &image.image, 0, 0);
match evt.response_type() {
xcb::EXPOSE => {
println!("EXPOSE");
}
xcb::BUTTON_PRESS => {
println!("PRESSED BUTTON");
}
v => {
println!("value: {:?}", v);
}
}
}
unimplemented!()
// Image2::create_from_drawable(window, 0, x, y, width as i32, height as i32, true)
// .map_err(|err| err.into())
}
/// Get the active window.
pub fn get_active_window(&self) -> Result<P::Window> {
self.inner.get_active_window()
}
/// Get the active window.
pub fn capture_full_screen(&self) -> Result<P::Image> {
self.inner.capture_full_screen()
todo!();
}
}
pub struct ScreenCapture {
image: xcb_image::Image,
}
impl ScreenCapture {
pub fn save_to(&self, to: impl AsRef<Path>) -> Result<()> {
let to = to.as_ref();
image::save_buffer(
to,
self.image.data(),
self.image.width() as u32,
self.image.height() as u32,
ColorType::Bgra8,
)?;
Ok(())
}
}

View file

@ -1,38 +0,0 @@
//! Screenshot capturing utility.
#![deny(missing_docs)]
#[macro_use]
extern crate anyhow;
#[macro_use]
extern crate log;
extern crate safex11 as x11;
mod platform;
use anyhow::Result;
pub use crate::platform::{x11::X11, Image, Platform};
type Rectangle = x11::Rectangle;
/// A region option
#[allow(missing_docs)]
pub enum Region {
Fullscreen,
ActiveWindow,
Selection,
}
impl Region {
/// Convert string to Region enum
// TODO: use FromStr trait
pub fn from_str(x: &str) -> Result<Self> {
match x {
"fullscreen" => Ok(Region::Fullscreen),
"window" => Ok(Region::ActiveWindow),
"select" | "selection" => Ok(Region::Selection),
_ => bail!("please choose a region from 'fullscreen', 'window', or 'select'"),
}
}
}

View file

@ -1,107 +1,20 @@
#[macro_use]
extern crate log;
use std::path::PathBuf;
mod gui;
use anyhow::Result;
use image::{imageops, RgbImage};
use leanshot::{Image, Platform, Region, X11};
use structopt::StructOpt;
#[allow(missing_docs)]
pub fn main() -> Result<()> {
let opt = Options::from_args();
stderrlog::new()
.module(module_path!())
.module("safex11")
.verbosity(opt.verbose)
.init()
.unwrap();
use crate::gui::Gui;
let gui = X11::new()?;
fn main() -> Result<()> {
stderrlog::new().module(module_path!()).init().unwrap();
let screen_layout = gui.get_screen_layout()?;
debug!("screen layout: {:?}", screen_layout);
let gui = Gui::new()?;
let capture = gui.capture_entire_screen()?;
capture.save_to("shiet.jpg")?;
let _image = match opt.region {
Region::ActiveWindow => {
let window = gui.get_active_window()?;
let _image = gui.capture_window(window);
}
Region::Fullscreen | Region::Selection => {
let _image = gui.capture_full_screen()?;
// calculate the full size of the image
let mut min_x = 0;
let mut min_y = 0;
let mut max_x = 0;
let mut max_y = 0;
for (i, (_, screen)) in screen_layout.iter().enumerate() {
let left = screen.x;
let right = screen.x + screen.width as i16;
let top = screen.y;
let bottom = screen.y + screen.height as i16;
if i == 0 {
min_x = left;
min_y = top;
max_x = right;
max_y = bottom;
} else {
min_x = min_x.min(left);
min_y = min_y.min(top);
max_x = max_x.max(right);
max_y = max_y.max(bottom);
}
}
// make a new image
let width = (max_x - min_x) as u32;
let height = (max_y - min_y) as u32;
let mut base_image = RgbImage::new(width, height);
// copy all of the images into it
let images = gui.capture_full_screen()?;
for (id, image) in images.iter() {
let screen = screen_layout.get(&id).expect("shouldn't fail");
let x = (screen.x + min_x) as u32;
let y = (screen.y + min_y) as u32;
let image = image.to_rgb_image();
imageops::overlay(&mut base_image, &image, x, y);
}
if let Region::Selection = opt.region {
// bring up the interactive selection
let _rect = gui.show_interactive_selection(&images)?;
}
// save the image
base_image.save(&opt.outfile)?;
}
};
gui.interactive_select(&capture)?;
Ok(())
}
/// Options for screenshot
#[derive(StructOpt)]
pub struct Options {
/// The region to select (fullscreen | window | select)
#[structopt(parse(try_from_str = "Region::from_str"))]
pub region: Region,
/// The file to save the screenshot to
#[structopt(short = "o", long = "out", parse(from_os_str))]
pub outfile: PathBuf,
/// Whether or not to also copy it to the clipboard
#[structopt(short = "c")]
pub clipboard: bool,
/// Verbosity of output
#[structopt(short = "v", long = "verbose", parse(from_occurrences))]
pub verbose: usize,
/// X display connection string to use
#[structopt(long = "x-display", default_value = ":0")]
pub x_display: String,
}

View file

@ -1,279 +0,0 @@
/// Brings up an interactive selection GUI.
pub fn interactive_select(&self, capture: &Image2) -> Result<Rectangle> {
// let window = SelectWindow::new(&self.display);
// let root = self.display.get_default_root_window()?;
// let root_im = root.get_image();
// let mut done = 0;
// let mut button_press = false;
// while done == 0 && self.display.pending()? > 0 {
// let ev = self.display.next_event()?;
// match ev.kind() {
// EventKind::ButtonPress => {
// button_press = true;
// }
// EventKind::ButtonRelease => {
// if button_press {
// done = 1;
// }
// button_press = false;
// }
// _ => (),
// }
// }
use glutin::{
dpi::{PhysicalPosition, PhysicalSize},
os::unix::{WindowBuilderExt, WindowExt, XWindowType},
ElementState, Event, EventsLoop, GlContext, GlWindow, KeyboardInput, MouseButton,
MouseCursor, VirtualKeyCode, WindowBuilder, WindowEvent,
};
use nanovg::{Image, ImagePattern, PathOptions, StrokeOptions};
use std::{f32::consts, mem, slice};
// let attr = window.get_attributes()?;
// let width = attr.get_width();
// let height = attr.get_height();
// let root = attr.get_root();
// let (x, y, _) = self.display.translate_coordinates(window, 0, 0, root)?;
let width = capture.get_width();
let height = capture.get_height();
let mut evl = EventsLoop::new();
let mon = evl.get_primary_monitor();
// TODO: handle error
let wb = WindowBuilder::new()
.with_x11_window_type(XWindowType::Splash)
.with_decorations(false)
.with_visibility(false)
.with_always_on_top(true)
.with_dimensions(
PhysicalSize::new(width.into(), height.into()).to_logical(mon.get_hidpi_factor()),
)
.with_fullscreen(Some(mon));
let ctx = glutin::ContextBuilder::new()
.with_vsync(false)
.with_multisampling(4)
.with_double_buffer(Some(true))
.with_srgb(true);
let win = GlWindow::new(wb, ctx, &evl).expect("couldn't make window");
win.set_position((0.0, 0.0).into());
let f = win.get_hidpi_factor() as f64;
// crosshair
win.set_cursor(MouseCursor::Crosshair);
// win.set_inner_size((width, height).into());
// println!("size={:?} pos={:?} outer={:?}", win.get_inner_size(), win.get_inner_position(), win.get_outer_size());
// println!("{:?}", win.get_hidpi_factor());
let x = Display::from_handle(win.get_xlib_display().unwrap() as u64);
let len;
let raw_data;
{
let _g = x.grab();
// let img = Image2::create_from_drawable(window, 0, 0, 0, width as i32, height as i32, true)?;
imlib2::context_set_image(&capture);
len = (width * height) as usize;
// println!("{}", window.as_raw());
raw_data = unsafe { slice::from_raw_parts(imlib2::image_get_data(), len) };
unsafe {
win.make_current().expect("couldn't make window");
gl::load_with(|sym| win.get_proc_address(sym) as *const _);
}
}
mem::forget(x);
// convert ARGB to RGBA
let mut data = vec![0u32; raw_data.len()];
data.copy_from_slice(raw_data);
for i in &mut data {
// fix the colors
*i = (*i & 0xff00ff00) | ((*i & 0xff) << 16) | ((*i >> 16) & 0xff);
}
// invert image
let mut inverted = vec![0u32; raw_data.len()];
inverted.copy_from_slice(raw_data);
for i in &mut inverted {
// fix the colors
*i = (*i & 0xff000000) | !(*i & 0xffffff);
}
let ctx = nanovg::ContextBuilder::new()
.build()
.expect("couldn't init nanovg");
let image = Image::new(&ctx)
.build_from_rgba(width as usize, height as usize, data.as_slice())
.expect("couldn't create image");
let inverted_image = Image::new(&ctx)
.build_from_rgba(width as usize, height as usize, inverted.as_slice())
.expect("couldn't create image");
let mut running = true;
let mut down = false;
// drag start
let mut dx = -1.0f64;
let mut dy = -1.0f64;
// curr mouse
let mut mx = -1.0f64;
let mut my = -1.0f64;
// rect
let mut rectw = 0.0f64;
let mut recth = 0.0f64;
let mut delayed_down = false;
let mut redraw = true;
win.show();
win.set_position(PhysicalPosition::new(0.0, 0.0).to_logical(f));
while running {
if redraw {
// let size = win.get_inner_size().unwrap();
// let (width, height) = (size.width as i32, size.height as i32);
unsafe {
gl::Viewport(0, 0, width as i32, height as i32);
gl::ClearColor(0.3, 0.3, 0.32, 1.0);
gl::Clear(gl::COLOR_BUFFER_BIT | gl::DEPTH_BUFFER_BIT | gl::STENCIL_BUFFER_BIT);
}
let (width, height) = (width as f64, height as f64);
ctx.frame((width as f32, height as f32), f as f32, |frame| {
let path_opts = PathOptions::default();
frame.path(
|path| {
path.rect((0.0, 0.0), ((width * f) as f32, (height * f) as f32));
// path.fill(Color::from_rgba(200, 200, 200, 255), Default::default());
path.fill(
ImagePattern {
image: &image,
origin: (0.0, 0.0),
size: (width as f32, height as f32),
angle: 0.0 / 180.0 * consts::PI,
alpha: 1.0,
},
Default::default(),
)
},
path_opts,
);
if down && rectw.abs() > 0.0 && recth.abs() > 0.0 {
frame.path(
|path| {
path.rect(
((dx * f) as f32, (dy * f) as f32),
((rectw * f) as f32, (recth * f) as f32),
);
path.stroke(
// Color::from_rgba(0, 0, 0, 255),
ImagePattern {
image: &inverted_image,
origin: (0.0, 0.0),
size: (width as f32, height as f32),
angle: 0.0 / 180.0 * consts::PI,
alpha: 1.0,
},
StrokeOptions {
width: 1.0,
..Default::default()
},
);
},
path_opts,
);
}
});
}
evl.poll_events(|event| {
if let Event::WindowEvent { event, .. } = event {
match event {
WindowEvent::Destroyed => running = false,
WindowEvent::KeyboardInput {
input:
KeyboardInput {
virtual_keycode,
state,
..
},
..
} => {
if let (Some(VirtualKeyCode::Escape), ElementState::Released) =
(virtual_keycode, state)
{
if down {
down = false;
rectw = 0.0;
recth = 0.0;
} else {
running = false;
}
}
}
WindowEvent::CursorMoved { position, .. } => {
mx = position.x;
my = position.y;
if down {
if delayed_down {
dx = mx;
dy = my;
delayed_down = false;
} else {
redraw = true;
}
rectw = mx - dx;
recth = my - dy;
}
}
WindowEvent::MouseInput { button, state, .. } => {
if let MouseButton::Left = button {
down = match state {
ElementState::Pressed => {
delayed_down = true;
if mx < 0.0 || my < 0.0 {
} else {
dx = mx;
dy = my;
}
true
}
ElementState::Released => {
if down && rectw.abs() > 0.0 && recth.abs() > 0.0 {
running = false;
}
false
}
}
}
}
_ => (),
}
}
});
win.swap_buffers().expect("couldn't swap buffers");
}
if rectw.abs() > 0.0 && recth.abs() > 0.0 {
let mut x = dx;
let mut y = dy;
if rectw < 0.0 {
x += rectw;
}
if recth < 0.0 {
y += recth;
}
Ok(Rectangle::new(
(x * f) as i32,
(y * f) as i32,
(rectw.abs() * f) as u32,
(recth.abs() * f) as u32,
))
} else {
Err(ScreenshotError::Error)
}
}

View file

@ -1,54 +0,0 @@
#[cfg(feature = "x11")]
pub mod x11;
use std::collections::HashMap;
use std::hash::Hash;
use anyhow::Result;
use image::RgbImage;
use crate::Rectangle;
#[derive(Debug)]
pub struct ScreenInfo {
pub width: u16,
pub height: u16,
pub x: i16,
pub y: i16,
}
/// Set of functions that all platforms must implement
pub trait Platform {
/// Type of window handles
type Window;
/// Type of images
type Image: Image;
/// Type of screen handles (must be hashable and uniquely identifying)
type ScreenId: Hash;
/// Get a handle to the currently active window
fn get_active_window(&self) -> Result<Self::Window>;
/// Capture a specific window by handle
fn capture_window(&self, window: Self::Window) -> Result<Self::Image>;
/// Get the screen layout (list of screens, where they are)
fn get_screen_layout(&self) -> Result<HashMap<Self::ScreenId, ScreenInfo>>;
/// Capture full screen
fn capture_full_screen(&self) -> Result<HashMap<Self::ScreenId, Self::Image>>;
/// Open the interactive selection interface
fn show_interactive_selection(
&self,
image: &HashMap<Self::ScreenId, Self::Image>,
) -> Result<Rectangle>;
}
/// Set of functions platform-specific images must implement
pub trait Image {
/// Convert the image into an image::RgbImage
fn to_rgb_image(&self) -> RgbImage;
}

View file

@ -1,113 +0,0 @@
use std::collections::HashMap;
use anyhow::Result;
use image::{Rgb, RgbImage};
use x11::{
xinerama::ScreensInfo,
xlib::{Display, ImageByteOrder, Window},
};
use crate::platform::{Image as ImageT, Platform, ScreenInfo};
use crate::Rectangle;
/// Interface to x11
pub struct X11 {
inner: Display,
}
impl X11 {
/// Create a new x11 instace
pub fn new() -> Result<Self> {
// TODO: configure connection string
let display = Display::connect(":0")?;
Ok(X11 { inner: display })
}
}
impl Platform for X11 {
type Window = x11::x11::xlib::Window;
type Image = Image;
type ScreenId = i32;
fn get_active_window(&self) -> Result<Self::Window> {
let (window, _) = self.inner.get_input_focus()?;
Ok(window.inner)
}
fn capture_window(&self, window: Self::Window) -> Result<Self::Image> {
let window = Window {
display: &self.inner,
inner: window,
};
let image = window.get_image()?;
Ok(Image(image))
}
fn capture_full_screen(&self) -> Result<HashMap<Self::ScreenId, Self::Image>> {
let mut result = HashMap::new();
// wait what?
// x11 doesn't have a root window for each screen apparently
let window = self.inner.get_default_root_window()?;
result.insert(0, Image(window.get_image()?));
Ok(result)
}
fn get_screen_layout(&self) -> Result<HashMap<Self::ScreenId, ScreenInfo>> {
let screens_info = ScreensInfo::query(&self.inner)?;
Ok(screens_info
.iter()
.map(|(num, screen)| {
(
num,
ScreenInfo {
x: screen.x,
y: screen.y,
width: screen.width as u16,
height: screen.height as u16,
},
)
})
.collect())
}
fn show_interactive_selection(
&self,
image: &HashMap<Self::ScreenId, Self::Image>,
) -> Result<Rectangle> {
let root_window = self.inner.get_default_root_window()?;
let window = Window::create(
&self.inner,
Some(root_window),
Rectangle::new(0, 0, 500, 500),
Default::default(),
)?;
window.map();
loop {
let event = self.inner.next_event()?;
debug!("event: {:?}", event);
// quit
}
unimplemented!()
}
}
pub struct Image(x11::xlib::Image);
impl ImageT for Image {
fn to_rgb_image(&self) -> RgbImage {
let width = self.0.get_width();
let height = self.0.get_height();
let pixbuf = self.0.buffer();
let byte_order = self.0.get_byte_order().unwrap();
RgbImage::from_fn(width, height, |x, y| {
let (r, g, b) = pixbuf.get_pixel(x, y).unwrap();
[r, g, b].into()
})
}
}