Mayo commited on
fix: PSD text placement & editable
Browse files- koharu-psd/src/export.rs +49 -4
- koharu-rpc/src/psd_export.rs +76 -4
koharu-psd/src/export.rs
CHANGED
|
@@ -38,7 +38,7 @@ impl Default for PsdExportOptions {
|
|
| 38 |
include_inpainted: true,
|
| 39 |
include_segment_mask: true,
|
| 40 |
include_brush_layer: true,
|
| 41 |
-
text_layer_mode: TextLayerMode::
|
| 42 |
}
|
| 43 |
}
|
| 44 |
}
|
|
@@ -730,15 +730,20 @@ fn is_probably_latin(text: &str) -> bool {
|
|
| 730 |
|
| 731 |
#[cfg(test)]
|
| 732 |
mod tests {
|
|
|
|
|
|
|
|
|
|
| 733 |
use image::{Rgba, RgbaImage};
|
| 734 |
|
| 735 |
use crate::writer::PsdWriter;
|
| 736 |
|
| 737 |
-
use crate::input::{
|
|
|
|
|
|
|
| 738 |
|
| 739 |
use super::{
|
| 740 |
-
|
| 741 |
-
place_on_canvas, write_image_data,
|
| 742 |
};
|
| 743 |
|
| 744 |
#[test]
|
|
@@ -815,4 +820,44 @@ mod tests {
|
|
| 815 |
|
| 816 |
assert_eq!(infer_font_name(&block), "ArialMT");
|
| 817 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 818 |
}
|
|
|
|
| 38 |
include_inpainted: true,
|
| 39 |
include_segment_mask: true,
|
| 40 |
include_brush_layer: true,
|
| 41 |
+
text_layer_mode: TextLayerMode::Editable,
|
| 42 |
}
|
| 43 |
}
|
| 44 |
}
|
|
|
|
| 730 |
|
| 731 |
#[cfg(test)]
|
| 732 |
mod tests {
|
| 733 |
+
use std::collections::HashMap;
|
| 734 |
+
|
| 735 |
+
use image::DynamicImage;
|
| 736 |
use image::{Rgba, RgbaImage};
|
| 737 |
|
| 738 |
use crate::writer::PsdWriter;
|
| 739 |
|
| 740 |
+
use crate::input::{
|
| 741 |
+
PsdDocument, PsdTextBlock, PsdTextDirection, PsdTextStyle, ResolvedDocument,
|
| 742 |
+
};
|
| 743 |
|
| 744 |
use super::{
|
| 745 |
+
PsdExportOptions, TextLayerMode, TextOrientation, contains_cjk, export_document,
|
| 746 |
+
infer_font_name, infer_orientation, is_probably_latin, place_on_canvas, write_image_data,
|
| 747 |
};
|
| 748 |
|
| 749 |
#[test]
|
|
|
|
| 820 |
|
| 821 |
assert_eq!(infer_font_name(&block), "ArialMT");
|
| 822 |
}
|
| 823 |
+
|
| 824 |
+
#[test]
|
| 825 |
+
fn default_export_writes_editable_text_layer_metadata() {
|
| 826 |
+
let source =
|
| 827 |
+
DynamicImage::ImageRgba8(RgbaImage::from_pixel(16, 16, Rgba([255, 255, 255, 255])));
|
| 828 |
+
let document = PsdDocument {
|
| 829 |
+
width: 16,
|
| 830 |
+
height: 16,
|
| 831 |
+
text_blocks: vec![PsdTextBlock {
|
| 832 |
+
x: 2.0,
|
| 833 |
+
y: 3.0,
|
| 834 |
+
width: 10.0,
|
| 835 |
+
height: 8.0,
|
| 836 |
+
translation: Some("Hello".to_string()),
|
| 837 |
+
..Default::default()
|
| 838 |
+
}],
|
| 839 |
+
};
|
| 840 |
+
let block_images = HashMap::new();
|
| 841 |
+
let resolved = ResolvedDocument {
|
| 842 |
+
document: &document,
|
| 843 |
+
source: &source,
|
| 844 |
+
segment: None,
|
| 845 |
+
inpainted: None,
|
| 846 |
+
rendered: None,
|
| 847 |
+
brush_layer: None,
|
| 848 |
+
block_images: &block_images,
|
| 849 |
+
};
|
| 850 |
+
|
| 851 |
+
let options = PsdExportOptions {
|
| 852 |
+
include_original: false,
|
| 853 |
+
..Default::default()
|
| 854 |
+
};
|
| 855 |
+
assert_eq!(options.text_layer_mode, TextLayerMode::Editable);
|
| 856 |
+
|
| 857 |
+
let bytes = export_document(&resolved, &options).expect("export PSD");
|
| 858 |
+
assert!(
|
| 859 |
+
bytes.windows(4).any(|window| window == b"TySh"),
|
| 860 |
+
"editable PSD export should include text layer metadata",
|
| 861 |
+
);
|
| 862 |
+
}
|
| 863 |
}
|
koharu-rpc/src/psd_export.rs
CHANGED
|
@@ -157,12 +157,17 @@ fn text_to_psd(
|
|
| 157 |
transform: &koharu_core::Transform,
|
| 158 |
text: &TextData,
|
| 159 |
) -> PsdTextBlock {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
PsdTextBlock {
|
| 161 |
id: node_id.to_string(),
|
| 162 |
-
x:
|
| 163 |
-
y:
|
| 164 |
-
width:
|
| 165 |
-
height:
|
| 166 |
translation: text.translation.clone(),
|
| 167 |
style: text.style.as_ref().map(convert_style),
|
| 168 |
rendered: text.sprite.as_ref().map(blob_ref_to_psd),
|
|
@@ -220,3 +225,70 @@ fn convert_prediction(p: &FontPrediction) -> PsdFontPrediction {
|
|
| 220 |
fn blob_ref_to_psd(r: &BlobRef) -> PsdBlobRef {
|
| 221 |
PsdBlobRef::new(r.hash())
|
| 222 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
transform: &koharu_core::Transform,
|
| 158 |
text: &TextData,
|
| 159 |
) -> PsdTextBlock {
|
| 160 |
+
let layer_transform = match (&text.sprite, &text.sprite_transform) {
|
| 161 |
+
(Some(_), Some(sprite_transform)) => sprite_transform,
|
| 162 |
+
_ => transform,
|
| 163 |
+
};
|
| 164 |
+
|
| 165 |
PsdTextBlock {
|
| 166 |
id: node_id.to_string(),
|
| 167 |
+
x: layer_transform.x,
|
| 168 |
+
y: layer_transform.y,
|
| 169 |
+
width: layer_transform.width,
|
| 170 |
+
height: layer_transform.height,
|
| 171 |
translation: text.translation.clone(),
|
| 172 |
style: text.style.as_ref().map(convert_style),
|
| 173 |
rendered: text.sprite.as_ref().map(blob_ref_to_psd),
|
|
|
|
| 225 |
fn blob_ref_to_psd(r: &BlobRef) -> PsdBlobRef {
|
| 226 |
PsdBlobRef::new(r.hash())
|
| 227 |
}
|
| 228 |
+
|
| 229 |
+
#[cfg(test)]
|
| 230 |
+
mod tests {
|
| 231 |
+
use koharu_core::{BlobRef, NodeId, TextData, Transform};
|
| 232 |
+
|
| 233 |
+
use super::text_to_psd;
|
| 234 |
+
|
| 235 |
+
#[test]
|
| 236 |
+
fn text_to_psd_uses_sprite_transform_for_rendered_text_layer() {
|
| 237 |
+
let node_transform = Transform {
|
| 238 |
+
x: 10.0,
|
| 239 |
+
y: 20.0,
|
| 240 |
+
width: 30.0,
|
| 241 |
+
height: 40.0,
|
| 242 |
+
rotation_deg: 0.0,
|
| 243 |
+
};
|
| 244 |
+
let sprite_transform = Transform {
|
| 245 |
+
x: 100.0,
|
| 246 |
+
y: 200.0,
|
| 247 |
+
width: 300.0,
|
| 248 |
+
height: 400.0,
|
| 249 |
+
rotation_deg: 0.0,
|
| 250 |
+
};
|
| 251 |
+
let text = TextData {
|
| 252 |
+
translation: Some("Hello".to_string()),
|
| 253 |
+
sprite: Some(BlobRef::new("sprite")),
|
| 254 |
+
sprite_transform: Some(sprite_transform),
|
| 255 |
+
..Default::default()
|
| 256 |
+
};
|
| 257 |
+
|
| 258 |
+
let block = text_to_psd(&NodeId::new(), &node_transform, &text);
|
| 259 |
+
|
| 260 |
+
assert_eq!(block.x, sprite_transform.x);
|
| 261 |
+
assert_eq!(block.y, sprite_transform.y);
|
| 262 |
+
assert_eq!(block.width, sprite_transform.width);
|
| 263 |
+
assert_eq!(block.height, sprite_transform.height);
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
#[test]
|
| 267 |
+
fn text_to_psd_ignores_stale_sprite_transform_without_sprite() {
|
| 268 |
+
let node_transform = Transform {
|
| 269 |
+
x: 10.0,
|
| 270 |
+
y: 20.0,
|
| 271 |
+
width: 30.0,
|
| 272 |
+
height: 40.0,
|
| 273 |
+
rotation_deg: 0.0,
|
| 274 |
+
};
|
| 275 |
+
let text = TextData {
|
| 276 |
+
translation: Some("Hello".to_string()),
|
| 277 |
+
sprite_transform: Some(Transform {
|
| 278 |
+
x: 100.0,
|
| 279 |
+
y: 200.0,
|
| 280 |
+
width: 300.0,
|
| 281 |
+
height: 400.0,
|
| 282 |
+
rotation_deg: 0.0,
|
| 283 |
+
}),
|
| 284 |
+
..Default::default()
|
| 285 |
+
};
|
| 286 |
+
|
| 287 |
+
let block = text_to_psd(&NodeId::new(), &node_transform, &text);
|
| 288 |
+
|
| 289 |
+
assert_eq!(block.x, node_transform.x);
|
| 290 |
+
assert_eq!(block.y, node_transform.y);
|
| 291 |
+
assert_eq!(block.width, node_transform.width);
|
| 292 |
+
assert_eq!(block.height, node_transform.height);
|
| 293 |
+
}
|
| 294 |
+
}
|