koishi-plugin-manosaba-text-box 1.0.1 → 1.0.2
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 +5 -5
- package/lib/index.d.ts +2 -0
- 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`);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}function b(e){let t=l.readFileSync(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}let S=null;function C(e){return S||=(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)),S}const w=global.__manosaba_resvg_state||{initialized:!1,initializing:null};global.__manosaba_resvg_state||(global.__manosaba_resvg_state=w);let T=null;async function E(e){return T||=await C(e),T}async function D(e){if(!w.initialized){if(w.initializing){await w.initializing;return}w.initializing=(async()=>{try{let t=require.resolve(`@resvg/resvg-wasm/index_bg.wasm`);await(0,p.initWasm)(l.readFileSync(t)),w.initialized=!0,e.logger.debug(`Resvg initialized successfully`)}catch(t){if(t instanceof Error&&t.message.includes(`Already initialized`))w.initialized=!0,e.logger.debug(`Resvg was already initialized`);else throw e.logger.error(`Failed to initialize resvg`,{err:t}),w.initializing=null,t}})(),await w.initializing}}let O,k={},A={};const j=[[728,355],[2339,800]],M=120;function N(){return Object.entries(k).map(([e,t])=>({id:e,name:t.full_name}))}function P(e,t){O=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{k=b(r).mahoshojo||{},A=b(i).text_configs||{},e.logger.debug(`Loaded character meta and text configs`,{characters:Object.keys(k).length,textConfigs:Object.keys(A).length})}catch(t){e.logger.error(`Failed to load config files`,{err:t})}}function F(){let e=u.join(O,`background`),t=l.readdirSync(e).filter(e=>e.startsWith(`c`)&&e.endsWith(`.avif`));return x(Array.from({length:t.length},(e,t)=>t+1))[0]}function I(e){let t=k[e];return t?x(Array.from({length:t.emotion_count},(e,t)=>t+1))[0]:1}async function L(e,t,n,r){let
|
|
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`);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}function b(e){let t=l.readFileSync(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}let S=null;function C(e){return S||=(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)),S}const w=global.__manosaba_resvg_state||{initialized:!1,initializing:null};global.__manosaba_resvg_state||(global.__manosaba_resvg_state=w);let T=null;async function E(e){return T||=await C(e),T}async function D(e){if(!w.initialized){if(w.initializing){await w.initializing;return}w.initializing=(async()=>{try{let t=require.resolve(`@resvg/resvg-wasm/index_bg.wasm`);await(0,p.initWasm)(l.readFileSync(t)),w.initialized=!0,e.logger.debug(`Resvg initialized successfully`)}catch(t){if(t instanceof Error&&t.message.includes(`Already initialized`))w.initialized=!0,e.logger.debug(`Resvg was already initialized`);else throw e.logger.error(`Failed to initialize resvg`,{err:t}),w.initializing=null,t}})(),await w.initializing}}let O,k={},A={};const j=[[728,355],[2339,800]],M=120;function N(){return Object.entries(k).map(([e,t])=>({id:e,name:t.full_name}))}function P(e,t){O=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{k=b(r).mahoshojo||{},A=b(i).text_configs||{},e.logger.debug(`Loaded character meta and text configs`,{characters:Object.keys(k).length,textConfigs:Object.keys(A).length})}catch(t){e.logger.error(`Failed to load config files`,{err:t})}}function F(){let e=u.join(O,`background`),t=l.readdirSync(e).filter(e=>e.startsWith(`c`)&&e.endsWith(`.avif`));return x(Array.from({length:t.length},(e,t)=>t+1))[0]}function I(e){let t=k[e];return t?x(Array.from({length:t.emotion_count},(e,t)=>t+1))[0]:1}async function L(e,t,n,r,i){let a=await E(e),o=null,s=null,c=null;try{let d=u.join(O,`background`,`c${n}.avif`);e.logger.debug(`Loading background`,{backgroundPath:d}),o=a.Image.newFromFile(d);let f=u.join(O,`chara`,t,`${t} (${r}).avif`);if(e.logger.debug(`Loading character`,{characterPath:f}),s=a.Image.newFromFile(f),c=o.composite2(s,`over`,{x:0,y:134}),A[t]){await D(e);let n=k[t]?.font||`font3.ttf`,r=u.join(O,`fonts`,n),i=l.readFileSync(r);for(let e of A[t]){if(!e.text)continue;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,s=(0,h.encodeXML)(e.text),l=new p.Resvg(`
|
|
2
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">${
|
|
4
|
-
<text x="0" y="${o}" font-size="${e.font_size}" fill="rgb(${e.font_color.join(`,`)})" font-family="CustomFont">${
|
|
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
5
|
</svg>
|
|
6
|
-
`,{fitTo:{mode:`original`},font:{fontBuffers:[
|
|
6
|
+
`,{fitTo:{mode:`original`},font:{fontBuffers:[i]}}).render().asPng(),u=a.Image.newFromBuffer(l),d=c.composite2(u,`over`,{x:e.position[0],y:e.position[1]});try{c[Symbol.dispose]()}catch{}c=d;try{u[Symbol.dispose]()}catch{}}}let m=i.lossless?{lossless:!0}:{Q:i.imageQuality};e.logger.debug(`Encoding base image with options`,m);let g=c.writeToBuffer(`.avif`,m);return Buffer.from(g)}finally{if(c)try{c[Symbol.dispose]()}catch{}if(s)try{s[Symbol.dispose]()}catch{}if(o)try{o[Symbol.dispose]()}catch{}}}function R(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 z(e,t,n,r,i,a){e.logger.debug(`drawUserText called`,{text:n,textLength:n.length,initialFontSize:i,fontPath:a,boxRect:r});let
|
|
13
|
+
`}async function z(e,t,n,r,i,a,o){e.logger.debug(`drawUserText called`,{text:n,textLength:n.length,initialFontSize:i,fontPath:a,boxRect:r});let s=await E(e),c=null,u=null,d=null;try{await D(e),c=s.Image.newFromBuffer(t),e.logger.debug(`Base image loaded to vips`,{width:c.width,height:c.height,bands:c.bands}),c.hasAlpha()||(c=c.bandjoin(255),e.logger.debug(`Added alpha channel to base image`));let[[f,m],[h,g]]=r,_=h-f,v=g-m,y=l.readFileSync(a);e.logger.debug(`Font file loaded`,{fontPath:a,size:y.length});let b=i,x=``,S=0;for(let t=b;t>=24;t-=6){let r=Math.floor(_/t),a=Math.ceil(n.length/r);if(S=t*1.2*a+t*.3,S<=v){b=t,e.logger.debug(`Auto-adjusted font size`,{originalSize:i,adjustedSize:b,textLength:n.length,lineCount:a,maxCharsPerLine:r,svgHeight:S,boxHeight:v});break}}x=R(e,n,_,b,a),e.logger.debug(`Generated SVG`,{svgLength:x.length,fontPath:a}),e.logger.debug(`SVG content:`,x);let C=new p.Resvg(x,{fitTo:{mode:`original`},font:{fontBuffers:[y]}}).render(),w=C.asPng();e.logger.debug(`Rendered text image`,{width:C.width,height:C.height,bufferSize:w.length}),u=s.Image.newFromBuffer(w),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 T=f+20,E=m+20;e.logger.debug(`Compositing text`,{baseWidth:c.width,baseHeight:c.height,imageBands:c.bands,textWidth:u.width,textHeight:u.height,textBands:u.bands,boxWidth:_,boxHeight:v,textX:T,textY:E}),d=c.composite2(u,`over`,{x:T,y:E}),e.logger.debug(`Composite2 completed`,{resultWidth:d.width,resultHeight:d.height,resultBands:d.bands});let O=o.lossless?{lossless:!0}:{Q:o.imageQuality};e.logger.debug(`Encoding final image with options`,O);let k=d.writeToBuffer(`.avif`,O);return e.logger.debug(`Text drawing completed successfully`,{outputSize:k.length}),Buffer.from(k)}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(c)try{c[Symbol.dispose]()}catch{}}}async function B(e,t,n,r,i,a){if(!k[t])throw Error(`Unknown character: ${t}`);let o=i??F(),s=a??I(t);e.logger.debug(`Generating text box image`,{character:t,backgroundIndex:o,emotionIndex:s,textLength:n.length,imageQuality:r.imageQuality,lossless:r.lossless});let c=await L(e,t,o,s,r),l=k[t]?.font||`font3.ttf`;return await z(e,c,n,j,120,u.join(O,`fonts`,l),r)}var V=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 B(this.ctx,t,n,this.config,r,i)}catch(e){throw this.ctx.logger.error(`生成图片失败`,{error:e}),e}}getCharacters(){return N()}};const H=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 日志`),imageQuality:c.Schema.number().min(1).max(100).default(85).description(`AVIF 图片质量(1-100),质量越高文件越大,建议 70-90`),lossless:c.Schema.boolean().default(!1).description(`是否使用无损压缩(启用后会忽略 imageQuality 设置,文件会更大但质量最佳)`)});let U;const W=`manosaba-text-box`;function G(e,t){U=v(e),K(t),P(e,__dirname),e.plugin(V,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 B(e,r.character,i,t);await n.send(c.h.image(a,`image/avif`))}catch(e){return U.error(`生成图片失败`,{err:e}),`生成图片失败: ${e.message}`}}),e.command(`mtb.list`,`列出所有可用的角色`).action(()=>{let e=N();return e.length===0?`暂无可用角色`:`可用角色列表:\n${e.map(e=>`${e.id}: ${e.name}`).join(`
|
|
14
14
|
`)}\n\n使用方法:mtb -c <角色ID> <文本内容>`})}function K(e){e.isLog&&y(c.Logger.DEBUG)}exports.Config=H,exports.ManosabaTextBoxService=V,exports.apply=G,Object.defineProperty(exports,`logger`,{enumerable:!0,get:function(){return U}}),exports.name=`manosaba-text-box`;
|
package/lib/index.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { Context, Logger, Schema, Service } from "koishi";
|
|
|
4
4
|
interface Config {
|
|
5
5
|
defaultCharacter: 'ema' | 'hiro' | 'sherri' | 'hanna' | 'nanoka' | 'noa' | 'miria' | 'yuki' | 'coco' | 'meruru' | 'reia' | 'warden' | 'mago' | 'alisa' | 'anan';
|
|
6
6
|
isLog: boolean;
|
|
7
|
+
imageQuality: number;
|
|
8
|
+
lossless: boolean;
|
|
7
9
|
}
|
|
8
10
|
declare const Config: Schema<Config>;
|
|
9
11
|
//#endregion
|
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";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}function _(e){let t=i.readFileSync(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}let y=null;function b(e){return y||=s({dynamicLibraries:[`vips-heif.wasm`]}).then(t=>(t.concurrency(1),t.Cache.max(0),e.logger.debug(`wasm-vips initialized with AVIF support`),t)),y}const x=global.__manosaba_resvg_state||{initialized:!1,initializing:null};global.__manosaba_resvg_state||(global.__manosaba_resvg_state=x);let S=null;async function C(e){return S||=await b(e),S}async function w(e){if(!x.initialized){if(x.initializing){await x.initializing;return}x.initializing=(async()=>{try{let t=f.resolve(`@resvg/resvg-wasm/index_bg.wasm`);await l(i.readFileSync(t)),x.initialized=!0,e.logger.debug(`Resvg initialized successfully`)}catch(t){if(t instanceof Error&&t.message.includes(`Already initialized`))x.initialized=!0,e.logger.debug(`Resvg was already initialized`);else throw e.logger.error(`Failed to initialize resvg`,{err:t}),x.initializing=null,t}})(),await x.initializing}}let T,E={},D={};const O=[[728,355],[2339,800]];function k(){return Object.entries(E).map(([e,t])=>({id:e,name:t.full_name}))}function A(e,t){T=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{E=_(r).mahoshojo||{},D=_(i).text_configs||{},e.logger.debug(`Loaded character meta and text configs`,{characters:Object.keys(E).length,textConfigs:Object.keys(D).length})}catch(t){e.logger.error(`Failed to load config files`,{err:t})}}function j(){let e=a.join(T,`background`),t=i.readdirSync(e).filter(e=>e.startsWith(`c`)&&e.endsWith(`.avif`));return v(Array.from({length:t.length},(e,t)=>t+1))[0]}function M(e){let t=E[e];return t?v(Array.from({length:t.emotion_count},(e,t)=>t+1))[0]:1}async function N(e,t,n,r){let
|
|
1
|
+
import{Logger as e,Schema as t,Service as n,h as r}from"koishi";import*as i from"node:fs";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}function _(e){let t=i.readFileSync(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}let y=null;function b(e){return y||=s({dynamicLibraries:[`vips-heif.wasm`]}).then(t=>(t.concurrency(1),t.Cache.max(0),e.logger.debug(`wasm-vips initialized with AVIF support`),t)),y}const x=global.__manosaba_resvg_state||{initialized:!1,initializing:null};global.__manosaba_resvg_state||(global.__manosaba_resvg_state=x);let S=null;async function C(e){return S||=await b(e),S}async function w(e){if(!x.initialized){if(x.initializing){await x.initializing;return}x.initializing=(async()=>{try{let t=f.resolve(`@resvg/resvg-wasm/index_bg.wasm`);await l(i.readFileSync(t)),x.initialized=!0,e.logger.debug(`Resvg initialized successfully`)}catch(t){if(t instanceof Error&&t.message.includes(`Already initialized`))x.initialized=!0,e.logger.debug(`Resvg was already initialized`);else throw e.logger.error(`Failed to initialize resvg`,{err:t}),x.initializing=null,t}})(),await x.initializing}}let T,E={},D={};const O=[[728,355],[2339,800]];function k(){return Object.entries(E).map(([e,t])=>({id:e,name:t.full_name}))}function A(e,t){T=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{E=_(r).mahoshojo||{},D=_(i).text_configs||{},e.logger.debug(`Loaded character meta and text configs`,{characters:Object.keys(E).length,textConfigs:Object.keys(D).length})}catch(t){e.logger.error(`Failed to load config files`,{err:t})}}function j(){let e=a.join(T,`background`),t=i.readdirSync(e).filter(e=>e.startsWith(`c`)&&e.endsWith(`.avif`));return v(Array.from({length:t.length},(e,t)=>t+1))[0]}function M(e){let t=E[e];return t?v(Array.from({length:t.emotion_count},(e,t)=>t+1))[0]:1}async function N(e,t,n,r,o){let s=await C(e),l=null,u=null,f=null;try{let p=a.join(T,`background`,`c${n}.avif`);e.logger.debug(`Loading background`,{backgroundPath:p}),l=s.Image.newFromFile(p);let m=a.join(T,`chara`,t,`${t} (${r}).avif`);if(e.logger.debug(`Loading character`,{characterPath:m}),u=s.Image.newFromFile(m),f=l.composite2(u,`over`,{x:0,y:134}),D[t]){await w(e);let n=E[t]?.font||`font3.ttf`,r=a.join(T,`fonts`,n),o=i.readFileSync(r);for(let e of D[t]){if(!e.text)continue;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:[
|
|
6
|
+
`,{fitTo:{mode:`original`},font:{fontBuffers:[o]}}).render().asPng(),u=s.Image.newFromBuffer(l),p=f.composite2(u,`over`,{x:e.position[0],y:e.position[1]});try{f[Symbol.dispose]()}catch{}f=p;try{u[Symbol.dispose]()}catch{}}}let h=o.lossless?{lossless:!0}:{Q:o.imageQuality};e.logger.debug(`Encoding base image with options`,h);let g=f.writeToBuffer(`.avif`,h);return Buffer.from(g)}finally{if(f)try{f[Symbol.dispose]()}catch{}if(u)try{u[Symbol.dispose]()}catch{}if(l)try{l[Symbol.dispose]()}catch{}}}function P(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 F(e,t,n,r,a,o){e.logger.debug(`drawUserText called`,{text:n,textLength:n.length,initialFontSize:a,fontPath:o,boxRect:r});let
|
|
13
|
+
`}async function F(e,t,n,r,a,o,s){e.logger.debug(`drawUserText called`,{text:n,textLength:n.length,initialFontSize:a,fontPath:o,boxRect:r});let l=await C(e),u=null,d=null,f=null;try{await w(e),u=l.Image.newFromBuffer(t),e.logger.debug(`Base image loaded to vips`,{width:u.width,height:u.height,bands:u.bands}),u.hasAlpha()||(u=u.bandjoin(255),e.logger.debug(`Added alpha channel to base image`));let[[p,m],[h,g]]=r,_=h-p,v=g-m,y=i.readFileSync(o);e.logger.debug(`Font file loaded`,{fontPath:o,size:y.length});let b=a,x=``,S=0;for(let t=b;t>=24;t-=6){let r=Math.floor(_/t),i=Math.ceil(n.length/r);if(S=t*1.2*i+t*.3,S<=v){b=t,e.logger.debug(`Auto-adjusted font size`,{originalSize:a,adjustedSize:b,textLength:n.length,lineCount:i,maxCharsPerLine:r,svgHeight:S,boxHeight:v});break}}x=P(e,n,_,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:[y]}}).render(),T=C.asPng();e.logger.debug(`Rendered text image`,{width:C.width,height:C.height,bufferSize:T.length}),d=l.Image.newFromBuffer(T),e.logger.debug(`Text image loaded to vips`,{width:d.width,height:d.height,bands:d.bands,hasAlpha:d.hasAlpha()}),d.hasAlpha()||(d=d.bandjoin(255),e.logger.debug(`Added alpha channel to text image`));let E=p+20,D=m+20;e.logger.debug(`Compositing text`,{baseWidth:u.width,baseHeight:u.height,imageBands:u.bands,textWidth:d.width,textHeight:d.height,textBands:d.bands,boxWidth:_,boxHeight:v,textX:E,textY:D}),f=u.composite2(d,`over`,{x:E,y:D}),e.logger.debug(`Composite2 completed`,{resultWidth:f.width,resultHeight:f.height,resultBands:f.bands});let O=s.lossless?{lossless:!0}:{Q:s.imageQuality};e.logger.debug(`Encoding final image with options`,O);let k=f.writeToBuffer(`.avif`,O);return e.logger.debug(`Text drawing completed successfully`,{outputSize:k.length}),Buffer.from(k)}catch(n){return e.logger.error(`Failed to draw text`,{err:n}),t}finally{if(f)try{f[Symbol.dispose]()}catch{}if(d)try{d[Symbol.dispose]()}catch{}if(u)try{u[Symbol.dispose]()}catch{}}}async function I(e,t,n,r,i,o){if(!E[t])throw Error(`Unknown character: ${t}`);let s=i??j(),c=o??M(t);e.logger.debug(`Generating text box image`,{character:t,backgroundIndex:s,emotionIndex:c,textLength:n.length,imageQuality:r.imageQuality,lossless:r.lossless});let l=await N(e,t,s,c,r),u=E[t]?.font||`font3.ttf`;return await F(e,l,n,O,120,a.join(T,`fonts`,u),r)}var L=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 I(this.ctx,t,n,this.config,r,i)}catch(e){throw this.ctx.logger.error(`生成图片失败`,{error:e}),e}}getCharacters(){return k()}};const R=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 日志`),imageQuality:t.number().min(1).max(100).default(85).description(`AVIF 图片质量(1-100),质量越高文件越大,建议 70-90`),lossless:t.boolean().default(!1).description(`是否使用无损压缩(启用后会忽略 imageQuality 设置,文件会更大但质量最佳)`)});let z;const B=`manosaba-text-box`;function V(e,t){z=h(e),H(t),A(e,__dirname),e.plugin(L,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 I(e,i.character,a,t);await n.send(r.image(o,`image/avif`))}catch(e){return z.error(`生成图片失败`,{err:e}),`生成图片失败: ${e.message}`}}),e.command(`mtb.list`,`列出所有可用的角色`).action(()=>{let e=k();return e.length===0?`暂无可用角色`:`可用角色列表:\n${e.map(e=>`${e.id}: ${e.name}`).join(`
|
|
14
14
|
`)}\n\n使用方法:mtb -c <角色ID> <文本内容>`})}function H(t){t.isLog&&g(e.DEBUG)}export{R as Config,L as ManosabaTextBoxService,V as apply,z as logger,B as name};
|
package/package.json
CHANGED