* feat(ui): 新增 APNG/GIF 动图解码与动画帧时钟 AnimatedImage 按 APNG 规范解析 acTL/fcTL/fdAT 并校验序号、CRC 与帧边界,逐帧重建 PNG 交给 WIC 解码后按 dispose_op/blend_op 合成;GIF 走 WIC 多帧解码。延时规则与浏览器一致,超限或损坏的动画退回静态首帧。 DeviceResources::GetImageFrame 按动画时钟取帧并汇报下一次换帧时刻,动图帧按 64 MB 预算缓存;Scene 与 Window 据此按需重绘,可整体关闭动画。DrawNineSlice 按 CSS border-image 语义区分源切片与目标边宽、支持 fill,并用夹取画刷消除接缝渗色。 * feat(server): 新增 canvas 基础皮肤并要求 v2 皮肤基于它 canvas 是全贴图基底:透明面板、无描边、无圆角、无阴影,外观来自九宫格面板图与挂件。它只能作为外部皮肤的 base,不进内置皮肤列表;v1 皮肤用它直接报错。 schema_version = 2 而 base 不是 canvas 的皮肤按 v1 加载、忽略全部 v2 字段,并在扫描结果里记一条问题。v2 字段补齐校验:图片限 .png/.apng/.gif/.webp,fill 必须是布尔,偏移与 z_index 有范围,挂件最多 32 层。 * feat(server): D2D 候选窗补齐 v2 九宫格、挂件分层与动图 canvas 皮肤在 D2D 候选窗里按 CSS border-image 语义绘制九宫格面板:切片以源图像素计、边宽以 DIP 计、fill = false 不画中心;有面板图时卡片的底色、描边、圆角与阴影让给贴图。content_insets_dip 从面板外缘量起且不小于边框宽度,line_gap_dip 只作用于预编辑行与候选之间。 挂件拆成卡片前后两层:z_index < 1 在面板之下,其余在面板之上,同层按 z_index 与声明顺序稳定排序;图片在框内等比居中。动图按场景给出的下一次换帧时刻排一次性定时器续帧,只在候选窗显示时运行,隐藏、换皮肤时停;系统关闭动画效果时只显示首帧,v1 皮肤保持静态。 设置页原生预览认得 canvas 基底并绘制面板图。 * feat(examples): 新增基于 canvas 的 v2 全贴图皮肤样例 canvas-demo 用 2 倍九宫格面板、面板下层的叶子挂件和面板上层循环播放的 APNG 星光演示 v2 皮肤,素材均为程序生成的几何图形,随目录以 MIT 授权。样例测试同步校验 schema 2 与 canvas 互为前提、v2 图片扩展名与数值范围。 * docs(examples): 说明 canvas 基底、v2 九宫格与挂件分层及动图规则 schema 说明去掉「仅 WebView2」,写明 v2 必须以 canvas 为 base、canvas 只能当 base;九宫格切片按源图像素、边宽按 DIP,content_insets_dip 不小于边宽,line_gap_dip 只管预编辑行与候选之间;挂件以 z_index 1 为界分在面板上下两层;动图格式、帧时长、上限与系统减少动画时的行为。server/docs/ui-backends.md 与 skins/README.md 同步更新。 * docs(server): 候选窗与皮肤目录的注释不再引用已删除的 WebView2 页面
915 lines
32 KiB
C++
915 lines
32 KiB
C++
// AnimatedImage: decodes still images, APNG and animated GIF into composited BGRA frames.
|
|
#include "msimeui/AnimatedImage.h"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cstring>
|
|
#include <shlwapi.h>
|
|
#include <utility>
|
|
#include <windows.h>
|
|
#include <wrl/client.h>
|
|
|
|
namespace msimeui
|
|
{
|
|
using Microsoft::WRL::ComPtr;
|
|
|
|
namespace
|
|
{
|
|
constexpr uint8_t kPngSignature[8] = {0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A};
|
|
|
|
constexpr uint32_t Tag(char a, char b, char c, char d)
|
|
{
|
|
return (static_cast<uint32_t>(static_cast<uint8_t>(a)) << 24) |
|
|
(static_cast<uint32_t>(static_cast<uint8_t>(b)) << 16) |
|
|
(static_cast<uint32_t>(static_cast<uint8_t>(c)) << 8) | static_cast<uint32_t>(static_cast<uint8_t>(d));
|
|
}
|
|
|
|
constexpr uint32_t kIhdr = Tag('I', 'H', 'D', 'R');
|
|
constexpr uint32_t kIdat = Tag('I', 'D', 'A', 'T');
|
|
constexpr uint32_t kIend = Tag('I', 'E', 'N', 'D');
|
|
constexpr uint32_t kActl = Tag('a', 'c', 'T', 'L');
|
|
constexpr uint32_t kFctl = Tag('f', 'c', 'T', 'L');
|
|
constexpr uint32_t kFdat = Tag('f', 'd', 'A', 'T');
|
|
|
|
// APNG dispose_op / blend_op, also used for GIF disposal.
|
|
enum class Dispose : uint8_t
|
|
{
|
|
None = 0,
|
|
Background = 1,
|
|
Previous = 2,
|
|
};
|
|
|
|
uint32_t ReadBE32(const uint8_t *p)
|
|
{
|
|
return (static_cast<uint32_t>(p[0]) << 24) | (static_cast<uint32_t>(p[1]) << 16) |
|
|
(static_cast<uint32_t>(p[2]) << 8) | static_cast<uint32_t>(p[3]);
|
|
}
|
|
|
|
uint16_t ReadBE16(const uint8_t *p)
|
|
{
|
|
return static_cast<uint16_t>((p[0] << 8) | p[1]);
|
|
}
|
|
|
|
void AppendBE32(std::vector<uint8_t> &out, uint32_t value)
|
|
{
|
|
out.push_back(static_cast<uint8_t>(value >> 24));
|
|
out.push_back(static_cast<uint8_t>(value >> 16));
|
|
out.push_back(static_cast<uint8_t>(value >> 8));
|
|
out.push_back(static_cast<uint8_t>(value));
|
|
}
|
|
|
|
const std::array<uint32_t, 256> &CrcTable()
|
|
{
|
|
static const std::array<uint32_t, 256> table = []() {
|
|
std::array<uint32_t, 256> result{};
|
|
for (uint32_t n = 0; n < 256; ++n)
|
|
{
|
|
uint32_t c = n;
|
|
for (int k = 0; k < 8; ++k)
|
|
{
|
|
c = (c & 1U) ? 0xEDB88320U ^ (c >> 1) : c >> 1;
|
|
}
|
|
result[n] = c;
|
|
}
|
|
return result;
|
|
}();
|
|
return table;
|
|
}
|
|
|
|
uint32_t UpdateCrc(uint32_t crc, const uint8_t *data, size_t size)
|
|
{
|
|
const auto &table = CrcTable();
|
|
for (size_t i = 0; i < size; ++i)
|
|
{
|
|
crc = table[(crc ^ data[i]) & 0xFFU] ^ (crc >> 8);
|
|
}
|
|
return crc;
|
|
}
|
|
|
|
// CRC of a chunk covers its type and data.
|
|
uint32_t ChunkCrc(uint32_t type, const uint8_t *data, size_t size)
|
|
{
|
|
const uint8_t typeBytes[4] = {static_cast<uint8_t>(type >> 24), static_cast<uint8_t>(type >> 16),
|
|
static_cast<uint8_t>(type >> 8), static_cast<uint8_t>(type)};
|
|
uint32_t crc = UpdateCrc(0xFFFFFFFFU, typeBytes, 4);
|
|
crc = UpdateCrc(crc, data, size);
|
|
return crc ^ 0xFFFFFFFFU;
|
|
}
|
|
|
|
void AppendChunk(std::vector<uint8_t> &out, uint32_t type, const uint8_t *data, size_t size)
|
|
{
|
|
AppendBE32(out, static_cast<uint32_t>(size));
|
|
AppendBE32(out, type);
|
|
if (size > 0)
|
|
{
|
|
out.insert(out.end(), data, data + size);
|
|
}
|
|
AppendBE32(out, ChunkCrc(type, data, size));
|
|
}
|
|
|
|
struct PngChunk
|
|
{
|
|
uint32_t type = 0;
|
|
const uint8_t *data = nullptr;
|
|
uint32_t length = 0;
|
|
};
|
|
|
|
bool HasPngSignature(const uint8_t *data, size_t size)
|
|
{
|
|
return size >= sizeof(kPngSignature) && std::memcmp(data, kPngSignature, sizeof(kPngSignature)) == 0;
|
|
}
|
|
|
|
// Splits a PNG stream into chunks up to IEND. With `verifyCrc`, any chunk whose CRC does not match
|
|
// fails the whole stream; a truncated stream always fails.
|
|
bool ReadPngChunks(const uint8_t *data, size_t size, bool verifyCrc, std::vector<PngChunk> &chunks)
|
|
{
|
|
if (!HasPngSignature(data, size))
|
|
{
|
|
return false;
|
|
}
|
|
size_t pos = sizeof(kPngSignature);
|
|
while (pos + 12 <= size)
|
|
{
|
|
const uint32_t length = ReadBE32(data + pos);
|
|
if (length > 0x7FFFFFFFU || length > size - pos - 12)
|
|
{
|
|
return false;
|
|
}
|
|
PngChunk chunk;
|
|
chunk.type = ReadBE32(data + pos + 4);
|
|
chunk.data = data + pos + 8;
|
|
chunk.length = length;
|
|
if (verifyCrc && ChunkCrc(chunk.type, chunk.data, length) != ReadBE32(data + pos + 8 + length))
|
|
{
|
|
return false;
|
|
}
|
|
chunks.push_back(chunk);
|
|
pos += 12 + static_cast<size_t>(length);
|
|
if (chunk.type == kIend)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
ComPtr<IWICBitmapDecoder> CreateDecoder(IWICImagingFactory *factory, const uint8_t *data, size_t size)
|
|
{
|
|
ComPtr<IWICBitmapDecoder> decoder;
|
|
if (!factory || !data || size == 0 || size > 0xFFFFFFFFULL)
|
|
{
|
|
return decoder;
|
|
}
|
|
ComPtr<IStream> stream;
|
|
stream.Attach(SHCreateMemStream(data, static_cast<UINT>(size)));
|
|
if (!stream || FAILED(factory->CreateDecoderFromStream(stream.Get(), nullptr, WICDecodeMetadataCacheOnLoad,
|
|
decoder.GetAddressOf())))
|
|
{
|
|
decoder.Reset();
|
|
}
|
|
return decoder;
|
|
}
|
|
|
|
// Converts any WIC source to premultiplied BGRA.
|
|
bool CopyPremultipliedBgra(IWICImagingFactory *factory, IWICBitmapSource *source, uint32_t maxDimension,
|
|
uint32_t &width, uint32_t &height, std::vector<uint8_t> &pixels)
|
|
{
|
|
UINT w = 0;
|
|
UINT h = 0;
|
|
ComPtr<IWICFormatConverter> converter;
|
|
if (!source || FAILED(source->GetSize(&w, &h)) || w == 0 || h == 0 || w > maxDimension || h > maxDimension ||
|
|
FAILED(factory->CreateFormatConverter(converter.GetAddressOf())) ||
|
|
FAILED(converter->Initialize(source, GUID_WICPixelFormat32bppPBGRA, WICBitmapDitherTypeNone, nullptr, 0.0,
|
|
WICBitmapPaletteTypeMedianCut)))
|
|
{
|
|
return false;
|
|
}
|
|
const size_t stride = static_cast<size_t>(w) * 4;
|
|
pixels.assign(stride * h, 0);
|
|
if (FAILED(
|
|
converter->CopyPixels(nullptr, static_cast<UINT>(stride), static_cast<UINT>(pixels.size()), pixels.data())))
|
|
{
|
|
return false;
|
|
}
|
|
width = w;
|
|
height = h;
|
|
return true;
|
|
}
|
|
|
|
// A frame rectangle on the canvas, already known to lie inside it.
|
|
struct Region
|
|
{
|
|
uint32_t x = 0;
|
|
uint32_t y = 0;
|
|
uint32_t width = 0;
|
|
uint32_t height = 0;
|
|
};
|
|
|
|
class Canvas
|
|
{
|
|
public:
|
|
Canvas(uint32_t width, uint32_t height)
|
|
: width_(width), height_(height), pixels_(static_cast<size_t>(width) * height * 4, 0)
|
|
{
|
|
}
|
|
|
|
// Draws `frame` (frameWidth x frameHeight) with its top-left at (left, top). Parts outside the
|
|
// canvas are dropped. `over` blends premultiplied source over destination; otherwise the source
|
|
// replaces the destination, transparent pixels included.
|
|
void Draw(const std::vector<uint8_t> &frame, uint32_t frameWidth, uint32_t frameHeight, int64_t left, int64_t top,
|
|
bool over)
|
|
{
|
|
const Region region = Clip(left, top, frameWidth, frameHeight);
|
|
for (uint32_t row = 0; row < region.height; ++row)
|
|
{
|
|
const size_t srcY = static_cast<size_t>(region.y - top + row);
|
|
const size_t srcX = static_cast<size_t>(region.x - left);
|
|
const uint8_t *src = frame.data() + (srcY * frameWidth + srcX) * 4;
|
|
uint8_t *dst = pixels_.data() + ((static_cast<size_t>(region.y) + row) * width_ + region.x) * 4;
|
|
if (!over)
|
|
{
|
|
std::memcpy(dst, src, static_cast<size_t>(region.width) * 4);
|
|
continue;
|
|
}
|
|
for (uint32_t col = 0; col < region.width; ++col, src += 4, dst += 4)
|
|
{
|
|
const uint32_t alpha = src[3];
|
|
if (alpha == 255)
|
|
{
|
|
std::memcpy(dst, src, 4);
|
|
}
|
|
else if (alpha != 0)
|
|
{
|
|
for (int c = 0; c < 4; ++c)
|
|
{
|
|
dst[c] =
|
|
static_cast<uint8_t>((std::min)(255U, src[c] + (dst[c] * (255U - alpha) + 127U) / 255U));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Region Clip(int64_t left, int64_t top, uint32_t width, uint32_t height) const
|
|
{
|
|
const int64_t x0 = (std::max)(left, int64_t{0});
|
|
const int64_t y0 = (std::max)(top, int64_t{0});
|
|
const int64_t x1 = (std::min)(left + static_cast<int64_t>(width), static_cast<int64_t>(width_));
|
|
const int64_t y1 = (std::min)(top + static_cast<int64_t>(height), static_cast<int64_t>(height_));
|
|
if (x1 <= x0 || y1 <= y0)
|
|
{
|
|
return {};
|
|
}
|
|
return {static_cast<uint32_t>(x0), static_cast<uint32_t>(y0), static_cast<uint32_t>(x1 - x0),
|
|
static_cast<uint32_t>(y1 - y0)};
|
|
}
|
|
|
|
void Clear(const Region ®ion)
|
|
{
|
|
for (uint32_t row = 0; row < region.height; ++row)
|
|
{
|
|
std::memset(pixels_.data() + ((static_cast<size_t>(region.y) + row) * width_ + region.x) * 4, 0,
|
|
static_cast<size_t>(region.width) * 4);
|
|
}
|
|
}
|
|
|
|
std::vector<uint8_t> Save(const Region ®ion) const
|
|
{
|
|
std::vector<uint8_t> saved(static_cast<size_t>(region.width) * region.height * 4);
|
|
for (uint32_t row = 0; row < region.height; ++row)
|
|
{
|
|
std::memcpy(saved.data() + static_cast<size_t>(row) * region.width * 4,
|
|
pixels_.data() + ((static_cast<size_t>(region.y) + row) * width_ + region.x) * 4,
|
|
static_cast<size_t>(region.width) * 4);
|
|
}
|
|
return saved;
|
|
}
|
|
|
|
void Restore(const Region ®ion, const std::vector<uint8_t> &saved)
|
|
{
|
|
for (uint32_t row = 0; row < region.height; ++row)
|
|
{
|
|
std::memcpy(pixels_.data() + ((static_cast<size_t>(region.y) + row) * width_ + region.x) * 4,
|
|
saved.data() + static_cast<size_t>(row) * region.width * 4,
|
|
static_cast<size_t>(region.width) * 4);
|
|
}
|
|
}
|
|
|
|
const std::vector<uint8_t> &Pixels() const
|
|
{
|
|
return pixels_;
|
|
}
|
|
|
|
private:
|
|
uint32_t width_;
|
|
uint32_t height_;
|
|
std::vector<uint8_t> pixels_;
|
|
};
|
|
|
|
// One composited frame of the source animation: where it lands, how it blends, what happens after.
|
|
struct FrameStep
|
|
{
|
|
std::vector<uint8_t> pixels;
|
|
uint32_t width = 0;
|
|
uint32_t height = 0;
|
|
int64_t left = 0;
|
|
int64_t top = 0;
|
|
bool over = false;
|
|
Dispose dispose = Dispose::None;
|
|
};
|
|
|
|
// Applies one frame to the canvas and returns what is on screen while it shows. Dispose runs
|
|
// afterwards, so it shapes the canvas the next frame starts from.
|
|
std::vector<uint8_t> ComposeStep(Canvas &canvas, const FrameStep &step, bool first)
|
|
{
|
|
Dispose dispose = step.dispose;
|
|
// Nothing precedes the first frame, so "restore previous" means "clear" there.
|
|
if (first && dispose == Dispose::Previous)
|
|
{
|
|
dispose = Dispose::Background;
|
|
}
|
|
const Region region = canvas.Clip(step.left, step.top, step.width, step.height);
|
|
std::vector<uint8_t> saved;
|
|
if (dispose == Dispose::Previous)
|
|
{
|
|
saved = canvas.Save(region);
|
|
}
|
|
canvas.Draw(step.pixels, step.width, step.height, step.left, step.top, step.over);
|
|
std::vector<uint8_t> shown = canvas.Pixels();
|
|
if (dispose == Dispose::Background)
|
|
{
|
|
canvas.Clear(region);
|
|
}
|
|
else if (dispose == Dispose::Previous)
|
|
{
|
|
canvas.Restore(region, saved);
|
|
}
|
|
return shown;
|
|
}
|
|
|
|
bool ReadMetadataUInt(IWICMetadataQueryReader *reader, const wchar_t *name, uint32_t &value)
|
|
{
|
|
if (!reader)
|
|
{
|
|
return false;
|
|
}
|
|
PROPVARIANT variant;
|
|
PropVariantInit(&variant);
|
|
bool ok = false;
|
|
if (SUCCEEDED(reader->GetMetadataByName(name, &variant)))
|
|
{
|
|
switch (variant.vt)
|
|
{
|
|
case VT_UI1:
|
|
value = variant.bVal;
|
|
ok = true;
|
|
break;
|
|
case VT_UI2:
|
|
value = variant.uiVal;
|
|
ok = true;
|
|
break;
|
|
case VT_UI4:
|
|
value = variant.ulVal;
|
|
ok = true;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
PropVariantClear(&variant);
|
|
return ok;
|
|
}
|
|
|
|
bool ReadMetadataBytes(IWICMetadataQueryReader *reader, const wchar_t *name, std::vector<uint8_t> &bytes)
|
|
{
|
|
if (!reader)
|
|
{
|
|
return false;
|
|
}
|
|
PROPVARIANT variant;
|
|
PropVariantInit(&variant);
|
|
bool ok = false;
|
|
if (SUCCEEDED(reader->GetMetadataByName(name, &variant)) && variant.vt == (VT_UI1 | VT_VECTOR))
|
|
{
|
|
bytes.assign(variant.caub.pElems, variant.caub.pElems + variant.caub.cElems);
|
|
ok = true;
|
|
}
|
|
PropVariantClear(&variant);
|
|
return ok;
|
|
}
|
|
|
|
bool ReadFileBytes(const std::wstring &filePath, uint64_t maxBytes, std::vector<uint8_t> &bytes)
|
|
{
|
|
HANDLE file = CreateFileW(filePath.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, nullptr,
|
|
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
|
|
if (file == INVALID_HANDLE_VALUE)
|
|
{
|
|
return false;
|
|
}
|
|
LARGE_INTEGER size{};
|
|
bool ok = GetFileSizeEx(file, &size) && size.QuadPart > 0 && static_cast<uint64_t>(size.QuadPart) <= maxBytes;
|
|
if (ok)
|
|
{
|
|
bytes.resize(static_cast<size_t>(size.QuadPart));
|
|
size_t done = 0;
|
|
while (ok && done < bytes.size())
|
|
{
|
|
DWORD read = 0;
|
|
const DWORD chunk = static_cast<DWORD>((std::min)(bytes.size() - done, size_t{1} << 30));
|
|
ok = ReadFile(file, bytes.data() + done, chunk, &read, nullptr) && read > 0;
|
|
done += read;
|
|
}
|
|
}
|
|
CloseHandle(file);
|
|
return ok;
|
|
}
|
|
|
|
bool FitsLimits(uint32_t width, uint32_t height, size_t frames, const AnimatedImage::Limits &limits)
|
|
{
|
|
if (width == 0 || height == 0 || width > limits.maxDimension || height > limits.maxDimension ||
|
|
frames > limits.maxFrames)
|
|
{
|
|
return false;
|
|
}
|
|
return static_cast<uint64_t>(width) * height * 4 * frames <= limits.maxDecodedBytes;
|
|
}
|
|
} // namespace
|
|
|
|
struct AnimatedImageDecoder
|
|
{
|
|
// APNG per https://wiki.mozilla.org/APNG_Specification. Anything that breaks the rules there
|
|
// (sequence numbers, CRCs, frame bounds, frame count, frames without data) returns false and the
|
|
// caller shows the still default image instead.
|
|
static bool DecodeApng(IWICImagingFactory *factory, const uint8_t *data, size_t size,
|
|
const AnimatedImage::Limits &limits, AnimatedImage &out)
|
|
{
|
|
std::vector<PngChunk> chunks;
|
|
if (!ReadPngChunks(data, size, true, chunks) || chunks.empty() || chunks.front().type != kIhdr ||
|
|
chunks.front().length != 13)
|
|
{
|
|
return false;
|
|
}
|
|
const uint8_t *ihdr = chunks.front().data;
|
|
const uint32_t width = ReadBE32(ihdr);
|
|
const uint32_t height = ReadBE32(ihdr + 4);
|
|
|
|
struct Frame
|
|
{
|
|
uint32_t width = 0;
|
|
uint32_t height = 0;
|
|
uint32_t x = 0;
|
|
uint32_t y = 0;
|
|
uint16_t delayNum = 0;
|
|
uint16_t delayDen = 0;
|
|
uint8_t dispose = 0;
|
|
uint8_t blend = 0;
|
|
std::vector<std::pair<const uint8_t *, uint32_t>> data;
|
|
};
|
|
std::vector<Frame> frames;
|
|
std::vector<PngChunk> header; // chunks between IHDR and the first IDAT, copied into every frame
|
|
bool haveActl = false;
|
|
uint32_t declaredFrames = 0;
|
|
uint32_t playCount = 0;
|
|
uint32_t sequence = 0;
|
|
bool idatSeen = false;
|
|
bool idatDone = false;
|
|
// The default image (IDAT) is the first frame only when an fcTL precedes it.
|
|
bool defaultIsFrame = false;
|
|
uint32_t previousType = kIhdr;
|
|
for (size_t i = 1; i < chunks.size(); ++i)
|
|
{
|
|
const PngChunk &chunk = chunks[i];
|
|
if (idatSeen && previousType == kIdat && chunk.type != kIdat)
|
|
{
|
|
idatDone = true;
|
|
}
|
|
previousType = chunk.type;
|
|
switch (chunk.type)
|
|
{
|
|
case kActl:
|
|
if (haveActl || idatSeen || chunk.length != 8)
|
|
{
|
|
return false;
|
|
}
|
|
haveActl = true;
|
|
declaredFrames = ReadBE32(chunk.data);
|
|
playCount = ReadBE32(chunk.data + 4);
|
|
break;
|
|
case kFctl: {
|
|
if (chunk.length != 26 || ReadBE32(chunk.data) != sequence++)
|
|
{
|
|
return false;
|
|
}
|
|
// A frame must have its data before the next frame begins.
|
|
if (!frames.empty() && frames.back().data.empty())
|
|
{
|
|
return false;
|
|
}
|
|
Frame frame;
|
|
frame.width = ReadBE32(chunk.data + 4);
|
|
frame.height = ReadBE32(chunk.data + 8);
|
|
frame.x = ReadBE32(chunk.data + 12);
|
|
frame.y = ReadBE32(chunk.data + 16);
|
|
frame.delayNum = ReadBE16(chunk.data + 20);
|
|
frame.delayDen = ReadBE16(chunk.data + 22);
|
|
frame.dispose = chunk.data[24];
|
|
frame.blend = chunk.data[25];
|
|
if (frame.width == 0 || frame.height == 0 || frame.x > width || frame.y > height ||
|
|
frame.width > width - frame.x || frame.height > height - frame.y || frame.dispose > 2 ||
|
|
frame.blend > 1)
|
|
{
|
|
return false;
|
|
}
|
|
if (!idatSeen)
|
|
{
|
|
// The fcTL before IDAT describes the default image, which fills the canvas.
|
|
if (!frames.empty() || frame.x != 0 || frame.y != 0 || frame.width != width ||
|
|
frame.height != height)
|
|
{
|
|
return false;
|
|
}
|
|
defaultIsFrame = true;
|
|
}
|
|
frames.push_back(std::move(frame));
|
|
break;
|
|
}
|
|
case kIdat:
|
|
if (idatDone)
|
|
{
|
|
return false;
|
|
}
|
|
idatSeen = true;
|
|
if (defaultIsFrame)
|
|
{
|
|
if (frames.size() != 1)
|
|
{
|
|
return false;
|
|
}
|
|
frames.front().data.emplace_back(chunk.data, chunk.length);
|
|
}
|
|
break;
|
|
case kFdat:
|
|
// fdAT belongs to the latest fcTL after the default image; it never extends IDAT.
|
|
if (chunk.length < 4 || ReadBE32(chunk.data) != sequence++ || !idatSeen || frames.empty() ||
|
|
(defaultIsFrame && frames.size() == 1))
|
|
{
|
|
return false;
|
|
}
|
|
frames.back().data.emplace_back(chunk.data + 4, chunk.length - 4);
|
|
break;
|
|
case kIend:
|
|
break;
|
|
default:
|
|
if (!idatSeen)
|
|
{
|
|
header.push_back(chunk);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
if (!haveActl || !idatSeen || declaredFrames == 0 || frames.size() != declaredFrames ||
|
|
!FitsLimits(width, height, frames.size(), limits))
|
|
{
|
|
return false;
|
|
}
|
|
for (const Frame &frame : frames)
|
|
{
|
|
if (frame.data.empty())
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
Canvas canvas(width, height);
|
|
out.frames_.clear();
|
|
out.timeline_ = {};
|
|
for (size_t index = 0; index < frames.size(); ++index)
|
|
{
|
|
const Frame &frame = frames[index];
|
|
// A standalone PNG of this frame: IHDR with the frame size, the shared header chunks
|
|
// (PLTE, tRNS, gAMA, iCCP ...), the frame data as IDAT, IEND.
|
|
std::vector<uint8_t> png(kPngSignature, kPngSignature + sizeof(kPngSignature));
|
|
uint8_t frameIhdr[13];
|
|
std::memcpy(frameIhdr, ihdr, sizeof(frameIhdr));
|
|
for (int b = 0; b < 4; ++b)
|
|
{
|
|
frameIhdr[b] = static_cast<uint8_t>(frame.width >> (24 - 8 * b));
|
|
frameIhdr[4 + b] = static_cast<uint8_t>(frame.height >> (24 - 8 * b));
|
|
}
|
|
AppendChunk(png, kIhdr, frameIhdr, sizeof(frameIhdr));
|
|
for (const PngChunk &chunk : header)
|
|
{
|
|
if (chunk.type != kActl && chunk.type != kFctl)
|
|
{
|
|
AppendChunk(png, chunk.type, chunk.data, chunk.length);
|
|
}
|
|
}
|
|
for (const auto &part : frame.data)
|
|
{
|
|
AppendChunk(png, kIdat, part.first, part.second);
|
|
}
|
|
AppendChunk(png, kIend, nullptr, 0);
|
|
|
|
ComPtr<IWICBitmapDecoder> decoder = CreateDecoder(factory, png.data(), png.size());
|
|
ComPtr<IWICBitmapFrameDecode> decoded;
|
|
FrameStep step;
|
|
if (!decoder || FAILED(decoder->GetFrame(0, decoded.GetAddressOf())) ||
|
|
!CopyPremultipliedBgra(factory, decoded.Get(), limits.maxDimension, step.width, step.height,
|
|
step.pixels) ||
|
|
step.width != frame.width || step.height != frame.height)
|
|
{
|
|
return false;
|
|
}
|
|
step.left = frame.x;
|
|
step.top = frame.y;
|
|
step.over = frame.blend == 1;
|
|
step.dispose = static_cast<Dispose>(frame.dispose);
|
|
out.frames_.push_back(ComposeStep(canvas, step, index == 0));
|
|
|
|
const uint32_t den = frame.delayDen == 0 ? 100U : frame.delayDen;
|
|
const uint32_t delayMs = (static_cast<uint32_t>(frame.delayNum) * 1000U + den / 2U) / den;
|
|
out.timeline_.frameDelaysMs.push_back(NormalizeFrameDelayMs(delayMs));
|
|
}
|
|
out.width_ = width;
|
|
out.height_ = height;
|
|
out.timeline_.playCount = playCount;
|
|
out.format_ = AnimatedImage::Format::Apng;
|
|
return true;
|
|
}
|
|
|
|
static bool DecodeGif(IWICImagingFactory *factory, IWICBitmapDecoder *decoder, const AnimatedImage::Limits &limits,
|
|
AnimatedImage &out)
|
|
{
|
|
UINT count = 0;
|
|
if (FAILED(decoder->GetFrameCount(&count)) || count < 2)
|
|
{
|
|
return false;
|
|
}
|
|
ComPtr<IWICMetadataQueryReader> reader;
|
|
decoder->GetMetadataQueryReader(reader.GetAddressOf());
|
|
uint32_t width = 0;
|
|
uint32_t height = 0;
|
|
ReadMetadataUInt(reader.Get(), L"/logscrdesc/Width", width);
|
|
ReadMetadataUInt(reader.Get(), L"/logscrdesc/Height", height);
|
|
if (width == 0 || height == 0)
|
|
{
|
|
ComPtr<IWICBitmapFrameDecode> first;
|
|
UINT w = 0;
|
|
UINT h = 0;
|
|
if (FAILED(decoder->GetFrame(0, first.GetAddressOf())) || FAILED(first->GetSize(&w, &h)))
|
|
{
|
|
return false;
|
|
}
|
|
width = w;
|
|
height = h;
|
|
}
|
|
if (!FitsLimits(width, height, count, limits))
|
|
{
|
|
return false;
|
|
}
|
|
// Without a NETSCAPE2.0 block a GIF plays once. Its loop count says how often to repeat
|
|
// after the first play (0 forever), which is how browsers read it.
|
|
uint32_t playCount = 1;
|
|
std::vector<uint8_t> application;
|
|
std::vector<uint8_t> loop;
|
|
if (ReadMetadataBytes(reader.Get(), L"/appext/application", application) && application.size() == 11 &&
|
|
(std::memcmp(application.data(), "NETSCAPE2.0", 11) == 0 ||
|
|
std::memcmp(application.data(), "ANIMEXTS1.0", 11) == 0) &&
|
|
ReadMetadataBytes(reader.Get(), L"/appext/data", loop) && loop.size() >= 4 && loop[0] >= 3 && loop[1] == 1)
|
|
{
|
|
const uint32_t repeats = static_cast<uint32_t>(loop[2]) | (static_cast<uint32_t>(loop[3]) << 8);
|
|
playCount = repeats == 0 ? 0 : repeats + 1;
|
|
}
|
|
|
|
Canvas canvas(width, height);
|
|
out.frames_.clear();
|
|
out.timeline_ = {};
|
|
for (UINT index = 0; index < count; ++index)
|
|
{
|
|
ComPtr<IWICBitmapFrameDecode> frame;
|
|
if (FAILED(decoder->GetFrame(index, frame.GetAddressOf())))
|
|
{
|
|
return false;
|
|
}
|
|
ComPtr<IWICMetadataQueryReader> frameReader;
|
|
frame->GetMetadataQueryReader(frameReader.GetAddressOf());
|
|
uint32_t left = 0;
|
|
uint32_t top = 0;
|
|
uint32_t delay = 0;
|
|
uint32_t disposal = 0;
|
|
ReadMetadataUInt(frameReader.Get(), L"/imgdesc/Left", left);
|
|
ReadMetadataUInt(frameReader.Get(), L"/imgdesc/Top", top);
|
|
ReadMetadataUInt(frameReader.Get(), L"/grctlext/Delay", delay);
|
|
ReadMetadataUInt(frameReader.Get(), L"/grctlext/Disposal", disposal);
|
|
FrameStep step;
|
|
if (!CopyPremultipliedBgra(factory, frame.Get(), limits.maxDimension, step.width, step.height, step.pixels))
|
|
{
|
|
return false;
|
|
}
|
|
step.left = left;
|
|
step.top = top;
|
|
// Transparent GIF pixels leave the canvas alone.
|
|
step.over = true;
|
|
step.dispose = disposal == 2 ? Dispose::Background : disposal == 3 ? Dispose::Previous : Dispose::None;
|
|
out.frames_.push_back(ComposeStep(canvas, step, index == 0));
|
|
out.timeline_.frameDelaysMs.push_back(NormalizeFrameDelayMs(delay * 10U));
|
|
}
|
|
out.width_ = width;
|
|
out.height_ = height;
|
|
out.timeline_.playCount = playCount;
|
|
out.format_ = AnimatedImage::Format::Gif;
|
|
return true;
|
|
}
|
|
|
|
// Multi-frame containers without frame timing in WIC (animated WebP when the codec exposes the
|
|
// frames): every frame is full size and replaces the previous one every 100 ms, forever.
|
|
static bool DecodeMultiFrame(IWICImagingFactory *factory, IWICBitmapDecoder *decoder,
|
|
const AnimatedImage::Limits &limits, AnimatedImage &out)
|
|
{
|
|
UINT count = 0;
|
|
if (FAILED(decoder->GetFrameCount(&count)) || count < 2 || count > limits.maxFrames)
|
|
{
|
|
return false;
|
|
}
|
|
out.frames_.clear();
|
|
out.timeline_ = {};
|
|
for (UINT index = 0; index < count; ++index)
|
|
{
|
|
ComPtr<IWICBitmapFrameDecode> frame;
|
|
uint32_t width = 0;
|
|
uint32_t height = 0;
|
|
std::vector<uint8_t> pixels;
|
|
if (FAILED(decoder->GetFrame(index, frame.GetAddressOf())) ||
|
|
!CopyPremultipliedBgra(factory, frame.Get(), limits.maxDimension, width, height, pixels) ||
|
|
(index > 0 && (width != out.width_ || height != out.height_)) ||
|
|
!FitsLimits(width, height, count, limits))
|
|
{
|
|
return false;
|
|
}
|
|
out.width_ = width;
|
|
out.height_ = height;
|
|
out.frames_.push_back(std::move(pixels));
|
|
out.timeline_.frameDelaysMs.push_back(100U);
|
|
}
|
|
out.format_ = AnimatedImage::Format::MultiFrame;
|
|
return true;
|
|
}
|
|
|
|
static bool DecodeStill(IWICImagingFactory *factory, IWICBitmapDecoder *decoder,
|
|
const AnimatedImage::Limits &limits, AnimatedImage &out)
|
|
{
|
|
ComPtr<IWICBitmapFrameDecode> frame;
|
|
std::vector<uint8_t> pixels;
|
|
uint32_t width = 0;
|
|
uint32_t height = 0;
|
|
if (FAILED(decoder->GetFrame(0, frame.GetAddressOf())) ||
|
|
!CopyPremultipliedBgra(factory, frame.Get(), limits.maxDimension, width, height, pixels))
|
|
{
|
|
return false;
|
|
}
|
|
out.width_ = width;
|
|
out.height_ = height;
|
|
out.frames_.clear();
|
|
out.frames_.push_back(std::move(pixels));
|
|
out.timeline_ = {};
|
|
out.format_ = AnimatedImage::Format::Still;
|
|
return true;
|
|
}
|
|
};
|
|
|
|
uint32_t NormalizeFrameDelayMs(uint32_t delayMs)
|
|
{
|
|
return delayMs <= 10U ? 100U : delayMs;
|
|
}
|
|
|
|
uint64_t AnimationTimeline::LoopDurationMs() const
|
|
{
|
|
uint64_t total = 0;
|
|
for (const uint32_t delay : frameDelaysMs)
|
|
{
|
|
total += delay;
|
|
}
|
|
return total;
|
|
}
|
|
|
|
AnimationTimeline::Sample AnimationTimeline::SampleAt(uint64_t elapsedMs) const
|
|
{
|
|
if (frameDelaysMs.size() <= 1)
|
|
{
|
|
return {};
|
|
}
|
|
const size_t last = frameDelaysMs.size() - 1;
|
|
const uint64_t loop = LoopDurationMs();
|
|
if (loop == 0)
|
|
{
|
|
return {};
|
|
}
|
|
const uint64_t play = elapsedMs / loop;
|
|
if (playCount != 0 && play >= playCount)
|
|
{
|
|
return {last, std::nullopt};
|
|
}
|
|
const uint64_t loopStart = play * loop;
|
|
const uint64_t intoLoop = elapsedMs - loopStart;
|
|
uint64_t frameEnd = 0;
|
|
for (size_t index = 0; index <= last; ++index)
|
|
{
|
|
frameEnd += frameDelaysMs[index];
|
|
if (intoLoop < frameEnd)
|
|
{
|
|
const bool finalFrame = playCount != 0 && play + 1 == playCount && index == last;
|
|
if (finalFrame)
|
|
{
|
|
return {index, std::nullopt};
|
|
}
|
|
return {index, loopStart + frameEnd};
|
|
}
|
|
}
|
|
return {last, std::nullopt};
|
|
}
|
|
|
|
std::optional<AnimatedImage> AnimatedImage::DecodeMemory(IWICImagingFactory *factory, const uint8_t *data, size_t size,
|
|
const Limits &limits)
|
|
{
|
|
if (!factory || !data || size == 0 || size > limits.maxFileBytes)
|
|
{
|
|
return std::nullopt;
|
|
}
|
|
AnimatedImage image;
|
|
if (HasPngSignature(data, size) && AnimatedImageDecoder::DecodeApng(factory, data, size, limits, image))
|
|
{
|
|
return image;
|
|
}
|
|
ComPtr<IWICBitmapDecoder> decoder = CreateDecoder(factory, data, size);
|
|
if (!decoder)
|
|
{
|
|
return std::nullopt;
|
|
}
|
|
GUID container = {};
|
|
if (SUCCEEDED(decoder->GetContainerFormat(&container)))
|
|
{
|
|
if (IsEqualGUID(container, GUID_ContainerFormatGif) &&
|
|
AnimatedImageDecoder::DecodeGif(factory, decoder.Get(), limits, image))
|
|
{
|
|
return image;
|
|
}
|
|
if (IsEqualGUID(container, GUID_ContainerFormatWebp) &&
|
|
AnimatedImageDecoder::DecodeMultiFrame(factory, decoder.Get(), limits, image))
|
|
{
|
|
return image;
|
|
}
|
|
}
|
|
if (AnimatedImageDecoder::DecodeStill(factory, decoder.Get(), limits, image))
|
|
{
|
|
return image;
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::optional<AnimatedImage> AnimatedImage::DecodeFile(IWICImagingFactory *factory, const std::wstring &filePath,
|
|
const Limits &limits)
|
|
{
|
|
std::vector<uint8_t> bytes;
|
|
if (!ReadFileBytes(filePath, limits.maxFileBytes, bytes))
|
|
{
|
|
return std::nullopt;
|
|
}
|
|
return DecodeMemory(factory, bytes.data(), bytes.size(), limits);
|
|
}
|
|
|
|
bool AnimatedImage::MayBeAnimated(IWICImagingFactory *factory, const std::wstring &filePath)
|
|
{
|
|
std::vector<uint8_t> bytes;
|
|
if (!factory || !ReadFileBytes(filePath, Limits().maxFileBytes, bytes))
|
|
{
|
|
return false;
|
|
}
|
|
if (HasPngSignature(bytes.data(), bytes.size()))
|
|
{
|
|
// acTL has to come before the first IDAT; no need to look further.
|
|
size_t pos = sizeof(kPngSignature);
|
|
while (pos + 12 <= bytes.size())
|
|
{
|
|
const uint32_t length = ReadBE32(bytes.data() + pos);
|
|
const uint32_t type = ReadBE32(bytes.data() + pos + 4);
|
|
if (type == kActl)
|
|
{
|
|
return true;
|
|
}
|
|
if (type == kIdat || type == kIend || length > bytes.size() - pos - 12)
|
|
{
|
|
return false;
|
|
}
|
|
pos += 12 + static_cast<size_t>(length);
|
|
}
|
|
return false;
|
|
}
|
|
ComPtr<IWICBitmapDecoder> decoder = CreateDecoder(factory, bytes.data(), bytes.size());
|
|
GUID container = {};
|
|
UINT count = 0;
|
|
return decoder && SUCCEEDED(decoder->GetContainerFormat(&container)) &&
|
|
(IsEqualGUID(container, GUID_ContainerFormatGif) || IsEqualGUID(container, GUID_ContainerFormatWebp)) &&
|
|
SUCCEEDED(decoder->GetFrameCount(&count)) && count > 1;
|
|
}
|
|
} // namespace msimeui
|