feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -0,0 +1,191 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type ReactCropperElement } from 'react-cropper';
import { type RefObject } from 'react';
import { floor, pick } from 'lodash-es';
import ColorThief from 'colorthief';
import { MODE_CONFIG } from '@coze-common/chat-uikit';
import { I18n } from '@coze-arch/i18n';
import { UIToast } from '@coze-arch/bot-semi';
import { CustomError } from '@coze-arch/bot-error';
import { type PicTask } from '@coze-arch/bot-api/playground_api';
import {
type BackgroundImageInfo,
type CanvasPosition,
type GradientPosition,
} from '@coze-arch/bot-api/developer_api';
const MIN_HEIGHT = 640;
// 图片上传限制宽高的函数如果符合条件返回true否者返回false
export const checkImageWidthAndHeight = (file: Blob): Promise<boolean> =>
new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = event => {
const result = event.target?.result;
if (!result || typeof result !== 'string') {
reject(
new CustomError('checkImageWidthAndHeight', 'file read invalid'),
);
return;
}
const image = new Image();
image.src = result;
image.onload = function () {
if (image.height < MIN_HEIGHT) {
UIToast.error(I18n.t('bgi_upload_image_format_requirement'));
resolve(false);
} else if (image.complete) {
resolve(true);
}
};
};
fileReader.onerror = () => {
reject(new CustomError('checkImageWidthAndHeight', 'file read fail'));
};
fileReader.onabort = () => {
reject(new CustomError('checkImageWidthAndHeight', 'file read abort'));
};
fileReader.readAsDataURL(file);
});
export const getModeInfo = (mode: 'pc' | 'mobile') => MODE_CONFIG[mode];
export const getOriginImageFromBackgroundInfo = (
value: BackgroundImageInfo[],
): {
url: string;
uri: string;
} => ({
url: value[0]?.web_background_image?.origin_image_url ?? '',
uri: value[0]?.web_background_image?.origin_image_uri ?? '',
});
export const getInitBackground = ({
isGenerateSuccess,
selectedImageInfo,
originBackground,
}: {
isGenerateSuccess: boolean;
originBackground: BackgroundImageInfo[];
selectedImageInfo: PicTask['img_info'];
}) => {
if (isGenerateSuccess && selectedImageInfo?.tar_url) {
return {
url: selectedImageInfo.tar_url,
uri: selectedImageInfo.tar_uri,
};
}
if (getOriginImageFromBackgroundInfo(originBackground).url) {
return getOriginImageFromBackgroundInfo(originBackground);
}
return {};
};
// 计算阴影位置
export const computePosition = (
mode: 'pc' | 'mobile',
cropperRef: RefObject<ReactCropperElement>,
): GradientPosition => {
const cropperObj = cropperRef?.current?.cropper;
if (!cropperObj) {
return {
left: 0,
right: 0,
};
}
const { size } = getModeInfo(mode);
const cropperWidth = size.width;
const canvasData = cropperObj?.getCanvasData();
const imgData = cropperObj?.getImageData();
// 图片左边缘 距离 裁剪区左 边缘的偏移距离
const imgToScreenLeft = imgData.left + canvasData?.left;
// 图片距离右侧屏幕的距离, > 0 时,右侧未充满图片
const imgToScreenRight = cropperWidth - imgData.width - imgToScreenLeft;
// 左侧渲染的渐变需要的left值: 左侧有空隙时渲染left否则无需
const leftPercent = floor(imgToScreenLeft / cropperWidth, 2);
// 右侧渲染的渐变需要的right值
const rightPercent = floor(imgToScreenRight / cropperWidth, 2);
return {
left: leftPercent,
right: rightPercent,
};
};
export const canvasPosition = (
cropperRef: RefObject<ReactCropperElement>,
): CanvasPosition =>
pick(cropperRef.current?.cropper.getCanvasData(), [
'left',
'top',
'width',
'height',
]);
// 计算主题色
export const computeThemeColor = (
cropperRefList: RefObject<ReactCropperElement>[],
): Promise<string[]> =>
new Promise((resolve, reject) => {
const promises: Promise<string>[] = [];
// 处理每个canvas元素
cropperRefList.forEach(cropperEle => {
promises.push(
new Promise<string>((resolveColor, rejectColor) => {
const cropperObj = cropperEle.current?.cropper;
const corp = cropperObj
?.getCroppedCanvas()
?.toDataURL('image/webp', 0.7);
if (!corp) {
rejectColor(
new CustomError('computeThemeColor', 'cropper not exist'),
);
} else {
getImageThemeColor(corp).then((res: string) => {
resolveColor(res);
});
}
}),
);
});
Promise.all(promises)
.then(colors => resolve(colors))
.catch(error => reject(error));
});
export function getImageThemeColor(url: string): Promise<string> {
return new Promise((resolve, reject) => {
const colorThief = new ColorThief();
const img = new Image();
img.src = url;
img.onload = () => {
const thiefColor = colorThief.getColor(img);
if (thiefColor) {
const [a, b, c] = thiefColor;
const color = `rgba(${a}, ${b}, ${c})`;
resolve(color);
} else {
reject(new CustomError('getImageThemeColor', 'not get theme color'));
}
};
});
}