image-area-cropping.js 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. /**
  2. * Func 标签:image-area-cropping
  3. *
  4. * 约定:src/pages/processing/func/ 目录下每个文件名就是一个"可用标签/能力"。
  5. * 本文件用于声明该标签存在(供文档/提示词/后续动态加载使用)。
  6. *
  7. * 语义:根据区域坐标裁剪当前截图(ScreenShot.jpg)的指定区域,并保存到指定路径。
  8. */
  9. import ScrcpyConfig from '../../screenshot/scrcpy-config.js';
  10. export const tagName = 'image-area-cropping';
  11. export const schema = {
  12. description: '根据区域坐标裁剪当前截图(ScreenShot.jpg)的指定区域,并保存到指定路径。',
  13. inputs: {
  14. area: '区域坐标(JSON字符串格式,包含 topLeft 和 bottomRight,或包含 x, y, width, height)',
  15. savePath: '保存路径(相对于工作流目录或绝对路径)',
  16. },
  17. outputs: {
  18. result: '保存结果(成功返回 "1",失败返回 "0")',
  19. },
  20. };
  21. /**
  22. * 执行 image-area-cropping 功能
  23. * 这个函数会被 ActionParser 调用
  24. *
  25. * @param {Object} params - 参数对象
  26. * @param {string} params.area - 区域坐标(JSON字符串或对象,格式:{topLeft: {x, y}, bottomRight: {x, y}} 或 {x, y, width, height})
  27. * @param {string} params.savePath - 保存路径
  28. * @param {string} params.folderPath - 工作流文件夹路径
  29. * @param {string} params.device - 设备 ID/IP:Port(可选,用于获取最新截图)
  30. * @returns {Promise<{success: boolean, error?: string}>}
  31. */
  32. export async function executeImageAreaCropping({ area, savePath, folderPath, device }) {
  33. try {
  34. if (!window.electronAPI || !window.electronAPI.cropAndSaveImage) {
  35. return {
  36. success: false,
  37. error: 'cropAndSaveImage API 不可用'
  38. };
  39. }
  40. // 解析区域坐标
  41. let areaObj = area;
  42. if (typeof area === 'string') {
  43. try {
  44. areaObj = JSON.parse(area);
  45. } catch (e) {
  46. return {
  47. success: false,
  48. error: `区域坐标格式错误,无法解析JSON: ${e.message}`
  49. };
  50. }
  51. }
  52. if (!areaObj || typeof areaObj !== 'object') {
  53. return {
  54. success: false,
  55. error: '区域坐标必须是对象格式'
  56. };
  57. }
  58. // 提取坐标信息(支持多种格式)
  59. let x, y, width, height;
  60. if (areaObj.topLeft && areaObj.bottomRight) {
  61. // 格式1:{topLeft: {x, y}, bottomRight: {x, y}}
  62. x = parseInt(areaObj.topLeft.x);
  63. y = parseInt(areaObj.topLeft.y);
  64. width = parseInt(areaObj.bottomRight.x - areaObj.topLeft.x);
  65. height = parseInt(areaObj.bottomRight.y - areaObj.topLeft.y);
  66. } else if (areaObj.topLeft && areaObj.topRight && areaObj.bottomLeft && areaObj.bottomRight) {
  67. // 格式1.5:{topLeft, topRight, bottomLeft, bottomRight} - 使用 topLeft 和 bottomRight
  68. x = parseInt(areaObj.topLeft.x);
  69. y = parseInt(areaObj.topLeft.y);
  70. width = parseInt(areaObj.bottomRight.x - areaObj.topLeft.x);
  71. height = parseInt(areaObj.bottomRight.y - areaObj.topLeft.y);
  72. } else if (areaObj.x !== undefined && areaObj.y !== undefined && areaObj.width !== undefined && areaObj.height !== undefined) {
  73. // 格式2:{x, y, width, height}
  74. x = parseInt(areaObj.x);
  75. y = parseInt(areaObj.y);
  76. width = parseInt(areaObj.width);
  77. height = parseInt(areaObj.height);
  78. } else {
  79. return {
  80. success: false,
  81. error: '区域坐标格式不正确,需要包含 topLeft/bottomRight 或 x/y/width/height'
  82. };
  83. }
  84. // 验证坐标有效性
  85. if (isNaN(x) || isNaN(y) || isNaN(width) || isNaN(height) || width <= 0 || height <= 0) {
  86. return {
  87. success: false,
  88. error: `区域坐标无效: x=${x}, y=${y}, width=${width}, height=${height}`
  89. };
  90. }
  91. // 先通过 ADB 截图当前手机屏幕并保存到 history 文件夹
  92. // 参考 screenshot.js 的实现方式
  93. let imageBase64 = null; // 声明 imageBase64 变量
  94. let screenshotPath;
  95. // 根据配置确定文件扩展名
  96. const screencapFormat = ScrcpyConfig['screencap-format'] || 'jpg';
  97. const fileExtension = screencapFormat === 'jpeg' || screencapFormat === 'jpg' ? 'jpg' : 'png';
  98. if (folderPath.includes(':')) {
  99. // 绝对路径
  100. screenshotPath = `${folderPath}/history/ScreenShot.${fileExtension}`;
  101. } else {
  102. // 相对路径,构建相对于项目根目录的路径
  103. screenshotPath = `${folderPath}/history/ScreenShot.${fileExtension}`;
  104. }
  105. // 如果有设备ID,先尝试从缓存获取截图,如果没有再调用 ADB 截图
  106. if (device && window.electronAPI) {
  107. try {
  108. // 优先从主进程缓存获取截图(避免并发冲突)
  109. let screenshotResult = null;
  110. if (window.electronAPI.getCachedScreenshot) {
  111. screenshotResult = await window.electronAPI.getCachedScreenshot(device);
  112. if (screenshotResult && screenshotResult.success && screenshotResult.data) {
  113. imageBase64 = screenshotResult.data;
  114. }
  115. }
  116. // 如果缓存不可用,调用 ADB 截图(参考 screenshot.js 的实现方式)
  117. if (!imageBase64 && window.electronAPI.captureScreenshot) {
  118. screenshotResult = await window.electronAPI.captureScreenshot(device, {
  119. format: ScrcpyConfig['screencap-format'],
  120. quality: ScrcpyConfig['screencap-quality'],
  121. scale: ScrcpyConfig['screencap-scale']
  122. });
  123. if (screenshotResult && screenshotResult.success && screenshotResult.data) {
  124. imageBase64 = screenshotResult.data;
  125. }
  126. }
  127. // 如果有截图数据,保存到文件
  128. if (imageBase64) {
  129. await window.electronAPI.saveBase64Image(
  130. imageBase64,
  131. screenshotPath
  132. );
  133. }
  134. } catch (error) {
  135. // 截屏循环异常(参考 screenshot.js 的错误处理)
  136. }
  137. }
  138. // 处理保存路径(如果是相对路径,相对于工作流目录)
  139. let absoluteSavePath = savePath;
  140. if (!savePath.includes(':')) {
  141. // 相对路径,相对于工作流目录
  142. if (folderPath.includes(':')) {
  143. absoluteSavePath = `${folderPath}/${savePath}`;
  144. } else {
  145. absoluteSavePath = `${folderPath}/${savePath}`;
  146. }
  147. }
  148. // 如果还没有通过ADB获取base64数据,则从文件读取
  149. if (!imageBase64) {
  150. try {
  151. // 使用 Electron API 读取文件
  152. if (window.electronAPI && window.electronAPI.readImageFileAsBase64) {
  153. // 读取文件为 base64
  154. const fileContent = await window.electronAPI.readImageFileAsBase64(screenshotPath);
  155. if (!fileContent || !fileContent.success) {
  156. return {
  157. success: false,
  158. error: `无法读取截图文件: ${fileContent?.error || '未知错误'}`
  159. };
  160. }
  161. imageBase64 = fileContent.data;
  162. } else {
  163. return {
  164. success: false,
  165. error: 'readImageFileAsBase64 API 不可用'
  166. };
  167. }
  168. } catch (error) {
  169. return {
  170. success: false,
  171. error: `读取截图文件失败: ${error.message}`
  172. };
  173. }
  174. }
  175. // 验证 base64 数据是否有效(至少应该是几百字节)
  176. if (!imageBase64 || imageBase64.length < 100) {
  177. return {
  178. success: false,
  179. error: `截图数据无效,base64长度: ${imageBase64?.length || 0},应该是至少几百字节。可能是截图失败或读取失败`
  180. };
  181. }
  182. // 使用 Canvas API 裁剪图片
  183. try {
  184. // 创建 Image 对象
  185. const img = new Image();
  186. // 先尝试 JPEG,如果失败再尝试 PNG
  187. let imageMimeType = 'image/jpeg';
  188. let dataUrl = `data:${imageMimeType};base64,${imageBase64}`;
  189. await new Promise((resolve, reject) => {
  190. img.onload = () => {
  191. resolve();
  192. };
  193. img.onerror = (error) => {
  194. // 尝试 PNG 格式
  195. imageMimeType = 'image/png';
  196. dataUrl = `data:${imageMimeType};base64,${imageBase64}`;
  197. img.src = dataUrl;
  198. };
  199. img.src = dataUrl;
  200. }).catch((error) => {
  201. throw new Error(`无法加载图片数据,请检查截图文件是否有效`);
  202. });
  203. // 验证坐标是否在图片范围内
  204. if (x < 0 || y < 0 || x + width > img.width || y + height > img.height) {
  205. return {
  206. success: false,
  207. error: `裁剪区域超出图片范围。图片尺寸: ${img.width}x${img.height}, 裁剪区域: x=${x}, y=${y}, width=${width}, height=${height}`
  208. };
  209. }
  210. // 创建 Canvas 并裁剪
  211. const canvas = document.createElement('canvas');
  212. canvas.width = width;
  213. canvas.height = height;
  214. const ctx = canvas.getContext('2d');
  215. // 绘制裁剪后的区域
  216. ctx.drawImage(img, x, y, width, height, 0, 0, width, height);
  217. // 转换为 base64(PNG 格式)
  218. const croppedBase64 = canvas.toDataURL('image/png').split(',')[1]; // 去掉 data:image/png;base64, 前缀
  219. // 调用主进程保存 base64 图片
  220. const result = await window.electronAPI.saveBase64Image(
  221. croppedBase64,
  222. absoluteSavePath
  223. );
  224. if (!result.success) {
  225. return {
  226. success: false,
  227. error: result.error || '保存图片失败'
  228. };
  229. }
  230. return {
  231. success: true
  232. };
  233. } catch (error) {
  234. return {
  235. success: false,
  236. error: `Canvas裁剪失败: ${error.message}`
  237. };
  238. }
  239. } catch (error) {
  240. return {
  241. success: false,
  242. error: error.message || '裁剪图片失败'
  243. };
  244. }
  245. }