158 lines
7.2 KiB
Python
158 lines
7.2 KiB
Python
import os
|
|
from OpenGL import GL
|
|
from PIL import Image, ImageChops, GifImagePlugin
|
|
|
|
from framebuffer import ExportFramebuffer, ExportFramebufferNoCRT
|
|
|
|
def get_frame_image(app, art, frame, allow_crt=True, scale=1, bg_color=(0, 0, 0, 0)):
|
|
"returns a PIL image of given frame of given art, None on failure"
|
|
post_fb_class = ExportFramebuffer if allow_crt else ExportFramebufferNoCRT
|
|
# determine art's native size in pixels
|
|
w = art.charset.char_width * art.width
|
|
h = art.charset.char_height * art.height
|
|
w, h = int(w * scale), int(h * scale)
|
|
# error out if over max texture size
|
|
if w > app.max_texture_size or h > app.max_texture_size:
|
|
app.log("ERROR: Image output size (%s x %s) exceeds your hardware's max supported texture size (%s x %s)!" % (w, h, app.max_texture_size, app.max_texture_size), error=True)
|
|
app.log(' Please export at a smaller scale or chop up your artwork :[', error=True)
|
|
return None
|
|
# create CRT framebuffer
|
|
post_fb = post_fb_class(app, w, h)
|
|
# create render target and target framebuffer that will become image
|
|
export_fb = GL.glGenFramebuffers(1)
|
|
render_buffer = GL.glGenRenderbuffers(1)
|
|
GL.glBindRenderbuffer(GL.GL_RENDERBUFFER, render_buffer)
|
|
GL.glRenderbufferStorage(GL.GL_RENDERBUFFER, GL.GL_RGBA8, w, h)
|
|
GL.glBindFramebuffer(GL.GL_FRAMEBUFFER, export_fb)
|
|
GL.glFramebufferRenderbuffer(GL.GL_DRAW_FRAMEBUFFER, GL.GL_COLOR_ATTACHMENT0,
|
|
GL.GL_RENDERBUFFER, render_buffer)
|
|
GL.glViewport(0, 0, w, h)
|
|
# do render
|
|
GL.glBindFramebuffer(GL.GL_FRAMEBUFFER, post_fb.framebuffer)
|
|
# bg_color might be None
|
|
GL.glClearColor(*bg_color or (0, 0, 0, 0))
|
|
GL.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT)
|
|
# render to it
|
|
art.renderables[0].render_frame_for_export(frame)
|
|
GL.glBindFramebuffer(GL.GL_FRAMEBUFFER, export_fb)
|
|
post_fb.render()
|
|
GL.glReadBuffer(GL.GL_COLOR_ATTACHMENT0)
|
|
# read pixels from it
|
|
pixels = GL.glReadPixels(0, 0, w, h, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE,
|
|
outputType=None)
|
|
# cleanup / deinit of GL stuff
|
|
GL.glBindFramebuffer(GL.GL_FRAMEBUFFER, 0)
|
|
GL.glViewport(0, 0, app.window_width, app.window_height)
|
|
GL.glDeleteFramebuffers(1, [export_fb])
|
|
GL.glDeleteRenderbuffers(1, [render_buffer])
|
|
post_fb.destroy()
|
|
# GL pixel data as numpy array -> bytes for PIL image export
|
|
pixel_bytes = pixels.flatten().tobytes()
|
|
src_img = Image.frombytes(mode='RGBA', size=(w, h), data=pixel_bytes)
|
|
src_img = src_img.transpose(Image.FLIP_TOP_BOTTOM)
|
|
return src_img
|
|
|
|
def export_animation(app, art, out_filename, bg_color=None, loop=True):
|
|
# get list of rendered frame images
|
|
frames = []
|
|
# use arbitrary color for transparency
|
|
i_transp = art.palette.get_random_non_palette_color()
|
|
# if bg color is specified, this isn't art mode; play along
|
|
if bg_color is not None:
|
|
f_transp = bg_color
|
|
art.palette.colors[0] = (round(bg_color[0] * 255),
|
|
round(bg_color[1] * 255),
|
|
round(bg_color[2] * 255),
|
|
255)
|
|
else:
|
|
# GL wants floats
|
|
f_transp = (i_transp[0]/255, i_transp[1]/255, i_transp[2]/255, 1.)
|
|
for frame in range(art.frames):
|
|
frame_img = get_frame_image(app, art, frame, allow_crt=False,
|
|
scale=1, bg_color=f_transp)
|
|
if bg_color is not None:
|
|
# if bg color is specified, assume no transparency
|
|
frame_img = art.palette.get_palettized_image(frame_img, force_no_transparency=True)
|
|
else:
|
|
frame_img = art.palette.get_palettized_image(frame_img, i_transp[:3])
|
|
frames.append(frame_img)
|
|
# compile frames into animated GIF with proper frame delays
|
|
# technique thanks to:
|
|
# https://github.com/python-pillow/Pillow/blob/master/Scripts/gifmaker.py
|
|
output_img = open(out_filename, 'wb')
|
|
for i,img in enumerate(frames):
|
|
delay = art.frame_delays[i] * 1000
|
|
if i == 0:
|
|
data = GifImagePlugin.getheader(img)[0]
|
|
# PIL only wants to write GIF87a for some reason...
|
|
# welcome to 1989 B]
|
|
data[0] = data[0].replace(b'7', b'9')
|
|
# TODO: loop doesn't work?
|
|
if bg_color is not None:
|
|
# if bg color is specified, assume no transparency
|
|
if loop:
|
|
data += GifImagePlugin.getdata(img, duration=delay, loop=0)
|
|
else:
|
|
data += GifImagePlugin.getdata(img, duration=delay)
|
|
else:
|
|
data += GifImagePlugin.getdata(img, duration=delay,
|
|
transparency=0, loop=0)
|
|
for b in data:
|
|
output_img.write(b)
|
|
continue
|
|
delta = ImageChops.subtract_modulo(img, frames[i-1])
|
|
# Image.getbbox() rather unhelpfully returns None if no delta
|
|
dw, dh = delta.size
|
|
bbox = delta.getbbox() or (0, 0, dw, dh)
|
|
for b in GifImagePlugin.getdata(img.crop(bbox), offset=bbox[:2],
|
|
duration=delay, transparency=0,
|
|
loop=0):
|
|
output_img.write(b)
|
|
output_img.write(b';')
|
|
output_img.close()
|
|
output_format = 'Animated GIF'
|
|
#app.log('%s exported (%s)' % (out_filename, output_format))
|
|
|
|
|
|
def export_still_image(app, art, out_filename, crt=True, scale=1, bg_color=None):
|
|
# respect "disable CRT entirely" setting for slow GPUs
|
|
crt = False if app.fb.disable_crt else crt
|
|
# just write RGBA if palette has more than one color with <1 alpha
|
|
# TODO: add PNG/PNGset export option for palettized;
|
|
# for now always export 32bit
|
|
if crt or not art.palette.all_colors_opaque() or bg_color or True:
|
|
src_img = get_frame_image(app, art, art.active_frame, crt, scale, bg_color)
|
|
if not src_img:
|
|
return False
|
|
src_img.save(out_filename, 'PNG')
|
|
output_format = '32-bit w/ alpha'
|
|
else:
|
|
# else convert to current palette.
|
|
# as with aniGIF export, use arbitrary color for transparency
|
|
i_transp = art.palette.get_random_non_palette_color()
|
|
f_transp = (i_transp[0]/255, i_transp[1]/255, i_transp[2]/255, 1.)
|
|
src_img = get_frame_image(app, art, art.active_frame, False, scale, f_transp)
|
|
if not src_img:
|
|
return False
|
|
output_img = art.palette.get_palettized_image(src_img, i_transp[:3])
|
|
output_img.save(out_filename, 'PNG', transparency=0)
|
|
output_format = '8-bit palettized w/ transparency'
|
|
#app.log('%s exported (%s)' % (out_filename, output_format))
|
|
return True
|
|
|
|
|
|
def write_thumbnail(app, art_filename, thumb_filename):
|
|
"write thumbnail. assume art is not loaded, tear down everything when done."
|
|
art = app.load_art(art_filename, False)
|
|
# don't bother if art is None, must be created at runtime eg via Game Mode
|
|
if not art:
|
|
return
|
|
renderable = None
|
|
if len(art.renderables) == 0:
|
|
renderable = app.thumbnail_renderable_class(app, art)
|
|
art.renderables.append(renderable)
|
|
img = get_frame_image(app, art, 0, allow_crt=False)
|
|
if img:
|
|
img.save(thumb_filename, 'PNG')
|
|
if renderable:
|
|
renderable.destroy()
|