feat(lib): 添加批量图片优化功能与实时进度显示

- 新增 `batchImageOptimization` 函数支持批量处理图片
- 为图片优化添加 spinner 进度指示器和实时计时显示
- 更新版本号至 0.3.2
This commit is contained in:
2026-06-18 16:17:38 +08:00
parent ffcb2801b2
commit 97b30ace84
9 changed files with 553 additions and 479 deletions

View File

@@ -1,244 +1,123 @@
"""Tests for PPT_longPic function."""
import pytest
from pathlib import Path
from unittest.mock import Mock, MagicMock, patch
from PIL import Image
from unittest.mock import MagicMock, patch
import pytest
from pptopic.lib import PPT_longPic
def _make_img_mocks(tmp_path, num_images=1):
"""Build reusable mock objects for PPT_longPic tests.
Returns (img_paths, mock_img, mock_canvas):
- img_paths: real Path objects so ``sorted()`` works
- mock_img: PIL Image mock with .size, .width, .height, .resize
- mock_canvas: PIL Image.new mock
"""
img_paths = [tmp_path / f"slide{i}.jpg" for i in range(1, num_images + 1)]
mock_img = MagicMock()
mock_img.mode = "RGB"
mock_img.size = (100, 100)
mock_img.width = 100
mock_img.height = 100
mock_img.resize.return_value = mock_img
mock_canvas = MagicMock()
return img_paths, mock_img, mock_canvas
class TestPPTLongPic:
"""Test PPT_longPic function."""
@patch('pptopic.lib.convertPPT')
def test_ppt_longpic_with_save_name(self, mock_convert_ppt, tmp_path):
"""Test PPT_longPic with saveName parameter."""
ppt_file = tmp_path / "test.pptx"
ppt_file.touch()
mock_ppt = MagicMock()
mock_convert_ppt.return_value.__enter__.return_value = mock_ppt
with patch('pptopic.lib.Path.glob') as mock_glob:
mock_img_path = MagicMock()
mock_img_path.suffix = '.jpg'
mock_glob.return_value = [mock_img_path]
with patch('pptopic.lib.Image.open') as mock_open:
mock_img = MagicMock()
mock_img.mode = 'RGB'
mock_img.width = 100
mock_img.height = 100
mock_img.resize.return_value = mock_img
mock_open.return_value.__enter__.return_value = mock_img
with patch('pptopic.lib.Image.new') as mock_new:
mock_canvas = MagicMock()
mock_new.return_value = mock_canvas
PPT_longPic(ppt_file, saveName="output.jpg", saveto=tmp_path)
mock_canvas.save.assert_called_once()
# -- width variations --------------------------------------------------
@patch('pptopic.lib.convertPPT')
def test_ppt_longpic_without_save_name(self, mock_convert_ppt, tmp_path):
"""Test PPT_longPic without saveName uses default JPG."""
ppt_file = tmp_path / "test.pptx"
ppt_file.touch()
mock_ppt = MagicMock()
mock_convert_ppt.return_value.__enter__.return_value = mock_ppt
with patch('pptopic.lib.Path.glob') as mock_glob:
mock_img_path = MagicMock()
mock_img_path.suffix = '.jpg'
mock_glob.return_value = [mock_img_path]
with patch('pptopic.lib.Image.open') as mock_open:
mock_img = MagicMock()
mock_img.mode = 'RGB'
mock_img.width = 100
mock_img.height = 100
mock_img.resize.return_value = mock_img
mock_open.return_value.__enter__.return_value = mock_img
with patch('pptopic.lib.Image.new') as mock_new:
mock_canvas = MagicMock()
mock_new.return_value = mock_canvas
PPT_longPic(ppt_file, saveto=tmp_path)
mock_canvas.save.assert_called_once()
@pytest.mark.parametrize(
"width,expected_size",
[
(None, (100, 100)), # no scaling → uses img.size
(50, (50, 50)), # pixel width → (w, h*w//W)
("50%", (50, 50)), # percentage → half
],
ids=["none", "pixel", "percentage"],
)
@patch("pptopic.lib.convertPPT")
def test_width_variations(self, mock_convert_ppt, width, expected_size, tmp_path):
"""PPT_longPic handles None / pixel / percentage width correctly."""
img_paths, mock_img, mock_canvas = _make_img_mocks(tmp_path)
def test_ppt_longpic_invalid_image_type(self, tmp_path):
"""Test PPT_longPic with invalid image type raises ValueError."""
mock_convert_ppt.return_value.__enter__.return_value = MagicMock()
with patch("pptopic.lib.Path.glob", return_value=img_paths):
with patch("pptopic.lib.Image.open") as mock_open:
mock_open.return_value.__enter__.return_value = mock_img
with patch("pptopic.lib.Image.new", return_value=mock_canvas):
PPT_longPic(tmp_path / "test.pptx", width=width, saveto=tmp_path)
mock_canvas.save.assert_called_once()
# -- saveName / saveto variations -------------------------------------
@pytest.mark.parametrize(
"save_name,saveto",
[
(None, None), # defaults: JPG, ppt parent dir
("output.jpg", None), # named, ppt parent dir
(None, "."), # default name, custom dir
],
ids=["defaults", "named", "custom_dir"],
)
@patch("pptopic.lib.convertPPT")
def test_save_params(self, mock_convert_ppt, save_name, saveto, tmp_path):
"""PPT_longPic handles saveName / saveto combinations."""
img_paths, mock_img, mock_canvas = _make_img_mocks(tmp_path)
mock_convert_ppt.return_value.__enter__.return_value = MagicMock()
with patch("pptopic.lib.Path.glob", return_value=img_paths):
with patch("pptopic.lib.Image.open") as mock_open:
mock_open.return_value.__enter__.return_value = mock_img
with patch("pptopic.lib.Image.new", return_value=mock_canvas):
PPT_longPic(
tmp_path / "test.pptx",
saveName=save_name,
saveto=saveto if saveto else tmp_path,
)
mock_canvas.save.assert_called_once()
# -- error paths -------------------------------------------------------
def test_invalid_image_type(self, tmp_path):
ppt_file = tmp_path / "test.pptx"
ppt_file.touch()
with pytest.raises(ValueError, match="Unable to save this type"):
PPT_longPic(ppt_file, saveName="output.bmp")
@patch('pptopic.lib.convertPPT')
def test_ppt_longpic_no_images_generated(self, mock_convert_ppt, tmp_path):
"""Test PPT_longPic when no images are generated raises ValueError."""
@patch("pptopic.lib.convertPPT")
def test_no_images_generated(self, mock_convert_ppt, tmp_path):
ppt_file = tmp_path / "test.pptx"
ppt_file.touch()
mock_ppt = MagicMock()
mock_convert_ppt.return_value.__enter__.return_value = mock_ppt
with patch('pptopic.lib.Path.glob', return_value=[]):
mock_convert_ppt.return_value.__enter__.return_value = MagicMock()
with patch("pptopic.lib.Path.glob", return_value=[]):
with pytest.raises(ValueError, match="No images generated"):
PPT_longPic(ppt_file)
@patch('pptopic.lib.convertPPT')
def test_ppt_longpic_with_percentage_width(self, mock_convert_ppt, tmp_path):
"""Test PPT_longPic with percentage width string."""
ppt_file = tmp_path / "test.pptx"
ppt_file.touch()
mock_ppt = MagicMock()
mock_convert_ppt.return_value.__enter__.return_value = mock_ppt
with patch('pptopic.lib.Path.glob') as mock_glob:
mock_img_path = MagicMock()
mock_img_path.suffix = '.jpg'
mock_glob.return_value = [mock_img_path]
with patch('pptopic.lib.Image.open') as mock_open:
mock_img = MagicMock()
mock_img.mode = 'RGB'
mock_img.width = 100
mock_img.height = 100
mock_img.resize.return_value = mock_img
mock_open.return_value.__enter__.return_value = mock_img
with patch('pptopic.lib.Image.new') as mock_new:
mock_canvas = MagicMock()
mock_new.return_value = mock_canvas
PPT_longPic(ppt_file, width="50%", saveto=tmp_path)
mock_new.assert_called_once()
# -- multiple images ---------------------------------------------------
@patch('pptopic.lib.convertPPT')
def test_ppt_longpic_with_pixel_width(self, mock_convert_ppt, tmp_path):
"""Test PPT_longPic with pixel width."""
ppt_file = tmp_path / "test.pptx"
ppt_file.touch()
mock_ppt = MagicMock()
mock_convert_ppt.return_value.__enter__.return_value = mock_ppt
with patch('pptopic.lib.Path.glob') as mock_glob:
mock_img_path = MagicMock()
mock_img_path.suffix = '.jpg'
mock_glob.return_value = [mock_img_path]
with patch('pptopic.lib.Image.open') as mock_open:
mock_img = MagicMock()
mock_img.mode = 'RGB'
mock_img.width = 100
mock_img.height = 100
mock_img.resize.return_value = mock_img
mock_open.return_value.__enter__.return_value = mock_img
with patch('pptopic.lib.Image.new') as mock_new:
mock_canvas = MagicMock()
mock_new.return_value = mock_canvas
PPT_longPic(ppt_file, width=50, saveto=tmp_path)
mock_new.assert_called_once()
@patch("pptopic.lib.convertPPT")
def test_multiple_images(self, mock_convert_ppt, tmp_path):
"""PPT_longPic pastes every slide into the canvas."""
img_paths, mock_img, mock_canvas = _make_img_mocks(tmp_path, num_images=3)
@patch('pptopic.lib.convertPPT')
def test_ppt_longpic_with_none_width(self, mock_convert_ppt, tmp_path):
"""Test PPT_longPic with None width (no scaling)."""
ppt_file = tmp_path / "test.pptx"
ppt_file.touch()
mock_ppt = MagicMock()
mock_convert_ppt.return_value.__enter__.return_value = mock_ppt
with patch('pptopic.lib.Path.glob') as mock_glob:
mock_img_path = MagicMock()
mock_img_path.suffix = '.jpg'
mock_glob.return_value = [mock_img_path]
with patch('pptopic.lib.Image.open') as mock_open:
mock_img = MagicMock()
mock_img.mode = 'RGB'
mock_img.width = 100
mock_img.height = 100
mock_img.resize.return_value = mock_img
mock_open.return_value.__enter__.return_value = mock_img
with patch('pptopic.lib.Image.new') as mock_new:
mock_canvas = MagicMock()
mock_new.return_value = mock_canvas
PPT_longPic(ppt_file, width=None, saveto=tmp_path)
mock_new.assert_called_once()
mock_convert_ppt.return_value.__enter__.return_value = MagicMock()
@patch('pptopic.lib.convertPPT')
def test_ppt_longpic_save_to_current_dir(self, mock_convert_ppt, tmp_path):
"""Test PPT_longPic saving to current directory."""
ppt_file = tmp_path / "test.pptx"
ppt_file.touch()
mock_ppt = MagicMock()
mock_convert_ppt.return_value.__enter__.return_value = mock_ppt
with patch('pptopic.lib.Path.glob') as mock_glob:
mock_img_path = MagicMock()
mock_img_path.suffix = '.jpg'
mock_glob.return_value = [mock_img_path]
with patch('pptopic.lib.Image.open') as mock_open:
mock_img = MagicMock()
mock_img.mode = 'RGB'
mock_img.width = 100
mock_img.height = 100
mock_img.resize.return_value = mock_img
with patch("pptopic.lib.Path.glob", return_value=img_paths):
with patch("pptopic.lib.Image.open") as mock_open:
mock_open.return_value.__enter__.return_value = mock_img
with patch('pptopic.lib.Image.new') as mock_new:
mock_canvas = MagicMock()
mock_new.return_value = mock_canvas
PPT_longPic(ppt_file, saveto=".")
mock_canvas.save.assert_called_once()
with patch("pptopic.lib.Image.new", return_value=mock_canvas):
PPT_longPic(tmp_path / "test.pptx", saveto=tmp_path)
@patch('pptopic.lib.convertPPT')
def test_ppt_longpic_multiple_images(self, mock_convert_ppt, tmp_path):
"""Test PPT_longPic with multiple images."""
ppt_file = tmp_path / "test.pptx"
ppt_file.touch()
mock_ppt = MagicMock()
mock_convert_ppt.return_value.__enter__.return_value = mock_ppt
with patch('pptopic.lib.Path.glob') as mock_glob:
mock_img_path1 = MagicMock()
mock_img_path1.suffix = '.jpg'
mock_img_path2 = MagicMock()
mock_img_path2.suffix = '.jpg'
mock_glob.return_value = [mock_img_path1, mock_img_path2]
with patch('pptopic.lib.Image.open') as mock_open:
mock_img = MagicMock()
mock_img.mode = 'RGB'
mock_img.width = 100
mock_img.height = 100
mock_img.resize.return_value = mock_img
mock_open.return_value.__enter__.return_value = mock_img
with patch('pptopic.lib.Image.new') as mock_new:
mock_canvas = MagicMock()
mock_new.return_value = mock_canvas
PPT_longPic(ppt_file, saveto=tmp_path)
mock_canvas.paste.assert_called()
assert mock_canvas.paste.call_count == 3
mock_canvas.save.assert_called_once()