Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions MiniApp/Demo/background-remover/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Background Remover MiniApp

Run the focused checks from the repository root:

```sh
node MiniApp/Demo/background-remover/source/build.mjs
python3 MiniApp/Demo/background-remover/source/package.py
node --test MiniApp/Demo/background-remover/tests/*.test.mjs
```

After UI or color changes, also run `pnpm run theme:color-audit:all`.
Edit `source/ui/*.js`, then rebuild the generated `source/ui.js` bundle.
The standalone HTML is generated under repository-level ignored `output/miniapps/`, separate from host-owned `compiled.html`.
Do not use Computer Use or browser control for verification unless the user asks.
79 changes: 79 additions & 0 deletions MiniApp/Demo/background-remover/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# 轻抠 / Background Remover

给图标、Logo 去掉纯色或近似纯色背景,下载真正带 Alpha 通道的透明 PNG。
所有像素处理都在当前打开页面的设备上完成;不上传图片,不加载模型,不需要网络、账号或 Node Worker。

## 使用

在仓库根目录运行:

```sh
node MiniApp/Demo/background-remover/source/build.mjs
```

打开仓库根目录下生成的 `output/miniapps/background-remover.html` 即可离线使用,无需启动服务器。

1. 点击上传、拖入或粘贴一张 PNG、JPG、WebP 图片。
2. 自动读取图片边缘的主要背景颜色,并显示抠图结果。棋盘格代表真实透明。
3. 背景未去干净时,在原图上点击背景取色,或调整颜色容差和边缘柔化。
4. 点击「下载透明 PNG」,保留原图像素尺寸。对比底色只改变预览。

默认只移除与图片四边连通的背景,保留主体内部同色区域。
需要去掉字母、镂空区域内的背景时,取消勾选该选项;这也可能移除主体内部的同色内容。
对已有透明边缘的图片,自动模式先保留原图,手动取色后可继续处理。

最大输入为 30 MiB、1600 万像素,单边不超过 8192 像素;超限会明确提示,不会自动缩图。
适合简单背景;复杂照片、已经烘焙进图片的棋盘格、与背景相连的同色主体无法可靠分离。
容差过大可能清空图片,此时禁用导出并提示调整设置。

## OpenBitFun MiniApp

**源码文件夹和独立 HTML 不会自动出现在应用列表里,需要先导入。**

在 OpenBitFun 中打开 Mini Apps,点击「导入小应用」→「从文件夹导入」,
选择本目录 `MiniApp/Demo/background-remover`(含 `meta.json` 的目录,不是 `source` 子目录)。
客户端会完成安装、编译并更新应用列表;无需把它改成内置应用,也无需重新构建客户端。

也可以在构建后生成安装包:

```sh
python3 MiniApp/Demo/background-remover/source/package.py
```

然后在「导入小应用」菜单中导入 `output/miniapps/background-remover.bfminiapp`,
查看权限并确认。安装包不会发布到市场。

本目录同时包含标准 `meta.json`、`source/index.html`、`source/style.css`、`source/ui.js`、
`source/worker.js` 和空的 `source/esm_dependencies.json`。
宿主应通过标准 MiniApp 创建/导入流程读取这些源码并编译;不要把独立 HTML 复制为 `compiled.html`。
本次交付未调用宿主 `InitMiniApp` / `FinalizeMiniApp`,未安装到用户的 Mini App 列表。

仅申请 `{user-selected}` 写权限,用于 `app.dialog.save` 选择位置后保存 PNG。
图片输入来自页面文件选择器、拖拽或粘贴,不读取工作区、宿主路径或任意目录。
在 OpenBitFun 中保存使用 `app.fs.writeFile`;独立 HTML 使用页面下载。
不支持文件保存的宿主会显示明确提示。
随宿主切换明暗主题和中英文;独立版也提供语言和主题切换。

## 开发与验证

```sh
node MiniApp/Demo/background-remover/source/build.mjs
node --test MiniApp/Demo/background-remover/tests/*.test.mjs
pnpm run theme:color-audit:all
```

编辑 `source/ui/*.js` 后重新构建,`source/ui.js` 为生成入口。
算法测试覆盖背景识别、边缘连通、封闭区域、既有透明度、柔化和去色边、取消处理。
未使用浏览器控制或 Computer Use 验证界面。

远程场景:本应用不调用工作区、Agent Runtime 或命令执行接口,像素处理不依赖宿主文件系统。
远程工作区、远程控制、Peer Device Mode 和 Detached Dispatch 均未做端到端验证;
不据本地算法测试宣称这些宿主支持图片上传、保存或独立运行界面。

## English

Build using the command above, then open `output/miniapps/background-remover.html` from the repository root locally.
Upload, drop, or paste an icon, pick its background color if necessary, adjust tolerance and edge softness,
and download a transparent PNG at the original resolution. Processing stays on the viewing device.
Edge-connected removal preserves enclosed foreground regions; disable it to remove matching colors inside holes.
This tool targets solid or near-solid backgrounds rather than complex photographic segmentation.
31 changes: 31 additions & 0 deletions MiniApp/Demo/background-remover/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"id": "background-remover",
"name": "轻抠",
"description": "为图标和 Logo 去除纯色或近似纯色背景。图片仅在当前设备处理,支持背景取色、容差调整和透明 PNG 导出。",
"icon": "Scissors",
"category": "creative",
"tags": ["image", "background", "transparent", "icon"],
"version": 1,
"created_at": 0,
"updated_at": 0,
"permissions": {
"fs": { "read": [], "write": ["{user-selected}"] },
"shell": { "allow": [] },
"net": { "allow": [] },
"node": { "enabled": false }
},
"i18n": {
"locales": {
"zh-CN": {
"name": "轻抠",
"description": "图标、Logo 简单背景抠图,自动识别背景、手动取色,本地导出透明 PNG。",
"tags": ["图片", "抠图", "透明背景", "图标"]
},
"en-US": {
"name": "Background Remover",
"description": "Remove simple backgrounds from icons and logos, pick a background color, and export transparent PNGs locally.",
"tags": ["image", "background removal", "transparent", "icon"]
}
}
}
}
8 changes: 8 additions & 0 deletions MiniApp/Demo/background-remover/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "miniapp-background-remover",
"private": true,
"scripts": {
"build": "node source/build.mjs",
"test": "node --test tests/*.test.mjs"
}
}
25 changes: 25 additions & 0 deletions MiniApp/Demo/background-remover/source/build.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const sourceRoot = path.dirname(fileURLToPath(import.meta.url));
const appRoot = path.dirname(sourceRoot);
const repoRoot = path.resolve(appRoot, '../../..');
const inputs = ['ui/core.js', 'ui/i18n.js', 'ui/main.js'];
const bundle = inputs.map(file => `/* ${file} */\n${fs.readFileSync(path.join(sourceRoot, file), 'utf8')}\n`).join('');
fs.writeFileSync(path.join(sourceRoot, 'ui.js'), bundle);

// This is an offline user deliverable, not the host-managed compiled.html.
// Reuse the canonical first-paint projection instead of creating another palette.
const appearance = fs.readFileSync(path.join(repoRoot,
'src/crates/contracts/product-domains/src/miniapp/generated/default_appearance_style.html'), 'utf8');
const css = fs.readFileSync(path.join(sourceRoot, 'style.css'), 'utf8');
const html = fs.readFileSync(path.join(sourceRoot, 'index.html'), 'utf8');
const escapeScript = script => script.replace(/<\/script/gi, '<\\/script');
const standalone = html
.replace('</head>', `${appearance}\n<style>\n${css}\n</style>\n</head>`)
.replace('</body>', `<script>\n${escapeScript(bundle)}\n</script>\n</body>`);
const outputDir = path.join(repoRoot, 'output', 'miniapps');
fs.mkdirSync(outputDir, { recursive: true });
fs.writeFileSync(path.join(outputDir, 'background-remover.html'), standalone);
console.log('Built MiniApp source/ui.js and standalone output/miniapps/background-remover.html');
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
91 changes: 91 additions & 0 deletions MiniApp/Demo/background-remover/source/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>轻抠 · Background Remover</title>
</head>
<body>
<main class="app">
<header class="topbar">
<div>
<h1 data-i18n="title"></h1>
<p data-i18n="subtitle"></p>
</div>
<div class="header-actions">
<span class="local-badge" data-i18n="local"></span>
<button id="locale" class="quiet" type="button" data-i18n-aria="switchLanguage">EN</button>
<button id="theme" class="quiet" type="button" data-i18n="theme" hidden></button>
</div>
</header>

<input id="file" type="file" accept="image/png,image/jpeg,image/webp" hidden />
<section id="upload" class="upload">
<button id="choose" class="upload-button" type="button">
<svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path d="M12 16V4m-4 4 4-4 4 4M4 15v5h16v-5" /></svg>
<strong data-i18n="upload"></strong>
<span data-i18n="uploadHint"></span>
</button>
<p class="formats" data-i18n="formats"></p>
</section>

<section id="editor" class="editor" hidden>
<div class="filebar">
<div class="file-info"><strong id="filename"></strong><span id="dimensions"></span></div>
<button id="replace" type="button" data-i18n="replace"></button>
</div>
<div class="workspace">
<div class="previews">
<figure class="preview-panel">
<figcaption><span data-i18n="original"></span><span id="pick-hint" class="muted" data-i18n="pickHint"></span></figcaption>
<div class="canvas-stage checker">
<canvas id="original" tabindex="0" role="img" data-i18n-aria="originalCanvas"></canvas>
</div>
</figure>
<figure class="preview-panel">
<figcaption><span data-i18n="result"></span><span class="muted">PNG</span></figcaption>
<div id="result-stage" class="canvas-stage checker">
<canvas id="result" role="img" data-i18n-aria="resultCanvas"></canvas>
</div>
</figure>
</div>
<aside class="settings" aria-labelledby="settings-title">
<h2 id="settings-title" data-i18n="settings"></h2>
<fieldset id="controls">
<div class="control-group">
<label for="background" data-i18n="background"></label>
<div class="color-row">
<input id="background" type="color" data-i18n-aria="background" />
<output id="color-value"></output>
<button id="auto" type="button" data-i18n="auto"></button>
</div>
<button id="eyedropper" type="button" class="wide" aria-pressed="false" data-i18n="pick"></button>
</div>
<div class="control-group">
<div class="label-row"><label for="tolerance" data-i18n="tolerance"></label><output id="tolerance-value" for="tolerance">24</output></div>
<input id="tolerance" type="range" min="0" max="160" step="1" value="24" />
<p class="hint" data-i18n="toleranceHint"></p>
</div>
<div class="control-group">
<div class="label-row"><label for="feather" data-i18n="feather"></label><output id="feather-value" for="feather">16</output></div>
<input id="feather" type="range" min="0" max="64" step="1" value="16" />
<p class="hint" data-i18n="featherHint"></p>
</div>
<label class="check-row"><input id="connected" type="checkbox" checked /><span data-i18n="connected"></span></label>
<p class="hint" data-i18n="connectedHint"></p>
<label class="check-row"><input id="decontaminate" type="checkbox" checked /><span data-i18n="decontaminate"></span></label>
<button id="reset" type="button" class="wide" data-i18n="reset"></button>
</fieldset>
<div class="export-group">
<label class="check-row"><input id="contrast" type="checkbox" /><span data-i18n="contrast"></span></label>
<button id="download" type="button" class="primary wide" data-i18n="download" disabled></button>
<p class="hint" data-i18n="exportHint"></p>
</div>
</aside>
</div>
</section>
<p id="status" class="status" role="status" aria-live="polite" aria-atomic="true"></p>
<footer data-i18n="scope"></footer>
</main>
</body>
</html>
40 changes: 40 additions & 0 deletions MiniApp/Demo/background-remover/source/package.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Package the built MiniApp for OpenBitFun's local .bfminiapp importer."""

import json
from pathlib import Path
from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo


def main():
app_root = Path(__file__).resolve().parent.parent
repo_root = app_root.parents[2]
modules = ["ui/core.js", "ui/i18n.js", "ui/main.js"]
expected = "".join(
f"/* {name} */\n{(app_root / 'source' / name).read_text(encoding='utf-8')}\n"
for name in modules
)
if (app_root / "source/ui.js").read_text(encoding="utf-8") != expected:
raise SystemExit("Source bundle is stale. Run node source/build.mjs first.")

source_meta = json.loads((app_root / "meta.json").read_text(encoding="utf-8"))
# Local packages use MarketPackageMeta. Identity and install timestamps belong
# to the importing host; no source paths or user storage enter the package.
fields = ["name", "description", "icon", "category", "tags", "version", "permissions", "i18n"]
meta = {key: source_meta[key] for key in fields if key in source_meta}
files = {"meta.json": json.dumps(meta, ensure_ascii=False, indent=2).encode("utf-8")}
for name in ["index.html", "style.css", "ui.js", "worker.js", "esm_dependencies.json"]:
files[f"source/{name}"] = (app_root / "source" / name).read_bytes()

destination = repo_root / "output/miniapps/background-remover.bfminiapp"
destination.parent.mkdir(parents=True, exist_ok=True)
with ZipFile(destination, "w") as package:
for name, content in files.items():
entry = ZipInfo(name, date_time=(1980, 1, 1, 0, 0, 0))
entry.compress_type = ZIP_DEFLATED
entry.external_attr = 0o100644 << 16
package.writestr(entry, content)
print(f"Built {destination.name}")


if __name__ == "__main__":
main()
Loading
Loading