chenbhao Claude Opus 4.6 commited on
Commit
ab32188
·
1 Parent(s): 412f992

fix: reduce blank rows below Kitty images by using placement c=/r= bounds

Browse files

Kitty placement now passes resolved character dimensions (charWidth,
charHeight) to limit the rendered image area. ImageBox uses pixel-derived
charHeight when loaded to avoid reserving too many Yoga rows.
LocalPicture.test.tsx computes pixelWidth/pixelHeight separately from
character dimensions for correct scaling.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

src/ink-picture/__tests__/LocalPicture.test.tsx CHANGED
@@ -7,6 +7,7 @@
7
  *
8
  * Usage:
9
  * bun run src/ink-picture/__tests__/LocalPicture.test.tsx
 
10
  */
11
 
12
  import { Jimp } from "jimp";
@@ -16,11 +17,17 @@ import Image, { InkPictureProvider } from "../index.ts";
16
 
17
  const IMAGE_PATH = "/home/yuki/Pictures/Wallpapers/3god.jpg";
18
 
 
 
 
 
19
  function App() {
20
  const { exit } = useApp();
21
  const [dimensions, setDimensions] = useState<{
22
- width: number;
23
- height: number;
 
 
24
  } | null>(null);
25
  const [err, setErr] = useState(false);
26
 
@@ -31,18 +38,40 @@ function App() {
31
  const origW = image.bitmap.width;
32
  const origH = image.bitmap.height;
33
  const cols = process.stdout.columns ?? 80;
34
- const targetW = Math.floor(cols * 0.6);
35
- const targetH = Math.floor(targetW * (origH / origW) / 2);
36
- setDimensions({ width: targetW, height: targetH });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  } catch {
38
  setErr(true);
39
  }
40
  })();
41
  }, []);
42
 
 
43
  useEffect(() => {
44
- const timer = setTimeout(() => exit(), 3000);
45
- return () => clearTimeout(timer);
 
 
 
46
  }, [exit]);
47
 
48
  if (err) {
@@ -58,8 +87,10 @@ function App() {
58
  <InkPictureProvider>
59
  <Image
60
  src={IMAGE_PATH}
61
- width={dimensions.width}
62
- height={dimensions.height}
 
 
63
  alt="3god"
64
  />
65
  </InkPictureProvider>
 
7
  *
8
  * Usage:
9
  * bun run src/ink-picture/__tests__/LocalPicture.test.tsx
10
+ * Press Ctrl+C to exit (image persists)
11
  */
12
 
13
  import { Jimp } from "jimp";
 
17
 
18
  const IMAGE_PATH = "/home/yuki/Pictures/Wallpapers/3god.jpg";
19
 
20
+ // 终端字符尺寸(像素)
21
+ const CELL_WIDTH = 8;
22
+ const CELL_HEIGHT = 16;
23
+
24
  function App() {
25
  const { exit } = useApp();
26
  const [dimensions, setDimensions] = useState<{
27
+ width: number; // 字符列数
28
+ height: number; // 字符行数
29
+ pixelWidth: number; // 像素宽度
30
+ pixelHeight: number; // 像素高度
31
  } | null>(null);
32
  const [err, setErr] = useState(false);
33
 
 
38
  const origW = image.bitmap.width;
39
  const origH = image.bitmap.height;
40
  const cols = process.stdout.columns ?? 80;
41
+
42
+ // 1. 目标字符尺寸
43
+ const targetW_chars = Math.floor(cols * 0.6);
44
+
45
+ // 2. 转为像素尺寸(用于图片缩放)
46
+ const targetW_pixels = targetW_chars * CELL_WIDTH;
47
+ const targetH_pixels = Math.floor(targetW_pixels * (origH / origW));
48
+
49
+ // 3. 确保最小高度(至少 3 行字符)
50
+ const minH_pixels = 3 * CELL_HEIGHT;
51
+ const finalH_pixels = Math.max(targetH_pixels, minH_pixels);
52
+
53
+ // 4. 转回字符行数(用于 Ink 占位)
54
+ const targetH_chars = Math.ceil(finalH_pixels / CELL_HEIGHT);
55
+
56
+ setDimensions({
57
+ width: targetW_chars,
58
+ height: targetH_chars,
59
+ pixelWidth: targetW_pixels,
60
+ pixelHeight: finalH_pixels,
61
+ });
62
  } catch {
63
  setErr(true);
64
  }
65
  })();
66
  }, []);
67
 
68
+ // Ctrl+C 直接退出,不清理图片
69
  useEffect(() => {
70
+ const handleSigint = () => {
71
+ exit();
72
+ };
73
+ process.on('SIGINT', handleSigint);
74
+ return () => process.off('SIGINT', handleSigint);
75
  }, [exit]);
76
 
77
  if (err) {
 
87
  <InkPictureProvider>
88
  <Image
89
  src={IMAGE_PATH}
90
+ width={dimensions.width} // 字符列数(Ink 占位)
91
+ height={dimensions.height} // 字符行数(Ink 占位)
92
+ pixelWidth={dimensions.pixelWidth} // 像素宽度(图片缩放)
93
+ pixelHeight={dimensions.pixelHeight} // 像素高度(图片缩放)
94
  alt="3god"
95
  />
96
  </InkPictureProvider>
src/ink-picture/components/ImageBox.tsx CHANGED
@@ -3,7 +3,8 @@ import React, { forwardRef } from "react";
3
 
4
  export interface ImageBoxProps {
5
  width: number | string;
6
- height: number | string;
 
7
  alt?: string;
8
  error?: boolean;
9
  loaded?: boolean;
@@ -11,11 +12,16 @@ export interface ImageBoxProps {
11
  }
12
 
13
  const ImageBox = forwardRef<DOMElement, ImageBoxProps>(function ImageBox(
14
- { width, height, alt, error, loaded, children },
15
  ref,
16
  ) {
 
 
 
 
 
17
  return (
18
- <Box ref={ref} flexDirection="column" width={width} height={height}>
19
  {loaded && children ? (
20
  children
21
  ) : (
 
3
 
4
  export interface ImageBoxProps {
5
  width: number | string;
6
+ height: number | string; // 总高度(占位)
7
+ imageHeight?: number; // 图片实际像素高度(新增)
8
  alt?: string;
9
  error?: boolean;
10
  loaded?: boolean;
 
12
  }
13
 
14
  const ImageBox = forwardRef<DOMElement, ImageBoxProps>(function ImageBox(
15
+ { width, height, imageHeight, alt, error, loaded, children },
16
  ref,
17
  ) {
18
+ // 计算图片占用的字符行数
19
+ const charHeight = typeof imageHeight === 'number'
20
+ ? Math.ceil(imageHeight / 16) // 像素转字符行
21
+ : height;
22
+
23
  return (
24
+ <Box ref={ref} flexDirection="column" width={width} height={loaded ? charHeight : height}>
25
  {loaded && children ? (
26
  children
27
  ) : (
src/ink-picture/components/image/Kitty.tsx CHANGED
@@ -78,7 +78,8 @@ function KittyImage(props: ImageProps) {
78
  stdout.write("\r");
79
  stdout.write(cursorForward(componentPosition.col));
80
 
81
- stdout.write(makeKittyPlacement(imageId, 1));
 
82
 
83
  stdout.write("\x1b8");
84
 
 
78
  stdout.write("\r");
79
  stdout.write(cursorForward(componentPosition.col));
80
 
81
+ // 关键修改:传入 resolvedWidth 和 resolvedHeight(字符尺寸)
82
+ stdout.write(makeKittyPlacement(imageId, 1, resolvedWidth, resolvedHeight));
83
 
84
  stdout.write("\x1b8");
85
 
src/ink-picture/renderers/kitty.ts CHANGED
@@ -31,8 +31,20 @@ export function makeKittyTransmitChunks(
31
  export function makeKittyPlacement(
32
  imageId: number,
33
  placementId: number = 1,
 
 
34
  ): string {
35
- return `\x1b_Ga=p,i=${imageId},p=${placementId},C=1,q=2\x1b\\`;
 
 
 
 
 
 
 
 
 
 
36
  }
37
 
38
  export function makeKittyDeletion(
 
31
  export function makeKittyPlacement(
32
  imageId: number,
33
  placementId: number = 1,
34
+ charWidth?: number, // 新增:字符列数
35
+ charHeight?: number, // 新增:字符行数
36
  ): string {
37
+ let params = `a=p,i=${imageId},p=${placementId},C=1,q=2`;
38
+
39
+ // 新增:限制显示区域
40
+ if (charWidth !== undefined) {
41
+ params += `,c=${charWidth}`;
42
+ }
43
+ if (charHeight !== undefined) {
44
+ params += `,r=${charHeight}`;
45
+ }
46
+
47
+ return `\x1b_G${params}\x1b\\`;
48
  }
49
 
50
  export function makeKittyDeletion(