koishi-plugin-manosaba-text-box 1.0.4 → 1.0.5
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/lib/index.cjs +3 -3
- package/lib/index.mjs +3 -3
- package/package.json +1 -1
package/lib/index.cjs
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
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}),a=i.Image.
|
|
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
2
|
<svg width="${n}" height="${r}" xmlns="http://www.w3.org/2000/svg">
|
|
3
3
|
<text x="2" y="${o+2}" font-size="${e.font_size}" fill="#000000" font-family="CustomFont">${c}</text>
|
|
4
4
|
<text x="0" y="${o}" font-size="${e.font_size}" fill="rgb(${e.font_color.join(`,`)})" font-family="CustomFont">${c}</text>
|
|
5
5
|
</svg>
|
|
6
|
-
`,{fitTo:{mode:`original`},font:{fontBuffers:[a]}}).render();await S();let u=l.asPng()
|
|
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
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
8
|
${o.map((e,t)=>{let n=r+t*l,i=(0,h.encodeXML)(e);return`
|
|
9
9
|
<text x="2" y="${n+2}" font-size="${r}" fill="#000000" fill-opacity="0.5" font-family="${u}">${i}</text>
|
|
10
10
|
<text x="0" y="${n}" font-size="${r}" fill="${a}" font-family="${u}">${i}</text>
|
|
11
11
|
`}).join(``)}
|
|
12
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),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),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);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),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();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),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(`
|
|
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
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`;
|
package/lib/index.mjs
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
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}),s=o.Image.
|
|
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
2
|
<svg width="${n}" height="${r}" xmlns="http://www.w3.org/2000/svg">
|
|
3
3
|
<text x="2" y="${i+2}" font-size="${e.font_size}" fill="#000000" font-family="CustomFont">${a}</text>
|
|
4
4
|
<text x="0" y="${i}" font-size="${e.font_size}" fill="rgb(${e.font_color.join(`,`)})" font-family="CustomFont">${a}</text>
|
|
5
5
|
</svg>
|
|
6
|
-
`,{fitTo:{mode:`original`},font:{fontBuffers:[s]}}).render();await y();let f=l.asPng()
|
|
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
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
8
|
${o.map((e,t)=>{let n=r+t*l,i=d(e);return`
|
|
9
9
|
<text x="2" y="${n+2}" font-size="${r}" fill="#000000" fill-opacity="0.5" font-family="${u}">${i}</text>
|
|
10
10
|
<text x="0" y="${n}" font-size="${r}" fill="${a}" font-family="${u}">${i}</text>
|
|
11
11
|
`}).join(``)}
|
|
12
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),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),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);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),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();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),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(`
|
|
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
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};
|
package/package.json
CHANGED