remove-ac 1.1.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 remove.ac
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,160 @@
1
+ Metadata-Version: 2.4
2
+ Name: remove-ac
3
+ Version: 1.1.0
4
+ Summary: Official zero-dependency Python SDK & removebg 1:1 drop-in replacement for remove.ac
5
+ License: MIT
6
+ Project-URL: Homepage, https://remove.ac
7
+ Project-URL: Documentation, https://remove.ac/api-info
8
+ Project-URL: Benchmarks, https://remove.ac/benchmarks
9
+ Project-URL: Repository, https://github.com/szp2005/remove-ac-sdk
10
+ Keywords: remove.ac,remove.bg,background-removal,matting,image-segmentation
11
+ Requires-Python: >=3.8
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Dynamic: license-file
15
+
16
+ # remove.ac Official SDKs, CLI & MCP Server (Node.js · Python · MCP · Drop-in `remove.bg` Migration)
17
+
18
+ Zero-dependency official developer SDKs, hot-folder CLI, and Model Context Protocol (`MCP`) server for **[remove.ac](https://remove.ac)** — the dual-mode background removal platform pairing a free on-device WebAssembly studio with a `remove.bg`-compatible Cloud GPU REST API (`POST https://remove.ac/v1.0/removebg`).
19
+
20
+ - **Live Web Workbench (Free On-Device WASM + Batch 50 ZIP):** https://remove.ac
21
+ - **1-Line `remove.bg` API Migration Guide (Dec 1, 2026 Shutdown):** https://remove.ac/guides/remove-bg-api-migration
22
+ - **Interactive API Sandbox & Docs:** https://remove.ac/api-info
23
+ - **Figma, Photoshop UXP, GIMP, Sketch & Make/n8n Integrations:** https://remove.ac/plugins
24
+ - **Open Inference & Pricing Dataset (`CC BY 4.0`):** https://remove.ac/benchmarks
25
+
26
+ ---
27
+
28
+ ## Key Capabilities
29
+
30
+ - **1:1 `remove.bg` v1.0 Field Compatibility:** Swap `https://api.remove.bg/v1.0/removebg` to `https://remove.ac/v1.0/removebg` using standard `X-Api-Key: rac_live_...` headers and form fields (`image_file`, `image_file_b64`, `image_url`, `size`, `format`, `channels`, `type`, `roi`, `crop`, `crop_margin`, `scale`, `position`, `bg_color`, `bg_image_url`, `add_shadow`, `shadow_type`, `shadow_opacity`, `semitransparency`).
31
+ - **Up to 25 MB / 40 MP Zero-Disk Direct GPU Streaming:** Payloads above 4 MB automatically negotiate a short-lived HMAC ticket (`POST /v1.0/removebg/ticket`) and stream directly to ephemeral GPU RAM without disk or database blob persistence.
32
+ - **Atomic Credit Holds (`Idempotency-Key`):** Upstream timeouts or errors automatically release held credits with zero charge. Every verified account starts with **1 free credit**; pay-as-you-go credit packs never expire.
33
+ - **Zero External Runtime Dependencies:** The Node.js 18+ SDK, CLI, MCP stdio server, and Python 3.8+ SDK run purely on standard library primitives (`fetch`/`crypto` in Node, `urllib` in Python).
34
+
35
+ ---
36
+
37
+ ## 1. Quick `curl` Migration
38
+
39
+ ```bash
40
+ curl -X POST https://remove.ac/v1.0/removebg \
41
+ -H "X-Api-Key: rac_live_your_key_here" \
42
+ -F "image_file=@photo.jpg" \
43
+ -F "size=full" \
44
+ -o cutout.png
45
+ ```
46
+
47
+ ---
48
+
49
+ ## 2. Node.js / TypeScript SDK (`Node 18+`)
50
+
51
+ ### Install directly via GitHub
52
+
53
+ ```bash
54
+ npm install github:szp2005/remove-ac-sdk
55
+ ```
56
+
57
+ ### Usage
58
+
59
+ ```ts
60
+ import fs from "node:fs";
61
+ import { RemoveAcClient } from "remove-ac";
62
+
63
+ const client = new RemoveAcClient({
64
+ apiKey: process.env.REMOVE_AC_API_KEY, // rac_live_...
65
+ });
66
+
67
+ // Files > 4 MB (up to 25 MB) automatically use /v1.0/removebg/ticket zero-disk GPU streaming
68
+ const result = await client.removeBackground("./studio-shot-18mb.png", {
69
+ size: "full",
70
+ format: "png",
71
+ shadowType: "contact",
72
+ });
73
+
74
+ fs.writeFileSync("./studio-shot-cutout.png", result.buffer);
75
+ console.log(`Saved ${result.width}x${result.height} (${result.creditsCharged} credit)`);
76
+ ```
77
+
78
+ ---
79
+
80
+ ## 3. CLI & `--watch` Hot-Folder Batch Tool
81
+
82
+ Run directly with `npx` or after installing:
83
+
84
+ ```bash
85
+ export REMOVE_AC_API_KEY="rac_live_..."
86
+
87
+ # Process an entire folder with 4 parallel workers
88
+ npx github:szp2005/remove-ac-sdk ./input-photos ./output-pngs --concurrency=4 --size=full
89
+
90
+ # Watch a hot folder continuously and output transparent WebP cutouts
91
+ npx github:szp2005/remove-ac-sdk ./incoming ./cutouts --watch --format=webp --crop
92
+ ```
93
+
94
+ ---
95
+
96
+ ## 4. Python SDK (`Python 3.8+`, Zero External Dependencies)
97
+
98
+ ### Install directly via GitHub
99
+
100
+ ```bash
101
+ pip install git+https://github.com/szp2005/remove-ac-sdk.git
102
+ ```
103
+
104
+ ### Native Client
105
+
106
+ ```python
107
+ from remove_ac import RemoveAcClient
108
+
109
+ client = RemoveAcClient() # reads REMOVE_AC_API_KEY
110
+ png_bytes = client.remove_background(
111
+ "product.jpg",
112
+ size="full",
113
+ shadow_type="contact",
114
+ )
115
+ with open("product-nobg.png", "wb") as fh:
116
+ fh.write(png_bytes)
117
+ ```
118
+
119
+ ### 1:1 `removebg` Drop-In Replacement Shim
120
+
121
+ Existing scripts written for `from removebg import RemoveBg` work without rewriting call sites:
122
+
123
+ ```python
124
+ from removebg import RemoveBg
125
+
126
+ rmbg = RemoveBg("rac_live_your_key_here")
127
+ rmbg.remove_background_from_img_file("car.jpg", size="full", new_file_name="car-cutout.png")
128
+ ```
129
+
130
+ ---
131
+
132
+ ## 5. MCP Server (`Claude Desktop` · `Cursor` · `Windsurf` · `Cline`)
133
+
134
+ `remove-ac` includes a zero-dependency Model Context Protocol (`MCP`) stdio server (`remove-ac-mcp`) exposing three tools to AI coding and design agents:
135
+
136
+ - `remove_background_file` — remove background from any local `.png` / `.jpg` / `.webp` up to `25 MB / 40 MP` (auto-streams `>4 MB` via `/v1.0/removebg/ticket`)
137
+ - `remove_background_url` — remove background from a public `https://` image URL and save locally
138
+ - `get_account_balance` — inspect remaining `remove.ac` API credits
139
+
140
+ ### Claude Desktop / Cursor Configuration
141
+
142
+ ```json
143
+ {
144
+ "mcpServers": {
145
+ "remove-ac": {
146
+ "command": "npx",
147
+ "args": ["-y", "-p", "github:szp2005/remove-ac-sdk", "remove-ac-mcp"],
148
+ "env": {
149
+ "REMOVE_AC_API_KEY": "rac_live_your_key_here"
150
+ }
151
+ }
152
+ }
153
+ }
154
+ ```
155
+
156
+ ---
157
+
158
+ ## License
159
+
160
+ MIT © [remove.ac](https://remove.ac)
@@ -0,0 +1,145 @@
1
+ # remove.ac Official SDKs, CLI & MCP Server (Node.js · Python · MCP · Drop-in `remove.bg` Migration)
2
+
3
+ Zero-dependency official developer SDKs, hot-folder CLI, and Model Context Protocol (`MCP`) server for **[remove.ac](https://remove.ac)** — the dual-mode background removal platform pairing a free on-device WebAssembly studio with a `remove.bg`-compatible Cloud GPU REST API (`POST https://remove.ac/v1.0/removebg`).
4
+
5
+ - **Live Web Workbench (Free On-Device WASM + Batch 50 ZIP):** https://remove.ac
6
+ - **1-Line `remove.bg` API Migration Guide (Dec 1, 2026 Shutdown):** https://remove.ac/guides/remove-bg-api-migration
7
+ - **Interactive API Sandbox & Docs:** https://remove.ac/api-info
8
+ - **Figma, Photoshop UXP, GIMP, Sketch & Make/n8n Integrations:** https://remove.ac/plugins
9
+ - **Open Inference & Pricing Dataset (`CC BY 4.0`):** https://remove.ac/benchmarks
10
+
11
+ ---
12
+
13
+ ## Key Capabilities
14
+
15
+ - **1:1 `remove.bg` v1.0 Field Compatibility:** Swap `https://api.remove.bg/v1.0/removebg` to `https://remove.ac/v1.0/removebg` using standard `X-Api-Key: rac_live_...` headers and form fields (`image_file`, `image_file_b64`, `image_url`, `size`, `format`, `channels`, `type`, `roi`, `crop`, `crop_margin`, `scale`, `position`, `bg_color`, `bg_image_url`, `add_shadow`, `shadow_type`, `shadow_opacity`, `semitransparency`).
16
+ - **Up to 25 MB / 40 MP Zero-Disk Direct GPU Streaming:** Payloads above 4 MB automatically negotiate a short-lived HMAC ticket (`POST /v1.0/removebg/ticket`) and stream directly to ephemeral GPU RAM without disk or database blob persistence.
17
+ - **Atomic Credit Holds (`Idempotency-Key`):** Upstream timeouts or errors automatically release held credits with zero charge. Every verified account starts with **1 free credit**; pay-as-you-go credit packs never expire.
18
+ - **Zero External Runtime Dependencies:** The Node.js 18+ SDK, CLI, MCP stdio server, and Python 3.8+ SDK run purely on standard library primitives (`fetch`/`crypto` in Node, `urllib` in Python).
19
+
20
+ ---
21
+
22
+ ## 1. Quick `curl` Migration
23
+
24
+ ```bash
25
+ curl -X POST https://remove.ac/v1.0/removebg \
26
+ -H "X-Api-Key: rac_live_your_key_here" \
27
+ -F "image_file=@photo.jpg" \
28
+ -F "size=full" \
29
+ -o cutout.png
30
+ ```
31
+
32
+ ---
33
+
34
+ ## 2. Node.js / TypeScript SDK (`Node 18+`)
35
+
36
+ ### Install directly via GitHub
37
+
38
+ ```bash
39
+ npm install github:szp2005/remove-ac-sdk
40
+ ```
41
+
42
+ ### Usage
43
+
44
+ ```ts
45
+ import fs from "node:fs";
46
+ import { RemoveAcClient } from "remove-ac";
47
+
48
+ const client = new RemoveAcClient({
49
+ apiKey: process.env.REMOVE_AC_API_KEY, // rac_live_...
50
+ });
51
+
52
+ // Files > 4 MB (up to 25 MB) automatically use /v1.0/removebg/ticket zero-disk GPU streaming
53
+ const result = await client.removeBackground("./studio-shot-18mb.png", {
54
+ size: "full",
55
+ format: "png",
56
+ shadowType: "contact",
57
+ });
58
+
59
+ fs.writeFileSync("./studio-shot-cutout.png", result.buffer);
60
+ console.log(`Saved ${result.width}x${result.height} (${result.creditsCharged} credit)`);
61
+ ```
62
+
63
+ ---
64
+
65
+ ## 3. CLI & `--watch` Hot-Folder Batch Tool
66
+
67
+ Run directly with `npx` or after installing:
68
+
69
+ ```bash
70
+ export REMOVE_AC_API_KEY="rac_live_..."
71
+
72
+ # Process an entire folder with 4 parallel workers
73
+ npx github:szp2005/remove-ac-sdk ./input-photos ./output-pngs --concurrency=4 --size=full
74
+
75
+ # Watch a hot folder continuously and output transparent WebP cutouts
76
+ npx github:szp2005/remove-ac-sdk ./incoming ./cutouts --watch --format=webp --crop
77
+ ```
78
+
79
+ ---
80
+
81
+ ## 4. Python SDK (`Python 3.8+`, Zero External Dependencies)
82
+
83
+ ### Install directly via GitHub
84
+
85
+ ```bash
86
+ pip install git+https://github.com/szp2005/remove-ac-sdk.git
87
+ ```
88
+
89
+ ### Native Client
90
+
91
+ ```python
92
+ from remove_ac import RemoveAcClient
93
+
94
+ client = RemoveAcClient() # reads REMOVE_AC_API_KEY
95
+ png_bytes = client.remove_background(
96
+ "product.jpg",
97
+ size="full",
98
+ shadow_type="contact",
99
+ )
100
+ with open("product-nobg.png", "wb") as fh:
101
+ fh.write(png_bytes)
102
+ ```
103
+
104
+ ### 1:1 `removebg` Drop-In Replacement Shim
105
+
106
+ Existing scripts written for `from removebg import RemoveBg` work without rewriting call sites:
107
+
108
+ ```python
109
+ from removebg import RemoveBg
110
+
111
+ rmbg = RemoveBg("rac_live_your_key_here")
112
+ rmbg.remove_background_from_img_file("car.jpg", size="full", new_file_name="car-cutout.png")
113
+ ```
114
+
115
+ ---
116
+
117
+ ## 5. MCP Server (`Claude Desktop` · `Cursor` · `Windsurf` · `Cline`)
118
+
119
+ `remove-ac` includes a zero-dependency Model Context Protocol (`MCP`) stdio server (`remove-ac-mcp`) exposing three tools to AI coding and design agents:
120
+
121
+ - `remove_background_file` — remove background from any local `.png` / `.jpg` / `.webp` up to `25 MB / 40 MP` (auto-streams `>4 MB` via `/v1.0/removebg/ticket`)
122
+ - `remove_background_url` — remove background from a public `https://` image URL and save locally
123
+ - `get_account_balance` — inspect remaining `remove.ac` API credits
124
+
125
+ ### Claude Desktop / Cursor Configuration
126
+
127
+ ```json
128
+ {
129
+ "mcpServers": {
130
+ "remove-ac": {
131
+ "command": "npx",
132
+ "args": ["-y", "-p", "github:szp2005/remove-ac-sdk", "remove-ac-mcp"],
133
+ "env": {
134
+ "REMOVE_AC_API_KEY": "rac_live_your_key_here"
135
+ }
136
+ }
137
+ }
138
+ }
139
+ ```
140
+
141
+ ---
142
+
143
+ ## License
144
+
145
+ MIT © [remove.ac](https://remove.ac)
@@ -0,0 +1,23 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "remove-ac"
7
+ version = "1.1.0"
8
+ description = "Official zero-dependency Python SDK & removebg 1:1 drop-in replacement for remove.ac"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.8"
12
+ dependencies = []
13
+ keywords = ["remove.ac", "remove.bg", "background-removal", "matting", "image-segmentation"]
14
+
15
+ [project.urls]
16
+ Homepage = "https://remove.ac"
17
+ Documentation = "https://remove.ac/api-info"
18
+ Benchmarks = "https://remove.ac/benchmarks"
19
+ Repository = "https://github.com/szp2005/remove-ac-sdk"
20
+
21
+ [tool.setuptools]
22
+ packages = ["remove_ac"]
23
+ py-modules = ["removebg"]
@@ -0,0 +1,15 @@
1
+ from .client import (
2
+ RemoveAcClient,
3
+ RemoveAcError,
4
+ RemoveBg,
5
+ derive_sub_idempotency_key,
6
+ parse_image_dimensions,
7
+ )
8
+
9
+ __all__ = [
10
+ "RemoveAcClient",
11
+ "RemoveBg",
12
+ "RemoveAcError",
13
+ "parse_image_dimensions",
14
+ "derive_sub_idempotency_key",
15
+ ]
@@ -0,0 +1,433 @@
1
+ """remove.ac Official Zero-Dependency Python SDK + 1:1 removebg drop-in client."""
2
+ from __future__ import annotations
3
+
4
+ import base64
5
+ import hashlib
6
+ import json
7
+ import os
8
+ import struct
9
+ import urllib.error
10
+ import urllib.request
11
+ import uuid
12
+ from typing import Any, Callable, Dict, Optional, Tuple, Union
13
+
14
+ DEFAULT_BASE_URL = "https://remove.ac"
15
+ DIRECT_THRESHOLD_BYTES = 4 * 1024 * 1024
16
+ DIRECT_RGBA_PNG_PIXELS = 5_000_000
17
+ DIRECT_CROPPED_RGBA_PNG_PIXELS = 10_000_000
18
+
19
+
20
+ def parse_image_dimensions(data: bytes) -> Optional[Tuple[int, int]]:
21
+ if not data or len(data) < 10:
22
+ return None
23
+ if len(data) >= 24 and data[:8] == b"\x89PNG\r\n\x1a\n" and data[12:16] == b"IHDR":
24
+ w, h = struct.unpack(">II", data[16:24])
25
+ return (w, h) if 0 < w <= 1_000_000 and 0 < h <= 1_000_000 else None
26
+ if data[:6] in (b"GIF87a", b"GIF89a"):
27
+ w, h = struct.unpack("<HH", data[6:10])
28
+ return (w, h) if w > 0 and h > 0 else None
29
+ if len(data) >= 25 and data[:4] == b"RIFF" and data[8:12] == b"WEBP":
30
+ chunk = data[12:16]
31
+ if chunk == b"VP8 " and len(data) >= 30 and data[23:26] == b"\x9d\x01\x2a":
32
+ w, h = struct.unpack("<HH", data[26:30])
33
+ w &= 0x3FFF
34
+ h &= 0x3FFF
35
+ return (w, h) if w > 0 and h > 0 else None
36
+ if chunk == b"VP8L" and data[20] == 0x2F:
37
+ b0, b1, b2, b3 = data[21], data[22], data[23], data[24]
38
+ w = 1 + (b0 | ((b1 & 0x3F) << 8))
39
+ h = 1 + ((b1 >> 6) | (b2 << 2) | ((b3 & 0x0F) << 10))
40
+ return (w, h) if w > 0 and h > 0 else None
41
+ if chunk == b"VP8X" and len(data) >= 30:
42
+ w = 1 + (data[24] | (data[25] << 8) | (data[26] << 16))
43
+ h = 1 + (data[27] | (data[28] << 8) | (data[29] << 16))
44
+ return (w, h) if w > 0 and h > 0 else None
45
+ if data[:2] == b"\xff\xd8":
46
+ pos = 2
47
+ steps = 0
48
+ while steps < 512 and pos + 3 < len(data):
49
+ steps += 1
50
+ if data[pos] != 0xFF:
51
+ break
52
+ while pos < len(data) and data[pos] == 0xFF:
53
+ pos += 1
54
+ if pos >= len(data):
55
+ break
56
+ marker = data[pos]
57
+ pos += 1
58
+ if marker in (0xD9, 0xDA):
59
+ break
60
+ if marker in (0x00, 0x01) or (0xD0 <= marker <= 0xD7):
61
+ continue
62
+ if pos + 1 >= len(data):
63
+ break
64
+ seg_len = (data[pos] << 8) | data[pos + 1]
65
+ if seg_len < 2:
66
+ break
67
+ if marker in (0xC0, 0xC1, 0xC2, 0xC3, 0xC5, 0xC6, 0xC7, 0xC9, 0xCA, 0xCB, 0xCD, 0xCE, 0xCF):
68
+ if pos + 6 >= len(data):
69
+ break
70
+ h = (data[pos + 3] << 8) | data[pos + 4]
71
+ w = (data[pos + 5] << 8) | data[pos + 6]
72
+ return (w, h) if w > 0 and h > 0 else None
73
+ pos += seg_len
74
+ return None
75
+
76
+
77
+ def _should_preflight_via_ticket(
78
+ img_bytes: Optional[bytes], size: str, fmt: str, channels: str, crop: bool
79
+ ) -> bool:
80
+ if not img_bytes:
81
+ return False
82
+ if fmt not in ("png", "auto", "zip") or channels == "alpha":
83
+ return False
84
+ dims = parse_image_dimensions(img_bytes)
85
+ if not dims:
86
+ return False
87
+ input_px = dims[0] * dims[1]
88
+ if size in ("preview", "small", "regular"):
89
+ eff = min(input_px, 250_000)
90
+ elif size == "medium":
91
+ eff = min(input_px, 1_500_000)
92
+ elif size == "hd":
93
+ eff = min(input_px, 4_000_000)
94
+ elif size == "4k":
95
+ eff = min(input_px, 10_000_000)
96
+ else:
97
+ eff = input_px
98
+ return eff > (DIRECT_CROPPED_RGBA_PNG_PIXELS if crop else DIRECT_RGBA_PNG_PIXELS)
99
+
100
+
101
+ class RemoveAcError(Exception):
102
+ def __init__(self, status_code: int, message: str):
103
+ super().__init__(f"[HTTP {status_code}] {message}")
104
+ self.status_code = status_code
105
+
106
+
107
+ def derive_sub_idempotency_key(idem: str, suffix: str) -> str:
108
+ trimmed = (idem or "").strip()
109
+ if not trimmed or len(trimmed) > 255:
110
+ raise RemoveAcError(400, "idempotency_key must be between 1 and 255 characters")
111
+ direct = f"{trimmed}:{suffix}"
112
+ if len(direct) <= 255:
113
+ return direct
114
+ digest = hashlib.sha256(trimmed.encode("utf-8")).hexdigest()[:32]
115
+ return f"{trimmed[:180]}:{digest}:{suffix}"
116
+
117
+
118
+ def _encode_multipart(
119
+ fields: Dict[str, str],
120
+ file_tuple: Optional[Tuple[str, str, bytes]] = None,
121
+ ) -> Tuple[bytes, str]:
122
+ boundary = f"----RemoveAcBoundary{uuid.uuid4().hex}"
123
+ chunks = []
124
+ for key, val in fields.items():
125
+ chunks.append(f"--{boundary}\r\n".encode("utf-8"))
126
+ chunks.append(f'Content-Disposition: form-data; name="{key}"\r\n\r\n'.encode("utf-8"))
127
+ chunks.append(f"{val}\r\n".encode("utf-8"))
128
+ if file_tuple is not None:
129
+ field_name, filename, raw_bytes = file_tuple
130
+ safe_name = filename.replace('"', "_")
131
+ chunks.append(f"--{boundary}\r\n".encode("utf-8"))
132
+ chunks.append(
133
+ f'Content-Disposition: form-data; name="{field_name}"; filename="{safe_name}"\r\n'
134
+ f"Content-Type: application/octet-stream\r\n\r\n".encode("utf-8")
135
+ )
136
+ chunks.append(raw_bytes)
137
+ chunks.append(b"\r\n")
138
+ chunks.append(f"--{boundary}--\r\n".encode("utf-8"))
139
+ return b"".join(chunks), f"multipart/form-data; boundary={boundary}"
140
+
141
+
142
+ TransportFn = Callable[[str, str, Dict[str, str], Optional[bytes], int], Tuple[int, Dict[str, str], bytes]]
143
+
144
+
145
+ def _default_transport(
146
+ method: str,
147
+ url: str,
148
+ headers: Dict[str, str],
149
+ body: Optional[bytes],
150
+ timeout: int,
151
+ ) -> Tuple[int, Dict[str, str], bytes]:
152
+ req = urllib.request.Request(url, data=body, headers=headers, method=method)
153
+ try:
154
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
155
+ resp_headers = {k.lower(): v for k, v in resp.headers.items()}
156
+ return resp.status, resp_headers, resp.read()
157
+ except urllib.error.HTTPError as exc:
158
+ err_body = exc.read() if exc.fp else b""
159
+ resp_headers = {k.lower(): v for k, v in (exc.headers.items() if exc.headers else [])}
160
+ return exc.code, resp_headers, err_body
161
+
162
+
163
+ class RemoveAcClient:
164
+ def __init__(
165
+ self,
166
+ api_key: Optional[str] = None,
167
+ base_url: Optional[str] = None,
168
+ transport: Optional[TransportFn] = None,
169
+ ):
170
+ self.api_key = (
171
+ api_key
172
+ or os.environ.get("REMOVE_AC_API_KEY")
173
+ or os.environ.get("REMOVE_BG_API_KEY")
174
+ or ""
175
+ )
176
+ resolved_base = base_url or os.environ.get("REMOVE_AC_BASE_URL") or DEFAULT_BASE_URL
177
+ self.base_url = resolved_base.rstrip("/")
178
+ self._transport = transport or _default_transport
179
+
180
+ def get_account(self, timeout: int = 15) -> Dict[str, Any]:
181
+ if not self.api_key:
182
+ raise RemoveAcError(403, "Missing remove.ac API key")
183
+ status, _, body = self._transport(
184
+ "GET",
185
+ f"{self.base_url}/v1.0/account",
186
+ {"X-Api-Key": self.api_key},
187
+ None,
188
+ timeout,
189
+ )
190
+ if status < 200 or status >= 300:
191
+ raise RemoveAcError(status, body.decode("utf-8", errors="replace"))
192
+ return json.loads(body.decode("utf-8"))
193
+
194
+ def remove_background(
195
+ self,
196
+ image: Union[str, bytes],
197
+ *,
198
+ size: str = "auto",
199
+ format: str = "png",
200
+ channels: str = "rgba",
201
+ type: str = "auto",
202
+ type_level: Optional[str] = None,
203
+ roi: Optional[str] = None,
204
+ scale: Optional[str] = None,
205
+ position: Optional[str] = None,
206
+ bg_color: Optional[str] = None,
207
+ bg_image_url: Optional[str] = None,
208
+ crop: bool = False,
209
+ crop_margin: Optional[str] = None,
210
+ add_shadow: bool = False,
211
+ shadow_type: Optional[str] = None,
212
+ shadow_opacity: Optional[float] = None,
213
+ semitransparency: bool = True,
214
+ direct: bool = False,
215
+ idempotency_key: Optional[str] = None,
216
+ timeout: int = 90,
217
+ ) -> bytes:
218
+ if not self.api_key:
219
+ raise RemoveAcError(403, "Missing remove.ac API key")
220
+ if idempotency_key is not None:
221
+ trimmed_idem = idempotency_key.strip()
222
+ if not trimmed_idem or len(trimmed_idem) > 255:
223
+ raise RemoveAcError(400, "idempotency_key must be between 1 and 255 characters")
224
+ idem = trimmed_idem
225
+ else:
226
+ idem = f"pysdk-{uuid.uuid4()}"
227
+ img_bytes: Optional[bytes] = None
228
+ img_name = "image.png"
229
+ img_url: Optional[str] = None
230
+
231
+ if isinstance(image, (bytes, bytearray)):
232
+ img_bytes = bytes(image)
233
+ elif isinstance(image, str) and image.startswith(("http://", "https://")):
234
+ img_url = image
235
+ elif isinstance(image, str):
236
+ img_name = os.path.basename(image)
237
+ with open(image, "rb") as fh:
238
+ img_bytes = fh.read()
239
+ else:
240
+ raise RemoveAcError(400, "Unsupported image input")
241
+
242
+ def _via_ticket(suffix: str) -> bytes:
243
+ if img_bytes is None:
244
+ raise RemoveAcError(413, "Ticket mode requires binary input")
245
+ payload: Dict[str, Any] = {
246
+ "foregroundSha256": hashlib.sha256(img_bytes).hexdigest(),
247
+ "foregroundByteLength": len(img_bytes),
248
+ "size": size,
249
+ "format": format,
250
+ "channels": channels,
251
+ "semitransparency": semitransparency,
252
+ }
253
+ if type and type != "auto":
254
+ payload["type"] = type
255
+ if type_level:
256
+ payload["type_level"] = type_level
257
+ if roi:
258
+ payload["roi"] = roi
259
+ if scale:
260
+ payload["scale"] = scale
261
+ if position:
262
+ payload["position"] = position
263
+ if bg_color:
264
+ payload["bg_color"] = bg_color
265
+ if bg_image_url:
266
+ payload["bg_image_url"] = bg_image_url
267
+ if crop:
268
+ payload["crop"] = True
269
+ if crop_margin:
270
+ payload["crop_margin"] = crop_margin
271
+ if add_shadow:
272
+ payload["add_shadow"] = True
273
+ if shadow_type:
274
+ payload["shadow_type"] = shadow_type
275
+ if shadow_opacity is not None:
276
+ payload["shadow_opacity"] = shadow_opacity
277
+
278
+ t_status, _, t_body = self._transport(
279
+ "POST",
280
+ f"{self.base_url}/v1.0/removebg/ticket",
281
+ {
282
+ "Content-Type": "application/json",
283
+ "X-Api-Key": self.api_key,
284
+ "Idempotency-Key": derive_sub_idempotency_key(idem, suffix),
285
+ },
286
+ json.dumps(payload).encode("utf-8"),
287
+ 30,
288
+ )
289
+ if t_status < 200 or t_status >= 300:
290
+ raise RemoveAcError(t_status, t_body.decode("utf-8", errors="replace"))
291
+ t_data = json.loads(t_body.decode("utf-8"))["data"]
292
+ mp_body, mp_ct = _encode_multipart({}, ("image_file", img_name, img_bytes))
293
+ d_status, _, d_body = self._transport(
294
+ "POST",
295
+ t_data["directEndpoint"],
296
+ {
297
+ "Authorization": f"Bearer {t_data['ticket']}",
298
+ "Content-Type": mp_ct,
299
+ },
300
+ mp_body,
301
+ timeout,
302
+ )
303
+ if d_status < 200 or d_status >= 300:
304
+ raise RemoveAcError(d_status, d_body.decode("utf-8", errors="replace"))
305
+ return d_body
306
+
307
+ if img_bytes is not None and (
308
+ direct
309
+ or len(img_bytes) > DIRECT_THRESHOLD_BYTES
310
+ or _should_preflight_via_ticket(img_bytes, size, format, channels, crop)
311
+ ):
312
+ return _via_ticket("direct")
313
+
314
+ fields: Dict[str, str] = {
315
+ "size": size,
316
+ "format": format,
317
+ "channels": channels,
318
+ "semitransparency": str(semitransparency).lower(),
319
+ }
320
+ if img_url:
321
+ fields["image_url"] = img_url
322
+ if type and type != "auto":
323
+ fields["type"] = type
324
+ if type_level:
325
+ fields["type_level"] = type_level
326
+ if roi:
327
+ fields["roi"] = roi
328
+ if scale:
329
+ fields["scale"] = scale
330
+ if position:
331
+ fields["position"] = position
332
+ if bg_color:
333
+ fields["bg_color"] = bg_color
334
+ if bg_image_url:
335
+ fields["bg_image_url"] = bg_image_url
336
+ if crop:
337
+ fields["crop"] = "true"
338
+ if crop_margin:
339
+ fields["crop_margin"] = crop_margin
340
+ if add_shadow:
341
+ fields["add_shadow"] = "true"
342
+ if shadow_type:
343
+ fields["shadow_type"] = shadow_type
344
+ if shadow_opacity is not None:
345
+ fields["shadow_opacity"] = str(shadow_opacity)
346
+
347
+ file_tuple = ("image_file", img_name, img_bytes) if img_bytes is not None else None
348
+ mp_body, mp_ct = _encode_multipart(fields, file_tuple)
349
+ status, _, body = self._transport(
350
+ "POST",
351
+ f"{self.base_url}/v1.0/removebg",
352
+ {
353
+ "X-Api-Key": self.api_key,
354
+ "Idempotency-Key": idem,
355
+ "Content-Type": mp_ct,
356
+ },
357
+ mp_body,
358
+ timeout,
359
+ )
360
+ if (
361
+ status == 409
362
+ and idempotency_key is None
363
+ and b"idempotency_replay_unavailable" in body
364
+ ):
365
+ status, _, body = self._transport(
366
+ "POST",
367
+ f"{self.base_url}/v1.0/removebg",
368
+ {
369
+ "X-Api-Key": self.api_key,
370
+ "Idempotency-Key": f"pysdk-{uuid.uuid4()}",
371
+ "Content-Type": mp_ct,
372
+ },
373
+ mp_body,
374
+ timeout,
375
+ )
376
+ if status == 413 and img_bytes is not None:
377
+ return _via_ticket("fallback413")
378
+ if status < 200 or status >= 300:
379
+ raise RemoveAcError(status, body.decode("utf-8", errors="replace"))
380
+ return body
381
+
382
+
383
+ class RemoveBg:
384
+ """1:1 drop-in replacement for brilam/remove-bg Python package."""
385
+
386
+ def __init__(
387
+ self,
388
+ api_key: str,
389
+ error_log_file: Optional[str] = None,
390
+ base_url: str = DEFAULT_BASE_URL,
391
+ transport: Optional[TransportFn] = None,
392
+ ):
393
+ self.client = RemoveAcClient(api_key=api_key, base_url=base_url, transport=transport)
394
+ self.error_log_file = error_log_file
395
+
396
+ def remove_background_from_img_file(
397
+ self,
398
+ img_file_path: str,
399
+ size: str = "regular",
400
+ new_file_name: str = "no-bg.png",
401
+ **kwargs: Any,
402
+ ) -> bytes:
403
+ out = self.client.remove_background(img_file_path, size=size, **kwargs)
404
+ if new_file_name:
405
+ with open(new_file_name, "wb") as fh:
406
+ fh.write(out)
407
+ return out
408
+
409
+ def remove_background_from_img_url(
410
+ self,
411
+ img_url: str,
412
+ size: str = "regular",
413
+ new_file_name: str = "no-bg.png",
414
+ **kwargs: Any,
415
+ ) -> bytes:
416
+ out = self.client.remove_background(img_url, size=size, **kwargs)
417
+ if new_file_name:
418
+ with open(new_file_name, "wb") as fh:
419
+ fh.write(out)
420
+ return out
421
+
422
+ def remove_background_from_base64_img(
423
+ self,
424
+ base64_img: str,
425
+ size: str = "regular",
426
+ new_file_name: str = "no-bg.png",
427
+ **kwargs: Any,
428
+ ) -> bytes:
429
+ out = self.client.remove_background(base64.b64decode(base64_img), size=size, **kwargs)
430
+ if new_file_name:
431
+ with open(new_file_name, "wb") as fh:
432
+ fh.write(out)
433
+ return out
@@ -0,0 +1,160 @@
1
+ Metadata-Version: 2.4
2
+ Name: remove-ac
3
+ Version: 1.1.0
4
+ Summary: Official zero-dependency Python SDK & removebg 1:1 drop-in replacement for remove.ac
5
+ License: MIT
6
+ Project-URL: Homepage, https://remove.ac
7
+ Project-URL: Documentation, https://remove.ac/api-info
8
+ Project-URL: Benchmarks, https://remove.ac/benchmarks
9
+ Project-URL: Repository, https://github.com/szp2005/remove-ac-sdk
10
+ Keywords: remove.ac,remove.bg,background-removal,matting,image-segmentation
11
+ Requires-Python: >=3.8
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Dynamic: license-file
15
+
16
+ # remove.ac Official SDKs, CLI & MCP Server (Node.js · Python · MCP · Drop-in `remove.bg` Migration)
17
+
18
+ Zero-dependency official developer SDKs, hot-folder CLI, and Model Context Protocol (`MCP`) server for **[remove.ac](https://remove.ac)** — the dual-mode background removal platform pairing a free on-device WebAssembly studio with a `remove.bg`-compatible Cloud GPU REST API (`POST https://remove.ac/v1.0/removebg`).
19
+
20
+ - **Live Web Workbench (Free On-Device WASM + Batch 50 ZIP):** https://remove.ac
21
+ - **1-Line `remove.bg` API Migration Guide (Dec 1, 2026 Shutdown):** https://remove.ac/guides/remove-bg-api-migration
22
+ - **Interactive API Sandbox & Docs:** https://remove.ac/api-info
23
+ - **Figma, Photoshop UXP, GIMP, Sketch & Make/n8n Integrations:** https://remove.ac/plugins
24
+ - **Open Inference & Pricing Dataset (`CC BY 4.0`):** https://remove.ac/benchmarks
25
+
26
+ ---
27
+
28
+ ## Key Capabilities
29
+
30
+ - **1:1 `remove.bg` v1.0 Field Compatibility:** Swap `https://api.remove.bg/v1.0/removebg` to `https://remove.ac/v1.0/removebg` using standard `X-Api-Key: rac_live_...` headers and form fields (`image_file`, `image_file_b64`, `image_url`, `size`, `format`, `channels`, `type`, `roi`, `crop`, `crop_margin`, `scale`, `position`, `bg_color`, `bg_image_url`, `add_shadow`, `shadow_type`, `shadow_opacity`, `semitransparency`).
31
+ - **Up to 25 MB / 40 MP Zero-Disk Direct GPU Streaming:** Payloads above 4 MB automatically negotiate a short-lived HMAC ticket (`POST /v1.0/removebg/ticket`) and stream directly to ephemeral GPU RAM without disk or database blob persistence.
32
+ - **Atomic Credit Holds (`Idempotency-Key`):** Upstream timeouts or errors automatically release held credits with zero charge. Every verified account starts with **1 free credit**; pay-as-you-go credit packs never expire.
33
+ - **Zero External Runtime Dependencies:** The Node.js 18+ SDK, CLI, MCP stdio server, and Python 3.8+ SDK run purely on standard library primitives (`fetch`/`crypto` in Node, `urllib` in Python).
34
+
35
+ ---
36
+
37
+ ## 1. Quick `curl` Migration
38
+
39
+ ```bash
40
+ curl -X POST https://remove.ac/v1.0/removebg \
41
+ -H "X-Api-Key: rac_live_your_key_here" \
42
+ -F "image_file=@photo.jpg" \
43
+ -F "size=full" \
44
+ -o cutout.png
45
+ ```
46
+
47
+ ---
48
+
49
+ ## 2. Node.js / TypeScript SDK (`Node 18+`)
50
+
51
+ ### Install directly via GitHub
52
+
53
+ ```bash
54
+ npm install github:szp2005/remove-ac-sdk
55
+ ```
56
+
57
+ ### Usage
58
+
59
+ ```ts
60
+ import fs from "node:fs";
61
+ import { RemoveAcClient } from "remove-ac";
62
+
63
+ const client = new RemoveAcClient({
64
+ apiKey: process.env.REMOVE_AC_API_KEY, // rac_live_...
65
+ });
66
+
67
+ // Files > 4 MB (up to 25 MB) automatically use /v1.0/removebg/ticket zero-disk GPU streaming
68
+ const result = await client.removeBackground("./studio-shot-18mb.png", {
69
+ size: "full",
70
+ format: "png",
71
+ shadowType: "contact",
72
+ });
73
+
74
+ fs.writeFileSync("./studio-shot-cutout.png", result.buffer);
75
+ console.log(`Saved ${result.width}x${result.height} (${result.creditsCharged} credit)`);
76
+ ```
77
+
78
+ ---
79
+
80
+ ## 3. CLI & `--watch` Hot-Folder Batch Tool
81
+
82
+ Run directly with `npx` or after installing:
83
+
84
+ ```bash
85
+ export REMOVE_AC_API_KEY="rac_live_..."
86
+
87
+ # Process an entire folder with 4 parallel workers
88
+ npx github:szp2005/remove-ac-sdk ./input-photos ./output-pngs --concurrency=4 --size=full
89
+
90
+ # Watch a hot folder continuously and output transparent WebP cutouts
91
+ npx github:szp2005/remove-ac-sdk ./incoming ./cutouts --watch --format=webp --crop
92
+ ```
93
+
94
+ ---
95
+
96
+ ## 4. Python SDK (`Python 3.8+`, Zero External Dependencies)
97
+
98
+ ### Install directly via GitHub
99
+
100
+ ```bash
101
+ pip install git+https://github.com/szp2005/remove-ac-sdk.git
102
+ ```
103
+
104
+ ### Native Client
105
+
106
+ ```python
107
+ from remove_ac import RemoveAcClient
108
+
109
+ client = RemoveAcClient() # reads REMOVE_AC_API_KEY
110
+ png_bytes = client.remove_background(
111
+ "product.jpg",
112
+ size="full",
113
+ shadow_type="contact",
114
+ )
115
+ with open("product-nobg.png", "wb") as fh:
116
+ fh.write(png_bytes)
117
+ ```
118
+
119
+ ### 1:1 `removebg` Drop-In Replacement Shim
120
+
121
+ Existing scripts written for `from removebg import RemoveBg` work without rewriting call sites:
122
+
123
+ ```python
124
+ from removebg import RemoveBg
125
+
126
+ rmbg = RemoveBg("rac_live_your_key_here")
127
+ rmbg.remove_background_from_img_file("car.jpg", size="full", new_file_name="car-cutout.png")
128
+ ```
129
+
130
+ ---
131
+
132
+ ## 5. MCP Server (`Claude Desktop` · `Cursor` · `Windsurf` · `Cline`)
133
+
134
+ `remove-ac` includes a zero-dependency Model Context Protocol (`MCP`) stdio server (`remove-ac-mcp`) exposing three tools to AI coding and design agents:
135
+
136
+ - `remove_background_file` — remove background from any local `.png` / `.jpg` / `.webp` up to `25 MB / 40 MP` (auto-streams `>4 MB` via `/v1.0/removebg/ticket`)
137
+ - `remove_background_url` — remove background from a public `https://` image URL and save locally
138
+ - `get_account_balance` — inspect remaining `remove.ac` API credits
139
+
140
+ ### Claude Desktop / Cursor Configuration
141
+
142
+ ```json
143
+ {
144
+ "mcpServers": {
145
+ "remove-ac": {
146
+ "command": "npx",
147
+ "args": ["-y", "-p", "github:szp2005/remove-ac-sdk", "remove-ac-mcp"],
148
+ "env": {
149
+ "REMOVE_AC_API_KEY": "rac_live_your_key_here"
150
+ }
151
+ }
152
+ }
153
+ }
154
+ ```
155
+
156
+ ---
157
+
158
+ ## License
159
+
160
+ MIT © [remove.ac](https://remove.ac)
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ removebg.py
5
+ remove_ac/__init__.py
6
+ remove_ac/client.py
7
+ remove_ac.egg-info/PKG-INFO
8
+ remove_ac.egg-info/SOURCES.txt
9
+ remove_ac.egg-info/dependency_links.txt
10
+ remove_ac.egg-info/top_level.txt
11
+ tests/test_python_sdk.py
@@ -0,0 +1,2 @@
1
+ remove_ac
2
+ removebg
@@ -0,0 +1,3 @@
1
+ from remove_ac.client import RemoveBg, RemoveAcClient, RemoveAcError
2
+
3
+ __all__ = ["RemoveBg", "RemoveAcClient", "RemoveAcError"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,138 @@
1
+ import base64
2
+ import json
3
+ import unittest
4
+
5
+ from remove_ac import (
6
+ RemoveAcClient,
7
+ RemoveAcError,
8
+ RemoveBg,
9
+ derive_sub_idempotency_key,
10
+ parse_image_dimensions,
11
+ )
12
+
13
+
14
+ class PythonSdkTest(unittest.TestCase):
15
+ def test_standard_removebg_call(self):
16
+ calls = []
17
+
18
+ def fake_transport(method, url, headers, body, timeout):
19
+ calls.append((method, url, headers, body))
20
+ return 200, {"content-type": "image/png"}, b"\x89PNG\r\n"
21
+
22
+ client = RemoveAcClient(api_key="rac_live_test_example", transport=fake_transport)
23
+ out = client.remove_background(b"abc", size="full", shadow_type="contact")
24
+ self.assertEqual(out, b"\x89PNG\r\n")
25
+ self.assertEqual(len(calls), 1)
26
+ self.assertEqual(calls[0][1], "https://remove.ac/v1.0/removebg")
27
+ self.assertEqual(calls[0][2]["X-Api-Key"], "rac_live_test_example")
28
+
29
+ def test_large_file_uses_direct_ticket(self):
30
+ calls = []
31
+ big = b"x" * (4 * 1024 * 1024 + 16)
32
+
33
+ def fake_transport(method, url, headers, body, timeout):
34
+ calls.append((method, url, headers))
35
+ if url.endswith("/v1.0/removebg/ticket"):
36
+ payload = json.dumps(
37
+ {
38
+ "data": {
39
+ "ticket": "tkt_123",
40
+ "directEndpoint": "https://gpu.example.invalid/v1.0/direct-cutout",
41
+ }
42
+ }
43
+ ).encode("utf-8")
44
+ return 200, {"content-type": "application/json"}, payload
45
+ return 200, {"content-type": "image/png"}, b"\x89PNG_DIRECT"
46
+
47
+ client = RemoveAcClient(api_key="rac_live_test_example", transport=fake_transport)
48
+ out = client.remove_background(big, size="full")
49
+ self.assertEqual(out, b"\x89PNG_DIRECT")
50
+ self.assertEqual(len(calls), 2)
51
+ self.assertTrue(calls[0][1].endswith("/v1.0/removebg/ticket"))
52
+ self.assertEqual(calls[1][1], "https://gpu.example.invalid/v1.0/direct-cutout")
53
+ self.assertEqual(calls[1][2]["Authorization"], "Bearer tkt_123")
54
+
55
+ def test_removebg_drop_in_shim_and_missing_key(self):
56
+ with self.assertRaises(RemoveAcError):
57
+ RemoveAcClient(api_key="").remove_background(b"abc")
58
+
59
+ rmbg = RemoveBg(
60
+ "rac_live_test_example",
61
+ transport=lambda m, u, h, b, t: (200, {"content-type": "image/png"}, b"OK"),
62
+ )
63
+ res = rmbg.remove_background_from_base64_img(
64
+ base64.b64encode(b"raw").decode("ascii"),
65
+ new_file_name="",
66
+ )
67
+ self.assertEqual(res, b"OK")
68
+
69
+ def test_high_mp_small_file_preflights_via_ticket(self):
70
+ # Minimal JPEG with SOF0 6000x4000 (24 MP)
71
+ jpeg_24mp = (
72
+ b"\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00"
73
+ b"\xff\xc0\x00\x11\x08\x0f\xa0\x17\x70\x03\x01\x11\x00\x02\x11\x00\x03\x11\x00"
74
+ )
75
+ calls = []
76
+
77
+ def fake_transport(method, url, headers, body, timeout):
78
+ calls.append(url)
79
+ if url.endswith("/v1.0/removebg/ticket"):
80
+ payload = json.dumps(
81
+ {
82
+ "data": {
83
+ "ticket": "tkt_py_24mp",
84
+ "directEndpoint": "https://gpu.example.invalid/v1.0/direct-cutout",
85
+ }
86
+ }
87
+ ).encode("utf-8")
88
+ return 200, {"content-type": "application/json"}, payload
89
+ return 200, {"content-type": "image/png"}, b"DIRECT_PY_PNG"
90
+
91
+ client = RemoveAcClient(api_key="rac_live_test_example", transport=fake_transport)
92
+ out = client.remove_background(jpeg_24mp, size="auto", format="png")
93
+ self.assertEqual(out, b"DIRECT_PY_PNG")
94
+ self.assertEqual(
95
+ calls,
96
+ [
97
+ "https://remove.ac/v1.0/removebg/ticket",
98
+ "https://gpu.example.invalid/v1.0/direct-cutout",
99
+ ],
100
+ )
101
+
102
+ def test_long_idempotency_key_stays_under_255_chars_on_ticket(self):
103
+ long_250 = "p" * 250
104
+ derived = derive_sub_idempotency_key(long_250, "fallback413")
105
+ self.assertLessEqual(len(derived), 255)
106
+ with self.assertRaises(RemoveAcError):
107
+ derive_sub_idempotency_key("p" * 256, "direct")
108
+ self.assertIsNone(
109
+ parse_image_dimensions(b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\xff\xff\xff\xff\x00\x00\x01\x00")
110
+ )
111
+
112
+ seen_headers = []
113
+
114
+ def fake_transport(method, url, headers, body, timeout):
115
+ seen_headers.append((url, headers))
116
+ if url.endswith("/v1.0/removebg"):
117
+ return 413, {"content-type": "application/json"}, b'{"errors":[{"code":"result_too_large"}]}'
118
+ if url.endswith("/v1.0/removebg/ticket"):
119
+ payload = json.dumps(
120
+ {
121
+ "data": {
122
+ "ticket": "tkt_long_idem",
123
+ "directEndpoint": "https://gpu.example.invalid/v1.0/direct-cutout",
124
+ }
125
+ }
126
+ ).encode("utf-8")
127
+ return 200, {"content-type": "application/json"}, payload
128
+ return 200, {"content-type": "image/png"}, b"OK_FALLBACK"
129
+
130
+ client = RemoveAcClient(api_key="rac_live_test_example", transport=fake_transport)
131
+ res = client.remove_background(b"small_input", idempotency_key=long_250)
132
+ self.assertEqual(res, b"OK_FALLBACK")
133
+ ticket_hdr = seen_headers[1][1]["Idempotency-Key"]
134
+ self.assertLessEqual(len(ticket_hdr), 255)
135
+
136
+
137
+ if __name__ == "__main__":
138
+ unittest.main()