fix: reduce blank rows below Kitty images by using placement c=/r= bounds
Browse filesKitty 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 |
-
|
| 35 |
-
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
} catch {
|
| 38 |
setErr(true);
|
| 39 |
}
|
| 40 |
})();
|
| 41 |
}, []);
|
| 42 |
|
|
|
|
| 43 |
useEffect(() => {
|
| 44 |
-
const
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|