Skip to content
Draft
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
15 changes: 11 additions & 4 deletions ms_agent/utils/markdown_converter.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# flake8: noqa
# isort: skip_file
# yapf: disable
import hashlib
import os
import re
import shutil
Expand Down Expand Up @@ -127,6 +128,8 @@ def _copy_and_process_images(
resources_dir = folder_path / 'resources'

for img_path, alt_text in images:
if img_path in image_mapping:
continue
# Handle relative paths
if not os.path.isabs(img_path):
# First try resources folder, then folder_path
Expand All @@ -137,11 +140,15 @@ def _copy_and_process_images(
source_path = Path(img_path)

if source_path.exists():
# Generate new filename
new_filename = f'{len(image_mapping)}_{source_path.name}'
new_path = output_images_dir / new_filename

try:
# A folder export shares this directory across documents.
# Per-document counters can overwrite another report's images.
digest = hashlib.sha256()
with source_path.open('rb') as source:
for chunk in iter(lambda: source.read(65536), b''):
digest.update(chunk)
new_filename = f'{digest.hexdigest()}{source_path.suffix}'
new_path = output_images_dir / new_filename
# Copy and possibly compress image
if source_path.suffix.lower() in ['.jpg', '.jpeg', '.png', '.gif', '.bmp']:
with Image.open(source_path) as img:
Expand Down
85 changes: 85 additions & 0 deletions tests/utils/test_markdown_image_assets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Copyright (c) ModelScope Contributors. All rights reserved.
"""Keep exported Markdown reports' local image references intact."""
from pathlib import Path

from bs4 import BeautifulSoup
from PIL import Image

from ms_agent.utils.markdown_converter import MarkdownConverter


def _image(path, color):
path.parent.mkdir(parents=True, exist_ok=True)
Image.new('RGB', (4, 4), color).save(path)


def _exported_colors(html_path):
soup = BeautifulSoup(html_path.read_text(encoding='utf-8'), 'html.parser')
paths = [html_path.parent / image['src'] for image in soup.find_all('img')]
colors = []
for path in paths:
with Image.open(path) as image:
colors.append(image.convert('RGB').getpixel((0, 0)))
return paths, colors


def test_repeated_reference_does_not_overwrite_distinct_image(tmp_path):
source = tmp_path / 'source'
_image(source / 'before' / 'chart.png', 'red')
_image(source / 'after' / 'chart.png', 'blue')
document = source / 'report.md'
document.write_text(
'# Report\n\n![Before](before/chart.png)\n\n'
'![Before again](before/chart.png)\n\n![After](after/chart.png)\n',
encoding='utf-8')
output = tmp_path / 'export' / 'report.html'
MarkdownConverter.markdown_to_html(str(document), str(output))
paths, colors = _exported_colors(output)
assert colors == [(255, 0, 0), (255, 0, 0), (0, 0, 255)]
assert paths[0] == paths[1]
assert paths[0] != paths[2]


def test_folder_export_preserves_same_named_images_across_reports(tmp_path):
source = tmp_path / 'source'
_image(source / 'before' / 'chart.png', 'red')
_image(source / 'after' / 'chart.png', 'blue')
for name, directory in [('first', 'before'), ('second', 'after')]:
(source / f'{name}.md').write_text(
f'# {name}\n\n![Chart]({directory}/chart.png)\n', encoding='utf-8')
output = tmp_path / 'export'
MarkdownConverter.markdown_to_html(str(source), str(output))
first_paths, first_colors = _exported_colors(output / 'first.html')
second_paths, second_colors = _exported_colors(output / 'second.html')
assert first_colors == [(255, 0, 0)]
assert second_colors == [(0, 0, 255)]
assert first_paths != second_paths


def test_reexport_changed_image_does_not_change_previous_report(tmp_path):
source = tmp_path / 'source'
image = source / 'chart.png'
_image(image, 'red')
document = source / 'report.md'
document.write_text('# Report\n\n![Chart](chart.png)\n', encoding='utf-8')
first = tmp_path / 'export' / 'first.html'
second = tmp_path / 'export' / 'second.html'
MarkdownConverter.markdown_to_html(str(document), str(first))
_image(image, 'blue')
MarkdownConverter.markdown_to_html(str(document), str(second))
assert _exported_colors(first)[1] == [(255, 0, 0)]
assert _exported_colors(second)[1] == [(0, 0, 255)]


def test_identical_images_share_asset_and_svg_is_preserved(tmp_path):
source = tmp_path / 'source'
_image(source / 'one.png', 'red')
_image(source / 'two.png', 'red')
svg = '<svg xmlns="http://www.w3.org/2000/svg"><title>chart</title></svg>'
(source / 'chart.svg').write_text(svg, encoding='utf-8')
output = tmp_path / 'export'
output.mkdir()
mapping = MarkdownConverter._copy_and_process_images(
[('one.png', ''), ('two.png', ''), ('chart.svg', '')], output, source)
assert mapping['one.png'] == mapping['two.png']
assert (output / mapping['chart.svg']).read_text(encoding='utf-8') == svg