2021-04-16 23:21:06 +08:00
|
|
|
# ----------------------------------------------------------------------------
|
|
|
|
# pyglet
|
|
|
|
# Copyright (c) 2006-2008 Alex Holkner
|
2022-04-30 13:56:57 +08:00
|
|
|
# Copyright (c) 2008-2022 pyglet contributors
|
2021-04-16 23:21:06 +08:00
|
|
|
# All rights reserved.
|
|
|
|
#
|
|
|
|
# Redistribution and use in source and binary forms, with or without
|
|
|
|
# modification, are permitted provided that the following conditions
|
|
|
|
# are met:
|
|
|
|
#
|
|
|
|
# * Redistributions of source code must retain the above copyright
|
|
|
|
# notice, this list of conditions and the following disclaimer.
|
|
|
|
# * Redistributions in binary form must reproduce the above copyright
|
|
|
|
# notice, this list of conditions and the following disclaimer in
|
|
|
|
# the documentation and/or other materials provided with the
|
|
|
|
# distribution.
|
|
|
|
# * Neither the name of pyglet nor the names of its
|
|
|
|
# contributors may be used to endorse or promote products
|
|
|
|
# derived from this software without specific prior written
|
|
|
|
# permission.
|
|
|
|
#
|
|
|
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
|
|
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
|
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
|
|
|
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
|
|
|
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
|
|
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
|
|
|
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
|
|
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
|
|
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
|
|
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
|
|
|
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
|
|
# POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
# ----------------------------------------------------------------------------
|
|
|
|
|
2022-06-04 11:08:30 +08:00
|
|
|
from pyglet.libs.win32.com import pIUnknown
|
2021-04-16 23:21:06 +08:00
|
|
|
from pyglet.image import *
|
|
|
|
from pyglet.image.codecs import *
|
|
|
|
from pyglet.libs.win32.constants import *
|
|
|
|
from pyglet.libs.win32.types import *
|
|
|
|
from pyglet.libs.win32 import _kernel32 as kernel32
|
|
|
|
from pyglet.libs.win32 import _ole32 as ole32
|
|
|
|
|
|
|
|
|
|
|
|
gdiplus = windll.gdiplus
|
|
|
|
|
|
|
|
REAL = c_float
|
|
|
|
|
|
|
|
PixelFormat1bppIndexed = 196865
|
|
|
|
PixelFormat4bppIndexed = 197634
|
|
|
|
PixelFormat8bppIndexed = 198659
|
|
|
|
PixelFormat16bppGrayScale = 1052676
|
|
|
|
PixelFormat16bppRGB555 = 135173
|
|
|
|
PixelFormat16bppRGB565 = 135174
|
|
|
|
PixelFormat16bppARGB1555 = 397319
|
|
|
|
PixelFormat24bppRGB = 137224
|
|
|
|
PixelFormat32bppRGB = 139273
|
|
|
|
PixelFormat32bppARGB = 2498570
|
|
|
|
PixelFormat32bppPARGB = 925707
|
|
|
|
PixelFormat48bppRGB = 1060876
|
|
|
|
PixelFormat64bppARGB = 3424269
|
|
|
|
PixelFormat64bppPARGB = 29622286
|
|
|
|
PixelFormatMax = 15
|
|
|
|
|
|
|
|
ImageLockModeRead = 1
|
|
|
|
ImageLockModeWrite = 2
|
|
|
|
ImageLockModeUserInputBuf = 4
|
|
|
|
|
|
|
|
PropertyTagFrameDelay = 0x5100
|
|
|
|
|
|
|
|
|
|
|
|
class GdiplusStartupInput(Structure):
|
|
|
|
_fields_ = [
|
|
|
|
('GdiplusVersion', c_uint32),
|
|
|
|
('DebugEventCallback', c_void_p),
|
|
|
|
('SuppressBackgroundThread', BOOL),
|
|
|
|
('SuppressExternalCodecs', BOOL)
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
class GdiplusStartupOutput(Structure):
|
|
|
|
_fields = [
|
|
|
|
('NotificationHookProc', c_void_p),
|
|
|
|
('NotificationUnhookProc', c_void_p)
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
class BitmapData(Structure):
|
|
|
|
_fields_ = [
|
|
|
|
('Width', c_uint),
|
|
|
|
('Height', c_uint),
|
|
|
|
('Stride', c_int),
|
|
|
|
('PixelFormat', c_int),
|
|
|
|
('Scan0', POINTER(c_byte)),
|
|
|
|
('Reserved', POINTER(c_uint))
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
class Rect(Structure):
|
|
|
|
_fields_ = [
|
|
|
|
('X', c_int),
|
|
|
|
('Y', c_int),
|
|
|
|
('Width', c_int),
|
|
|
|
('Height', c_int)
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
class PropertyItem(Structure):
|
|
|
|
_fields_ = [
|
|
|
|
('id', c_uint),
|
|
|
|
('length', c_ulong),
|
|
|
|
('type', c_short),
|
|
|
|
('value', c_void_p)
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
INT_PTR = POINTER(INT)
|
|
|
|
UINT_PTR = POINTER(UINT)
|
|
|
|
|
|
|
|
|
|
|
|
gdiplus.GdipBitmapLockBits.restype = c_int
|
|
|
|
gdiplus.GdipBitmapLockBits.argtypes = [c_void_p, c_void_p, UINT, c_int, c_void_p]
|
|
|
|
gdiplus.GdipBitmapUnlockBits.restype = c_int
|
|
|
|
gdiplus.GdipBitmapUnlockBits.argtypes = [c_void_p, c_void_p]
|
|
|
|
gdiplus.GdipCloneStringFormat.restype = c_int
|
|
|
|
gdiplus.GdipCloneStringFormat.argtypes = [c_void_p, c_void_p]
|
|
|
|
gdiplus.GdipCreateBitmapFromScan0.restype = c_int
|
|
|
|
gdiplus.GdipCreateBitmapFromScan0.argtypes = [c_int, c_int, c_int, c_int, POINTER(BYTE), c_void_p]
|
|
|
|
gdiplus.GdipCreateBitmapFromStream.restype = c_int
|
|
|
|
gdiplus.GdipCreateBitmapFromStream.argtypes = [c_void_p, c_void_p]
|
|
|
|
gdiplus.GdipCreateFont.restype = c_int
|
|
|
|
gdiplus.GdipCreateFont.argtypes = [c_void_p, REAL, INT, c_int, c_void_p]
|
|
|
|
gdiplus.GdipCreateFontFamilyFromName.restype = c_int
|
|
|
|
gdiplus.GdipCreateFontFamilyFromName.argtypes = [c_wchar_p, c_void_p, c_void_p]
|
|
|
|
gdiplus.GdipCreateMatrix.restype = None
|
|
|
|
gdiplus.GdipCreateMatrix.argtypes = [c_void_p]
|
|
|
|
gdiplus.GdipCreateSolidFill.restype = c_int
|
|
|
|
gdiplus.GdipCreateSolidFill.argtypes = [c_int, c_void_p] # ARGB
|
|
|
|
gdiplus.GdipDisposeImage.restype = c_int
|
|
|
|
gdiplus.GdipDisposeImage.argtypes = [c_void_p]
|
|
|
|
gdiplus.GdipDrawString.restype = c_int
|
|
|
|
gdiplus.GdipDrawString.argtypes = [c_void_p, c_wchar_p, c_int, c_void_p, c_void_p, c_void_p, c_void_p]
|
|
|
|
gdiplus.GdipGetFamilyName.restype = c_int
|
|
|
|
gdiplus.GdipGetFamilyName.argtypes = [LONG_PTR, c_wchar_p, c_wchar]
|
|
|
|
gdiplus.GdipFlush.restype = c_int
|
|
|
|
gdiplus.GdipFlush.argtypes = [c_void_p, c_int]
|
|
|
|
gdiplus.GdipGetFontCollectionFamilyCount.restype = c_int
|
|
|
|
gdiplus.GdipGetFontCollectionFamilyCount.argtypes = [c_void_p, INT_PTR]
|
|
|
|
gdiplus.GdipGetFontCollectionFamilyList.restype = c_int
|
|
|
|
gdiplus.GdipGetFontCollectionFamilyList.argtypes = [c_void_p, INT, c_void_p, INT_PTR]
|
|
|
|
gdiplus.GdipGetImageDimension.restype = c_int
|
|
|
|
gdiplus.GdipGetImageDimension.argtypes = [c_void_p, POINTER(REAL), POINTER(REAL)]
|
|
|
|
gdiplus.GdipGetImageGraphicsContext.restype = c_int
|
|
|
|
gdiplus.GdipGetImageGraphicsContext.argtypes = [c_void_p, c_void_p]
|
|
|
|
gdiplus.GdipGetImagePixelFormat.restype = c_int
|
|
|
|
gdiplus.GdipGetImagePixelFormat.argtypes = [c_void_p, c_void_p]
|
|
|
|
gdiplus.GdipGetPropertyItem.restype = c_int
|
|
|
|
gdiplus.GdipGetPropertyItem.argtypes = [c_void_p, c_uint, c_uint, c_void_p]
|
|
|
|
gdiplus.GdipGetPropertyItemSize.restype = c_int
|
|
|
|
gdiplus.GdipGetPropertyItemSize.argtypes = [c_void_p, c_uint, UINT_PTR]
|
|
|
|
gdiplus.GdipGraphicsClear.restype = c_int
|
|
|
|
gdiplus.GdipGraphicsClear.argtypes = [c_void_p, c_int] # ARGB
|
|
|
|
gdiplus.GdipImageGetFrameCount.restype = c_int
|
|
|
|
gdiplus.GdipImageGetFrameCount.argtypes = [c_void_p, c_void_p, UINT_PTR]
|
|
|
|
gdiplus.GdipImageGetFrameDimensionsCount.restype = c_int
|
|
|
|
gdiplus.GdipImageGetFrameDimensionsCount.argtypes = [c_void_p, UINT_PTR]
|
|
|
|
gdiplus.GdipImageGetFrameDimensionsList.restype = c_int
|
|
|
|
gdiplus.GdipImageGetFrameDimensionsList.argtypes = [c_void_p, c_void_p, UINT]
|
|
|
|
gdiplus.GdipImageSelectActiveFrame.restype = c_int
|
|
|
|
gdiplus.GdipImageSelectActiveFrame.argtypes = [c_void_p, c_void_p, UINT]
|
|
|
|
gdiplus.GdipMeasureString.restype = c_int
|
|
|
|
gdiplus.GdipMeasureString.argtypes = [c_void_p, c_wchar_p, c_int, c_void_p, c_void_p, c_void_p, c_void_p, INT_PTR, INT_PTR]
|
|
|
|
gdiplus.GdipNewPrivateFontCollection.restype = c_int
|
|
|
|
gdiplus.GdipNewPrivateFontCollection.argtypes = [c_void_p]
|
|
|
|
gdiplus.GdipPrivateAddMemoryFont.restype = c_int
|
|
|
|
gdiplus.GdipPrivateAddMemoryFont.argtypes = [c_void_p, c_void_p, c_int]
|
|
|
|
gdiplus.GdipSetPageUnit.restype = c_int
|
|
|
|
gdiplus.GdipSetPageUnit.argtypes = [c_void_p, c_int]
|
|
|
|
gdiplus.GdipSetStringFormatFlags.restype = c_int
|
|
|
|
gdiplus.GdipSetStringFormatFlags.argtypes = [c_void_p, c_int]
|
|
|
|
gdiplus.GdipSetTextRenderingHint.restype = c_int
|
|
|
|
gdiplus.GdipSetTextRenderingHint.argtypes = [c_void_p, c_int]
|
|
|
|
gdiplus.GdipStringFormatGetGenericTypographic.restype = c_int
|
|
|
|
gdiplus.GdipStringFormatGetGenericTypographic.argtypes = [c_void_p]
|
|
|
|
gdiplus.GdiplusShutdown.restype = None
|
|
|
|
gdiplus.GdiplusShutdown.argtypes = [POINTER(ULONG)]
|
|
|
|
gdiplus.GdiplusStartup.restype = c_int
|
|
|
|
gdiplus.GdiplusStartup.argtypes = [c_void_p, c_void_p, c_void_p]
|
|
|
|
|
|
|
|
|
|
|
|
class GDIPlusDecoder(ImageDecoder):
|
|
|
|
def get_file_extensions(self):
|
2022-04-08 23:07:41 +08:00
|
|
|
return ['.bmp', '.gif', '.jpg', '.jpeg', '.exif', '.png', '.tif', '.tiff']
|
2021-04-16 23:21:06 +08:00
|
|
|
|
|
|
|
def get_animation_file_extensions(self):
|
|
|
|
# TIFF also supported as a multi-page image; but that's not really an
|
|
|
|
# animation, is it?
|
|
|
|
return ['.gif']
|
|
|
|
|
2022-04-08 23:07:41 +08:00
|
|
|
def _load_bitmap(self, filename, file):
|
2021-04-16 23:21:06 +08:00
|
|
|
data = file.read()
|
|
|
|
|
|
|
|
# Create a HGLOBAL with image data
|
|
|
|
hglob = kernel32.GlobalAlloc(GMEM_MOVEABLE, len(data))
|
|
|
|
ptr = kernel32.GlobalLock(hglob)
|
|
|
|
memmove(ptr, data, len(data))
|
|
|
|
kernel32.GlobalUnlock(hglob)
|
|
|
|
|
|
|
|
# Create IStream for the HGLOBAL
|
|
|
|
self.stream = pIUnknown()
|
|
|
|
ole32.CreateStreamOnHGlobal(hglob, True, byref(self.stream))
|
|
|
|
|
|
|
|
# Load image from stream
|
|
|
|
bitmap = c_void_p()
|
|
|
|
status = gdiplus.GdipCreateBitmapFromStream(self.stream, byref(bitmap))
|
|
|
|
if status != 0:
|
|
|
|
self.stream.Release()
|
|
|
|
raise ImageDecodeException('GDI+ cannot load %r' % (filename or file))
|
|
|
|
|
|
|
|
return bitmap
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _get_image(bitmap):
|
|
|
|
# Get size of image (Bitmap subclasses Image)
|
|
|
|
width = REAL()
|
|
|
|
height = REAL()
|
|
|
|
gdiplus.GdipGetImageDimension(bitmap, byref(width), byref(height))
|
|
|
|
width = int(width.value)
|
|
|
|
height = int(height.value)
|
|
|
|
|
|
|
|
# Get image pixel format
|
|
|
|
pf = c_int()
|
|
|
|
gdiplus.GdipGetImagePixelFormat(bitmap, byref(pf))
|
|
|
|
pf = pf.value
|
|
|
|
|
|
|
|
# Reverse from what's documented because of Intel little-endianness.
|
|
|
|
fmt = 'BGRA'
|
|
|
|
if pf == PixelFormat24bppRGB:
|
|
|
|
fmt = 'BGR'
|
|
|
|
elif pf == PixelFormat32bppRGB:
|
|
|
|
pass
|
|
|
|
elif pf == PixelFormat32bppARGB:
|
|
|
|
pass
|
|
|
|
elif pf in (PixelFormat16bppARGB1555, PixelFormat32bppPARGB,
|
|
|
|
PixelFormat64bppARGB, PixelFormat64bppPARGB):
|
|
|
|
pf = PixelFormat32bppARGB
|
|
|
|
else:
|
|
|
|
fmt = 'BGR'
|
|
|
|
pf = PixelFormat24bppRGB
|
|
|
|
|
|
|
|
# Lock pixel data in best format
|
|
|
|
rect = Rect()
|
|
|
|
rect.X = 0
|
|
|
|
rect.Y = 0
|
|
|
|
rect.Width = width
|
|
|
|
rect.Height = height
|
|
|
|
bitmap_data = BitmapData()
|
|
|
|
gdiplus.GdipBitmapLockBits(bitmap, byref(rect), ImageLockModeRead, pf, byref(bitmap_data))
|
|
|
|
|
|
|
|
# Create buffer for RawImage
|
|
|
|
buffer = create_string_buffer(bitmap_data.Stride * height)
|
|
|
|
memmove(buffer, bitmap_data.Scan0, len(buffer))
|
|
|
|
|
|
|
|
# Unlock data
|
|
|
|
gdiplus.GdipBitmapUnlockBits(bitmap, byref(bitmap_data))
|
|
|
|
|
|
|
|
return ImageData(width, height, fmt, buffer, -bitmap_data.Stride)
|
|
|
|
|
|
|
|
def _delete_bitmap(self, bitmap):
|
|
|
|
# Release image and stream
|
|
|
|
gdiplus.GdipDisposeImage(bitmap)
|
|
|
|
self.stream.Release()
|
|
|
|
|
2022-04-08 23:07:41 +08:00
|
|
|
def decode(self, filename, file):
|
|
|
|
if not file:
|
|
|
|
file = open(filename, 'rb')
|
|
|
|
bitmap = self._load_bitmap(filename, file)
|
2021-04-16 23:21:06 +08:00
|
|
|
image = self._get_image(bitmap)
|
|
|
|
self._delete_bitmap(bitmap)
|
|
|
|
return image
|
|
|
|
|
2022-04-08 23:07:41 +08:00
|
|
|
def decode_animation(self, filename, file):
|
|
|
|
if not file:
|
|
|
|
file = open(filename, 'rb')
|
|
|
|
bitmap = self._load_bitmap(filename, file)
|
2021-04-16 23:21:06 +08:00
|
|
|
|
|
|
|
dimension_count = c_uint()
|
|
|
|
gdiplus.GdipImageGetFrameDimensionsCount(bitmap, byref(dimension_count))
|
|
|
|
if dimension_count.value < 1:
|
|
|
|
self._delete_bitmap(bitmap)
|
|
|
|
raise ImageDecodeException('Image has no frame dimensions')
|
|
|
|
|
|
|
|
# XXX Make sure this dimension is time?
|
|
|
|
dimensions = (c_void_p * dimension_count.value)()
|
|
|
|
gdiplus.GdipImageGetFrameDimensionsList(bitmap, dimensions, dimension_count.value)
|
|
|
|
|
|
|
|
frame_count = c_uint()
|
|
|
|
gdiplus.GdipImageGetFrameCount(bitmap, dimensions, byref(frame_count))
|
|
|
|
|
|
|
|
prop_id = PropertyTagFrameDelay
|
|
|
|
prop_size = c_uint()
|
|
|
|
gdiplus.GdipGetPropertyItemSize(bitmap, prop_id, byref(prop_size))
|
|
|
|
|
|
|
|
prop_buffer = c_buffer(prop_size.value)
|
|
|
|
prop_item = cast(prop_buffer, POINTER(PropertyItem)).contents
|
|
|
|
gdiplus.GdipGetPropertyItem(bitmap, prop_id, prop_size.value, prop_buffer)
|
|
|
|
|
|
|
|
n_delays = prop_item.length // sizeof(c_long)
|
|
|
|
delays = cast(prop_item.value, POINTER(c_long * n_delays)).contents
|
|
|
|
|
|
|
|
frames = []
|
|
|
|
|
|
|
|
for i in range(frame_count.value):
|
|
|
|
gdiplus.GdipImageSelectActiveFrame(bitmap, dimensions, i)
|
|
|
|
image = self._get_image(bitmap)
|
|
|
|
|
|
|
|
delay = delays[i]
|
|
|
|
if delay <= 1:
|
|
|
|
delay = 10
|
|
|
|
frames.append(AnimationFrame(image, delay/100.))
|
|
|
|
|
|
|
|
self._delete_bitmap(bitmap)
|
|
|
|
|
|
|
|
return Animation(frames)
|
|
|
|
|
|
|
|
|
|
|
|
def get_decoders():
|
|
|
|
return [GDIPlusDecoder()]
|
|
|
|
|
|
|
|
|
|
|
|
def get_encoders():
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
def init():
|
|
|
|
token = c_ulong()
|
|
|
|
startup_in = GdiplusStartupInput()
|
|
|
|
startup_in.GdiplusVersion = 1
|
|
|
|
startup_out = GdiplusStartupOutput()
|
|
|
|
gdiplus.GdiplusStartup(byref(token), byref(startup_in), byref(startup_out))
|
|
|
|
|
|
|
|
# Shutdown later?
|
|
|
|
# gdiplus.GdiplusShutdown(token)
|
|
|
|
|
|
|
|
|
|
|
|
init()
|