> ## 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.

# Store document images locally

> Save remote images from parsing results locally so they remain available after their URLs expire.

To protect the privacy and security of your data, the platform automatically deletes image resources after **30 days**. Image URLs in parsing results therefore remain valid for 30 days. For long-term storage, download the Markdown and images before the images are deleted.

| Scenario                                                        | Recommended method              |
| --------------------------------------------------------------- | ------------------------------- |
| You have not parsed the document yet, or you can parse it again | Request a ZIP during parsing    |
| You already have Markdown and use a Skill-capable agent         | Use the image persistence Skill |
| You already have Markdown and want to process it yourself       | Reuse Python code directly      |

## Method 1: Request a ZIP during parsing

Add `zip` to `output_formats` when you parse the document. The ZIP contains the Markdown and images, making this the shortest workflow.

When you request `zip`, you must set `element_formats.image` to `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)
```

Read the download URL from `data.result.outputs.zip`, then download and extract the ZIP within 30 days.

[View the `output_formats` parameter reference](/en/api-reference/endpoint/sync#output-formats)

## Method 2: Use the image persistence Skill

If you already have Markdown with remote image URLs, a Skill-capable agent can download the images and rewrite the links for you.

Enter this prompt in the agent:

```text theme={null}
Use the SoMark image persistence Skill to process result.md.
Download the remote images and replace their URLs with local relative paths.
```

You can also run the script included with the Skill:

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

Without `-o`, the output directory uses the source Markdown filename without its extension. Spaces and invalid filename characters are replaced with `_`. For example, `document.md` creates:

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

The directory name is not fixed. You can also set it explicitly with `-o <output-directory>`. Open the generated `main.md` and confirm that its images render before you archive or move the directory.

## Method 3: Reuse Python code directly

If you do not use the Skill, copy the complete single-file example below. It supports Markdown images and HTML `<img>` elements, converts images to JPEG, and creates `main.md` and `images/`.

Install the dependency:

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

Save the code as `localize_somark_images.py`:

<Accordion title="View the complete Python code">
  ```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>

Run it:

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

<Note>
  This example handles one Markdown file. Use Method 2 for batch processing, domain allowlists, concurrent downloads, image size limits, or overwrite protection.
</Note>
