🐛 重构数据解析,修复HEIC格式图片渲染异常

This commit is contained in:
BTMuli
2025-09-28 22:53:02 +08:00
parent ed878dea9e
commit b62b0b4902
4 changed files with 108 additions and 55 deletions

View File

@@ -1,11 +1,6 @@
<template>
<div class="tp-image-box" v-if="localUrl !== undefined">
<img
:src="localUrl"
@click="showOverlay = true"
:alt="props.data.insert.image"
:title="getImageTitle()"
/>
<img :src="localUrl" @click="showOverlay = true" :alt="oriUrl" :title="getImageTitle()" />
<div
class="act"
@click.stop="showOri = true"
@@ -16,7 +11,7 @@
<v-icon size="16" color="white">mdi-magnify</v-icon>
</div>
</div>
<div v-else class="tp-image-load" :title="props.data.insert.image">
<div v-else class="tp-image-load" :title="oriUrl">
<v-progress-circular :indeterminate="true" color="primary" size="small" />
<span>加载中...</span>
</div>
@@ -26,6 +21,7 @@
v-model:link="localUrl"
v-model:ori="showOri"
v-model:bgColor="bgColor"
v-model:format="imgExt"
/>
</template>
<script lang="ts" setup>
@@ -39,7 +35,7 @@ import { computed, onMounted, onUnmounted, ref, watch } from "vue";
import VpOverlayImage from "./vp-overlay-image.vue";
export type TpImage = {
insert: { image: string };
insert: { image: string | TGApp.BBS.Post.Image };
attributes?: {
width: number;
height: number;
@@ -54,12 +50,17 @@ const appStore = useAppStore();
const { imageQualityPercent } = storeToRefs(appStore);
const props = defineProps<TpImageProps>();
const showOverlay = ref<boolean>(false);
const showOri = ref<boolean>(
props.data.insert.image.endsWith(".gif") || imageQualityPercent.value === 100,
);
const localUrl = ref<string>();
const bgColor = ref<string>("transparent");
const oriUrl = computed<string>(() => {
if (typeof props.data.insert.image === "string") return props.data.insert.image;
return props.data.insert.image.url;
});
const imgExt = computed<string>(() => getImageExt());
const showOri = ref<boolean>(imgExt.value === "gif" || imageQualityPercent.value === 100);
const imgWidth = computed<string>(() => {
if (props.data.attributes === undefined) return "auto";
if (props.data.attributes.width >= 690) return "100%";
@@ -69,7 +70,7 @@ const imgWidth = computed<string>(() => {
console.log("tp-image", props.data.insert.image, props.data.attributes);
onMounted(async () => {
const link = appStore.getImageUrl(props.data.insert.image);
const link = appStore.getImageUrl(oriUrl.value, imgExt.value);
localUrl.value = await saveImgLocal(link);
});
@@ -77,9 +78,12 @@ watch(
() => showOri.value,
async () => {
if (!showOri.value) return;
await showLoading.start("正在加载原图", props.data.insert.image);
await showLoading.start("正在加载原图", oriUrl.value);
if (localUrl.value) URL.revokeObjectURL(localUrl.value);
localUrl.value = await saveImgLocal(props.data.insert.image);
const ext = getImageExt();
if (!["png", "jpg", "jpeg", "gif", "webp"].includes(ext.toLowerCase())) {
localUrl.value = await saveImgLocal(`${oriUrl.value}?format=jpg`);
} else localUrl.value = await saveImgLocal(oriUrl.value);
await showLoading.end();
},
);
@@ -89,18 +93,39 @@ onUnmounted(() => {
});
function getImageTitle(): string {
if (props.data.attributes == undefined) return "";
const res: string[] = [];
res.push(`宽度:${props.data.attributes.width}px`);
res.push(`度:${props.data.attributes.height}px`);
if (props.data.attributes.size) {
const size = bytesToSize(props.data.attributes.size);
res.push(`大小:${size}`);
if (props.data.attributes) {
res.push(`度:${props.data.attributes.width}px`);
res.push(`高度:${props.data.attributes.height}px`);
if (props.data.attributes.size) {
const size = bytesToSize(props.data.attributes.size);
res.push(`大小:${size}`);
}
res.push(`格式:${getImageExt()}`);
return res.join("\n");
}
if (props.data.attributes.ext) {
res.push(`格式${props.data.attributes.ext}`);
if (typeof props.data.insert.image !== "string") {
res.push(`宽度${props.data.insert.image.width}px`);
res.push(`高度:${props.data.insert.image.height}px`);
if (props.data.insert.image.size) {
const size = bytesToSize(Number(props.data.insert.image.size));
res.push(`大小:${size}`);
}
res.push(`格式:${getImageExt()}`);
return res.join("\n");
}
return res.join("\n");
return "";
}
function getImageExt(): string {
if (props.data.attributes) {
if (props.data.attributes.ext) return props.data.attributes.ext;
}
if (typeof props.data.insert.image === "string") {
const arr = props.data.insert.image.split(".");
return arr[arr.length - 1];
}
return props.data.insert.image.format;
}
</script>
<style lang="scss" scoped>

View File

@@ -5,28 +5,46 @@
<img :src="localLink" alt="图片" @click="isOriSize = !isOriSize" />
</div>
<div class="tpoi-bottom">
<div class="tpoi-info" v-if="props.image.attributes">
<p v-if="props.image.attributes.size" class="tpoi-info-item">
<span>大小</span>
<span>{{ bytesToSize(props.image.attributes.size ?? 0) }}</span>
</p>
<p class="tpoi-info-item">
<span>尺寸</span>
<span>{{ props.image.attributes.width }}x{{ props.image.attributes.height }}</span>
</p>
<p class="tpoi-info-item">
<span>格式</span>
<span>{{ format }}</span>
</p>
</div>
<template v-if="typeof props.image.insert.image !== 'string'">
<div class="tpoi-info">
<p class="tpoi-info-item">
<span>大小</span>
<span>{{ bytesToSize(Number(props.image.insert.image.size) ?? 0) }}</span>
</p>
<p class="tpoi-info-item">
<span>尺寸</span>
<span>
{{ props.image.insert.image.width }}x{{ props.image.insert.image.height }}
</span>
</p>
<p class="tpoi-info-item">
<span>格式</span>
<span>{{ format }}</span>
</p>
</div>
</template>
<template v-else-if="props.image.attributes">
<div class="tpoi-info">
<p v-if="props.image.attributes.size" class="tpoi-info-item">
<span>大小</span>
<span>{{ bytesToSize(props.image.attributes.size ?? 0) }}</span>
</p>
<p class="tpoi-info-item">
<span>尺寸</span>
<span>{{ props.image.attributes.width }}x{{ props.image.attributes.height }}</span>
</p>
<p class="tpoi-info-item">
<span>格式</span>
<span>{{ format }}</span>
</p>
</div>
</template>
<div class="tpoi-tools">
<v-icon @click="setBlackBg" title="切换背景色" v-if="showOri">
mdi-format-color-fill
</v-icon>
<v-icon @click="showOri = true" title="查看原图" v-else>mdi-magnify</v-icon>
<v-icon @click="onCopy" title="复制到剪贴板" v-if="format !== 'gif'">
mdi-content-copy
</v-icon>
<v-icon @click="onCopy" title="复制到剪贴板" v-if="showCopy">mdi-content-copy</v-icon>
<v-icon @click="onDownload" title="下载到本地">mdi-download</v-icon>
<v-icon @click="visible = false" title="关闭浮窗">mdi-close</v-icon>
</div>
@@ -51,14 +69,17 @@ const visible = defineModel<boolean>();
const localLink = defineModel<string>("link");
const showOri = defineModel<boolean>("ori");
const bgColor = defineModel<string>("bgColor", { default: "transparent" });
const format = defineModel<string>("format", { default: "png" });
const bgMode = ref<number>(0); // 0: transparent, 1: black, 2: white
const isOriSize = ref<boolean>(false);
const buffer = shallowRef<Uint8Array | null>(null);
const format = computed<string>(() => {
if (props.image.attributes?.ext) return props.image.attributes.ext;
const imageFormat = props.image.insert.image.split(".").pop();
if (imageFormat !== undefined) return imageFormat;
return "png";
const oriLink = computed<string>(() => {
const image = props.image.insert.image;
return typeof image === "string" ? image : image.url;
});
const showCopy = computed<boolean>(() => {
// 只能显示 png/jpg/jpeg/webp 格式的复制按钮
return ["png", "jpg", "jpeg", "webp"].includes(format.value.toLowerCase());
});
function setBlackBg(): void {
@@ -93,14 +114,13 @@ async function onDownload(): Promise<void> {
showOri.value = true;
await nextTick();
}
const image = props.image.insert.image;
await showLoading.start("正在下载图片到本地", image);
if (buffer.value === null) buffer.value = await getImageBuffer(image);
await showLoading.start("正在下载图片到本地", oriLink.value);
if (buffer.value === null) buffer.value = await getImageBuffer(oriLink.value);
if (buffer.value.byteLength > 80000000) {
showSnackbar.warn("图片过大,无法下载到本地");
return;
}
let fileName = image.split("/").pop()?.split(".")[0];
let fileName = oriLink.value.split("/").pop()?.split(".")[0];
if (fileName === undefined) fileName = Date.now().toString();
await saveCanvasImg(buffer.value, fileName, format.value);
await showLoading.end();

View File

@@ -1,7 +1,7 @@
/**
* @file store/modules/app.ts
* @description App store module
* @since Beta v0.8.0
* @since Beta v0.8.3
*/
import { AnnoLangEnum } from "@enum/anno.js";
@@ -84,8 +84,10 @@ const useAppStore = defineStore(
deviceInfo.value = getInitDeviceInfo();
}
function getImageUrl(url: string): string {
if (url.endsWith(".gif") || imageQualityPercent.value === 100) return url;
function getImageUrl(url: string, fmt?: string): string {
let check = false;
if (fmt && !["jpg", "png", "webp", "gif", "jpeg"].includes(fmt.toLowerCase())) check = false;
if (check && (url.endsWith(".gif") || imageQualityPercent.value === 100)) return url;
return `${url}?x-oss-process=image/format,jpg/quality,Q_${imageQualityPercent.value}`;
}

View File

@@ -266,7 +266,7 @@ async function getRenderPost(
jsonParse = data.post.structured_content;
} else {
try {
jsonParse = await parseContent(data.post.content);
jsonParse = await parseContent(data);
} catch (e) {
if (e instanceof SyntaxError) {
await TGLogger.Warn(`[t-post][${postId}] ${e.name}: ${e.message}`);
@@ -277,7 +277,8 @@ async function getRenderPost(
return JSON.parse(jsonParse);
}
async function parseContent(content: string): Promise<string> {
async function parseContent(fullData: TGApp.BBS.Post.FullData): Promise<string> {
const content = fullData.post.content;
const data: TGApp.BBS.SctPost.Other = JSON.parse(content);
const result: TGApp.BBS.SctPost.Base[] = [];
for (const key of Object.keys(data)) {
@@ -286,7 +287,12 @@ async function parseContent(content: string): Promise<string> {
result.push({ insert: data.describe });
break;
case "imgs":
data.imgs.forEach((item) => result.push({ insert: { image: item } }));
for (const img of data.imgs) {
const imgFind = fullData.image_list.find((i) => i.url === img);
if (!imgFind) {
result.push({ insert: { image: img } });
} else result.push({ insert: { image: imgFind } });
}
break;
case "link_card_ids":
if (!data.link_card_ids) break;