| 场景 | 推荐方法 |
|---|---|
| 还没有解析文档,或可以重新解析 | 解析时直接输出 ZIP |
| 已有 Markdown,并使用支持 Skill 的 Agent | 使用图片持久化 Skill |
| 已有 Markdown,希望自己处理 | 直接复用 Python 代码 |
方法一:解析时直接输出 ZIP
解析时在输出结果配置参数output_formats 中加入 zip。ZIP 包中包含 Markdown 和图片,是步骤最少的方法。
当你请求 zip 时,必须将 element_formats.image 设置为 file:
import json
import requests
with open("example.pdf", "rb") as file:
response = requests.post(
"https://somark.cn/api/v1/parse/sync",
data={
"api_key": "sk-***",
"output_formats": ["zip", "json"],
"element_formats": json.dumps({"image": "file"}),
},
files={"file": file},
)
result = response.json()
zip_url = result["data"]["result"]["outputs"]["zip"]
print(zip_url)
data.result.outputs.zip 获取下载地址,并在 30 天内下载和解压 ZIP。
查看 output_formats 参数说明
方法二:使用图片持久化 Skill
如果已经有包含远程图片链接的 Markdown,可以让支持 Skill 的 Agent 自动下载图片并改写链接。 首次使用时,任选一种方式安装 Skill:- 在 Agent 中安装
- 使用命令安装
输入以下提示词:
请从 https://github.com/SoMarkAI/skills 安装 illustrations-local-storage Skill。
npx skills add SoMarkAI/skills --skill illustrations-local-storage
使用 SoMark 文档插图本地存储 Skill 处理 result.md,
下载远程图片,并将图片链接改为本地相对路径。
python <skill-directory>/illustrations_local_storage.py document.md
-o 时,输出目录使用原 Markdown 文件名(不含扩展名);空格和文件名非法字符会替换为 _。例如,document.md 会生成:
document/
├── main.md
└── images/
├── image_001.jpg
└── image_002.jpg
-o <输出目录> 自己指定。打开生成的 main.md,确认图片可以正常显示后,即可归档或迁移整个目录。
方法三:直接复用 Python 代码
如果不使用 Skill,可以复制下面的完整单文件示例。它支持 Markdown 图片和 HTML<img>,会将图片统一转换为 JPEG,并生成 main.md 和 images/。
先安装依赖:
python -m pip install "Pillow>=9.4.0,<11.0.0"
localize_somark_images.py:
查看完整 Python 代码
查看完整 Python 代码
#!/usr/bin/env python3
import argparse
import html
import re
import time
import urllib.error
import urllib.parse
import urllib.request
from io import BytesIO
from pathlib import Path
from PIL import Image, ImageOps, UnidentifiedImageError
MARKDOWN_IMAGE_RE = re.compile(
r"!\[[^\]\r\n]*\]\(\s*(?:<(?P<angle_url>https?://[^>\r\n]+)>|"
r"(?P<plain_url>https?://[^\s)\r\n]+))",
re.IGNORECASE,
)
HTML_IMAGE_RE = re.compile(
r"<img\b[^>]*?\bsrc\s*=\s*(?P<quote>[\"'])(?P<url>https?://.*?)(?P=quote)",
re.IGNORECASE | re.DOTALL,
)
RETRYABLE_STATUS = {408, 425, 429, 500, 502, 503, 504}
def find_urls(markdown):
spans = []
for match in MARKDOWN_IMAGE_RE.finditer(markdown):
group = "angle_url" if match.group("angle_url") else "plain_url"
spans.append(
(match.start(group), match.end(group), html.unescape(match.group(group)))
)
for match in HTML_IMAGE_RE.finditer(markdown):
spans.append(
(match.start("url"), match.end("url"), html.unescape(match.group("url")))
)
spans.sort(key=lambda item: item[0])
result = []
previous_end = -1
for span in spans:
if span[0] >= previous_end:
result.append(span)
previous_end = span[1]
return result
def download(url, timeout, retries):
request = urllib.request.Request(
url,
headers={
"Accept": "image/*,*/*;q=0.8",
"User-Agent": "somark-image-example/1.0",
},
)
for attempt in range(retries + 1):
try:
with urllib.request.urlopen(request, timeout=timeout) as response:
content_type = response.headers.get_content_type().lower()
if not (
content_type.startswith("image/")
or content_type == "application/octet-stream"
):
raise ValueError(
f"URL returned non-image content: {content_type}"
)
return response.read()
except urllib.error.HTTPError as error:
if error.code not in RETRYABLE_STATUS or attempt == retries:
raise
except (urllib.error.URLError, TimeoutError, OSError):
if attempt == retries:
raise
time.sleep(min(2**attempt, 8))
raise RuntimeError("unreachable")
def to_jpeg(data, destination):
try:
with Image.open(BytesIO(data)) as opened:
opened.seek(0)
oriented = ImageOps.exif_transpose(opened)
try:
oriented.load()
has_alpha = oriented.mode in {"RGBA", "LA"} or (
oriented.mode == "P" and "transparency" in oriented.info
)
if has_alpha:
rgba = oriented.convert("RGBA")
converted = Image.new("RGB", rgba.size, (255, 255, 255))
converted.paste(rgba, mask=rgba.getchannel("A"))
rgba.close()
else:
converted = oriented.convert("RGB")
try:
converted.save(
destination,
"JPEG",
quality=95,
subsampling=0,
optimize=True,
)
finally:
converted.close()
finally:
if oriented is not opened:
oriented.close()
except (UnidentifiedImageError, OSError, ValueError) as error:
raise RuntimeError(f"Invalid image: {error}") from error
def rewrite(markdown, spans, names):
chunks = []
cursor = 0
for start, end, url in spans:
chunks.append(markdown[cursor:start])
chunks.append(
urllib.parse.quote("./images/" + names[url], safe="/-._~")
)
cursor = end
chunks.append(markdown[cursor:])
return "".join(chunks)
def localize(input_path, output_dir, timeout, retries):
input_path = input_path.resolve()
output_dir = output_dir.resolve()
if (
input_path.suffix.lower() not in {".md", ".markdown"}
or not input_path.is_file()
):
raise ValueError("Input must be an existing .md or .markdown file")
if output_dir.exists():
raise FileExistsError(f"Output directory already exists: {output_dir}")
markdown = input_path.read_text(encoding="utf-8-sig")
spans = find_urls(markdown)
urls = list(dict.fromkeys(span[2] for span in spans))
names = {
url: f"image_{index:03d}.jpg"
for index, url in enumerate(urls, start=1)
}
downloaded = {url: download(url, timeout, retries) for url in urls}
output_dir.mkdir(parents=True)
image_dir = output_dir / "images"
image_dir.mkdir()
try:
for url in urls:
to_jpeg(downloaded[url], image_dir / names[url])
with (output_dir / "main.md").open(
"w", encoding="utf-8", newline=""
) as output:
output.write(rewrite(markdown, spans, names))
except Exception:
for path in image_dir.glob("image_*.jpg"):
path.unlink()
raise
def main():
parser = argparse.ArgumentParser()
parser.add_argument("input", type=Path)
parser.add_argument("-o", "--output-dir", type=Path)
parser.add_argument("--timeout", type=float, default=60.0)
parser.add_argument("--retries", type=int, default=3)
args = parser.parse_args()
output_dir = args.output_dir or args.input.with_suffix("")
localize(args.input, output_dir, args.timeout, args.retries)
print(f"Completed: {output_dir / 'main.md'}")
if __name__ == "__main__":
main()
python localize_somark_images.py result.md -o result
该示例面向单个 Markdown 文件。需要批量处理、域名白名单、并发下载、图片大小限制或覆盖保护时,请使用方法二。

