Skip to content

Commit 7d0beb7

Browse files
committed
Fix incorrect ImageBytes size and anchor for masked video layers in AE exporter (#3589)
1 parent 2ca1e29 commit 7d0beb7

1 file changed

Lines changed: 204 additions & 0 deletions

File tree

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
/////////////////////////////////////////////////////////////////////////////////////////////////
2+
//
3+
// Tencent is pleased to support the open source community by making libpag available.
4+
//
5+
// Copyright (C) 2025 Tencent. All rights reserved.
6+
//
7+
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
8+
// except in compliance with the License. You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// unless required by applicable law or agreed to in writing, software distributed under the
13+
// license is distributed on an "as is" basis, without warranties or conditions of any kind,
14+
// either express or implied. see the license for the specific language governing permissions
15+
// and limitations under the license.
16+
//
17+
/////////////////////////////////////////////////////////////////////////////////////////////////
18+
19+
#include "ImageBytes.h"
20+
#include "export/stream/StreamProperty.h"
21+
#include "utils/ImageData.h"
22+
#include "utils/PAGExportSession.h"
23+
24+
namespace exporter {
25+
26+
// AEGP_NewFromLayer uses the current time indicator position which may be outside the layer's
27+
// visible range. Explicitly set the render time to the layer's in point in layer time so the first
28+
// visible frame is rendered correctly. The in point is queried per layer rather than assumed to be
29+
// 0 because footage slip and time stretch can shift it away from the layer's source start.
30+
static void SetVideoLayerRenderTime(const AEGP_LayerH& layerHandle,
31+
const AEGP_LayerRenderOptionsH& renderOptions) {
32+
const auto& Suites = GetSuites();
33+
A_Time inPointLayer = {};
34+
Suites->LayerSuite6()->AEGP_GetLayerInPoint(layerHandle, AEGP_LTimeMode_LayerTime, &inPointLayer);
35+
Suites->LayerRenderOptionsSuite2()->AEGP_SetTime(renderOptions, inPointLayer);
36+
}
37+
38+
static void GetVideoLayerRenderImageSize(const AEGP_LayerH& layerHandle, A_u_long& srcStride,
39+
A_long& width, A_long& height) {
40+
const auto& Suites = GetSuites();
41+
const auto& PluginID = GetPluginID();
42+
43+
AEGP_LayerRenderOptionsH renderOptions = nullptr;
44+
Suites->LayerRenderOptionsSuite2()->AEGP_NewFromLayer(PluginID, layerHandle, &renderOptions);
45+
if (renderOptions == nullptr) {
46+
return;
47+
}
48+
SetVideoLayerRenderTime(layerHandle, renderOptions);
49+
GetLayerRenderFrameSize(renderOptions, srcStride, width, height);
50+
Suites->LayerRenderOptionsSuite2()->AEGP_Dispose(renderOptions);
51+
}
52+
53+
static void GetVideoLayerRenderImage(uint8* rgbaBytes, const AEGP_LayerH& layerHandle,
54+
A_u_long srcStride, A_u_long dstStride, A_long width,
55+
A_long height) {
56+
const auto& Suites = GetSuites();
57+
const auto& PluginID = GetPluginID();
58+
59+
AEGP_LayerRenderOptionsH renderOptions = nullptr;
60+
Suites->LayerRenderOptionsSuite2()->AEGP_NewFromLayer(PluginID, layerHandle, &renderOptions);
61+
if (renderOptions == nullptr) {
62+
return;
63+
}
64+
SetVideoLayerRenderTime(layerHandle, renderOptions);
65+
GetLayerRenderFrame(rgbaBytes, srcStride, dstStride, width, height, renderOptions);
66+
Suites->LayerRenderOptionsSuite2()->AEGP_Dispose(renderOptions);
67+
}
68+
69+
static void GetImageLayerRenderImageSize(const AEGP_LayerH& layerHandle, A_u_long& srcStride,
70+
A_long& width, A_long& height) {
71+
const auto& Suites = GetSuites();
72+
const auto& PluginID = GetPluginID();
73+
74+
AEGP_ItemH itemHandle = GetLayerItemH(layerHandle);
75+
AEGP_RenderOptionsH renderOptions = nullptr;
76+
Suites->RenderOptionsSuite3()->AEGP_NewFromItem(PluginID, itemHandle, &renderOptions);
77+
if (renderOptions == nullptr) {
78+
return;
79+
}
80+
GetRenderFrameSize(renderOptions, srcStride, width, height);
81+
Suites->RenderOptionsSuite3()->AEGP_Dispose(renderOptions);
82+
}
83+
84+
static void GetImageLayerRenderImage(uint8* rgbaBytes, const AEGP_LayerH& layerHandle,
85+
A_u_long srcStride, A_u_long dstStride, A_long width,
86+
A_long height) {
87+
const auto& Suites = GetSuites();
88+
const auto& PluginID = GetPluginID();
89+
90+
AEGP_ItemH itemHandle = GetLayerItemH(layerHandle);
91+
AEGP_RenderOptionsH renderOptions = nullptr;
92+
Suites->RenderOptionsSuite3()->AEGP_NewFromItem(PluginID, itemHandle, &renderOptions);
93+
if (renderOptions == nullptr) {
94+
return;
95+
}
96+
GetRenderFrame(rgbaBytes, srcStride, dstStride, width, height, renderOptions);
97+
Suites->RenderOptionsSuite3()->AEGP_Dispose(renderOptions);
98+
}
99+
100+
// Returns the masked bounds offset of a video layer in layer space. When a video layer has masks,
101+
// AEGP_RenderAndCheckoutLayerFrame returns a world cropped to the mask bounding box. This function
102+
// retrieves that bounding box so we can record the correct offset in the exported imageBytes.
103+
static void GetVideoLayerMaskedOffset(const AEGP_LayerH& layerHandle, A_long& offsetX,
104+
A_long& offsetY) {
105+
const auto& Suites = GetSuites();
106+
A_Time inPoint = {};
107+
A_Err err =
108+
Suites->LayerSuite6()->AEGP_GetLayerInPoint(layerHandle, AEGP_LTimeMode_CompTime, &inPoint);
109+
if (err != A_Err_NONE || inPoint.scale == 0) {
110+
return;
111+
}
112+
A_FloatRect maskedBounds = {};
113+
err = Suites->LayerSuite8()->AEGP_GetLayerMaskedBounds(layerHandle, AEGP_LTimeMode_CompTime,
114+
&inPoint, &maskedBounds);
115+
if (err != A_Err_NONE) {
116+
return;
117+
}
118+
// Floor toward negative infinity so masks with negative fractional bounds match AE's pixel-floor
119+
// crop origin.
120+
offsetX = static_cast<A_long>(floor(maskedBounds.left));
121+
offsetY = static_cast<A_long>(floor(maskedBounds.top));
122+
}
123+
124+
void GetImageBytes(std::shared_ptr<PAGExportSession> session, pag::ImageBytes* imageBytes,
125+
const AEGP_LayerH& layerHandle, bool isVideo, float factor) {
126+
A_long width = 0;
127+
A_long height = 0;
128+
A_u_long srcStride = 0;
129+
A_u_long dstStride = 0;
130+
uint8* rgbaBytes = nullptr;
131+
if (isVideo) {
132+
GetVideoLayerRenderImageSize(layerHandle, srcStride, width, height);
133+
} else {
134+
GetImageLayerRenderImageSize(layerHandle, srcStride, width, height);
135+
}
136+
if (width < 0 || height < 0) {
137+
return;
138+
}
139+
dstStride = width * 4;
140+
rgbaBytes = new uint8[dstStride * height];
141+
if (isVideo) {
142+
GetVideoLayerRenderImage(rgbaBytes, layerHandle, srcStride, dstStride, width, height);
143+
} else {
144+
GetImageLayerRenderImage(rgbaBytes, layerHandle, srcStride, dstStride, width, height);
145+
}
146+
147+
ImageRect rect = {0, 0, width, height};
148+
if (session->configParam.isTagCodeSupport(pag::TagCode::ImageBytesV3)) {
149+
ClipTransparentEdge(rect, rgbaBytes, width, height, dstStride);
150+
}
151+
152+
uint8_t* data = nullptr;
153+
uint8_t* scaledRGBA = nullptr;
154+
int theStride = 0;
155+
auto scaledWidth = static_cast<int>(ceil(rect.width * factor));
156+
auto scaledHeight = static_cast<int>(ceil(rect.height * factor));
157+
if (scaledWidth == rect.width && scaledHeight == rect.height) {
158+
data = rgbaBytes + rect.yPos * dstStride + rect.xPos * 4;
159+
theStride = dstStride;
160+
} else {
161+
theStride = scaledWidth * 4;
162+
scaledRGBA = new uint8_t[theStride * scaledHeight + theStride * 2];
163+
ScalePixels(scaledRGBA, theStride, scaledWidth, scaledHeight,
164+
rgbaBytes + rect.yPos * dstStride + rect.xPos * 4, dstStride, rect.width,
165+
rect.height);
166+
data = scaledRGBA;
167+
}
168+
169+
auto byteData = EncodeImageData(data, scaledWidth, scaledHeight, theStride,
170+
session->configParam.imageQuality);
171+
if (byteData != nullptr) {
172+
A_long maskedOffsetX = 0;
173+
A_long maskedOffsetY = 0;
174+
A_long sourceWidth = width;
175+
A_long sourceHeight = height;
176+
if (isVideo) {
177+
// For video layers, Layer Render may crop the output to the mask bounding box. We need to
178+
// use the source item dimensions as the logical content size and record the mask offset so
179+
// the player can position the placeholder image correctly in layer space.
180+
AEGP_ItemH itemHandle = GetLayerItemH(layerHandle);
181+
auto dimensions = GetItemDimensions(itemHandle);
182+
sourceWidth = dimensions.width();
183+
sourceHeight = dimensions.height();
184+
if (sourceWidth != width || sourceHeight != height) {
185+
GetVideoLayerMaskedOffset(layerHandle, maskedOffsetX, maskedOffsetY);
186+
}
187+
}
188+
imageBytes->width = sourceWidth;
189+
imageBytes->height = sourceHeight;
190+
imageBytes->anchorX = -(rect.xPos + maskedOffsetX);
191+
imageBytes->anchorY = -(rect.yPos + maskedOffsetY);
192+
imageBytes->scaleFactor = factor;
193+
imageBytes->fileBytes = byteData;
194+
}
195+
196+
if (scaledRGBA != nullptr) {
197+
delete[] scaledRGBA;
198+
}
199+
if (rgbaBytes != nullptr) {
200+
delete[] rgbaBytes;
201+
}
202+
}
203+
204+
} // namespace exporter

0 commit comments

Comments
 (0)