@xiuchang-midscene/shared 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/es/baseDB.mjs +109 -0
- package/dist/es/baseDB.mjs.bak +109 -0
- package/dist/es/build/copy-static.mjs +31 -0
- package/dist/es/build/copy-static.mjs.bak +31 -0
- package/dist/es/build/rspack-config.mjs +4 -0
- package/dist/es/build/rspack-config.mjs.bak +4 -0
- package/dist/es/cli/cli-runner.mjs +140 -0
- package/dist/es/cli/cli-runner.mjs.bak +140 -0
- package/dist/es/cli/index.mjs +2 -0
- package/dist/es/cli/index.mjs.bak +2 -0
- package/dist/es/common.mjs +37 -0
- package/dist/es/common.mjs.bak +37 -0
- package/dist/es/constants/example-code.mjs +223 -0
- package/dist/es/constants/example-code.mjs.bak +223 -0
- package/dist/es/constants/index.mjs +23 -0
- package/dist/es/constants/index.mjs.bak +23 -0
- package/dist/es/env/basic.mjs +6 -0
- package/dist/es/env/basic.mjs.bak +6 -0
- package/dist/es/env/constants.mjs +70 -0
- package/dist/es/env/constants.mjs.bak +70 -0
- package/dist/es/env/global-config-manager.mjs +94 -0
- package/dist/es/env/global-config-manager.mjs.bak +94 -0
- package/dist/es/env/helper.mjs +43 -0
- package/dist/es/env/helper.mjs.bak +43 -0
- package/dist/es/env/index.mjs +5 -0
- package/dist/es/env/index.mjs.bak +5 -0
- package/dist/es/env/init-debug.mjs +18 -0
- package/dist/es/env/init-debug.mjs.bak +18 -0
- package/dist/es/env/model-config-manager.mjs +79 -0
- package/dist/es/env/model-config-manager.mjs.bak +79 -0
- package/dist/es/env/parse-model-config.mjs +132 -0
- package/dist/es/env/parse-model-config.mjs.bak +132 -0
- package/dist/es/env/types.mjs +220 -0
- package/dist/es/env/types.mjs.bak +220 -0
- package/dist/es/env/utils.mjs +26 -0
- package/dist/es/env/utils.mjs.bak +26 -0
- package/dist/es/extractor/constants.mjs +2 -0
- package/dist/es/extractor/constants.mjs.bak +2 -0
- package/dist/es/extractor/debug.mjs +6 -0
- package/dist/es/extractor/debug.mjs.bak +6 -0
- package/dist/es/extractor/dom-util.mjs +92 -0
- package/dist/es/extractor/dom-util.mjs.bak +92 -0
- package/dist/es/extractor/index.mjs +5 -0
- package/dist/es/extractor/index.mjs.bak +5 -0
- package/dist/es/extractor/locator.mjs +250 -0
- package/dist/es/extractor/locator.mjs.bak +250 -0
- package/dist/es/extractor/tree.mjs +78 -0
- package/dist/es/extractor/tree.mjs.bak +78 -0
- package/dist/es/extractor/util.mjs +245 -0
- package/dist/es/extractor/util.mjs.bak +245 -0
- package/dist/es/extractor/web-extractor.mjs +303 -0
- package/dist/es/extractor/web-extractor.mjs.bak +303 -0
- package/dist/es/img/box-select.mjs +824 -0
- package/dist/es/img/box-select.mjs.bak +824 -0
- package/dist/es/img/canvas-fallback.mjs +238 -0
- package/dist/es/img/canvas-fallback.mjs.bak +238 -0
- package/dist/es/img/get-photon.mjs +45 -0
- package/dist/es/img/get-photon.mjs.bak +45 -0
- package/dist/es/img/get-sharp.mjs +11 -0
- package/dist/es/img/get-sharp.mjs.bak +11 -0
- package/dist/es/img/index.mjs +4 -0
- package/dist/es/img/index.mjs.bak +4 -0
- package/dist/es/img/info.mjs +29 -0
- package/dist/es/img/info.mjs.bak +29 -0
- package/dist/es/img/transform.mjs +295 -0
- package/dist/es/img/transform.mjs.bak +295 -0
- package/dist/es/index.mjs +4 -0
- package/dist/es/index.mjs.bak +4 -0
- package/dist/es/logger.mjs +64 -0
- package/dist/es/logger.mjs.bak +64 -0
- package/dist/es/mcp/base-server.mjs +281 -0
- package/dist/es/mcp/base-server.mjs.bak +281 -0
- package/dist/es/mcp/base-tools.mjs +91 -0
- package/dist/es/mcp/base-tools.mjs.bak +91 -0
- package/dist/es/mcp/chrome-path.mjs +35 -0
- package/dist/es/mcp/chrome-path.mjs.bak +35 -0
- package/dist/es/mcp/index.mjs +7 -0
- package/dist/es/mcp/index.mjs.bak +7 -0
- package/dist/es/mcp/inject-report-html-plugin.mjs +53 -0
- package/dist/es/mcp/inject-report-html-plugin.mjs.bak +53 -0
- package/dist/es/mcp/launcher-helper.mjs +52 -0
- package/dist/es/mcp/launcher-helper.mjs.bak +52 -0
- package/dist/es/mcp/tool-generator.mjs +297 -0
- package/dist/es/mcp/tool-generator.mjs.bak +297 -0
- package/dist/es/mcp/types.mjs +3 -0
- package/dist/es/mcp/types.mjs.bak +3 -0
- package/dist/es/node/fs.mjs +44 -0
- package/dist/es/node/fs.mjs.bak +44 -0
- package/dist/es/node/index.mjs +2 -0
- package/dist/es/node/index.mjs.bak +2 -0
- package/dist/es/node/port.mjs +24 -0
- package/dist/es/node/port.mjs.bak +24 -0
- package/dist/es/oss/demo.mjs +30 -0
- package/dist/es/oss/demo.mjs.bak +30 -0
- package/dist/es/oss/index.mjs +90 -0
- package/dist/es/oss/index.mjs.bak +90 -0
- package/dist/es/polyfills/async-hooks.mjs +2 -0
- package/dist/es/polyfills/async-hooks.mjs.bak +2 -0
- package/dist/es/polyfills/index.mjs +1 -0
- package/dist/es/polyfills/index.mjs.bak +1 -0
- package/dist/es/types/index.mjs +3 -0
- package/dist/es/types/index.mjs.bak +3 -0
- package/dist/es/us-keyboard-layout.mjs +1414 -0
- package/dist/es/us-keyboard-layout.mjs.LICENSE.txt +5 -0
- package/dist/es/us-keyboard-layout.mjs.bak +1414 -0
- package/dist/es/utils.mjs +72 -0
- package/dist/es/utils.mjs.bak +72 -0
- package/dist/es/zod-schema-utils.mjs +54 -0
- package/dist/es/zod-schema-utils.mjs.bak +54 -0
- package/dist/lib/baseDB.js +149 -0
- package/dist/lib/baseDB.js.bak +149 -0
- package/dist/lib/build/copy-static.js +79 -0
- package/dist/lib/build/copy-static.js.bak +79 -0
- package/dist/lib/build/rspack-config.js +38 -0
- package/dist/lib/build/rspack-config.js.bak +38 -0
- package/dist/lib/cli/cli-runner.js +196 -0
- package/dist/lib/cli/cli-runner.js.bak +196 -0
- package/dist/lib/cli/index.js +48 -0
- package/dist/lib/cli/index.js.bak +48 -0
- package/dist/lib/common.js +93 -0
- package/dist/lib/common.js.bak +93 -0
- package/dist/lib/constants/example-code.js +260 -0
- package/dist/lib/constants/example-code.js.bak +260 -0
- package/dist/lib/constants/index.js +96 -0
- package/dist/lib/constants/index.js.bak +96 -0
- package/dist/lib/env/basic.js +40 -0
- package/dist/lib/env/basic.js.bak +40 -0
- package/dist/lib/env/constants.js +113 -0
- package/dist/lib/env/constants.js.bak +113 -0
- package/dist/lib/env/global-config-manager.js +128 -0
- package/dist/lib/env/global-config-manager.js.bak +128 -0
- package/dist/lib/env/helper.js +80 -0
- package/dist/lib/env/helper.js.bak +80 -0
- package/dist/lib/env/index.js +90 -0
- package/dist/lib/env/index.js.bak +90 -0
- package/dist/lib/env/init-debug.js +52 -0
- package/dist/lib/env/init-debug.js.bak +52 -0
- package/dist/lib/env/model-config-manager.js +113 -0
- package/dist/lib/env/model-config-manager.js.bak +113 -0
- package/dist/lib/env/parse-model-config.js +178 -0
- package/dist/lib/env/parse-model-config.js.bak +178 -0
- package/dist/lib/env/types.js +554 -0
- package/dist/lib/env/types.js.bak +554 -0
- package/dist/lib/env/utils.js +72 -0
- package/dist/lib/env/utils.js.bak +72 -0
- package/dist/lib/extractor/constants.js +42 -0
- package/dist/lib/extractor/constants.js.bak +42 -0
- package/dist/lib/extractor/debug.js +12 -0
- package/dist/lib/extractor/debug.js.bak +12 -0
- package/dist/lib/extractor/dom-util.js +153 -0
- package/dist/lib/extractor/dom-util.js.bak +153 -0
- package/dist/lib/extractor/index.js +81 -0
- package/dist/lib/extractor/index.js.bak +81 -0
- package/dist/lib/extractor/locator.js +296 -0
- package/dist/lib/extractor/locator.js.bak +296 -0
- package/dist/lib/extractor/tree.js +124 -0
- package/dist/lib/extractor/tree.js.bak +124 -0
- package/dist/lib/extractor/util.js +336 -0
- package/dist/lib/extractor/util.js.bak +336 -0
- package/dist/lib/extractor/web-extractor.js +349 -0
- package/dist/lib/extractor/web-extractor.js.bak +349 -0
- package/dist/lib/img/box-select.js +875 -0
- package/dist/lib/img/box-select.js.bak +875 -0
- package/dist/lib/img/canvas-fallback.js +305 -0
- package/dist/lib/img/canvas-fallback.js.bak +305 -0
- package/dist/lib/img/get-photon.js +82 -0
- package/dist/lib/img/get-photon.js.bak +82 -0
- package/dist/lib/img/get-sharp.js +45 -0
- package/dist/lib/img/get-sharp.js.bak +45 -0
- package/dist/lib/img/index.js +95 -0
- package/dist/lib/img/index.js.bak +95 -0
- package/dist/lib/img/info.js +83 -0
- package/dist/lib/img/info.js.bak +83 -0
- package/dist/lib/img/transform.js +387 -0
- package/dist/lib/img/transform.js.bak +387 -0
- package/dist/lib/index.js +47 -0
- package/dist/lib/index.js.bak +47 -0
- package/dist/lib/logger.js +114 -0
- package/dist/lib/logger.js.bak +114 -0
- package/dist/lib/mcp/base-server.js +331 -0
- package/dist/lib/mcp/base-server.js.bak +331 -0
- package/dist/lib/mcp/base-tools.js +125 -0
- package/dist/lib/mcp/base-tools.js.bak +125 -0
- package/dist/lib/mcp/chrome-path.js +72 -0
- package/dist/lib/mcp/chrome-path.js.bak +72 -0
- package/dist/lib/mcp/index.js +100 -0
- package/dist/lib/mcp/index.js.bak +100 -0
- package/dist/lib/mcp/inject-report-html-plugin.js +98 -0
- package/dist/lib/mcp/inject-report-html-plugin.js.bak +98 -0
- package/dist/lib/mcp/launcher-helper.js +86 -0
- package/dist/lib/mcp/launcher-helper.js.bak +86 -0
- package/dist/lib/mcp/tool-generator.js +334 -0
- package/dist/lib/mcp/tool-generator.js.bak +334 -0
- package/dist/lib/mcp/types.js +40 -0
- package/dist/lib/mcp/types.js.bak +40 -0
- package/dist/lib/node/fs.js +97 -0
- package/dist/lib/node/fs.js.bak +97 -0
- package/dist/lib/node/index.js +65 -0
- package/dist/lib/node/index.js.bak +65 -0
- package/dist/lib/node/port.js +61 -0
- package/dist/lib/node/port.js.bak +61 -0
- package/dist/lib/oss/demo.js +36 -0
- package/dist/lib/oss/demo.js.bak +36 -0
- package/dist/lib/oss/index.js +138 -0
- package/dist/lib/oss/index.js.bak +138 -0
- package/dist/lib/polyfills/async-hooks.js +36 -0
- package/dist/lib/polyfills/async-hooks.js.bak +36 -0
- package/dist/lib/polyfills/index.js +58 -0
- package/dist/lib/polyfills/index.js.bak +58 -0
- package/dist/lib/types/index.js +37 -0
- package/dist/lib/types/index.js.bak +37 -0
- package/dist/lib/us-keyboard-layout.js +1457 -0
- package/dist/lib/us-keyboard-layout.js.LICENSE.txt +5 -0
- package/dist/lib/us-keyboard-layout.js.bak +1457 -0
- package/dist/lib/utils.js +148 -0
- package/dist/lib/utils.js.bak +148 -0
- package/dist/lib/zod-schema-utils.js +97 -0
- package/dist/lib/zod-schema-utils.js.bak +97 -0
- package/dist/types/baseDB.d.ts +25 -0
- package/dist/types/baseDB.d.ts.bak +25 -0
- package/dist/types/build/copy-static.d.ts +31 -0
- package/dist/types/build/copy-static.d.ts.bak +31 -0
- package/dist/types/build/rspack-config.d.ts +8 -0
- package/dist/types/build/rspack-config.d.ts.bak +8 -0
- package/dist/types/cli/cli-runner.d.ts +14 -0
- package/dist/types/cli/cli-runner.d.ts.bak +14 -0
- package/dist/types/cli/index.d.ts +2 -0
- package/dist/types/cli/index.d.ts.bak +2 -0
- package/dist/types/common.d.ts +12 -0
- package/dist/types/common.d.ts.bak +12 -0
- package/dist/types/constants/example-code.d.ts +2 -0
- package/dist/types/constants/example-code.d.ts.bak +2 -0
- package/dist/types/constants/index.d.ts +21 -0
- package/dist/types/constants/index.d.ts.bak +21 -0
- package/dist/types/env/basic.d.ts +6 -0
- package/dist/types/env/basic.d.ts.bak +6 -0
- package/dist/types/env/constants.d.ts +40 -0
- package/dist/types/env/constants.d.ts.bak +40 -0
- package/dist/types/env/global-config-manager.d.ts +32 -0
- package/dist/types/env/global-config-manager.d.ts.bak +32 -0
- package/dist/types/env/helper.d.ts +4 -0
- package/dist/types/env/helper.d.ts.bak +4 -0
- package/dist/types/env/index.d.ts +4 -0
- package/dist/types/env/index.d.ts.bak +4 -0
- package/dist/types/env/init-debug.d.ts +1 -0
- package/dist/types/env/init-debug.d.ts.bak +1 -0
- package/dist/types/env/model-config-manager.d.ts +25 -0
- package/dist/types/env/model-config-manager.d.ts.bak +25 -0
- package/dist/types/env/parse-model-config.d.ts +31 -0
- package/dist/types/env/parse-model-config.d.ts.bak +31 -0
- package/dist/types/env/types.d.ts +318 -0
- package/dist/types/env/types.d.ts.bak +318 -0
- package/dist/types/env/utils.d.ts +38 -0
- package/dist/types/env/utils.d.ts.bak +38 -0
- package/dist/types/extractor/constants.d.ts +1 -0
- package/dist/types/extractor/constants.d.ts.bak +1 -0
- package/dist/types/extractor/debug.d.ts +1 -0
- package/dist/types/extractor/debug.d.ts.bak +1 -0
- package/dist/types/extractor/dom-util.d.ts +56 -0
- package/dist/types/extractor/dom-util.d.ts.bak +56 -0
- package/dist/types/extractor/index.d.ts +32 -0
- package/dist/types/extractor/index.d.ts.bak +32 -0
- package/dist/types/extractor/locator.d.ts +9 -0
- package/dist/types/extractor/locator.d.ts.bak +9 -0
- package/dist/types/extractor/tree.d.ts +6 -0
- package/dist/types/extractor/tree.d.ts.bak +6 -0
- package/dist/types/extractor/util.d.ts +47 -0
- package/dist/types/extractor/util.d.ts.bak +47 -0
- package/dist/types/extractor/web-extractor.d.ts +19 -0
- package/dist/types/extractor/web-extractor.d.ts.bak +19 -0
- package/dist/types/img/box-select.d.ts +26 -0
- package/dist/types/img/box-select.d.ts.bak +26 -0
- package/dist/types/img/canvas-fallback.d.ts +105 -0
- package/dist/types/img/canvas-fallback.d.ts.bak +105 -0
- package/dist/types/img/get-photon.d.ts +19 -0
- package/dist/types/img/get-photon.d.ts.bak +19 -0
- package/dist/types/img/get-sharp.d.ts +3 -0
- package/dist/types/img/get-sharp.d.ts.bak +3 -0
- package/dist/types/img/index.d.ts +3 -0
- package/dist/types/img/index.d.ts.bak +3 -0
- package/dist/types/img/info.d.ts +29 -0
- package/dist/types/img/info.d.ts.bak +29 -0
- package/dist/types/img/transform.d.ts +107 -0
- package/dist/types/img/transform.d.ts.bak +107 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.bak +4 -0
- package/dist/types/logger.d.ts +5 -0
- package/dist/types/logger.d.ts.bak +5 -0
- package/dist/types/mcp/base-server.d.ts +93 -0
- package/dist/types/mcp/base-server.d.ts.bak +93 -0
- package/dist/types/mcp/base-tools.d.ts +79 -0
- package/dist/types/mcp/base-tools.d.ts.bak +79 -0
- package/dist/types/mcp/chrome-path.d.ts +2 -0
- package/dist/types/mcp/chrome-path.d.ts.bak +2 -0
- package/dist/types/mcp/index.d.ts +7 -0
- package/dist/types/mcp/index.d.ts.bak +7 -0
- package/dist/types/mcp/inject-report-html-plugin.d.ts +18 -0
- package/dist/types/mcp/inject-report-html-plugin.d.ts.bak +18 -0
- package/dist/types/mcp/launcher-helper.d.ts +94 -0
- package/dist/types/mcp/launcher-helper.d.ts.bak +94 -0
- package/dist/types/mcp/tool-generator.d.ts +10 -0
- package/dist/types/mcp/tool-generator.d.ts.bak +10 -0
- package/dist/types/mcp/types.d.ts +103 -0
- package/dist/types/mcp/types.d.ts.bak +103 -0
- package/dist/types/node/fs.d.ts +15 -0
- package/dist/types/node/fs.d.ts.bak +15 -0
- package/dist/types/node/index.d.ts +2 -0
- package/dist/types/node/index.d.ts.bak +2 -0
- package/dist/types/node/port.d.ts +8 -0
- package/dist/types/node/port.d.ts.bak +8 -0
- package/dist/types/oss/demo.d.ts +1 -0
- package/dist/types/oss/demo.d.ts.bak +1 -0
- package/dist/types/oss/index.d.ts +34 -0
- package/dist/types/oss/index.d.ts.bak +34 -0
- package/dist/types/polyfills/async-hooks.d.ts +6 -0
- package/dist/types/polyfills/async-hooks.d.ts.bak +6 -0
- package/dist/types/polyfills/index.d.ts +4 -0
- package/dist/types/polyfills/index.d.ts.bak +4 -0
- package/dist/types/types/index.d.ts +34 -0
- package/dist/types/types/index.d.ts.bak +34 -0
- package/dist/types/us-keyboard-layout.d.ts +32 -0
- package/dist/types/us-keyboard-layout.d.ts.bak +32 -0
- package/dist/types/utils.d.ts +34 -0
- package/dist/types/utils.d.ts.bak +34 -0
- package/dist/types/zod-schema-utils.d.ts +23 -0
- package/dist/types/zod-schema-utils.d.ts.bak +23 -0
- package/package.json +132 -0
- package/src/baseDB.ts +158 -0
- package/src/build/copy-static.ts +68 -0
- package/src/build/rspack-config.ts +12 -0
- package/src/cli/cli-runner.ts +224 -0
- package/src/cli/index.ts +8 -0
- package/src/common.ts +67 -0
- package/src/constants/example-code.ts +223 -0
- package/src/constants/index.ts +29 -0
- package/src/env/basic.ts +12 -0
- package/src/env/constants.ts +234 -0
- package/src/env/global-config-manager.ts +191 -0
- package/src/env/helper.ts +58 -0
- package/src/env/index.ts +4 -0
- package/src/env/init-debug.ts +34 -0
- package/src/env/model-config-manager.ts +149 -0
- package/src/env/parse-model-config.ts +294 -0
- package/src/env/types.ts +547 -0
- package/src/env/utils.ts +89 -0
- package/src/extractor/constants.ts +5 -0
- package/src/extractor/debug.ts +10 -0
- package/src/extractor/dom-util.ts +226 -0
- package/src/extractor/index.ts +48 -0
- package/src/extractor/locator.ts +469 -0
- package/src/extractor/tree.ts +179 -0
- package/src/extractor/util.ts +482 -0
- package/src/extractor/web-extractor.ts +481 -0
- package/src/img/box-select.ts +588 -0
- package/src/img/canvas-fallback.ts +393 -0
- package/src/img/get-photon.ts +108 -0
- package/src/img/get-sharp.ts +18 -0
- package/src/img/index.ts +26 -0
- package/src/img/info.ts +75 -0
- package/src/img/transform.ts +594 -0
- package/src/index.ts +8 -0
- package/src/logger.ts +96 -0
- package/src/mcp/base-server.ts +502 -0
- package/src/mcp/base-tools.ts +185 -0
- package/src/mcp/chrome-path.ts +48 -0
- package/src/mcp/index.ts +7 -0
- package/src/mcp/inject-report-html-plugin.ts +119 -0
- package/src/mcp/launcher-helper.ts +200 -0
- package/src/mcp/tool-generator.ts +429 -0
- package/src/mcp/types.ts +112 -0
- package/src/node/fs.ts +84 -0
- package/src/node/index.ts +2 -0
- package/src/node/port.ts +37 -0
- package/src/oss/demo.ts +61 -0
- package/src/oss/index.ts +187 -0
- package/src/polyfills/async-hooks.ts +6 -0
- package/src/polyfills/index.ts +4 -0
- package/src/types/index.ts +52 -0
- package/src/us-keyboard-layout.ts +723 -0
- package/src/utils.ts +149 -0
- package/src/zod-schema-utils.ts +133 -0
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { Buffer } from 'node:buffer';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { writeFile } from 'node:fs/promises';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import type { PhotonImage as PhotonImageType } from '@silvia-odwyer/photon-node';
|
|
7
|
+
import type { Rect } from 'src/types';
|
|
8
|
+
import { getDebug } from '../logger';
|
|
9
|
+
import { ifInNode } from '../utils';
|
|
10
|
+
import getPhoton from './get-photon';
|
|
11
|
+
import getSharp from './get-sharp';
|
|
12
|
+
|
|
13
|
+
const imgDebug = getDebug('img');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Saves a Base64-encoded image to a file
|
|
17
|
+
*
|
|
18
|
+
* @param options - An object containing the Base64-encoded image data and the output file path
|
|
19
|
+
* @param options.base64Data - The Base64-encoded image data
|
|
20
|
+
* @param options.outputPath - The path where the image will be saved
|
|
21
|
+
* @throws Error if there is an error during the saving process
|
|
22
|
+
*/
|
|
23
|
+
export async function saveBase64Image(options: {
|
|
24
|
+
base64Data: string;
|
|
25
|
+
outputPath: string;
|
|
26
|
+
}): Promise<void> {
|
|
27
|
+
const { base64Data, outputPath } = options;
|
|
28
|
+
const { body } = parseBase64(base64Data);
|
|
29
|
+
|
|
30
|
+
const imageBuffer = Buffer.from(body, 'base64');
|
|
31
|
+
await writeFile(outputPath, imageBuffer);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Resizes an image from Buffer, maybe return a new format
|
|
36
|
+
* - If the image is Resized, the returned format will be jpg.
|
|
37
|
+
* - If the image is not Resized, it will return to its original format.
|
|
38
|
+
* @returns { buffer: resized buffer, format: the new format}
|
|
39
|
+
*/
|
|
40
|
+
export async function resizeAndConvertImgBuffer(
|
|
41
|
+
inputFormat: string,
|
|
42
|
+
inputData: Buffer,
|
|
43
|
+
newSize: {
|
|
44
|
+
width: number;
|
|
45
|
+
height: number;
|
|
46
|
+
},
|
|
47
|
+
): Promise<{
|
|
48
|
+
buffer: Buffer;
|
|
49
|
+
// jpg, png, etc.
|
|
50
|
+
format: string;
|
|
51
|
+
}> {
|
|
52
|
+
if (typeof inputData === 'string')
|
|
53
|
+
throw Error('inputData is base64, use resizeImgBase64 instead');
|
|
54
|
+
|
|
55
|
+
assert(
|
|
56
|
+
newSize && newSize.width > 0 && newSize.height > 0,
|
|
57
|
+
'newSize must be positive',
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const resizeStartTime = Date.now();
|
|
61
|
+
imgDebug(`resizeImg start, target size: ${newSize.width}x${newSize.height}`);
|
|
62
|
+
|
|
63
|
+
if (ifInNode) {
|
|
64
|
+
// Node.js environment: use Sharp
|
|
65
|
+
try {
|
|
66
|
+
const Sharp = await getSharp();
|
|
67
|
+
const metadata = await Sharp(inputData).metadata();
|
|
68
|
+
const { width: originalWidth, height: originalHeight } = metadata;
|
|
69
|
+
|
|
70
|
+
if (!originalWidth || !originalHeight) {
|
|
71
|
+
throw Error('Undefined width or height from the input image.');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (
|
|
75
|
+
newSize.width === originalWidth &&
|
|
76
|
+
newSize.height === originalHeight
|
|
77
|
+
) {
|
|
78
|
+
return {
|
|
79
|
+
buffer: inputData,
|
|
80
|
+
format: inputFormat,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const resizedBuffer = await Sharp(inputData)
|
|
85
|
+
.resize(newSize.width, newSize.height)
|
|
86
|
+
.jpeg({ quality: 90 })
|
|
87
|
+
.toBuffer();
|
|
88
|
+
|
|
89
|
+
const resizeEndTime = Date.now();
|
|
90
|
+
imgDebug(
|
|
91
|
+
`resizeImg done (Sharp), target size: ${newSize.width}x${newSize.height}, cost: ${resizeEndTime - resizeStartTime}ms`,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
buffer: resizedBuffer,
|
|
96
|
+
// by Sharp.jpeg()
|
|
97
|
+
format: 'jpeg',
|
|
98
|
+
};
|
|
99
|
+
} catch (error) {
|
|
100
|
+
imgDebug('Sharp failed, falling back to Photon:', error);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// browser environment: use Photon (or Canvas fallback)
|
|
105
|
+
const { PhotonImage, SamplingFilter, resize } = await getPhoton();
|
|
106
|
+
const inputBytes = new Uint8Array(inputData);
|
|
107
|
+
// Support both sync (Photon) and async (Canvas fallback) versions
|
|
108
|
+
const bytesliceResult = PhotonImage.new_from_byteslice(inputBytes);
|
|
109
|
+
const inputImage =
|
|
110
|
+
bytesliceResult instanceof Promise
|
|
111
|
+
? await bytesliceResult
|
|
112
|
+
: bytesliceResult;
|
|
113
|
+
const originalWidth = inputImage.get_width();
|
|
114
|
+
const originalHeight = inputImage.get_height();
|
|
115
|
+
|
|
116
|
+
if (!originalWidth || !originalHeight) {
|
|
117
|
+
inputImage.free();
|
|
118
|
+
throw Error('Undefined width or height from the input image.');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (newSize.width === originalWidth && newSize.height === originalHeight) {
|
|
122
|
+
inputImage.free();
|
|
123
|
+
return {
|
|
124
|
+
buffer: inputData,
|
|
125
|
+
format: inputFormat,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Resize image using photon with bicubic-like sampling
|
|
130
|
+
const outputImage = resize(
|
|
131
|
+
inputImage,
|
|
132
|
+
newSize.width,
|
|
133
|
+
newSize.height,
|
|
134
|
+
SamplingFilter.CatmullRom,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const outputBytes = outputImage.get_bytes_jpeg(90);
|
|
138
|
+
const resizedBuffer = Buffer.from(outputBytes);
|
|
139
|
+
|
|
140
|
+
// Free memory
|
|
141
|
+
inputImage.free();
|
|
142
|
+
outputImage.free();
|
|
143
|
+
|
|
144
|
+
const resizeEndTime = Date.now();
|
|
145
|
+
|
|
146
|
+
imgDebug(
|
|
147
|
+
`resizeImg done (Photon), target size: ${newSize.width}x${newSize.height}, cost: ${resizeEndTime - resizeStartTime}ms`,
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
buffer: resizedBuffer,
|
|
152
|
+
// by Photon.get_bytes_jpeg()
|
|
153
|
+
format: 'jpeg',
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export const createImgBase64ByFormat = (format: string, body: string) => {
|
|
158
|
+
return `data:image/${format};base64,${body}`;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export async function resizeImgBase64(
|
|
162
|
+
inputBase64: string,
|
|
163
|
+
newSize: {
|
|
164
|
+
width: number;
|
|
165
|
+
height: number;
|
|
166
|
+
},
|
|
167
|
+
): Promise<string> {
|
|
168
|
+
const { body, mimeType } = parseBase64(inputBase64);
|
|
169
|
+
const imageBuffer = Buffer.from(body, 'base64');
|
|
170
|
+
const { buffer, format } = await resizeAndConvertImgBuffer(
|
|
171
|
+
mimeType.split('/')[1],
|
|
172
|
+
imageBuffer,
|
|
173
|
+
newSize,
|
|
174
|
+
);
|
|
175
|
+
return createImgBase64ByFormat(format, buffer.toString('base64'));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Calculates new dimensions for an image while maintaining its aspect ratio.
|
|
180
|
+
*
|
|
181
|
+
* This function is designed to resize an image to fit within a specified maximum width and height
|
|
182
|
+
* while maintaining the original aspect ratio. If the original width or height exceeds the maximum
|
|
183
|
+
* dimensions, the image will be scaled down to fit.
|
|
184
|
+
*
|
|
185
|
+
* @param {number} originalWidth - The original width of the image.
|
|
186
|
+
* @param {number} originalHeight - The original height of the image.
|
|
187
|
+
* @returns {Object} An object containing the new width and height.
|
|
188
|
+
* @throws {Error} Throws an error if the width or height is not a positive number.
|
|
189
|
+
*/
|
|
190
|
+
export function zoomForGPT4o(originalWidth: number, originalHeight: number) {
|
|
191
|
+
// In low mode, the image is scaled to 512x512 pixels and 85 tokens are used to represent the image.
|
|
192
|
+
// In high mode, the model looks at low-resolution images and then creates detailed crop images, using 170 tokens for each 512x512 pixel tile. In practical applications, it is recommended to control the image size within 2048x768 pixels
|
|
193
|
+
const maxWidth = 2048; // Maximum width
|
|
194
|
+
const maxHeight = 768; // Maximum height
|
|
195
|
+
let newWidth = originalWidth;
|
|
196
|
+
let newHeight = originalHeight;
|
|
197
|
+
|
|
198
|
+
// Calculate the aspect ratio
|
|
199
|
+
const aspectRatio = originalWidth / originalHeight;
|
|
200
|
+
|
|
201
|
+
// Width adjustment
|
|
202
|
+
if (originalWidth > maxWidth) {
|
|
203
|
+
newWidth = maxWidth;
|
|
204
|
+
newHeight = newWidth / aspectRatio;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Adjust height
|
|
208
|
+
if (newHeight > maxHeight) {
|
|
209
|
+
newHeight = maxHeight;
|
|
210
|
+
newWidth = newHeight * aspectRatio;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
width: Math.round(newWidth),
|
|
215
|
+
height: Math.round(newHeight),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export async function photonFromBase64(
|
|
220
|
+
base64: string,
|
|
221
|
+
): Promise<PhotonImageType> {
|
|
222
|
+
const { PhotonImage } = await getPhoton();
|
|
223
|
+
const { body } = parseBase64(base64);
|
|
224
|
+
// Support both sync (Photon) and async (Canvas fallback) versions
|
|
225
|
+
const result = PhotonImage.new_from_base64(body);
|
|
226
|
+
return result instanceof Promise ? await result : result;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// https://help.aliyun.com/zh/model-studio/user-guide/vision/
|
|
230
|
+
export async function paddingToMatchBlock(
|
|
231
|
+
image: PhotonImageType,
|
|
232
|
+
blockSize = 28,
|
|
233
|
+
): Promise<{
|
|
234
|
+
width: number;
|
|
235
|
+
height: number;
|
|
236
|
+
image: PhotonImageType;
|
|
237
|
+
}> {
|
|
238
|
+
const width = image.get_width();
|
|
239
|
+
const height = image.get_height();
|
|
240
|
+
|
|
241
|
+
const targetWidth = Math.ceil(width / blockSize) * blockSize;
|
|
242
|
+
const targetHeight = Math.ceil(height / blockSize) * blockSize;
|
|
243
|
+
|
|
244
|
+
if (targetWidth === width && targetHeight === height) {
|
|
245
|
+
return { width, height, image };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const { padding_right, padding_bottom, Rgba } = await getPhoton();
|
|
249
|
+
|
|
250
|
+
const rightPadding = targetWidth - width;
|
|
251
|
+
const bottomPadding = targetHeight - height;
|
|
252
|
+
|
|
253
|
+
let result = image;
|
|
254
|
+
if (rightPadding > 0) {
|
|
255
|
+
// Rgba object is consumed by padding_right, so create new one for each call
|
|
256
|
+
const white = new Rgba(255, 255, 255, 255);
|
|
257
|
+
result = padding_right(result, rightPadding, white);
|
|
258
|
+
}
|
|
259
|
+
if (bottomPadding > 0) {
|
|
260
|
+
const white = new Rgba(255, 255, 255, 255);
|
|
261
|
+
const previousResult = result;
|
|
262
|
+
result = padding_bottom(previousResult, bottomPadding, white);
|
|
263
|
+
// Free intermediate PhotonImage created by padding_right, but not the original input
|
|
264
|
+
if (previousResult !== image) {
|
|
265
|
+
previousResult.free();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return { width: targetWidth, height: targetHeight, image: result };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export async function paddingToMatchBlockByBase64(
|
|
273
|
+
imageBase64: string,
|
|
274
|
+
blockSize = 28,
|
|
275
|
+
): Promise<{
|
|
276
|
+
width: number;
|
|
277
|
+
height: number;
|
|
278
|
+
imageBase64: string;
|
|
279
|
+
}> {
|
|
280
|
+
const photonImage = await photonFromBase64(imageBase64);
|
|
281
|
+
try {
|
|
282
|
+
const paddedResult = await paddingToMatchBlock(photonImage, blockSize);
|
|
283
|
+
const result = {
|
|
284
|
+
width: paddedResult.width,
|
|
285
|
+
height: paddedResult.height,
|
|
286
|
+
imageBase64: await photonToBase64(paddedResult.image),
|
|
287
|
+
};
|
|
288
|
+
if (paddedResult.image !== photonImage) {
|
|
289
|
+
paddedResult.image.free();
|
|
290
|
+
}
|
|
291
|
+
return result;
|
|
292
|
+
} finally {
|
|
293
|
+
photonImage.free();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export async function cropByRect(
|
|
298
|
+
imageBase64: string,
|
|
299
|
+
rect: Rect,
|
|
300
|
+
paddingImage: boolean,
|
|
301
|
+
): Promise<{
|
|
302
|
+
width: number;
|
|
303
|
+
height: number;
|
|
304
|
+
imageBase64: string;
|
|
305
|
+
}> {
|
|
306
|
+
const { crop } = await getPhoton();
|
|
307
|
+
const photonImage = await photonFromBase64(imageBase64);
|
|
308
|
+
const { left, top, width, height } = rect;
|
|
309
|
+
|
|
310
|
+
// Photon crop uses coordinates (x1, y1, x2, y2), not (x, y, width, height)
|
|
311
|
+
const cropped = crop(photonImage, left, top, left + width, top + height);
|
|
312
|
+
photonImage.free();
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
if (paddingImage) {
|
|
316
|
+
const paddedResult = await paddingToMatchBlock(cropped);
|
|
317
|
+
const result = {
|
|
318
|
+
width: paddedResult.width,
|
|
319
|
+
height: paddedResult.height,
|
|
320
|
+
imageBase64: await photonToBase64(paddedResult.image),
|
|
321
|
+
};
|
|
322
|
+
if (paddedResult.image !== cropped) {
|
|
323
|
+
paddedResult.image.free();
|
|
324
|
+
}
|
|
325
|
+
return result;
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
width: cropped.get_width(),
|
|
329
|
+
height: cropped.get_height(),
|
|
330
|
+
imageBase64: await photonToBase64(cropped),
|
|
331
|
+
};
|
|
332
|
+
} finally {
|
|
333
|
+
cropped.free();
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export async function photonToBase64(
|
|
338
|
+
image: PhotonImageType,
|
|
339
|
+
quality = 90,
|
|
340
|
+
): Promise<string> {
|
|
341
|
+
const bytes = image.get_bytes_jpeg(quality);
|
|
342
|
+
const base64Body = Buffer.from(bytes).toString('base64');
|
|
343
|
+
return `data:image/jpeg;base64,${base64Body}`;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export const httpImg2Base64 = async (url: string): Promise<string> => {
|
|
347
|
+
const response = await fetch(url);
|
|
348
|
+
if (!response.ok) {
|
|
349
|
+
throw new Error(`Failed to fetch image: ${url}`);
|
|
350
|
+
}
|
|
351
|
+
const contentType = response.headers.get('content-type');
|
|
352
|
+
if (!contentType) {
|
|
353
|
+
throw new Error(`Failed to fetch image: ${url}`);
|
|
354
|
+
}
|
|
355
|
+
assert(
|
|
356
|
+
contentType.startsWith('image/'),
|
|
357
|
+
`The url ${url} is not a image, because of content-type in header is ${contentType}.`,
|
|
358
|
+
);
|
|
359
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
360
|
+
return `data:${contentType};base64,${buffer.toString('base64')}`;
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Convert image file to base64 string
|
|
365
|
+
* Because this method is synchronous, the npm package `sharp` cannot be used to detect the file type.
|
|
366
|
+
* TODO: convert to webp to reduce base64 size.
|
|
367
|
+
*/
|
|
368
|
+
export const localImg2Base64 = (
|
|
369
|
+
imgPath: string,
|
|
370
|
+
withoutHeader = false,
|
|
371
|
+
): string => {
|
|
372
|
+
const body = readFileSync(imgPath).toString('base64');
|
|
373
|
+
if (withoutHeader) {
|
|
374
|
+
return body;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Detect image type by extname.
|
|
378
|
+
const type = path.extname(imgPath).slice(1);
|
|
379
|
+
const finalType = type === 'svg' ? 'svg+xml' : type || 'jpg';
|
|
380
|
+
|
|
381
|
+
return `data:image/${finalType};base64,${body}`;
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* PreProcess image url to ensure image is accessible to LLM.
|
|
386
|
+
* @param url - The url of the image, it can be a http url or a base64 string or a file path
|
|
387
|
+
* @param convertHttpImage2Base64 - Whether to convert http image to base64, if true, the http image will be converted to base64, otherwise, the http image will be returned as is
|
|
388
|
+
* @returns The base64 string of the image (when convertHttpImage2Base64 is true or url is a file path) or the http image url
|
|
389
|
+
*/
|
|
390
|
+
export const preProcessImageUrl = async (
|
|
391
|
+
url: string,
|
|
392
|
+
convertHttpImage2Base64: boolean,
|
|
393
|
+
) => {
|
|
394
|
+
if (typeof url !== 'string') {
|
|
395
|
+
throw new Error(
|
|
396
|
+
`url must be a string, but got ${url} with type ${typeof url}`,
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
if (url.startsWith('data:')) {
|
|
400
|
+
return url;
|
|
401
|
+
} else if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
402
|
+
if (!convertHttpImage2Base64) {
|
|
403
|
+
return url;
|
|
404
|
+
}
|
|
405
|
+
return await httpImg2Base64(url);
|
|
406
|
+
} else {
|
|
407
|
+
return await localImg2Base64(url);
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* parse base64 string to get mimeType and body
|
|
413
|
+
*/
|
|
414
|
+
export const parseBase64 = (
|
|
415
|
+
fullBase64String: string,
|
|
416
|
+
): {
|
|
417
|
+
mimeType: string;
|
|
418
|
+
body: string;
|
|
419
|
+
} => {
|
|
420
|
+
try {
|
|
421
|
+
const separator = ';base64,';
|
|
422
|
+
const index = fullBase64String.indexOf(separator);
|
|
423
|
+
if (index === -1) {
|
|
424
|
+
throw new Error('Invalid base64 string');
|
|
425
|
+
}
|
|
426
|
+
return {
|
|
427
|
+
// 5 means 'data:'
|
|
428
|
+
mimeType: fullBase64String.slice(5, index),
|
|
429
|
+
body: fullBase64String.slice(index + separator.length),
|
|
430
|
+
};
|
|
431
|
+
} catch (e) {
|
|
432
|
+
throw new Error(
|
|
433
|
+
`parseBase64 fail because intput is not a valid base64 string: ${fullBase64String}`,
|
|
434
|
+
{
|
|
435
|
+
cause: e,
|
|
436
|
+
},
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Convert a base64 image to JPEG with specified quality (no resize).
|
|
443
|
+
* If the image is already JPEG, re-encodes at the given quality.
|
|
444
|
+
*
|
|
445
|
+
* @param inputBase64 - Full data-URI base64 string (e.g. "data:image/png;base64,...")
|
|
446
|
+
* @param quality - JPEG quality 1-100
|
|
447
|
+
* @returns JPEG data-URI base64 string
|
|
448
|
+
*/
|
|
449
|
+
export async function convertToJpegBase64(
|
|
450
|
+
inputBase64: string,
|
|
451
|
+
quality: number,
|
|
452
|
+
): Promise<string> {
|
|
453
|
+
const { body } = parseBase64(inputBase64);
|
|
454
|
+
const buffer = Buffer.from(body, 'base64');
|
|
455
|
+
|
|
456
|
+
if (ifInNode) {
|
|
457
|
+
try {
|
|
458
|
+
const Sharp = await getSharp();
|
|
459
|
+
const jpegBuffer = await Sharp(buffer).jpeg({ quality }).toBuffer();
|
|
460
|
+
return `data:image/jpeg;base64,${jpegBuffer.toString('base64')}`;
|
|
461
|
+
} catch (error) {
|
|
462
|
+
imgDebug(
|
|
463
|
+
'Sharp failed for JPEG conversion, falling back to Photon:',
|
|
464
|
+
error,
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Fallback: Photon / Canvas
|
|
470
|
+
const { PhotonImage } = await getPhoton();
|
|
471
|
+
const inputBytes = new Uint8Array(buffer);
|
|
472
|
+
const bytesliceResult = PhotonImage.new_from_byteslice(inputBytes);
|
|
473
|
+
const inputImage =
|
|
474
|
+
bytesliceResult instanceof Promise
|
|
475
|
+
? await bytesliceResult
|
|
476
|
+
: bytesliceResult;
|
|
477
|
+
const outputBytes = inputImage.get_bytes_jpeg(quality);
|
|
478
|
+
inputImage.free();
|
|
479
|
+
return `data:image/jpeg;base64,${Buffer.from(outputBytes).toString('base64')}`;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Scales an image by a specified factor using Sharp or Photon
|
|
484
|
+
* @param imageBase64 - Base64 encoded image
|
|
485
|
+
* @param scale - Scale factor (e.g., 2 for 2x, 1.5 for 1.5x)
|
|
486
|
+
* @returns Scaled image with new dimensions
|
|
487
|
+
*/
|
|
488
|
+
export async function scaleImage(
|
|
489
|
+
imageBase64: string,
|
|
490
|
+
scale: number,
|
|
491
|
+
): Promise<{
|
|
492
|
+
width: number;
|
|
493
|
+
height: number;
|
|
494
|
+
imageBase64: string;
|
|
495
|
+
}> {
|
|
496
|
+
if (scale <= 0) {
|
|
497
|
+
throw new Error('Scale factor must be positive');
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const { body } = parseBase64(imageBase64);
|
|
501
|
+
const buffer = Buffer.from(body, 'base64');
|
|
502
|
+
|
|
503
|
+
const scaleStartTime = Date.now();
|
|
504
|
+
imgDebug(`scaleImage start, scale factor: ${scale}`);
|
|
505
|
+
|
|
506
|
+
if (ifInNode) {
|
|
507
|
+
// Node.js environment: use Sharp
|
|
508
|
+
try {
|
|
509
|
+
const Sharp = await getSharp();
|
|
510
|
+
const metadata = await Sharp(buffer).metadata();
|
|
511
|
+
const originalWidth = metadata.width || 0;
|
|
512
|
+
const originalHeight = metadata.height || 0;
|
|
513
|
+
|
|
514
|
+
if (originalWidth === 0 || originalHeight === 0) {
|
|
515
|
+
throw new Error('Failed to get image dimensions');
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const newWidth = Math.round(originalWidth * scale);
|
|
519
|
+
const newHeight = Math.round(originalHeight * scale);
|
|
520
|
+
|
|
521
|
+
const resizedBuffer = await Sharp(buffer)
|
|
522
|
+
.resize(newWidth, newHeight, {
|
|
523
|
+
kernel: 'lanczos3',
|
|
524
|
+
fit: 'fill',
|
|
525
|
+
})
|
|
526
|
+
.jpeg({
|
|
527
|
+
quality: 90,
|
|
528
|
+
})
|
|
529
|
+
.toBuffer();
|
|
530
|
+
|
|
531
|
+
const scaleEndTime = Date.now();
|
|
532
|
+
imgDebug(
|
|
533
|
+
`scaleImage done (Sharp): ${originalWidth}x${originalHeight} -> ${newWidth}x${newHeight} (scale=${scale}), cost: ${scaleEndTime - scaleStartTime}ms`,
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
const base64 = `data:image/jpeg;base64,${resizedBuffer.toString('base64')}`;
|
|
537
|
+
|
|
538
|
+
return {
|
|
539
|
+
width: newWidth,
|
|
540
|
+
height: newHeight,
|
|
541
|
+
imageBase64: base64,
|
|
542
|
+
};
|
|
543
|
+
} catch (error) {
|
|
544
|
+
imgDebug('Sharp failed, falling back to Photon:', error);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Browser environment or Sharp failed: use Photon (or Canvas fallback)
|
|
549
|
+
const { PhotonImage, SamplingFilter, resize } = await getPhoton();
|
|
550
|
+
const inputBytes = new Uint8Array(buffer);
|
|
551
|
+
// Support both sync (Photon) and async (Canvas fallback) versions
|
|
552
|
+
const bytesliceResult = PhotonImage.new_from_byteslice(inputBytes);
|
|
553
|
+
const inputImage =
|
|
554
|
+
bytesliceResult instanceof Promise
|
|
555
|
+
? await bytesliceResult
|
|
556
|
+
: bytesliceResult;
|
|
557
|
+
const originalWidth = inputImage.get_width();
|
|
558
|
+
const originalHeight = inputImage.get_height();
|
|
559
|
+
|
|
560
|
+
if (!originalWidth || !originalHeight) {
|
|
561
|
+
inputImage.free();
|
|
562
|
+
throw new Error('Failed to get image dimensions');
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const newWidth = Math.round(originalWidth * scale);
|
|
566
|
+
const newHeight = Math.round(originalHeight * scale);
|
|
567
|
+
|
|
568
|
+
const outputImage = resize(
|
|
569
|
+
inputImage,
|
|
570
|
+
newWidth,
|
|
571
|
+
newHeight,
|
|
572
|
+
SamplingFilter.CatmullRom,
|
|
573
|
+
);
|
|
574
|
+
|
|
575
|
+
const outputBytes = outputImage.get_bytes_jpeg(90);
|
|
576
|
+
const resizedBuffer = Buffer.from(outputBytes);
|
|
577
|
+
|
|
578
|
+
// Free memory
|
|
579
|
+
inputImage.free();
|
|
580
|
+
outputImage.free();
|
|
581
|
+
|
|
582
|
+
const scaleEndTime = Date.now();
|
|
583
|
+
imgDebug(
|
|
584
|
+
`scaleImage done (Photon): ${originalWidth}x${originalHeight} -> ${newWidth}x${newHeight} (scale=${scale}), cost: ${scaleEndTime - scaleStartTime}ms`,
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
const base64 = `data:image/jpeg;base64,${resizedBuffer.toString('base64')}`;
|
|
588
|
+
|
|
589
|
+
return {
|
|
590
|
+
width: newWidth,
|
|
591
|
+
height: newHeight,
|
|
592
|
+
imageBase64: base64,
|
|
593
|
+
};
|
|
594
|
+
}
|
package/src/index.ts
ADDED
package/src/logger.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import util from 'node:util';
|
|
4
|
+
import debug from 'debug';
|
|
5
|
+
import { getMidsceneRunSubDir } from './common';
|
|
6
|
+
import { ifInNode } from './utils';
|
|
7
|
+
|
|
8
|
+
const topicPrefix = 'midscene';
|
|
9
|
+
// Map to store file streams
|
|
10
|
+
const logStreams = new Map<string, fs.WriteStream>();
|
|
11
|
+
// Map to store debug instances
|
|
12
|
+
const debugInstances = new Map<string, DebugFunction>();
|
|
13
|
+
|
|
14
|
+
// Function to get or create a log stream
|
|
15
|
+
function getLogStream(topic: string): fs.WriteStream {
|
|
16
|
+
const topicFileName = topic.replace(/:/g, '-');
|
|
17
|
+
if (!logStreams.has(topicFileName)) {
|
|
18
|
+
const logFile = path.join(
|
|
19
|
+
getMidsceneRunSubDir('log'),
|
|
20
|
+
`${topicFileName}.log`,
|
|
21
|
+
);
|
|
22
|
+
const stream = fs.createWriteStream(logFile, { flags: 'a' });
|
|
23
|
+
logStreams.set(topicFileName, stream);
|
|
24
|
+
}
|
|
25
|
+
return logStreams.get(topicFileName)!;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Function to write log to file
|
|
29
|
+
function writeLogToFile(topic: string, message: string): void {
|
|
30
|
+
if (!ifInNode) return;
|
|
31
|
+
|
|
32
|
+
const stream = getLogStream(topic);
|
|
33
|
+
// Generate ISO format timestamp with local timezone
|
|
34
|
+
const now = new Date();
|
|
35
|
+
// Use sv-SE locale to get ISO-like format (YYYY-MM-DD HH:mm:ss)
|
|
36
|
+
const isoDate = now.toLocaleDateString('sv-SE'); // YYYY-MM-DD
|
|
37
|
+
const isoTime = now.toLocaleTimeString('sv-SE'); // HH:mm:ss
|
|
38
|
+
const milliseconds = now.getMilliseconds().toString().padStart(3, '0');
|
|
39
|
+
// Calculate timezone offset manually for correct format (+HH:mm)
|
|
40
|
+
const timezoneOffsetMinutes = now.getTimezoneOffset();
|
|
41
|
+
const sign = timezoneOffsetMinutes <= 0 ? '+' : '-';
|
|
42
|
+
const hours = Math.floor(Math.abs(timezoneOffsetMinutes) / 60)
|
|
43
|
+
.toString()
|
|
44
|
+
.padStart(2, '0');
|
|
45
|
+
const minutes = (Math.abs(timezoneOffsetMinutes) % 60)
|
|
46
|
+
.toString()
|
|
47
|
+
.padStart(2, '0');
|
|
48
|
+
const timezoneString = `${sign}${hours}:${minutes}`;
|
|
49
|
+
const localISOTime = `${isoDate}T${isoTime}.${milliseconds}${timezoneString}`;
|
|
50
|
+
stream.write(`[${localISOTime}] ${message}\n`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type DebugFunction = (...args: unknown[]) => void;
|
|
54
|
+
|
|
55
|
+
export function getDebug(
|
|
56
|
+
topic: string,
|
|
57
|
+
options?: { console?: boolean },
|
|
58
|
+
): DebugFunction {
|
|
59
|
+
const fullTopic = `${topicPrefix}:${topic}`;
|
|
60
|
+
const withConsole = options?.console ?? false;
|
|
61
|
+
const cacheKey = withConsole ? `${fullTopic}:withConsole` : fullTopic;
|
|
62
|
+
|
|
63
|
+
if (!debugInstances.has(cacheKey)) {
|
|
64
|
+
if (withConsole) {
|
|
65
|
+
const baseFn = getDebug(topic);
|
|
66
|
+
const wrapper = (...args: unknown[]): void => {
|
|
67
|
+
baseFn(...args);
|
|
68
|
+
console.warn('[Midscene]', ...args);
|
|
69
|
+
};
|
|
70
|
+
debugInstances.set(cacheKey, wrapper);
|
|
71
|
+
} else {
|
|
72
|
+
const debugFn = debug(fullTopic) as DebugFunction;
|
|
73
|
+
|
|
74
|
+
// Create wrapper that handles both file logging and debug output
|
|
75
|
+
const wrapper = (...args: unknown[]): void => {
|
|
76
|
+
if (ifInNode) {
|
|
77
|
+
const message = util.format(...args);
|
|
78
|
+
writeLogToFile(topic, message);
|
|
79
|
+
}
|
|
80
|
+
debugFn(...args);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
debugInstances.set(cacheKey, wrapper);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return debugInstances.get(cacheKey)!;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function enableDebug(topic: string): void {
|
|
91
|
+
if (ifInNode) {
|
|
92
|
+
// In Node.js, we don't need to enable debug as we're using file logging
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
debug.enable(`${topicPrefix}:${topic}`);
|
|
96
|
+
}
|