koishi-plugin-manosaba-text-box 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # koishi-plugin-manosaba-text-box
2
2
  一个基于 Resvg 和 的 wasm-vips 的 Koishi 自动化表情包生成插件,能够快速生成带有自定义文本的魔法少女的魔女裁判文本框图片。
3
3
 
4
+ **注意:该插件目前存在严重的性能问题,在图片处理时会短暂阻塞 Koishi 主线程运行,等待解决**
5
+
4
6
  # 用法
5
7
  ```
6
8
  mtb -c <chara> <text>
@@ -127,4 +129,4 @@ export function apply(ctx: Context) {
127
129
  背景、立绘等图片素材 © Re,AER LLC./Acacia
128
130
 
129
131
  # 致谢
130
- [oplivilqo/manosaba_text_box](https://github.com/oplivilqo/manosaba_text_box)
132
+ [oplivilqo/manosaba_text_box](https://github.com/oplivilqo/manosaba_text_box)
@@ -0,0 +1,13 @@
1
+ var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(e&&(t=e(e=0)),t),s=(e,n)=>{let r={};for(var i in e)t(r,i,{get:e[i],enumerable:!0});return n&&t(r,Symbol.toStringTag,{value:`Module`}),r},c=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},l=(n,r,a)=>(a=n==null?{}:e(i(n)),c(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));let u=require(`node:fs/promises`);u=l(u);let d=require(`node:path`);d=l(d);let f=require(`node:module`),p=require(`node:worker_threads`),m=require(`node:url`),h=require(`@resvg/resvg-wasm`),g=require(`wasm-vips`);g=l(g);let _=require(`entities`),v=require(`node:crypto`),y=require(`jieba-wasm`),b=require(`js-yaml`);b=l(b);function x(e){if(e.length<=1)return[...e];let t=[...e],n=new Uint32Array(t.length);v.webcrypto.getRandomValues(n);for(let e=t.length-1;e>0;e--){let r=n[e]%(e+1);[t[e],t[r]]=[t[r],t[e]]}return t}var S=s({loadYaml:()=>C});async function C(e){let t=await u.readFile(e,`utf-8`);return b.load(t)}var w=o((()=>{}));const T=typeof __dirname<`u`?__dirname:d.dirname((0,m.fileURLToPath)(require(`url`).pathToFileURL(__filename).href)),E=typeof __dirname<`u`&&typeof require<`u`?require:(0,f.createRequire)(require(`url`).pathToFileURL(__filename).href),D={debug(e,t){O(`debug`,e,t)},info(e,t){O(`info`,e,t)},warn(e,t){O(`warn`,e,t)},error(e,t){O(`error`,e,t)}};function O(e,t,n){p.parentPort?.postMessage({type:`log`,level:e,message:t,meta:n})}let k=null;function A(){return k||=(0,g.default)({dynamicLibraries:[`vips-heif.wasm`]}).then(e=>(e.concurrency(1),e.Cache.max(0),D.debug(`wasm-vips initialized with AVIF support`),e)),k}const j=global.__manosaba_resvg_state||{initialized:!1,initializing:null};global.__manosaba_resvg_state||(global.__manosaba_resvg_state=j);let M=null;async function N(){return M||=await A(),M}async function P(){if(!j.initialized){if(j.initializing){await j.initializing;return}j.initializing=(async()=>{try{let e=E.resolve(`@resvg/resvg-wasm/index_bg.wasm`);await(0,h.initWasm)(await u.readFile(e)),j.initialized=!0,D.debug(`Resvg initialized successfully`)}catch(e){if(e instanceof Error&&e.message.includes(`Already initialized`))j.initialized=!0,D.debug(`Resvg was already initialized`);else throw D.error(`Failed to initialize resvg`,{err:e}),j.initializing=null,e}})(),await j.initializing}}let F=``,I={},L={};const R=new Map,z=new Map;let B=0;const V=[[728,355],[2500,800]],ee=160;function H(){return Object.entries(I).map(([e,t])=>({id:e,name:t.full_name}))}async function U(e){let t=e||T;F=d.join(t,`assets`);let n=d.join(t,`config`),r=d.join(n,`chara_meta.yml`),i=d.join(n,`text_configs.yml`);try{let{loadYaml:e}=await Promise.resolve().then(()=>(w(),S)),[t,n,a]=await Promise.all([e(r),e(i),u.readdir(d.join(F,`background`))]);I=t.mahoshojo||{},L=n.text_configs||{},B=a.filter(e=>e.startsWith(`c`)&&e.endsWith(`.avif`)).length,D.debug(`Loaded character meta and text configs`,{characters:Object.keys(I).length,textConfigs:Object.keys(L).length,backgrounds:B})}catch(e){D.error(`Failed to load config files`,{err:e})}return{characters:H()}}async function W(e){let t=R.get(e);if(t)return t;let n=await u.readFile(e);return R.set(e,n),n}async function G(e){let t=z.get(e);if(t)return t;let n=await u.readFile(e);return z.size<20&&z.set(e,n),n}async function K(){if(B===0){let e=d.join(F,`background`);B=(await u.readdir(e)).filter(e=>e.startsWith(`c`)&&e.endsWith(`.avif`)).length}return x(Array.from({length:B},(e,t)=>t+1))[0]}function q(e){let t=I[e];return t?x(Array.from({length:t.emotion_count},(e,t)=>t+1))[0]:1}async function te(e,t,n){let r=await N(),i=null,a=null,o=null;try{let s=d.join(F,`background`,`c${t}.avif`),c=d.join(F,`chara`,e,`${e} (${n}).avif`);D.debug(`Loading images`,{backgroundPath:s,characterPath:c});let[l,u]=await Promise.all([G(s),G(c)]);if([i,a]=await Promise.all([Promise.resolve(r.Image.newFromBuffer(l)),Promise.resolve(r.Image.newFromBuffer(u))]),o=i.composite2(a,`over`,{x:0,y:134}),L[e]){await P();let t=I[e]?.font||`font3.ttf`,n=await W(d.join(F,`fonts`,t)),i=await Promise.all(L[e].filter(e=>e.text).map(async e=>{let t=e.text.length,r=e.font_size*t*1.2+20,i=e.font_size*1.5+10,a=e.font_size*1.2,o=(0,_.encodeXML)(e.text);return{buffer:new h.Resvg(`
2
+ <svg width="${r}" height="${i}" xmlns="http://www.w3.org/2000/svg">
3
+ <text x="2" y="${a+2}" font-size="${e.font_size}" fill="#000000" font-family="CustomFont">${o}</text>
4
+ <text x="0" y="${a}" font-size="${e.font_size}" fill="rgb(${e.font_color.join(`,`)})" font-family="CustomFont">${o}</text>
5
+ </svg>
6
+ `,{fitTo:{mode:`original`},font:{fontBuffers:[n]}}).render().asPng(),position:e.position}}));for(let e of i){let t=r.Image.newFromBuffer(e.buffer),n=o.composite2(t,`over`,{x:e.position[0],y:e.position[1]});try{o[Symbol.dispose]()}catch{}o=n;try{t[Symbol.dispose]()}catch{}}}let f=o.writeToBuffer(`.avif`,{Q:100});return Buffer.from(f)}finally{if(o)try{o[Symbol.dispose]()}catch{}if(a)try{a[Symbol.dispose]()}catch{}if(i)try{i[Symbol.dispose]()}catch{}}}function J(e){let t=e.charCodeAt(0);return t>=19968&&t<=40959||t>=12352&&t<=12543||t>=65280&&t<=65519||t>=12288&&t<=12351}function Y(e){return/[-—–.,;:!?"'`~()[\]{}<>/\\|]/.test(e)}function X(e,t){return/\s/.test(e)?t*.25:J(e)?t:Y(e)?t*.35:t*.5}function Z(e,t){let n=0;for(let r of e)n+=X(r,t);return n}const ne=30,re=10,ie=10,ae=6,oe=6,Q=1.2;async function $(e,t,n){let r=Math.max(0,t-12)*.9,i=[],a=e.split(/\n/);for(let e=0;e<a.length;e++){let t=a[e];if(t.trim().length===0){i.push(``);continue}let o=(0,y.cut)(t),s=``,c=0;for(let e=0;e<o.length;e++){let t=o[e],a=Z(t,n),l=s&&!/^[\s\u3000-\u303f\uff00-\uffef]/.test(t)&&!J(t[0])&&!J(s[s.length-1]),u=l?X(` `,n):0;if(c+u+a<=r)l&&(s+=` `),s+=t,c+=u+a;else if(a>r){s&&(i.push(s),s=``,c=0);let e=``,a=0;for(let o of t){let t=X(o,n);a+t>r?(i.push(e),e=o,a=t):(e+=o,a+=t)}s=e,c=a}else s&&i.push(s),s=t,c=a}s&&i.push(s)}return i.length===0&&e&&i.push(e),i}async function se(e,t,n,r,i=`#FFFFFF`){D.debug(`generateTextSvg called`,{text:e,width:t,fontSize:n,color:i});let a=await $(e,t,n);D.debug(`Text lines after wrapping`,{lines:a,linesCount:a.length});let o=n*Q,s=`CustomFont`,c=Math.max(...a.map(e=>Z(e,n)));return`
7
+ <svg width="${Math.max(Math.ceil(c)+12,Math.ceil(t))}" height="${Math.ceil(12+a.length*o)}" xmlns="http://www.w3.org/2000/svg">
8
+ ${a.map((e,t)=>{let r=6+n+t*o,a=(0,_.encodeXML)(e);return`
9
+ <text x="8" y="${r+2}" font-size="${n}" fill="#000000" fill-opacity="0.5" font-family="${s}">${a}</text>
10
+ <text x="6" y="${r}" font-size="${n}" fill="${i}" font-family="${s}">${a}</text>
11
+ `}).join(``)}
12
+ </svg>
13
+ `}async function ce(e,t,n,r,i){D.debug(`drawUserText called`,{text:t,textLength:t.length,initialFontSize:r,fontPath:i,boxRect:n});let a=null,o=null,s=null;try{let[c]=await Promise.all([N(),P()]);a=c.Image.newFromBuffer(e),D.debug(`Base image loaded to vips`,{width:a.width,height:a.height,bands:a.bands}),a.hasAlpha()||(a=a.bandjoin(255),D.debug(`Added alpha channel to base image`));let[[l,u],[d,f]]=n,p=d-l,m=f-u,g=Math.max(0,p-30-10),_=Math.max(0,m-20),v=await W(i);D.debug(`Font file loaded`,{fontPath:i,size:v.length});let y=r,b=``,x=async e=>{let n=await $(t,g,e),r=e*Q;return 12+n.length*r<=_},S=40,C=r,w=S;for(;S<=C;){let e=Math.floor((S+C)/2);await x(e)?(w=e,S=e+1):C=e-1}y=w,D.debug(`Auto-adjusted font size`,{originalSize:r,adjustedSize:y,textLength:t.length}),b=await se(t,g,y,i),D.debug(`Generated SVG`,{svgLength:b.length,fontPath:i}),D.debug(`SVG content:`,{svg:b});let T=new h.Resvg(b,{fitTo:{mode:`original`},font:{fontBuffers:[v]}}).render(),E=T.asPng();D.debug(`Rendered text image`,{width:T.width,height:T.height,bufferSize:E.length}),o=c.Image.newFromBuffer(E),D.debug(`Text image loaded to vips`,{width:o.width,height:o.height,bands:o.bands,hasAlpha:o.hasAlpha()}),o.hasAlpha()||(o=o.bandjoin(255),D.debug(`Added alpha channel to text image`));let O=l+30,k=u+10;D.debug(`Compositing text`,{baseWidth:a.width,baseHeight:a.height,imageBands:a.bands,textWidth:o.width,textHeight:o.height,textBands:o.bands,boxWidth:p,boxHeight:m,textX:O,textY:k}),s=a.composite2(o,`over`,{x:O,y:k}),D.debug(`Composite2 completed`,{resultWidth:s.width,resultHeight:s.height,resultBands:s.bands});let A=s.writeToBuffer(`.png`,{compression:9});return D.debug(`Text drawing completed successfully`,{outputSize:A.length}),Buffer.from(A)}catch(t){return D.error(`Failed to draw text`,{err:t}),e}finally{if(s)try{s[Symbol.dispose]()}catch{}if(o)try{o[Symbol.dispose]()}catch{}if(a)try{a[Symbol.dispose]()}catch{}}}async function le(e,t,n,r){if(!I[e])throw Error(`Unknown character: ${e}`);let i=n??await K(),a=r??q(e);D.debug(`Generating text box image`,{character:e,backgroundIndex:i,emotionIndex:a,textLength:t.length});let o=await te(e,i,a),s=I[e]?.font||`font3.ttf`;return await ce(o,t,V,160,d.join(F,`fonts`,s))}function ue(e){return e instanceof Error?{message:e.message,stack:e.stack}:{message:String(e)}}async function de(e){try{if(e.type===`initAssets`){let t=await U(e.payload.basePath);return{id:e.id,ok:!0,result:t}}if(e.type===`getAvailableCharacters`)return{id:e.id,ok:!0,result:H()};if(e.type===`generateTextBoxImage`){let t=await le(e.payload.character,e.payload.text,e.payload.backgroundIndex,e.payload.emotionIndex);return{id:e.id,ok:!0,result:t}}let t=e;return{id:t.id,ok:!1,error:{message:`Unknown request type: ${t.type}`}}}catch(t){return{id:e.id,ok:!1,error:ue(t)}}}p.parentPort?.on(`message`,async e=>{let t=await de(e);p.parentPort?.postMessage(t)});
@@ -0,0 +1,13 @@
1
+ import*as e from"node:fs/promises";import*as t from"node:path";import{createRequire as n}from"node:module";import{parentPort as r}from"node:worker_threads";import{fileURLToPath as i}from"node:url";import{Resvg as a,initWasm as o}from"@resvg/resvg-wasm";import s from"wasm-vips";import{encodeXML as c}from"entities";import{webcrypto as l}from"node:crypto";import{cut as u}from"jieba-wasm";import*as d from"js-yaml";var f=Object.defineProperty,p=(e,t)=>()=>(e&&(t=e(e=0)),t),m=(e,t)=>{let n={};for(var r in e)f(n,r,{get:e[r],enumerable:!0});return t&&f(n,Symbol.toStringTag,{value:`Module`}),n},h=(e=>typeof require<`u`?require:typeof Proxy<`u`?new Proxy(e,{get:(e,t)=>(typeof require<`u`?require:e)[t]}):e)(function(e){if(typeof require<`u`)return require.apply(this,arguments);throw Error('Calling `require` for "'+e+"\" in an environment that doesn't expose the `require` function.")});function g(e){if(e.length<=1)return[...e];let t=[...e],n=new Uint32Array(t.length);l.getRandomValues(n);for(let e=t.length-1;e>0;e--){let r=n[e]%(e+1);[t[e],t[r]]=[t[r],t[e]]}return t}var _=m({loadYaml:()=>v});async function v(t){let n=await e.readFile(t,`utf-8`);return d.load(n)}var y=p((()=>{}));const b=typeof __dirname<`u`?__dirname:t.dirname(i(import.meta.url)),x=typeof __dirname<`u`&&h!==void 0?h:n(import.meta.url),S={debug(e,t){C(`debug`,e,t)},info(e,t){C(`info`,e,t)},warn(e,t){C(`warn`,e,t)},error(e,t){C(`error`,e,t)}};function C(e,t,n){r?.postMessage({type:`log`,level:e,message:t,meta:n})}let w=null;function T(){return w||=s({dynamicLibraries:[`vips-heif.wasm`]}).then(e=>(e.concurrency(1),e.Cache.max(0),S.debug(`wasm-vips initialized with AVIF support`),e)),w}const E=global.__manosaba_resvg_state||{initialized:!1,initializing:null};global.__manosaba_resvg_state||(global.__manosaba_resvg_state=E);let D=null;async function O(){return D||=await T(),D}async function k(){if(!E.initialized){if(E.initializing){await E.initializing;return}E.initializing=(async()=>{try{let t=x.resolve(`@resvg/resvg-wasm/index_bg.wasm`);await o(await e.readFile(t)),E.initialized=!0,S.debug(`Resvg initialized successfully`)}catch(e){if(e instanceof Error&&e.message.includes(`Already initialized`))E.initialized=!0,S.debug(`Resvg was already initialized`);else throw S.error(`Failed to initialize resvg`,{err:e}),E.initializing=null,e}})(),await E.initializing}}let A=``,j={},M={};const N=new Map,P=new Map;let F=0;const I=[[728,355],[2500,800]];function L(){return Object.entries(j).map(([e,t])=>({id:e,name:t.full_name}))}async function R(n){let r=n||b;A=t.join(r,`assets`);let i=t.join(r,`config`),a=t.join(i,`chara_meta.yml`),o=t.join(i,`text_configs.yml`);try{let{loadYaml:n}=await Promise.resolve().then(()=>(y(),_)),[r,i,s]=await Promise.all([n(a),n(o),e.readdir(t.join(A,`background`))]);j=r.mahoshojo||{},M=i.text_configs||{},F=s.filter(e=>e.startsWith(`c`)&&e.endsWith(`.avif`)).length,S.debug(`Loaded character meta and text configs`,{characters:Object.keys(j).length,textConfigs:Object.keys(M).length,backgrounds:F})}catch(e){S.error(`Failed to load config files`,{err:e})}return{characters:L()}}async function z(t){let n=N.get(t);if(n)return n;let r=await e.readFile(t);return N.set(t,r),r}async function B(t){let n=P.get(t);if(n)return n;let r=await e.readFile(t);return P.size<20&&P.set(t,r),r}async function V(){if(F===0){let n=t.join(A,`background`);F=(await e.readdir(n)).filter(e=>e.startsWith(`c`)&&e.endsWith(`.avif`)).length}return g(Array.from({length:F},(e,t)=>t+1))[0]}function H(e){let t=j[e];return t?g(Array.from({length:t.emotion_count},(e,t)=>t+1))[0]:1}async function U(e,n,r){let i=await O(),o=null,s=null,l=null;try{let u=t.join(A,`background`,`c${n}.avif`),d=t.join(A,`chara`,e,`${e} (${r}).avif`);S.debug(`Loading images`,{backgroundPath:u,characterPath:d});let[f,p]=await Promise.all([B(u),B(d)]);if([o,s]=await Promise.all([Promise.resolve(i.Image.newFromBuffer(f)),Promise.resolve(i.Image.newFromBuffer(p))]),l=o.composite2(s,`over`,{x:0,y:134}),M[e]){await k();let n=j[e]?.font||`font3.ttf`,r=await z(t.join(A,`fonts`,n)),o=await Promise.all(M[e].filter(e=>e.text).map(async e=>{let t=e.text.length,n=e.font_size*t*1.2+20,i=e.font_size*1.5+10,o=e.font_size*1.2,s=c(e.text);return{buffer:new a(`
2
+ <svg width="${n}" height="${i}" xmlns="http://www.w3.org/2000/svg">
3
+ <text x="2" y="${o+2}" font-size="${e.font_size}" fill="#000000" font-family="CustomFont">${s}</text>
4
+ <text x="0" y="${o}" font-size="${e.font_size}" fill="rgb(${e.font_color.join(`,`)})" font-family="CustomFont">${s}</text>
5
+ </svg>
6
+ `,{fitTo:{mode:`original`},font:{fontBuffers:[r]}}).render().asPng(),position:e.position}}));for(let e of o){let t=i.Image.newFromBuffer(e.buffer),n=l.composite2(t,`over`,{x:e.position[0],y:e.position[1]});try{l[Symbol.dispose]()}catch{}l=n;try{t[Symbol.dispose]()}catch{}}}let m=l.writeToBuffer(`.avif`,{Q:100});return Buffer.from(m)}finally{if(l)try{l[Symbol.dispose]()}catch{}if(s)try{s[Symbol.dispose]()}catch{}if(o)try{o[Symbol.dispose]()}catch{}}}function W(e){let t=e.charCodeAt(0);return t>=19968&&t<=40959||t>=12352&&t<=12543||t>=65280&&t<=65519||t>=12288&&t<=12351}function G(e){return/[-—–.,;:!?"'`~()[\]{}<>/\\|]/.test(e)}function K(e,t){return/\s/.test(e)?t*.25:W(e)?t:G(e)?t*.35:t*.5}function q(e,t){let n=0;for(let r of e)n+=K(r,t);return n}const J=1.2;async function Y(e,t,n){let r=Math.max(0,t-12)*.9,i=[],a=e.split(/\n/);for(let e=0;e<a.length;e++){let t=a[e];if(t.trim().length===0){i.push(``);continue}let o=u(t),s=``,c=0;for(let e=0;e<o.length;e++){let t=o[e],a=q(t,n),l=s&&!/^[\s\u3000-\u303f\uff00-\uffef]/.test(t)&&!W(t[0])&&!W(s[s.length-1]),u=l?K(` `,n):0;if(c+u+a<=r)l&&(s+=` `),s+=t,c+=u+a;else if(a>r){s&&(i.push(s),s=``,c=0);let e=``,a=0;for(let o of t){let t=K(o,n);a+t>r?(i.push(e),e=o,a=t):(e+=o,a+=t)}s=e,c=a}else s&&i.push(s),s=t,c=a}s&&i.push(s)}return i.length===0&&e&&i.push(e),i}async function X(e,t,n,r,i=`#FFFFFF`){S.debug(`generateTextSvg called`,{text:e,width:t,fontSize:n,color:i});let a=await Y(e,t,n);S.debug(`Text lines after wrapping`,{lines:a,linesCount:a.length});let o=n*J,s=`CustomFont`,l=Math.max(...a.map(e=>q(e,n)));return`
7
+ <svg width="${Math.max(Math.ceil(l)+12,Math.ceil(t))}" height="${Math.ceil(12+a.length*o)}" xmlns="http://www.w3.org/2000/svg">
8
+ ${a.map((e,t)=>{let r=6+n+t*o,a=c(e);return`
9
+ <text x="8" y="${r+2}" font-size="${n}" fill="#000000" fill-opacity="0.5" font-family="${s}">${a}</text>
10
+ <text x="6" y="${r}" font-size="${n}" fill="${i}" font-family="${s}">${a}</text>
11
+ `}).join(``)}
12
+ </svg>
13
+ `}async function Z(e,t,n,r,i){S.debug(`drawUserText called`,{text:t,textLength:t.length,initialFontSize:r,fontPath:i,boxRect:n});let o=null,s=null,c=null;try{let[l]=await Promise.all([O(),k()]);o=l.Image.newFromBuffer(e),S.debug(`Base image loaded to vips`,{width:o.width,height:o.height,bands:o.bands}),o.hasAlpha()||(o=o.bandjoin(255),S.debug(`Added alpha channel to base image`));let[[u,d],[f,p]]=n,m=f-u,h=p-d,g=Math.max(0,m-30-10),_=Math.max(0,h-20),v=await z(i);S.debug(`Font file loaded`,{fontPath:i,size:v.length});let y=r,b=``,x=async e=>{let n=await Y(t,g,e),r=e*J;return 12+n.length*r<=_},C=40,w=r,T=C;for(;C<=w;){let e=Math.floor((C+w)/2);await x(e)?(T=e,C=e+1):w=e-1}y=T,S.debug(`Auto-adjusted font size`,{originalSize:r,adjustedSize:y,textLength:t.length}),b=await X(t,g,y,i),S.debug(`Generated SVG`,{svgLength:b.length,fontPath:i}),S.debug(`SVG content:`,{svg:b});let E=new a(b,{fitTo:{mode:`original`},font:{fontBuffers:[v]}}).render(),D=E.asPng();S.debug(`Rendered text image`,{width:E.width,height:E.height,bufferSize:D.length}),s=l.Image.newFromBuffer(D),S.debug(`Text image loaded to vips`,{width:s.width,height:s.height,bands:s.bands,hasAlpha:s.hasAlpha()}),s.hasAlpha()||(s=s.bandjoin(255),S.debug(`Added alpha channel to text image`));let A=u+30,j=d+10;S.debug(`Compositing text`,{baseWidth:o.width,baseHeight:o.height,imageBands:o.bands,textWidth:s.width,textHeight:s.height,textBands:s.bands,boxWidth:m,boxHeight:h,textX:A,textY:j}),c=o.composite2(s,`over`,{x:A,y:j}),S.debug(`Composite2 completed`,{resultWidth:c.width,resultHeight:c.height,resultBands:c.bands});let M=c.writeToBuffer(`.png`,{compression:9});return S.debug(`Text drawing completed successfully`,{outputSize:M.length}),Buffer.from(M)}catch(t){return S.error(`Failed to draw text`,{err:t}),e}finally{if(c)try{c[Symbol.dispose]()}catch{}if(s)try{s[Symbol.dispose]()}catch{}if(o)try{o[Symbol.dispose]()}catch{}}}async function Q(e,n,r,i){if(!j[e])throw Error(`Unknown character: ${e}`);let a=r??await V(),o=i??H(e);S.debug(`Generating text box image`,{character:e,backgroundIndex:a,emotionIndex:o,textLength:n.length});let s=await U(e,a,o),c=j[e]?.font||`font3.ttf`;return await Z(s,n,I,160,t.join(A,`fonts`,c))}function $(e){return e instanceof Error?{message:e.message,stack:e.stack}:{message:String(e)}}async function ee(e){try{if(e.type===`initAssets`){let t=await R(e.payload.basePath);return{id:e.id,ok:!0,result:t}}if(e.type===`getAvailableCharacters`)return{id:e.id,ok:!0,result:L()};if(e.type===`generateTextBoxImage`){let t=await Q(e.payload.character,e.payload.text,e.payload.backgroundIndex,e.payload.emotionIndex);return{id:e.id,ok:!0,result:t}}let t=e;return{id:t.id,ok:!1,error:{message:`Unknown request type: ${t.type}`}}}catch(t){return{id:e.id,ok:!1,error:$(t)}}}r?.on(`message`,async e=>{let t=await ee(e);r?.postMessage(t)});
package/lib/index.cjs CHANGED
@@ -1,14 +1,2 @@
1
- var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));let c=require(`koishi`),l=require(`node:fs/promises`);l=s(l);let u=require(`node:path`);u=s(u);let d=require(`js-yaml`);d=s(d);let f=require(`wasm-vips`);f=s(f);let p=require(`@resvg/resvg-wasm`),m=require(`node:crypto`),h=require(`entities`);const g={};let _=-1;function v(e,t=`manosaba-text-box`){let n=g[t]||e.logger(t);return _>=0&&(n.level=_),g[t]=n,n}function y(e){for(let t in _=e,g)g[t].level=e}async function b(e){let t=await l.readFile(e,`utf-8`);return d.load(t)}function x(e){if(e.length<=1)return[...e];let t=[...e],n=new Uint32Array(t.length);m.webcrypto.getRandomValues(n);for(let e=t.length-1;e>0;e--){let r=n[e]%(e+1);[t[e],t[r]]=[t[r],t[e]]}return t}function S(){return new Promise(e=>setImmediate(e))}let C=null;function w(e){return C||=(0,f.default)({dynamicLibraries:[`vips-heif.wasm`]}).then(t=>(t.concurrency(1),t.Cache.max(0),e.logger.debug(`wasm-vips initialized with AVIF support`),t)),C}const T=global.__manosaba_resvg_state||{initialized:!1,initializing:null};global.__manosaba_resvg_state||(global.__manosaba_resvg_state=T);let E=null;async function D(e){return E||=await w(e),E}async function O(e){if(!T.initialized){if(T.initializing){await T.initializing;return}T.initializing=(async()=>{try{let t=require.resolve(`@resvg/resvg-wasm/index_bg.wasm`);await(0,p.initWasm)(await l.readFile(t)),T.initialized=!0,e.logger.debug(`Resvg initialized successfully`)}catch(t){if(t instanceof Error&&t.message.includes(`Already initialized`))T.initialized=!0,e.logger.debug(`Resvg was already initialized`);else throw e.logger.error(`Failed to initialize resvg`,{err:t}),T.initializing=null,t}})(),await T.initializing}}let k,A={},j={};const M=[[728,355],[2339,800]],N=120;function P(){return Object.entries(A).map(([e,t])=>({id:e,name:t.full_name}))}async function F(e,t){k=u.join(t,`assets`);let n=u.join(t,`config`),r=u.join(n,`chara_meta.yml`),i=u.join(n,`text_configs.yml`);try{A=(await b(r)).mahoshojo||{},j=(await b(i)).text_configs||{},e.logger.debug(`Loaded character meta and text configs`,{characters:Object.keys(A).length,textConfigs:Object.keys(j).length})}catch(t){e.logger.error(`Failed to load config files`,{err:t})}}async function I(){let e=u.join(k,`background`),t=(await l.readdir(e)).filter(e=>e.startsWith(`c`)&&e.endsWith(`.avif`));return x(Array.from({length:t.length},(e,t)=>t+1))[0]}function L(e){let t=A[e];return t?x(Array.from({length:t.emotion_count},(e,t)=>t+1))[0]:1}async function R(e,t,n,r){let i=await D(e),a=null,o=null,s=null;try{let c=u.join(k,`background`,`c${n}.avif`);e.logger.debug(`Loading background`,{backgroundPath:c});let d=await l.readFile(c);await S(),a=i.Image.newFromBuffer(d),await S();let f=u.join(k,`chara`,t,`${t} (${r}).avif`);e.logger.debug(`Loading character`,{characterPath:f});let m=await l.readFile(f);if(await S(),o=i.Image.newFromBuffer(m),await S(),s=a.composite2(o,`over`,{x:0,y:134}),await S(),j[t]){await O(e);let n=A[t]?.font||`font3.ttf`,r=u.join(k,`fonts`,n),a=await l.readFile(r);for(let e of j[t]){if(!e.text)continue;await S();let t=e.text.length,n=e.font_size*t*1.2+20,r=e.font_size*1.5+10,o=e.font_size*1.2,c=(0,h.encodeXML)(e.text),l=new p.Resvg(`
2
- <svg width="${n}" height="${r}" xmlns="http://www.w3.org/2000/svg">
3
- <text x="2" y="${o+2}" font-size="${e.font_size}" fill="#000000" font-family="CustomFont">${c}</text>
4
- <text x="0" y="${o}" font-size="${e.font_size}" fill="rgb(${e.font_color.join(`,`)})" font-family="CustomFont">${c}</text>
5
- </svg>
6
- `,{fitTo:{mode:`original`},font:{fontBuffers:[a]}}).render();await S();let u=l.asPng();await S();let d=i.Image.newFromBuffer(u);await S();let f=s.composite2(d,`over`,{x:e.position[0],y:e.position[1]});await S();try{s[Symbol.dispose]()}catch{}s=f;try{d[Symbol.dispose]()}catch{}}}await S();let g=s.writeToBuffer(`.avif`,{Q:100});return await S(),Buffer.from(g)}finally{if(s)try{s[Symbol.dispose]()}catch{}if(o)try{o[Symbol.dispose]()}catch{}if(a)try{a[Symbol.dispose]()}catch{}}}function z(e,t,n,r,i,a=`#FFFFFF`){e.logger.debug(`generateTextSvg called`,{text:t,width:n,fontSize:r,color:a});let o=[],s=Math.floor(n/r),c=``;for(let e of t)c.length>=s?(o.push(c),c=e):c+=e;c&&o.push(c),e.logger.debug(`Text lines after wrapping`,{lines:o,linesCount:o.length,maxCharsPerLine:s});let l=r*1.2,u=`CustomFont`;return`
7
- <svg width="${Math.max(...o.map(e=>e.length))*r+4}" height="${o.length*l+r*.3}" xmlns="http://www.w3.org/2000/svg">
8
- ${o.map((e,t)=>{let n=r+t*l,i=(0,h.encodeXML)(e);return`
9
- <text x="2" y="${n+2}" font-size="${r}" fill="#000000" fill-opacity="0.5" font-family="${u}">${i}</text>
10
- <text x="0" y="${n}" font-size="${r}" fill="${a}" font-family="${u}">${i}</text>
11
- `}).join(``)}
12
- </svg>
13
- `}async function B(e,t,n,r,i,a){e.logger.debug(`drawUserText called`,{text:n,textLength:n.length,initialFontSize:i,fontPath:a,boxRect:r});let o=await D(e),s=null,c=null,u=null;try{await O(e),await S(),s=o.Image.newFromBuffer(t),await S(),e.logger.debug(`Base image loaded to vips`,{width:s.width,height:s.height,bands:s.bands}),s.hasAlpha()||(s=s.bandjoin(255),await S(),e.logger.debug(`Added alpha channel to base image`));let[[d,f],[m,h]]=r,g=m-d,_=h-f,v=await l.readFile(a);await S(),e.logger.debug(`Font file loaded`,{fontPath:a,size:v.length});let y=i,b=``,x=0;for(let t=y;t>=24;t-=6){let r=Math.floor(g/t),a=Math.ceil(n.length/r);if(x=t*1.2*a+t*.3,x<=_){y=t,e.logger.debug(`Auto-adjusted font size`,{originalSize:i,adjustedSize:y,textLength:n.length,lineCount:a,maxCharsPerLine:r,svgHeight:x,boxHeight:_});break}}b=z(e,n,g,y,a),await S(),e.logger.debug(`Generated SVG`,{svgLength:b.length,fontPath:a}),e.logger.debug(`SVG content:`,b);let C=new p.Resvg(b,{fitTo:{mode:`original`},font:{fontBuffers:[v]}}).render();await S();let w=C.asPng();await S(),e.logger.debug(`Rendered text image`,{width:C.width,height:C.height,bufferSize:w.length}),c=o.Image.newFromBuffer(w),await S(),e.logger.debug(`Text image loaded to vips`,{width:c.width,height:c.height,bands:c.bands,hasAlpha:c.hasAlpha()}),c.hasAlpha()||(c=c.bandjoin(255),await S(),e.logger.debug(`Added alpha channel to text image`));let T=d+20,E=f+20;e.logger.debug(`Compositing text`,{baseWidth:s.width,baseHeight:s.height,imageBands:s.bands,textWidth:c.width,textHeight:c.height,textBands:c.bands,boxWidth:g,boxHeight:_,textX:T,textY:E}),u=s.composite2(c,`over`,{x:T,y:E}),await S(),e.logger.debug(`Composite2 completed`,{resultWidth:u.width,resultHeight:u.height,resultBands:u.bands}),await S();let D=u.writeToBuffer(`.png`,{compression:9});return e.logger.debug(`Text drawing completed successfully`,{outputSize:D.length}),Buffer.from(D)}catch(n){return e.logger.error(`Failed to draw text`,{err:n}),t}finally{if(u)try{u[Symbol.dispose]()}catch{}if(c)try{c[Symbol.dispose]()}catch{}if(s)try{s[Symbol.dispose]()}catch{}}}async function V(e,t,n,r,i,a){if(!A[t])throw Error(`Unknown character: ${t}`);let o=i??await I(),s=a??L(t);e.logger.debug(`Generating text box image`,{character:t,backgroundIndex:o,emotionIndex:s,textLength:n.length});let c=await R(e,t,o,s),l=A[t]?.font||`font3.ttf`;return await B(e,c,n,M,120,u.join(k,`fonts`,l))}var H=class extends c.Service{constructor(e,t){super(e,`manosaba`,!0),this.config=t}async generateImage(e){let{character:t=this.config.defaultCharacter,text:n,backgroundIndex:r,emotionIndex:i}=e;if(!n||n.trim().length===0)throw Error(`文本内容不能为空`);try{return await V(this.ctx,t,n,this.config,r,i)}catch(e){throw this.ctx.logger.error(`生成图片失败`,{error:e}),e}}getCharacters(){return P()}};const U=c.Schema.object({defaultCharacter:c.Schema.union([`ema`,`hiro`,`sherri`,`hanna`,`nanoka`,`noa`,`miria`,`yuki`,`coco`,`meruru`,`reia`,`warden`,`mago`,`alisa`,`anan`]).default(`ema`).description(`默认使用的角色`),isLog:c.Schema.boolean().default(!1).description(`是否输出 debug 日志`)});let W;const G=`manosaba-text-box`;function K(e,t){W=v(e),q(t),e.on(`ready`,async()=>{await F(e,__dirname)}),e.plugin(H,t),e.command(`mtb <text:text>`,`生成魔女裁判文本框图片`).option(`character`,`-c <character:string>`,{fallback:t.defaultCharacter}).action(async({session:n,options:r},i)=>{if(!i||i.trim()===``)return`请输入要生成的文本内容`;try{let a=await V(e,r.character,i,t);await n.send(c.h.image(a,`image/png`))}catch(e){return W.error(`生成图片失败`,{err:e}),`生成图片失败: ${e.message}`}}),e.command(`mtb.list`,`列出所有可用的角色`).action(()=>{let e=P();return e.length===0?`暂无可用角色`:`可用角色列表:\n${e.map(e=>`${e.id}: ${e.name}`).join(`
14
- `)}\n\n使用方法:mtb -c <角色ID> <文本内容>`})}function q(e){e.isLog&&y(c.Logger.DEBUG)}exports.Config=U,exports.ManosabaTextBoxService=H,exports.apply=K,Object.defineProperty(exports,`logger`,{enumerable:!0,get:function(){return W}}),exports.name=`manosaba-text-box`;
1
+ var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));let c=require(`koishi`),l=require(`node:path`);l=s(l);let u=require(`node:worker_threads`);const d={};let f=-1;function p(e,t=`manosaba-text-box`){let n=d[t]||e.logger(t);return f>=0&&(n.level=f),d[t]=n,n}function m(e){for(let t in f=e,d)d[t].level=e}let h=null,g=null,_=[],v=1;const y=new Map;function b(e){g=e}function x(){return typeof __dirname<`u`?{url:l.join(__dirname,`core.worker.cjs`)}:{url:new URL(`./core.worker.mjs`,require(`url`).pathToFileURL(__filename).href),type:`module`}}function S(){return new u.Worker(x().url)}function C(e){if(typeof e==`object`&&e&&`type`in e&&e.type===`log`){if(!g){console.warn(`[manosaba-text-box] Worker log without logger:`,e.level,e.message);return}g[e.level](e.message,e.meta);return}if(!e||typeof e!=`object`||!(`id`in e))return;let t=y.get(e.id);if(t)if(y.delete(e.id),e.ok)t.resolve(e.result);else{let n=e,r=Error(n.error.message);r.stack=n.error.stack,t.reject(r)}}async function w(e){return h||(h=new Promise((e,t)=>{try{let t=S();t.on(`message`,C),t.on(`error`,e=>{g?.error(`worker error`,{err:e})}),t.on(`exit`,e=>{e!==0&&g?.error(`worker exited unexpectedly`,{code:e});for(let{reject:e}of y.values())e(Error(`worker exited`));y.clear(),h=null}),e(t)}catch(e){h=null,t(e)}}),h)}async function T(e,t){let n=await w(e);return new Promise((e,r)=>{let i=v++;y.set(i,{resolve:e,reject:r}),n.postMessage({...t,id:i})})}function E(){return _}async function D(e,t){_=(await T(e,{type:`initAssets`,payload:{basePath:t}})).characters}async function O(e,t,n,r,i,a){let o=await T(e,{type:`generateTextBoxImage`,payload:{character:t,text:n,backgroundIndex:i,emotionIndex:a}});return Buffer.isBuffer(o)?o:Buffer.from(o)}var k=class extends c.Service{constructor(e,t){super(e,`manosaba`,!0),this.config=t}async generateImage(e){let{character:t=this.config.defaultCharacter,text:n,backgroundIndex:r,emotionIndex:i}=e;if(!n||n.trim().length===0)throw Error(`文本内容不能为空`);try{return await O(this.ctx,t,n,this.config,r,i)}catch(e){throw this.ctx.logger.error(`生成图片失败`,{error:e}),e}}getCharacters(){return E()}};const A=c.Schema.object({defaultCharacter:c.Schema.union([`ema`,`hiro`,`sherri`,`hanna`,`nanoka`,`noa`,`miria`,`yuki`,`coco`,`meruru`,`reia`,`warden`,`mago`,`alisa`,`anan`]).default(`ema`).description(`默认使用的角色`),isLog:c.Schema.boolean().default(!1).description(`是否输出 debug 日志`)});let j;const M=`manosaba-text-box`;function N(e,t){j=p(e),P(t),b(j),e.on(`ready`,async()=>{await D(e,__dirname)}),e.plugin(k,t),e.command(`mtb <text:text>`,`生成魔女裁判文本框图片`).option(`character`,`-c <character:string>`,{fallback:t.defaultCharacter}).action(async({session:n,options:r},i)=>{if(!i||i.trim()===``)return`请输入要生成的文本内容`;try{let a=await O(e,r.character,i,t);await n.send(c.h.image(a,`image/png`))}catch(e){return j.error(`生成图片失败`,{err:e}),`生成图片失败: ${e.message}`}}),e.command(`mtb.list`,`列出所有可用的角色`).action(()=>{let e=E();return e.length===0?`暂无可用角色`:`可用角色列表:\n${e.map(e=>`${e.id}: ${e.name}`).join(`
2
+ `)}\n\n使用方法:mtb -c <角色ID> <文本内容>`})}function P(e){e.isLog&&m(c.Logger.DEBUG)}exports.Config=A,exports.ManosabaTextBoxService=k,exports.apply=N,Object.defineProperty(exports,`logger`,{enumerable:!0,get:function(){return j}}),exports.name=`manosaba-text-box`;
package/lib/index.mjs CHANGED
@@ -1,14 +1,2 @@
1
- import{Logger as e,Schema as t,Service as n,h as r}from"koishi";import*as i from"node:fs/promises";import*as a from"node:path";import*as o from"js-yaml";import s from"wasm-vips";import{Resvg as c,initWasm as l}from"@resvg/resvg-wasm";import{webcrypto as u}from"node:crypto";import{encodeXML as d}from"entities";var f=(e=>typeof require<`u`?require:typeof Proxy<`u`?new Proxy(e,{get:(e,t)=>(typeof require<`u`?require:e)[t]}):e)(function(e){if(typeof require<`u`)return require.apply(this,arguments);throw Error('Calling `require` for "'+e+"\" in an environment that doesn't expose the `require` function.")});const p={};let m=-1;function h(e,t=`manosaba-text-box`){let n=p[t]||e.logger(t);return m>=0&&(n.level=m),p[t]=n,n}function g(e){for(let t in m=e,p)p[t].level=e}async function _(e){let t=await i.readFile(e,`utf-8`);return o.load(t)}function v(e){if(e.length<=1)return[...e];let t=[...e],n=new Uint32Array(t.length);u.getRandomValues(n);for(let e=t.length-1;e>0;e--){let r=n[e]%(e+1);[t[e],t[r]]=[t[r],t[e]]}return t}function y(){return new Promise(e=>setImmediate(e))}let b=null;function x(e){return b||=s({dynamicLibraries:[`vips-heif.wasm`]}).then(t=>(t.concurrency(1),t.Cache.max(0),e.logger.debug(`wasm-vips initialized with AVIF support`),t)),b}const S=global.__manosaba_resvg_state||{initialized:!1,initializing:null};global.__manosaba_resvg_state||(global.__manosaba_resvg_state=S);let C=null;async function w(e){return C||=await x(e),C}async function T(e){if(!S.initialized){if(S.initializing){await S.initializing;return}S.initializing=(async()=>{try{let t=f.resolve(`@resvg/resvg-wasm/index_bg.wasm`);await l(await i.readFile(t)),S.initialized=!0,e.logger.debug(`Resvg initialized successfully`)}catch(t){if(t instanceof Error&&t.message.includes(`Already initialized`))S.initialized=!0,e.logger.debug(`Resvg was already initialized`);else throw e.logger.error(`Failed to initialize resvg`,{err:t}),S.initializing=null,t}})(),await S.initializing}}let E,D={},O={};const k=[[728,355],[2339,800]];function A(){return Object.entries(D).map(([e,t])=>({id:e,name:t.full_name}))}async function j(e,t){E=a.join(t,`assets`);let n=a.join(t,`config`),r=a.join(n,`chara_meta.yml`),i=a.join(n,`text_configs.yml`);try{D=(await _(r)).mahoshojo||{},O=(await _(i)).text_configs||{},e.logger.debug(`Loaded character meta and text configs`,{characters:Object.keys(D).length,textConfigs:Object.keys(O).length})}catch(t){e.logger.error(`Failed to load config files`,{err:t})}}async function M(){let e=a.join(E,`background`),t=(await i.readdir(e)).filter(e=>e.startsWith(`c`)&&e.endsWith(`.avif`));return v(Array.from({length:t.length},(e,t)=>t+1))[0]}function N(e){let t=D[e];return t?v(Array.from({length:t.emotion_count},(e,t)=>t+1))[0]:1}async function P(e,t,n,r){let o=await w(e),s=null,l=null,u=null;try{let f=a.join(E,`background`,`c${n}.avif`);e.logger.debug(`Loading background`,{backgroundPath:f});let p=await i.readFile(f);await y(),s=o.Image.newFromBuffer(p),await y();let m=a.join(E,`chara`,t,`${t} (${r}).avif`);e.logger.debug(`Loading character`,{characterPath:m});let h=await i.readFile(m);if(await y(),l=o.Image.newFromBuffer(h),await y(),u=s.composite2(l,`over`,{x:0,y:134}),await y(),O[t]){await T(e);let n=D[t]?.font||`font3.ttf`,r=a.join(E,`fonts`,n),s=await i.readFile(r);for(let e of O[t]){if(!e.text)continue;await y();let t=e.text.length,n=e.font_size*t*1.2+20,r=e.font_size*1.5+10,i=e.font_size*1.2,a=d(e.text),l=new c(`
2
- <svg width="${n}" height="${r}" xmlns="http://www.w3.org/2000/svg">
3
- <text x="2" y="${i+2}" font-size="${e.font_size}" fill="#000000" font-family="CustomFont">${a}</text>
4
- <text x="0" y="${i}" font-size="${e.font_size}" fill="rgb(${e.font_color.join(`,`)})" font-family="CustomFont">${a}</text>
5
- </svg>
6
- `,{fitTo:{mode:`original`},font:{fontBuffers:[s]}}).render();await y();let f=l.asPng();await y();let p=o.Image.newFromBuffer(f);await y();let m=u.composite2(p,`over`,{x:e.position[0],y:e.position[1]});await y();try{u[Symbol.dispose]()}catch{}u=m;try{p[Symbol.dispose]()}catch{}}}await y();let g=u.writeToBuffer(`.avif`,{Q:100});return await y(),Buffer.from(g)}finally{if(u)try{u[Symbol.dispose]()}catch{}if(l)try{l[Symbol.dispose]()}catch{}if(s)try{s[Symbol.dispose]()}catch{}}}function F(e,t,n,r,i,a=`#FFFFFF`){e.logger.debug(`generateTextSvg called`,{text:t,width:n,fontSize:r,color:a});let o=[],s=Math.floor(n/r),c=``;for(let e of t)c.length>=s?(o.push(c),c=e):c+=e;c&&o.push(c),e.logger.debug(`Text lines after wrapping`,{lines:o,linesCount:o.length,maxCharsPerLine:s});let l=r*1.2,u=`CustomFont`;return`
7
- <svg width="${Math.max(...o.map(e=>e.length))*r+4}" height="${o.length*l+r*.3}" xmlns="http://www.w3.org/2000/svg">
8
- ${o.map((e,t)=>{let n=r+t*l,i=d(e);return`
9
- <text x="2" y="${n+2}" font-size="${r}" fill="#000000" fill-opacity="0.5" font-family="${u}">${i}</text>
10
- <text x="0" y="${n}" font-size="${r}" fill="${a}" font-family="${u}">${i}</text>
11
- `}).join(``)}
12
- </svg>
13
- `}async function I(e,t,n,r,a,o){e.logger.debug(`drawUserText called`,{text:n,textLength:n.length,initialFontSize:a,fontPath:o,boxRect:r});let s=await w(e),l=null,u=null,d=null;try{await T(e),await y(),l=s.Image.newFromBuffer(t),await y(),e.logger.debug(`Base image loaded to vips`,{width:l.width,height:l.height,bands:l.bands}),l.hasAlpha()||(l=l.bandjoin(255),await y(),e.logger.debug(`Added alpha channel to base image`));let[[f,p],[m,h]]=r,g=m-f,_=h-p,v=await i.readFile(o);await y(),e.logger.debug(`Font file loaded`,{fontPath:o,size:v.length});let b=a,x=``,S=0;for(let t=b;t>=24;t-=6){let r=Math.floor(g/t),i=Math.ceil(n.length/r);if(S=t*1.2*i+t*.3,S<=_){b=t,e.logger.debug(`Auto-adjusted font size`,{originalSize:a,adjustedSize:b,textLength:n.length,lineCount:i,maxCharsPerLine:r,svgHeight:S,boxHeight:_});break}}x=F(e,n,g,b,o),await y(),e.logger.debug(`Generated SVG`,{svgLength:x.length,fontPath:o}),e.logger.debug(`SVG content:`,x);let C=new c(x,{fitTo:{mode:`original`},font:{fontBuffers:[v]}}).render();await y();let w=C.asPng();await y(),e.logger.debug(`Rendered text image`,{width:C.width,height:C.height,bufferSize:w.length}),u=s.Image.newFromBuffer(w),await y(),e.logger.debug(`Text image loaded to vips`,{width:u.width,height:u.height,bands:u.bands,hasAlpha:u.hasAlpha()}),u.hasAlpha()||(u=u.bandjoin(255),await y(),e.logger.debug(`Added alpha channel to text image`));let E=f+20,D=p+20;e.logger.debug(`Compositing text`,{baseWidth:l.width,baseHeight:l.height,imageBands:l.bands,textWidth:u.width,textHeight:u.height,textBands:u.bands,boxWidth:g,boxHeight:_,textX:E,textY:D}),d=l.composite2(u,`over`,{x:E,y:D}),await y(),e.logger.debug(`Composite2 completed`,{resultWidth:d.width,resultHeight:d.height,resultBands:d.bands}),await y();let O=d.writeToBuffer(`.png`,{compression:9});return e.logger.debug(`Text drawing completed successfully`,{outputSize:O.length}),Buffer.from(O)}catch(n){return e.logger.error(`Failed to draw text`,{err:n}),t}finally{if(d)try{d[Symbol.dispose]()}catch{}if(u)try{u[Symbol.dispose]()}catch{}if(l)try{l[Symbol.dispose]()}catch{}}}async function L(e,t,n,r,i,o){if(!D[t])throw Error(`Unknown character: ${t}`);let s=i??await M(),c=o??N(t);e.logger.debug(`Generating text box image`,{character:t,backgroundIndex:s,emotionIndex:c,textLength:n.length});let l=await P(e,t,s,c),u=D[t]?.font||`font3.ttf`;return await I(e,l,n,k,120,a.join(E,`fonts`,u))}var R=class extends n{constructor(e,t){super(e,`manosaba`,!0),this.config=t}async generateImage(e){let{character:t=this.config.defaultCharacter,text:n,backgroundIndex:r,emotionIndex:i}=e;if(!n||n.trim().length===0)throw Error(`文本内容不能为空`);try{return await L(this.ctx,t,n,this.config,r,i)}catch(e){throw this.ctx.logger.error(`生成图片失败`,{error:e}),e}}getCharacters(){return A()}};const z=t.object({defaultCharacter:t.union([`ema`,`hiro`,`sherri`,`hanna`,`nanoka`,`noa`,`miria`,`yuki`,`coco`,`meruru`,`reia`,`warden`,`mago`,`alisa`,`anan`]).default(`ema`).description(`默认使用的角色`),isLog:t.boolean().default(!1).description(`是否输出 debug 日志`)});let B;const V=`manosaba-text-box`;function H(e,t){B=h(e),U(t),e.on(`ready`,async()=>{await j(e,__dirname)}),e.plugin(R,t),e.command(`mtb <text:text>`,`生成魔女裁判文本框图片`).option(`character`,`-c <character:string>`,{fallback:t.defaultCharacter}).action(async({session:n,options:i},a)=>{if(!a||a.trim()===``)return`请输入要生成的文本内容`;try{let o=await L(e,i.character,a,t);await n.send(r.image(o,`image/png`))}catch(e){return B.error(`生成图片失败`,{err:e}),`生成图片失败: ${e.message}`}}),e.command(`mtb.list`,`列出所有可用的角色`).action(()=>{let e=A();return e.length===0?`暂无可用角色`:`可用角色列表:\n${e.map(e=>`${e.id}: ${e.name}`).join(`
14
- `)}\n\n使用方法:mtb -c <角色ID> <文本内容>`})}function U(t){t.isLog&&g(e.DEBUG)}export{z as Config,R as ManosabaTextBoxService,H as apply,B as logger,V as name};
1
+ import{Logger as e,Schema as t,Service as n,h as r}from"koishi";import*as i from"node:path";import{Worker as a}from"node:worker_threads";const o={};let s=-1;function c(e,t=`manosaba-text-box`){let n=o[t]||e.logger(t);return s>=0&&(n.level=s),o[t]=n,n}function l(e){for(let t in s=e,o)o[t].level=e}let u=null,d=null,f=[],p=1;const m=new Map;function h(e){d=e}function g(){return typeof __dirname<`u`?{url:i.join(__dirname,`core.worker.cjs`)}:{url:new URL(`./core.worker.mjs`,import.meta.url),type:`module`}}function _(){return new a(g().url)}function v(e){if(typeof e==`object`&&e&&`type`in e&&e.type===`log`){if(!d){console.warn(`[manosaba-text-box] Worker log without logger:`,e.level,e.message);return}d[e.level](e.message,e.meta);return}if(!e||typeof e!=`object`||!(`id`in e))return;let t=m.get(e.id);if(t)if(m.delete(e.id),e.ok)t.resolve(e.result);else{let n=e,r=Error(n.error.message);r.stack=n.error.stack,t.reject(r)}}async function y(e){return u||(u=new Promise((e,t)=>{try{let t=_();t.on(`message`,v),t.on(`error`,e=>{d?.error(`worker error`,{err:e})}),t.on(`exit`,e=>{e!==0&&d?.error(`worker exited unexpectedly`,{code:e});for(let{reject:e}of m.values())e(Error(`worker exited`));m.clear(),u=null}),e(t)}catch(e){u=null,t(e)}}),u)}async function b(e,t){let n=await y(e);return new Promise((e,r)=>{let i=p++;m.set(i,{resolve:e,reject:r}),n.postMessage({...t,id:i})})}function x(){return f}async function S(e,t){f=(await b(e,{type:`initAssets`,payload:{basePath:t}})).characters}async function C(e,t,n,r,i,a){let o=await b(e,{type:`generateTextBoxImage`,payload:{character:t,text:n,backgroundIndex:i,emotionIndex:a}});return Buffer.isBuffer(o)?o:Buffer.from(o)}var w=class extends n{constructor(e,t){super(e,`manosaba`,!0),this.config=t}async generateImage(e){let{character:t=this.config.defaultCharacter,text:n,backgroundIndex:r,emotionIndex:i}=e;if(!n||n.trim().length===0)throw Error(`文本内容不能为空`);try{return await C(this.ctx,t,n,this.config,r,i)}catch(e){throw this.ctx.logger.error(`生成图片失败`,{error:e}),e}}getCharacters(){return x()}};const T=t.object({defaultCharacter:t.union([`ema`,`hiro`,`sherri`,`hanna`,`nanoka`,`noa`,`miria`,`yuki`,`coco`,`meruru`,`reia`,`warden`,`mago`,`alisa`,`anan`]).default(`ema`).description(`默认使用的角色`),isLog:t.boolean().default(!1).description(`是否输出 debug 日志`)});let E;const D=`manosaba-text-box`;function O(e,t){E=c(e),k(t),h(E),e.on(`ready`,async()=>{await S(e,__dirname)}),e.plugin(w,t),e.command(`mtb <text:text>`,`生成魔女裁判文本框图片`).option(`character`,`-c <character:string>`,{fallback:t.defaultCharacter}).action(async({session:n,options:i},a)=>{if(!a||a.trim()===``)return`请输入要生成的文本内容`;try{let o=await C(e,i.character,a,t);await n.send(r.image(o,`image/png`))}catch(e){return E.error(`生成图片失败`,{err:e}),`生成图片失败: ${e.message}`}}),e.command(`mtb.list`,`列出所有可用的角色`).action(()=>{let e=x();return e.length===0?`暂无可用角色`:`可用角色列表:\n${e.map(e=>`${e.id}: ${e.name}`).join(`
2
+ `)}\n\n使用方法:mtb -c <角色ID> <文本内容>`})}function k(t){t.isLog&&l(e.DEBUG)}export{T as Config,w as ManosabaTextBoxService,O as apply,E as logger,D as name};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-manosaba-text-box",
3
3
  "description": "一个自动化表情包生成工具,能够快速生成带有自定义文本的魔法少女的魔女裁判文本框图片",
4
- "version": "1.0.5",
4
+ "version": "1.0.7",
5
5
  "main": "lib/index.cjs",
6
6
  "module": "lib/index.mjs",
7
7
  "types": "lib/index.d.ts",
@@ -19,7 +19,7 @@
19
19
  ],
20
20
  "scripts": {
21
21
  "convert": "uv run convert_assets.py",
22
- "build": "yarn rolldown -c rolldown.config.js",
22
+ "build": "rimraf lib && yarn rolldown -c rolldown.config.js",
23
23
  "publish": "yarn npm publish",
24
24
  "lint": "biome check && biome lint",
25
25
  "lint-fix": "biome format --write && biome lint --write"
@@ -44,6 +44,7 @@
44
44
  "@types/js-yaml": "^4.0.9",
45
45
  "@types/node": "^25.0.2",
46
46
  "koishi": "^4.18.9",
47
+ "rimraf": "^6.1.2",
47
48
  "rolldown": "1.0.0-beta.54",
48
49
  "rolldown-plugin-dts": "^0.18.3"
49
50
  },
@@ -60,6 +61,7 @@
60
61
  "dependencies": {
61
62
  "@resvg/resvg-wasm": "^2.6.2",
62
63
  "entities": "^7.0.0",
64
+ "jieba-wasm": "^2.4.0",
63
65
  "js-yaml": "^4.1.1",
64
66
  "wasm-vips": "^0.0.16"
65
67
  }