Skip to content

Commit

Permalink
Merge pull request #58 from ChurchTao/fix/win/image
Browse files Browse the repository at this point in the history
fix: image color abnormal
  • Loading branch information
ChurchTao authored Feb 9, 2025
2 parents 460ec38 + 8e8efab commit bb7534f
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 67 deletions.
13 changes: 9 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,33 @@
# Changelog

## v0.2.3 (2025-02-07) [released]

- Fix: [issues#57](https://github.com/ChurchTao/clipboard-rs/issues/57)
- Fix: [pr#56](https://github.com/ChurchTao/clipboard-rs/pull/56)

## v0.2.2 (2024-11-19) [released]

- Convergence dep: `image` to `jpeg/png/tiff/bmp` [pr#54](https://github.com/ChurchTao/clipboard-rs/pull/54)

## v0.2.1 (2024-08-26) [released]

### zh:
### zh

- 增加 X11 启动参数,自定义读取的超时时间 [issues#45](https://github.com/ChurchTao/clipboard-rs/issues/45)

### en:
### en

- Add X11 startup parameters to customize the timeout for reading [issues#45](https://github.com/ChurchTao/clipboard-rs/issues/45)

## v0.2.0 (2024-08-25) [released]

### zh:
### zh

- macOS 性能优化,增强 test 类,替换使用 objc2
- 修复 windows 读取 rtf 可能失败的情况
- 修复读取 html 少<的情况,【因为 wps 写入的 StartFragment 有问题】

### en:
### en

- macOS performance optimization, enhanced test class, replaced with objc2
- Fixed the case where reading rtf on windows may fail
Expand Down
14 changes: 7 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "clipboard-rs"
version = "0.2.2"
version = "0.2.3"
authors = ["ChurchTao <[email protected]>"]
description = "Cross-platform clipboard API (text | image | rich text | html | files | monitoring changes) | 跨平台剪贴板 API(文本|图片|富文本|html|文件|监听变化) Windows,MacOS,Linux"
repository = "https://github.com/ChurchTao/clipboard-rs"
Expand All @@ -12,33 +12,33 @@ edition = "2021"
rust-version = "1.67.0"

[dependencies]
image = { version = "0.25.4", default-features = false, features = [
image = { version = "0.25.5", default-features = false, features = [
"png",
"jpeg",
] }

[target.'cfg(target_os = "windows")'.dependencies]
clipboard-win = { version = "5.4.0", features = ["monitor"] }
image = { version = "0.25.4", default-features = false, features = [
image = { version = "0.25.5", default-features = false, features = [
"bmp",
"png",
"jpeg",
] }

[target.'cfg(target_os = "macos")'.dependencies]
# cocoa = "0.26.0"
objc2 = { version = "0.5.2" }
objc2-foundation = { version = "0.2.2", features = [
objc2 = { version = "0.6.0" }
objc2-foundation = { version = "0.3.0", features = [
"NSArray",
"NSString",
"NSEnumerator",
] }
objc2-app-kit = { version = "0.2.2", features = [
objc2-app-kit = { version = "0.3.0", features = [
"NSPasteboard",
"NSPasteboardItem",
"NSImage",
] }
image = { version = "0.25.4", default-features = false, features = [
image = { version = "0.25.5", default-features = false, features = [
"tiff",
"png",
"jpeg",
Expand Down
67 changes: 45 additions & 22 deletions src/common.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use image::imageops::FilterType;
use image::{DynamicImage, GenericImageView, ImageFormat, RgbaImage};
use image::{ColorType, DynamicImage, GenericImageView, ImageFormat, RgbaImage};
use std::error::Error;
use std::io::Cursor;
pub type Result<T> = std::result::Result<T, Box<dyn Error + Send + Sync + 'static>>;
Expand Down Expand Up @@ -125,12 +125,19 @@ pub trait RustImage: Sized {
/// zh: 调整图片大小,不保留长宽比
fn resize(&self, width: u32, height: u32, filter: FilterType) -> Result<Self>;

fn encode_image(
&self,
target_color_type: ColorType,
format: ImageFormat,
) -> Result<RustImageBuffer>;

fn to_jpeg(&self) -> Result<RustImageBuffer>;

/// en: Convert to png format, the returned image is a new image, and the data itself will not be modified
/// zh: 转为 png 格式,返回的为新的图片,本身数据不会修改
fn to_png(&self) -> Result<RustImageBuffer>;

#[cfg(target_os = "windows")]
fn to_bitmap(&self) -> Result<RustImageBuffer>;

fn save_to_path(&self, path: &str) -> Result<()>;
Expand All @@ -140,21 +147,6 @@ pub trait RustImage: Sized {
fn to_rgba8(&self) -> Result<RgbaImage>;
}

macro_rules! image_to_format {
($name:ident, $format:expr) => {
fn $name(&self) -> Result<RustImageBuffer> {
match &self.data {
Some(image) => {
let mut bytes: Vec<u8> = Vec::new();
image.write_to(&mut Cursor::new(&mut bytes), $format)?;
Ok(RustImageBuffer(bytes))
}
None => Err("image is empty".into()),
}
}
};
}

impl RustImage for RustImageData {
fn empty() -> Self {
RustImageData {
Expand Down Expand Up @@ -229,12 +221,6 @@ impl RustImage for RustImageData {
}
}

image_to_format!(to_jpeg, ImageFormat::Jpeg);

image_to_format!(to_png, ImageFormat::Png);

image_to_format!(to_bitmap, ImageFormat::Bmp);

fn save_to_path(&self, path: &str) -> Result<()> {
match &self.data {
Some(image) => {
Expand All @@ -258,6 +244,43 @@ impl RustImage for RustImageData {
None => Err("image is empty".into()),
}
}

// 私有辅助函数,处理图像格式转换和编码
fn encode_image(
&self,
target_color_type: ColorType,
format: ImageFormat,
) -> Result<RustImageBuffer> {
let image = self.data.as_ref().ok_or("image is empty")?;

let mut bytes = Vec::new();
match (image.color(), target_color_type) {
(ColorType::Rgba8, ColorType::Rgb8) => image
.to_rgb8()
.write_to(&mut Cursor::new(&mut bytes), format)?,
(_, ColorType::Rgba8) => image
.to_rgba8()
.write_to(&mut Cursor::new(&mut bytes), format)?,
_ => image.write_to(&mut Cursor::new(&mut bytes), format)?,
};
Ok(RustImageBuffer(bytes))
}

fn to_jpeg(&self) -> Result<RustImageBuffer> {
// JPEG 需要 RGB 格式(不支持 alpha 通道)
self.encode_image(ColorType::Rgb8, ImageFormat::Jpeg)
}

fn to_png(&self) -> Result<RustImageBuffer> {
// PNG 使用 RGBA 格式以支持透明度
self.encode_image(ColorType::Rgba8, ImageFormat::Png)
}

#[cfg(target_os = "windows")]
fn to_bitmap(&self) -> Result<RustImageBuffer> {
// BMP 使用 RGBA 格式
self.encode_image(ColorType::Rgba8, ImageFormat::Bmp)
}
}

impl RustImageBuffer {
Expand Down
53 changes: 27 additions & 26 deletions src/platform/macos.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
use crate::common::{Result, RustImage, RustImageData};
use crate::{Clipboard, ClipboardContent, ClipboardHandler, ClipboardWatcher, ContentFormat};
use objc2::rc::Retained;
use objc2::{
rc::{autoreleasepool, Id},
runtime::ProtocolObject,
ClassType,
};
use objc2::AllocAnyThread;
use objc2::{rc::autoreleasepool, runtime::ProtocolObject};
use objc2_app_kit::{
NSFilenamesPboardType, NSImage, NSPasteboard, NSPasteboardItem, NSPasteboardType,
NSPasteboardTypeHTML, NSPasteboardTypePNG, NSPasteboardTypeRTF, NSPasteboardTypeString,
Expand All @@ -18,11 +15,11 @@ use std::time::Duration;
use std::vec;

pub struct ClipboardContext {
pasteboard: Id<NSPasteboard>,
pasteboard: Retained<NSPasteboard>,
}

pub struct ClipboardWatcherContext<T: ClipboardHandler> {
pasteboard: Id<NSPasteboard>,
pasteboard: Retained<NSPasteboard>,
handlers: Vec<T>,
stop_signal: Sender<()>,
stop_receiver: Receiver<()>,
Expand Down Expand Up @@ -114,11 +111,14 @@ impl ClipboardContext {
}

fn set_files(&self, files: &[String]) -> Result<()> {
let ns_string_arr =
NSArray::from_vec(files.iter().map(|f| NSString::from_str(f)).collect());
let ns_string_arr = files
.iter()
.map(|f| NSString::from_str(f))
.collect::<Vec<_>>();
let array: Retained<NSArray<NSString>> = NSArray::from_retained_slice(&ns_string_arr);
unsafe {
self.pasteboard
.setPropertyList_forType(&ns_string_arr, NSFilenamesPboardType)
.setPropertyList_forType(&array, NSFilenamesPboardType)
};
Ok(())
}
Expand All @@ -131,24 +131,25 @@ impl ClipboardContext {
}
}
autoreleasepool(|_| unsafe {
let mut write_objects: Vec<Id<ProtocolObject<(dyn NSPasteboardWriting + 'static)>>> =
vec![];
let mut write_objects: Vec<
Retained<ProtocolObject<(dyn NSPasteboardWriting + 'static)>>,
> = vec![];
for d in data {
match d {
ClipboardContent::Text(text) => {
let item = NSPasteboardItem::new();
item.setString_forType(&NSString::from_str(text), NSPasteboardTypeString);
write_objects.push(ProtocolObject::from_id(item));
write_objects.push(ProtocolObject::from_retained(item));
}
ClipboardContent::Rtf(rtf) => {
let item = NSPasteboardItem::new();
item.setString_forType(&NSString::from_str(rtf), NSPasteboardTypeRTF);
write_objects.push(ProtocolObject::from_id(item));
write_objects.push(ProtocolObject::from_retained(item));
}
ClipboardContent::Html(html) => {
let item = NSPasteboardItem::new();
item.setString_forType(&NSString::from_str(html), NSPasteboardTypeHTML);
write_objects.push(ProtocolObject::from_id(item));
write_objects.push(ProtocolObject::from_retained(item));
}
ClipboardContent::Image(image) => {
let png_img = image.to_png();
Expand All @@ -163,7 +164,7 @@ impl ClipboardContext {
};
let item = NSPasteboardItem::new();
item.setData_forType(&ns_data, NSPasteboardTypePNG);
write_objects.push(ProtocolObject::from_id(item));
write_objects.push(ProtocolObject::from_retained(item));
};
}
ClipboardContent::Files(files) => {
Expand All @@ -178,18 +179,18 @@ impl ClipboardContext {
)
};
self.pasteboard.declareTypes_owner(
&NSArray::from_vec(vec![NSString::from_str(format)]),
&NSArray::from_retained_slice(&[NSString::from_str(format)]),
None,
);
let item = NSPasteboardItem::new();
item.setData_forType(&ns_data, &NSString::from_str(format));
write_objects.push(ProtocolObject::from_id(item));
write_objects.push(ProtocolObject::from_retained(item));
}
}
}
if !self
.pasteboard
.writeObjects(&NSArray::from_vec(write_objects))
.writeObjects(&NSArray::from_retained_slice(&write_objects))
{
return Err("writeObjects failed");
}
Expand Down Expand Up @@ -230,7 +231,7 @@ impl Clipboard for ClipboardContext {
},
ContentFormat::Image => unsafe {
// Currently only judge whether there is a png format
let types = NSArray::from_vec(vec![
let types = NSArray::from_retained_slice(&[
NSPasteboardTypePNG.to_owned(),
NSPasteboardTypeTIFF.to_owned(),
]);
Expand All @@ -241,7 +242,7 @@ impl Clipboard for ClipboardContext {
self.pasteboard.availableTypeFromArray(&types).is_some()
},
ContentFormat::Other(format) => unsafe {
let types = NSArray::from_vec(vec![NSString::from_str(&format)]);
let types = NSArray::from_retained_slice(&[NSString::from_str(&format)]);
self.pasteboard.availableTypeFromArray(&types).is_some()
},
}
Expand All @@ -254,7 +255,7 @@ impl Clipboard for ClipboardContext {

fn get_buffer(&self, format: &str) -> Result<Vec<u8>> {
if let Some(data) = unsafe { self.pasteboard.dataForType(&NSString::from_str(format)) } {
return Ok(data.bytes().to_vec());
return Ok(data.to_vec());
}
Err("no data".into())
}
Expand All @@ -275,15 +276,15 @@ impl Clipboard for ClipboardContext {
autoreleasepool(|_| {
let png_data = unsafe { self.pasteboard.dataForType(NSPasteboardTypePNG) };
if let Some(data) = png_data {
return RustImageData::from_bytes(data.bytes());
return RustImageData::from_bytes(&data.to_vec());
};
// if no png data, read NSImage;
let ns_image =
unsafe { NSImage::initWithPasteboard(NSImage::alloc(), &self.pasteboard) };
if let Some(image) = ns_image {
let tiff_data = unsafe { image.TIFFRepresentation() };
if let Some(data) = tiff_data {
return RustImageData::from_bytes(data.bytes());
return RustImageData::from_bytes(&data.to_vec());
}
};
Err("no image data".into())
Expand All @@ -296,7 +297,7 @@ impl Clipboard for ClipboardContext {
unsafe {
if let Some(array) = ns_array {
// cast to NSArray<NSString>
let array: Retained<NSArray<NSString>> = Retained::cast(array);
let array: Retained<NSArray<NSString>> = Retained::cast_unchecked(array);
array.iter().for_each(|item| {
res.push(item.to_string());
});
Expand Down Expand Up @@ -357,7 +358,7 @@ impl Clipboard for ClipboardContext {
{
results.push(ClipboardContent::Other(
format_name.to_string(),
data.bytes().to_vec(),
data.to_vec(),
));
break;
}
Expand Down
Loading

0 comments on commit bb7534f

Please sign in to comment.