fix: preserve full photo framing in personal posters and covers

This commit is contained in:
Glore Zeng 2026-11-03 06:14:24 +08:00
parent 2d77142995
commit 2b6eab4121
5 changed files with 21 additions and 5 deletions

View File

@ -9,7 +9,7 @@
</view> </view>
<view class="personal-stage"> <view class="personal-stage">
<image v-if="poster" class="personal-poster" src="{{poster}}" mode="widthFix" @tap="preview" @error="posterImageFailed"></image> <image v-if="poster" class="personal-poster" src="{{poster}}" mode="widthFix" @tap="preview" @error="posterImageFailed"></image>
<view v-else class="personal-fallback"><view class="fallback-brand"><image src="/pages/users/assets/fulian-logo.png" mode="aspectFit"></image><text>福恋</text></view><image v-if="profile.photo" class="fallback-photo" src="{{profile.photo}}" mode="aspectFill"></image><view v-else class="fallback-photo fallback-blank"><image src="/pages/users/assets/fulian-logo.png" mode="aspectFit"></image><text>期待认真相识</text></view><view class="fallback-name">{{profile.nickname || '我的卡片'}}</view><view class="fallback-details">{{profile.details}}</view><view class="fallback-intro">{{profile.introduction || '上福恋认真找对象'}}</view><view class="fallback-footer">{{posterError || profileError ? '海报暂未生成' : '正在准备你的个人海报'}}</view></view> <view v-else class="personal-fallback"><view class="fallback-brand"><image src="/pages/users/assets/fulian-logo.png" mode="aspectFit"></image><text>福恋</text></view><image v-if="profile.photo" class="fallback-photo" src="{{profile.photo}}" mode="aspectFit"></image><view v-else class="fallback-photo fallback-blank"><image src="/pages/users/assets/fulian-logo.png" mode="aspectFit"></image><text>期待认真相识</text></view><view class="fallback-name">{{profile.nickname || '我的卡片'}}</view><view class="fallback-details">{{profile.details}}</view><view class="fallback-intro">{{profile.introduction || '上福恋认真找对象'}}</view><view class="fallback-footer">{{posterError || profileError ? '海报暂未生成' : '正在准备你的个人海报'}}</view></view>
</view> </view>
<view v-if="generating || (profileLoading && !poster)" class="asset-status card-status">{{profileLoading ? '正在读取个人资料' : posterStage}}</view> <view v-if="generating || (profileLoading && !poster)" class="asset-status card-status">{{profileLoading ? '正在读取个人资料' : posterStage}}</view>
<view v-else-if="posterError || profileError" class="asset-error card-status"><text>{{posterError || profileError}}</text><button class="text-button" @tap="refreshPoster">重新生成</button></view> <view v-else-if="posterError || profileError" class="asset-error card-status"><text>{{posterError || profileError}}</text><button class="text-button" @tap="refreshPoster">重新生成</button></view>

View File

@ -1,6 +1,6 @@
import {boundedCall, fileAccess} from './sharing-runtime' import {boundedCall, fileAccess} from './sharing-runtime'
const VERSION = 'v5' const VERSION = 'v6'
const MAX_AGE = 24 * 60 * 60 * 1000 const MAX_AGE = 24 * 60 * 60 * 1000
const INDEX = 'fulian-sharing-cache-' + VERSION const INDEX = 'fulian-sharing-cache-' + VERSION

View File

@ -34,10 +34,18 @@ function textLines(ctx,text,x,y,width,lineHeight,limit) {
} }
ctx.fillText(line,x,y+row*lineHeight) ctx.fillText(line,x,y+row*lineHeight)
} }
export function photoPlacement(iw,ih,w,h){
// Keep the whole source visible: a center cover crop can remove faces,
// especially when a portrait/group photo is placed in a landscape slot.
const scale=Math.min(w/iw,h/ih)
const width=iw*scale,height=ih*scale
return {x:(w-width)/2,y:(h-height)/2,width,height}
}
function photoFill(ctx,image,x,y,w,h,r=0){ function photoFill(ctx,image,x,y,w,h,r=0){
ctx.save();if(r){roundRect(ctx,x,y,w,h,r);ctx.clip()} ctx.save();if(r){roundRect(ctx,x,y,w,h,r);ctx.clip()}
const scale=Math.max(w/image.width,h/image.height),sw=w/scale,sh=h/scale ctx.fillStyle='#FFF1F4';ctx.fillRect(x,y,w,h)
ctx.drawImage(image,(image.width-sw)/2,(image.height-sh)/2,sw,sh,x,y,w,h);ctx.restore() const fit=photoPlacement(image.width,image.height,w,h)
ctx.drawImage(image,0,0,image.width,image.height,x+fit.x,y+fit.y,fit.width,fit.height);ctx.restore()
} }
async function canvasFor(page,id,width,height,job){ async function canvasFor(page,id,width,height,job){
check(job) check(job)

View File

@ -84,3 +84,11 @@ Node v24.19.0:六组分享回归退出0。check-photo 增加随机排除当前
Node v16.20.2 在 .release-build/invitation-card-candidate 执行 tools/build/build.cjs,退出0,89模板编译完成。改动仍在功能分支,未更新 main 验收包。 Node v16.20.2 在 .release-build/invitation-card-candidate 执行 tools/build/build.cjs,退出0,89模板编译完成。改动仍在功能分支,未更新 main 验收包。
2026-11-03 用户确认后端修复生效但线上换图未变化。核实上一交付前端 main f82dab2 仍固定优选,随机换图代码尚在功能分支。本轮补齐前端交付,合并后从 main 编译;多张合格照片排除当前项,缓存保留新选择。 2026-11-03 用户确认后端修复生效但线上换图未变化。核实上一交付前端 main f82dab2 仍固定优选,随机换图代码尚在功能分支。本轮补齐前端交付,合并后从 main 编译;多张合格照片排除当前项,缓存保留新选择。
## 2026-11-03 个人照片防裁头适配
用户反馈横向照片框居中裁剪导致头部出框。本轮采用完整照片等比例适配:保留全部源图,按海报/分享封面尺寸自动缩放居中,空余处填充福恋浅玫瑰背景;加载预览也改为 aspectFit。未引入人脸检测,不声称识别人脸;此方案直接避免照片框裁掉源图中的头部,无法恢复原图本身已缺失的内容。缓存升级v6使旧裁剪海报失效。
Node24几何检查覆盖竖图/横图/方图在686×470和264×364目标框内等比例完整显示,通过;check-photo/check-cache/check-image-path退出0,diff检查通过。后端无本轮修改。分支 feature/card-photo-framing,未上传发布。
2026-11-03 用户确认生成正式验收包。照片适配功能分支编译退出0,按授权合并本地 main 后重新编译。制品目录 releases/20261103-card-photo-framing,未上传部署。

View File

@ -8,7 +8,7 @@ async function render(h,name='shareCard'){const page=h.page(name);page.hook('onR
h.files.delete(changed.cover);const missing=await render(h);assert(missing.poster!==changed.poster,'missing file must regenerate') h.files.delete(changed.cover);const missing=await render(h);assert(missing.poster!==changed.poster,'missing file must regenerate')
h.storage.set('token','synthetic-other-token');h.storage.set('user_id',43);h.state.profile.id=43 h.storage.set('token','synthetic-other-token');h.storage.set('user_id',43);h.state.profile.id=43
const other=await render(h);assert(other.poster.includes('-43-personal-'));assert(!other.poster.includes('-42-personal-')) const other=await render(h);assert(other.poster.includes('-43-personal-'));assert(!other.poster.includes('-42-personal-'))
const index=h.storage.get('fulian-sharing-cache-v5');index['43-personal'].savedAt=Date.now()-2*24*3600*1000;h.storage.set('fulian-sharing-cache-v5',index) const index=h.storage.get('fulian-sharing-cache-v6');index['43-personal'].savedAt=Date.now()-2*24*3600*1000;h.storage.set('fulian-sharing-cache-v6',index)
const expired=await render(h);assert(expired.poster!==other.poster) const expired=await render(h);assert(expired.poster!==other.poster)
h.flags.copyFail=true;const disk=await render(h,'myShare');assert(disk.poster&&!disk.cached);assert(!disk.generating) h.flags.copyFail=true;const disk=await render(h,'myShare');assert(disk.poster&&!disk.cached);assert(!disk.generating)
for(const flag of ['imageInfoHang','imageHang','canvasHang','exportHang','requestHang']){ for(const flag of ['imageInfoHang','imageHang','canvasHang','exportHang','requestHang']){