Skip to content

Commit 039bd0d

Browse files
author
ryan
committed
Add png output support for opengl
1 parent fb68023 commit 039bd0d

File tree

5 files changed

+126
-29
lines changed

5 files changed

+126
-29
lines changed

manim/_config/utils.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1108,8 +1108,7 @@ def dry_run(self):
11081108
self.write_to_movie is False
11091109
and self.write_all is False
11101110
and self.save_last_frame is False
1111-
and self.save_pngs is False
1112-
and self.save_as_gif is False
1111+
and not self.format
11131112
)
11141113

11151114
@dry_run.setter
@@ -1118,8 +1117,7 @@ def dry_run(self, val: bool) -> None:
11181117
self.write_to_movie = False
11191118
self.write_all = False
11201119
self.save_last_frame = False
1121-
self.save_pngs = False
1122-
self.save_as_gif = False
1120+
self.format = None
11231121
else:
11241122
raise ValueError(
11251123
"It is unclear what it means to set dry_run to "

manim/renderer/opengl_renderer.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ def init_scene(self, scene):
242242
)
243243
self.scene = scene
244244
if not hasattr(self, "window"):
245-
if config["preview"]:
245+
if config["preview"] and not config["format"] == "png":
246246
from .opengl_renderer_window import Window
247247

248248
self.window = Window(self)
@@ -392,8 +392,7 @@ def render(self, scene, frame_offset, moving_mobjects):
392392
if self.skip_animations:
393393
return
394394

395-
if config["write_to_movie"]:
396-
self.file_writer.write_frame(self)
395+
self.file_writer.write_frame(self)
397396

398397
if self.window is not None:
399398
self.window.swap_buffers()
@@ -417,7 +416,11 @@ def update_frame(self, scene):
417416
self.animation_elapsed_time = time.time() - self.animation_start_time
418417

419418
def scene_finished(self, scene):
420-
if config["save_last_frame"]:
419+
# When num_plays is 0, no images have been output, so output a single
420+
# image in this case
421+
if config["save_last_frame"] or (
422+
config["format"] == "png" and self.num_plays == 0
423+
):
421424
self.update_frame(scene)
422425
self.file_writer.save_final_image(self.get_image())
423426
self.file_writer.finish()
@@ -435,10 +438,11 @@ def get_image(self) -> Image.Image:
435438
PIL.Image
436439
The PIL image of the array.
437440
"""
441+
raw_buffer_data = self.get_raw_frame_buffer_object_data()
438442
image = Image.frombytes(
439443
"RGBA",
440444
self.get_pixel_shape(),
441-
self.context.fbo.read(self.get_pixel_shape(), components=4),
445+
raw_buffer_data,
442446
"raw",
443447
"RGBA",
444448
0,

manim/scene/scene_file_writer.py

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -284,25 +284,37 @@ def write_frame(self, frame_or_renderer):
284284
Pixel array of the frame.
285285
"""
286286
if config.renderer == "opengl":
287-
renderer = frame_or_renderer
288-
self.writing_process.stdin.write(
289-
renderer.get_raw_frame_buffer_object_data()
290-
)
287+
self.write_opengl_frame(frame_or_renderer)
291288
else:
292289
frame = frame_or_renderer
293290
if write_to_movie():
294291
self.writing_process.stdin.write(frame.tobytes())
295292
if is_png_format() and not config["dry_run"]:
296-
target_dir, extension = os.path.splitext(self.image_file_path)
297-
if config["zero_pad"]:
298-
Image.fromarray(frame).save(
299-
f"{target_dir}{str(self.frame_count).zfill(config['zero_pad'])}{extension}"
300-
)
301-
else:
302-
Image.fromarray(frame).save(
303-
f"{target_dir}{self.frame_count}{extension}"
304-
)
305-
self.frame_count += 1
293+
self.output_image_from_array(frame)
294+
295+
def write_opengl_frame(self, renderer):
296+
if write_to_movie():
297+
self.writing_process.stdin.write(
298+
renderer.get_raw_frame_buffer_object_data()
299+
)
300+
elif is_png_format() and not config["dry_run"]:
301+
target_dir, extension = os.path.splitext(self.image_file_path)
302+
self.output_image(
303+
renderer.get_image(), target_dir, extension, config["zero_pad"]
304+
)
305+
306+
def output_image_from_array(self, frame_data):
307+
target_dir, extension = os.path.splitext(self.image_file_path)
308+
self.output_image(
309+
Image.fromarray(frame_data), target_dir, extension, config["zero_pad"]
310+
)
311+
312+
def output_image(self, image: Image.Image, target_dir, ext, zero_pad: bool):
313+
if zero_pad:
314+
image.save(f"{target_dir}{str(self.frame_count).zfill(zero_pad)}{ext}")
315+
else:
316+
image.save(f"{target_dir}{self.frame_count}{ext}")
317+
self.frame_count += 1
306318

307319
def save_final_image(self, image):
308320
"""

tests/test_config.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -141,19 +141,15 @@ def test_temporary_dry_run():
141141

142142
def test_dry_run_with_png_format():
143143
"""Test that there are no exceptions when running a png without output"""
144-
with tempconfig(
145-
{"write_to_movie": False, "disable_caching": True, "format": "png"}
146-
):
144+
with tempconfig({"write_to_movie": False, "disable_caching": True}):
147145
assert config["dry_run"] is True
148146
scene = MyScene()
149147
scene.render()
150148

151149

152150
def test_dry_run_with_png_format_skipped_animations():
153151
"""Test that there are no exceptions when running a png without output and skipped animations"""
154-
with tempconfig(
155-
{"write_to_movie": False, "disable_caching": True, "format": "png"}
156-
):
152+
with tempconfig({"write_to_movie": False, "disable_caching": True}):
157153
assert config["dry_run"] is True
158154
scene = MyScene(skip_animations=True)
159155
scene.render()

tests/test_scene_rendering/test_cli_flags.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,32 @@ def test_s_flag(tmp_path, manim_cfg_file, simple_scenes_path):
121121
assert not is_empty, "running manim with -s flag did not render an image"
122122

123123

124+
@pytest.mark.slow
125+
def test_s_flag_opengl_renderer(tmp_path, manim_cfg_file, simple_scenes_path):
126+
scene_name = "SquareToCircle"
127+
command = [
128+
sys.executable,
129+
"-m",
130+
"manim",
131+
"-ql",
132+
"-s",
133+
"--renderer",
134+
"opengl",
135+
"--media_dir",
136+
str(tmp_path),
137+
simple_scenes_path,
138+
scene_name,
139+
]
140+
out, err, exit_code = capture(command)
141+
assert exit_code == 0, err
142+
143+
exists = (tmp_path / "videos").exists()
144+
assert not exists, "running manim with -s flag rendered a video"
145+
146+
is_empty = not any((tmp_path / "images" / "simple_scenes").iterdir())
147+
assert not is_empty, "running manim with -s flag did not render an image"
148+
149+
124150
@pytest.mark.slow
125151
def test_r_flag(tmp_path, manim_cfg_file, simple_scenes_path):
126152
scene_name = "SquareToCircle"
@@ -365,6 +391,33 @@ def test_images_are_created_when_png_format_set(
365391
assert expected_png_path.exists(), "png file not found at " + str(expected_png_path)
366392

367393

394+
@pytest.mark.slow
395+
def test_images_are_created_when_png_format_set_for_opengl(
396+
tmp_path, manim_cfg_file, simple_scenes_path
397+
):
398+
"""Test images are created in media directory when --format png is set for opengl"""
399+
scene_name = "SquareToCircle"
400+
command = [
401+
sys.executable,
402+
"-m",
403+
"manim",
404+
"-ql",
405+
"--renderer",
406+
"opengl",
407+
"--media_dir",
408+
str(tmp_path),
409+
"--format",
410+
"png",
411+
simple_scenes_path,
412+
scene_name,
413+
]
414+
out, err, exit_code = capture(command)
415+
assert exit_code == 0, err
416+
417+
expected_png_path = tmp_path / "images" / "simple_scenes" / "SquareToCircle0000.png"
418+
assert expected_png_path.exists(), "png file not found at " + str(expected_png_path)
419+
420+
368421
@pytest.mark.slow
369422
def test_images_are_zero_padded_when_zero_pad_set(
370423
tmp_path, manim_cfg_file, simple_scenes_path
@@ -397,6 +450,40 @@ def test_images_are_zero_padded_when_zero_pad_set(
397450
assert expected_png_path.exists(), "png file not found at " + str(expected_png_path)
398451

399452

453+
@pytest.mark.slow
454+
def test_images_are_zero_padded_when_zero_pad_set_for_opengl(
455+
tmp_path, manim_cfg_file, simple_scenes_path
456+
):
457+
"""Test images are zero padded when --format png and --zero_pad n are set with the opengl renderer"""
458+
scene_name = "SquareToCircle"
459+
command = [
460+
sys.executable,
461+
"-m",
462+
"manim",
463+
"-ql",
464+
"--renderer",
465+
"opengl",
466+
"--media_dir",
467+
str(tmp_path),
468+
"--format",
469+
"png",
470+
"--zero_pad",
471+
"3",
472+
simple_scenes_path,
473+
scene_name,
474+
]
475+
out, err, exit_code = capture(command)
476+
assert exit_code == 0, err
477+
478+
unexpected_png_path = tmp_path / "images" / "simple_scenes" / "SquareToCircle0.png"
479+
assert not unexpected_png_path.exists(), "non zero padded png file found at " + str(
480+
unexpected_png_path
481+
)
482+
483+
expected_png_path = tmp_path / "images" / "simple_scenes" / "SquareToCircle000.png"
484+
assert expected_png_path.exists(), "png file not found at " + str(expected_png_path)
485+
486+
400487
@pytest.mark.slow
401488
def test_webm_format_output(tmp_path, manim_cfg_file, simple_scenes_path):
402489
"""Test only webm created when --format webm is set"""

0 commit comments

Comments
 (0)