|
@@ -9380,6 +9380,8 @@ class AnimeService
|
|
|
$anime = DB::table('mp_animes')->where('id', $animeId)->where('is_deleted', 0)->first();
|
|
$anime = DB::table('mp_animes')->where('id', $animeId)->where('is_deleted', 0)->first();
|
|
|
if (!$anime) Utils::throwError('20003:该动漫不存在!');
|
|
if (!$anime) Utils::throwError('20003:该动漫不存在!');
|
|
|
$anime = (array)$anime;
|
|
$anime = (array)$anime;
|
|
|
|
|
+ // 剧本名去掉末尾"_数字"标识后缀,只保留剧名本身
|
|
|
|
|
+ $anime['anime_name'] = trim((string)preg_replace('/_\d+$/u', '', getProp($anime, 'anime_name', '')));
|
|
|
|
|
|
|
|
// 获取分集(默认展示版本,按集数升序)
|
|
// 获取分集(默认展示版本,按集数升序)
|
|
|
$episodeQuery = DB::table('mp_anime_episodes')
|
|
$episodeQuery = DB::table('mp_anime_episodes')
|
|
@@ -9425,6 +9427,13 @@ class AnimeService
|
|
|
}
|
|
}
|
|
|
ksort($acts);
|
|
ksort($acts);
|
|
|
|
|
|
|
|
|
|
+ // 分镜序号按出现先后重新编号(整集范围内保持连续,兼容【镜头N】/【分镜N】两种格式)
|
|
|
|
|
+ $segmentCounter = 0;
|
|
|
|
|
+ foreach ($acts as &$act) {
|
|
|
|
|
+ $act['content'] = $this->renumberSegmentBlocks($act['content'], $segmentCounter);
|
|
|
|
|
+ }
|
|
|
|
|
+ unset($act);
|
|
|
|
|
+
|
|
|
// 旁白音色:优先取分集roles中"旁白"的voice_prompt,否则从片段内容解析
|
|
// 旁白音色:优先取分集roles中"旁白"的voice_prompt,否则从片段内容解析
|
|
|
$narratorVoice = '';
|
|
$narratorVoice = '';
|
|
|
$episodeRoles = json_decode(getProp($episode, 'roles', ''), true) ?: [];
|
|
$episodeRoles = json_decode(getProp($episode, 'roles', ''), true) ?: [];
|
|
@@ -9760,33 +9769,81 @@ class AnimeService
|
|
|
$image = @imagecreatefromstring($content);
|
|
$image = @imagecreatefromstring($content);
|
|
|
if ($image === false) return null;
|
|
if ($image === false) return null;
|
|
|
|
|
|
|
|
- $image = $this->resizeImageForPdf($image, 2048);
|
|
|
|
|
- $jpeg = dirname($rawFile) . '/anime_pdf_img_' . md5($url) . '.jpg';
|
|
|
|
|
- $blank = imagecreatetruecolor(imagesx($image), imagesy($image));
|
|
|
|
|
- $white = imagecolorallocate($blank, 255, 255, 255);
|
|
|
|
|
- imagefill($blank, 0, 0, $white);
|
|
|
|
|
- imagecopy($blank, $image, 0, 0, 0, 0, imagesx($image), imagesy($image));
|
|
|
|
|
- $saved = imagejpeg($blank, $jpeg, 85);
|
|
|
|
|
|
|
+ $jpeg = $this->compressImageForPdf($image, $rawFile, $url);
|
|
|
imagedestroy($image);
|
|
imagedestroy($image);
|
|
|
- imagedestroy($blank);
|
|
|
|
|
- if ($saved) {
|
|
|
|
|
|
|
+ if ($jpeg !== null) {
|
|
|
@unlink($rawFile);
|
|
@unlink($rawFile);
|
|
|
- return $jpeg;
|
|
|
|
|
}
|
|
}
|
|
|
- return null;
|
|
|
|
|
|
|
+ return $jpeg;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 图片等比缩放(限制最大边,降低内存与PDF体积)
|
|
|
|
|
- * @param resource $image GD图片资源
|
|
|
|
|
|
|
+ * 图片压缩:白底合成后转JPEG,分级缩小尺寸与降低质量,保证每张不超过2MB
|
|
|
|
|
+ * @param resource $srcImage GD图片资源
|
|
|
|
|
+ * @param string $rawFile 原始图片临时文件
|
|
|
|
|
+ * @param string $url 图片地址(用于命名)
|
|
|
|
|
+ * @return string|null 压缩后的JPEG文件路径
|
|
|
|
|
+ */
|
|
|
|
|
+ private function compressImageForPdf($srcImage, $rawFile, $url) {
|
|
|
|
|
+ $maxBytes = 2 * 1024 * 1024; // 2MB
|
|
|
|
|
+ $sizes = [2048, 1600, 1280, 1024, 800];
|
|
|
|
|
+ $qualities = [85, 70, 55, 40];
|
|
|
|
|
+ $jpeg = dirname($rawFile) . '/anime_pdf_img_' . md5($url) . '.jpg';
|
|
|
|
|
+
|
|
|
|
|
+ foreach ($sizes as $size) {
|
|
|
|
|
+ $resized = $this->copyAndResize($srcImage, $size);
|
|
|
|
|
+ $flat = $this->flattenToWhite($resized);
|
|
|
|
|
+ imagedestroy($resized);
|
|
|
|
|
+
|
|
|
|
|
+ foreach ($qualities as $quality) {
|
|
|
|
|
+ ob_start();
|
|
|
|
|
+ $saved = imagejpeg($flat, null, $quality);
|
|
|
|
|
+ $data = ob_get_clean();
|
|
|
|
|
+ if ($saved && $data !== false && $data !== '' && strlen($data) <= $maxBytes) {
|
|
|
|
|
+ file_put_contents($jpeg, $data);
|
|
|
|
|
+ imagedestroy($flat);
|
|
|
|
|
+ dLog('anime')->info('导出PDF图片压缩完成', [
|
|
|
|
|
+ 'url' => $url,
|
|
|
|
|
+ 'size' => filesize($jpeg),
|
|
|
|
|
+ 'edge' => $size,
|
|
|
|
|
+ 'quality' => $quality,
|
|
|
|
|
+ ]);
|
|
|
|
|
+ return $jpeg;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ imagedestroy($flat);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 兜底:最小尺寸、最低质量强制输出
|
|
|
|
|
+ $resized = $this->copyAndResize($srcImage, 800);
|
|
|
|
|
+ $flat = $this->flattenToWhite($resized);
|
|
|
|
|
+ imagedestroy($resized);
|
|
|
|
|
+ $saved = imagejpeg($flat, $jpeg, 30);
|
|
|
|
|
+ imagedestroy($flat);
|
|
|
|
|
+ if (!$saved) return null;
|
|
|
|
|
+
|
|
|
|
|
+ dLog('anime')->info('导出PDF图片压缩完成(兜底)', [
|
|
|
|
|
+ 'url' => $url,
|
|
|
|
|
+ 'size' => filesize($jpeg),
|
|
|
|
|
+ ]);
|
|
|
|
|
+ return $jpeg;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 等比缩放图片副本(不销毁原图)
|
|
|
|
|
+ * @param resource $srcImage GD图片资源
|
|
|
* @param int $maxEdge 最大边长
|
|
* @param int $maxEdge 最大边长
|
|
|
* @return resource
|
|
* @return resource
|
|
|
*/
|
|
*/
|
|
|
- private function resizeImageForPdf($image, $maxEdge) {
|
|
|
|
|
- $width = imagesx($image);
|
|
|
|
|
- $height = imagesy($image);
|
|
|
|
|
|
|
+ private function copyAndResize($srcImage, $maxEdge) {
|
|
|
|
|
+ $width = imagesx($srcImage);
|
|
|
|
|
+ $height = imagesy($srcImage);
|
|
|
$max = max($width, $height);
|
|
$max = max($width, $height);
|
|
|
- if ($max <= $maxEdge) return $image;
|
|
|
|
|
|
|
+ if ($max <= $maxEdge) {
|
|
|
|
|
+ $copy = imagecreatetruecolor($width, $height);
|
|
|
|
|
+ imagecopy($copy, $srcImage, 0, 0, 0, 0, $width, $height);
|
|
|
|
|
+ return $copy;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
$ratio = $maxEdge / $max;
|
|
$ratio = $maxEdge / $max;
|
|
|
$newWidth = max(1, (int)round($width * $ratio));
|
|
$newWidth = max(1, (int)round($width * $ratio));
|
|
@@ -9794,9 +9851,36 @@ class AnimeService
|
|
|
$resized = imagecreatetruecolor($newWidth, $newHeight);
|
|
$resized = imagecreatetruecolor($newWidth, $newHeight);
|
|
|
imagealphablending($resized, false);
|
|
imagealphablending($resized, false);
|
|
|
imagesavealpha($resized, true);
|
|
imagesavealpha($resized, true);
|
|
|
- imagecopyresampled($resized, $image, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height);
|
|
|
|
|
- imagedestroy($image);
|
|
|
|
|
|
|
+ imagecopyresampled($resized, $srcImage, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height);
|
|
|
return $resized;
|
|
return $resized;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 将GD图片合成到白色背景上(处理透明通道,输出RGB)
|
|
|
|
|
+ * @param resource $image GD图片资源
|
|
|
|
|
+ * @return resource
|
|
|
|
|
+ */
|
|
|
|
|
+ private function flattenToWhite($image) {
|
|
|
|
|
+ $width = imagesx($image);
|
|
|
|
|
+ $height = imagesy($image);
|
|
|
|
|
+ $flat = imagecreatetruecolor($width, $height);
|
|
|
|
|
+ $white = imagecolorallocate($flat, 255, 255, 255);
|
|
|
|
|
+ imagefill($flat, 0, 0, $white);
|
|
|
|
|
+ imagecopy($flat, $image, 0, 0, 0, 0, $width, $height);
|
|
|
|
|
+ return $flat;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 将片段内容中的分镜序号按出现先后重新编号(保证整集连续)
|
|
|
|
|
+ * @param string $content 片段内容
|
|
|
|
|
+ * @param int $counter 当前已编号数量(引用传递,整集累计)
|
|
|
|
|
+ * @return string
|
|
|
|
|
+ */
|
|
|
|
|
+ private function renumberSegmentBlocks($content, &$counter) {
|
|
|
|
|
+ return preg_replace_callback('/^【(分镜|镜头)\d+】/mu', function ($match) use (&$counter) {
|
|
|
|
|
+ $counter++;
|
|
|
|
|
+ return '【' . $match[1] . $counter . '】';
|
|
|
|
|
+ }, $content);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|