> ## Documentation Index
> Fetch the complete documentation index at: https://docs.somark-sit.soulcode.cn/llms.txt
> Use this file to discover all available pages before exploring further.

# 文档插图本地存储

> 将解析结果中的远程图片保存到本地，避免图片 URL 过期后无法访问。

为保护您的数据隐私与安全，平台会在 **30 天**后自动删除图片资源，因此解析结果中的图片 URL 有效期为 30 天。如果需要长期保存文档，请在图片被删除前将 Markdown 和图片下载到本地。

| 场景                              | 推荐方法           |
| ------------------------------- | -------------- |
| 还没有解析文档，或可以重新解析                 | 解析时直接输出 ZIP    |
| 已有 Markdown，并使用支持 Skill 的 Agent | 使用图片持久化 Skill  |
| 已有 Markdown，希望自己处理              | 直接复用 Python 代码 |

## 方法一：解析时直接输出 ZIP

解析时在 `output_formats` 中加入 `zip`。ZIP 包中包含 Markdown 和图片，是步骤最少的方法。

当你请求 `zip` 时，必须将 `element_formats.image` 设置为 `file`：

```python theme={null}
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` 参数说明](/api-reference/endpoint/sync#output-formats)

## 方法二：使用图片持久化 Skill

如果已经有包含远程图片链接的 Markdown，可以让支持 Skill 的 Agent 自动下载图片并改写链接。

在 Agent 中输入：

```text theme={null}
使用 SoMark 图片持久化 Skill 处理 result.md，
下载远程图片，并将图片链接改为本地相对路径。
```

也可以直接运行 Skill 自带的脚本：

```bash theme={null}
python <skill-directory>/scripts/somark_localize_images.py document.md
```

不传 `-o` 时，输出目录使用原 Markdown 文件名（不含扩展名）；空格和文件名非法字符会替换为 `_`。例如，`document.md` 会生成：

```text theme={null}
document/
├── main.md
└── images/
    ├── image_001.jpg
    └── image_002.jpg
```

目录名不是固定值。你也可以使用 `-o <输出目录>` 自己指定。打开生成的 `main.md`，确认图片可以正常显示后，即可归档或迁移整个目录。

## 方法三：直接复用 Python 代码

如果不使用 Skill，可以复制下面的完整单文件示例。它支持 Markdown 图片和 HTML `<img>`，会将图片统一转换为 JPEG，并生成 `main.md` 和 `images/`。

先安装依赖：

```bash theme={null}
python -m pip install "Pillow>=9.4.0,<11.0.0"
```

将代码保存为 `localize_somark_images.py`：

<Accordion title="查看完整 Python 代码">
  ```python theme={null}
  #!/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()
  ```
</Accordion>

运行：

```bash theme={null}
python localize_somark_images.py result.md -o result
```

<Note>
  该示例面向单个 Markdown 文件。需要批量处理、域名白名单、并发下载、图片大小限制或覆盖保护时，请使用方法二。
</Note>
