vue3-image-compressor 1.0.4 → 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.
@@ -1 +1 @@
1
- (function(s,e){typeof exports=="object"&&typeof module<"u"?e(exports,require("vue")):typeof define=="function"&&define.amd?define(["exports","vue"],e):(s=typeof globalThis<"u"?globalThis:s||self,e(s.VueImageCompressor={},s.Vue))})(this,function(s,e){"use strict";var v=typeof document<"u"?document.currentScript:null;const B={mozJPEG:{label:"MozJPEG",mimeType:"image/jpeg",extension:"jpg"},webP:{label:"WebP",mimeType:"image/webp",extension:"webp"},avif:{label:"AVIF",mimeType:"image/avif",extension:"avif"},jxl:{label:"JPEG XL",mimeType:"image/jxl",extension:"jxl"},oxiPNG:{label:"OxiPNG",mimeType:"image/png",extension:"png"},browserJPEG:{label:"Browser JPEG",mimeType:"image/jpeg",extension:"jpg"},browserPNG:{label:"Browser PNG",mimeType:"image/png",extension:"png"},browserGIF:{label:"Browser GIF",mimeType:"image/gif",extension:"gif"},qoi:{label:"QOI",mimeType:"image/qoi",extension:"qoi"},wp2:{label:"WebP2",mimeType:"image/webp2",extension:"wp2"}},S={mozJPEG:{quality:75,baseline:!1,arithmetic:!1,progressive:!0,optimize_coding:!0,smoothing:0,color_space:3,quant_table:3,trellis_multipass:!1,trellis_opt_zero:!1,trellis_opt_table:!1,trellis_loops:1,auto_subsample:!0,chroma_subsample:2,separate_chroma_quality:!1,chroma_quality:75},webP:{quality:75,target_size:0,target_PSNR:0,method:4,sns_strength:50,filter_strength:60,filter_sharpness:0,filter_type:1,partitions:0,segments:4,pass:1,show_compressed:0,preprocessing:0,autofilter:0,partition_limit:0,alpha_compression:1,alpha_filtering:1,alpha_quality:100,lossless:0,exact:0,use_delta_palette:0,vlnr:0,near_lossless:60},avif:{cqLevel:33,denoiseLevel:0,cqAlphaLevel:-1,tileRows:0,tileCols:0,speed:6,subsample:1,chromaDeltaQ:!1,sharpness:0,tune:0},jxl:{effort:7,quality:75,progressive:!1,targetPsize:0},oxiPNG:{level:2},browserJPEG:{quality:.75},browserPNG:{},browserGIF:{},qoi:{},wp2:{quality:75}},G=Object.keys(B);async function M(n){const t=await createImageBitmap(n),r=document.createElement("canvas");r.width=t.width,r.height=t.height;const o=r.getContext("2d");return o.drawImage(t,0,0),o.getImageData(0,0,r.width,r.height)}async function L(n,t,r){const o=document.createElement("canvas");return o.width=n.width,o.height=n.height,o.getContext("2d").putImageData(n,0,0),new Promise(p=>{o.toBlob(m=>p(m),t,r)})}function Z(n){return new Promise((t,r)=>{const o=new Image;o.onload=()=>{t({width:o.width,height:o.height}),URL.revokeObjectURL(o.src)},o.onerror=r,o.src=URL.createObjectURL(n)})}async function x(n){const t=new Uint8Array(await n.slice(0,4).arrayBuffer());return t[0]===137&&t[1]===80?"image/png":t[0]===255&&t[1]===216?"image/jpeg":t[0]===71&&t[1]===73?"image/gif":t[0]===82&&t[1]===73?"image/webp":t[0]===0&&t[1]===0?"image/avif":n.type||"image/jpeg"}function F(n){return new Promise(t=>{const r=new Image;r.onload=()=>t(!0),r.onerror=()=>t(!1),r.src=`data:${n};base64,`})}function A(n,t,r){return new File([n],t,{type:r})}function V(n,t=2){if(n===0)return"0 Bytes";const r=1024,o=t<0?0:t,u=["Bytes","KB","MB","GB"],p=Math.floor(Math.log(n)/Math.log(r));return parseFloat((n/Math.pow(r,p)).toFixed(o))+" "+u[p]}function W(n,t){return n===0?0:Math.round((n-t)/n*100)}function J(n,t){return`${n.replace(/\.[^.]+$/,"")}_compressed.${t}`}function q(n){return URL.createObjectURL(n)}function $(n){URL.revokeObjectURL(n)}const X=1e4;function H(){const n=e.ref(!1),t=e.ref(!1),r=e.ref(null);let o=null,u=null;function p(){if(!o)try{t.value=!0,o=new Worker(new URL("/assets/imageWorker-DyeUTFOy.js",typeof document>"u"&&typeof location>"u"?require("url").pathToFileURL(__filename).href:typeof document>"u"?location.href:v&&v.tagName.toUpperCase()==="SCRIPT"&&v.src||new URL("vue-image-compressor.umd.cjs",document.baseURI).href),{type:"module"}),n.value=!0,r.value=null}catch(_){r.value=_ instanceof Error?_:new Error("Failed to start worker"),console.error("Worker start failed:",_)}finally{t.value=!1}}function m(){u&&(clearTimeout(u),u=null),o&&(o.terminate(),o=null,n.value=!1)}function w(){u&&clearTimeout(u),u=window.setTimeout(()=>{m()},X)}async function E(){if(o||p(),!o)throw new Error("Worker failed to initialize");return o}async function T(_,C){if(_.aborted)throw new DOMException("AbortError","AbortError");return clearTimeout(u),new Promise((R,N)=>{const k=()=>{m(),N(new DOMException("AbortError","AbortError"))};_.addEventListener("abort",k),C().then(R).catch(N).finally(()=>{_.removeEventListener("abort",k),w()})})}return e.onUnmounted(()=>{m()}),{isReady:n,isLoading:t,error:r,worker:o,getWorkerApi:E,executeTask:T,terminateWorker:m}}function Q(){const n=e.ref(!1),t=e.ref(0),r=e.ref(null),o=e.ref(null),u=e.ref(null),{getWorkerApi:p,executeTask:m}=H(),w=e.computed(()=>n.value&&!!u.value);async function E(f,a){const i=await x(a),c=await F(i),g=await p();if(!c){if(i==="image/avif")return m(f,()=>g.avifDecode(a));if(i==="image/webp")return m(f,()=>g.webpDecode(a));if(i==="image/jxl")return m(f,()=>g.jxlDecode(a));if(i==="image/webp2")return m(f,()=>g.wp2Decode(a));if(i==="image/qoi")return m(f,()=>g.qoiDecode(a))}return M(a)}async function T(f,a,i){if(i===0)return a;const c=await p();return m(f,()=>c.rotate(a,{rotate:i}))}async function _(f,a,i){var b,h;let c=a;const g=await p();return(b=i.resize)!=null&&b.enabled&&(c=await m(f,()=>g.resize(c,{width:i.resize.width||c.width,height:i.resize.height||c.height,method:i.resize.method||"lanczos3",fitMethod:i.resize.fitMethod||"stretch",premultiply:!0,linearRGB:!0}))),(h=i.quantize)!=null&&h.enabled&&(c=await m(f,()=>g.quantize(c,{numColors:i.quantize.numColors||256,dither:i.quantize.dither||1}))),c}async function C(f,a,i){const c=await p(),g=i.encoder;if(g==="browserJPEG")return(await L(a,"image/jpeg",i.encoderOptions.quality)).arrayBuffer();if(g==="browserPNG")return(await L(a,"image/png")).arrayBuffer();if(g==="browserGIF")throw new Error("Browser GIF encoding not supported directly");const h={mozJPEG:c.mozjpegEncode,webP:c.webpEncode,avif:c.avifEncode,jxl:c.jxlEncode,oxiPNG:c.oxipngEncode,qoi:c.qoiEncode,wp2:c.wp2Encode}[g];if(!h)throw new Error(`Unknown encoder: ${g}`);return(await m(f,()=>h.call(c,a,i.encoderOptions))).buffer}async function R(f,a){var g;u.value=new AbortController;const i=u.value.signal;n.value=!0,t.value=0,r.value=null;const c=o.value;o.value=null,(g=c==null?void 0:c.compressed)!=null&&g.blobUrl&&$(c.compressed.blobUrl);try{t.value=10;const b={file:f,size:f.size,width:0,height:0,blobUrl:q(f)},h=await E(i,f);b.width=h.width,b.height=h.height,t.value=25;const D=await T(i,h,a.rotate||0);t.value=35;const I=await _(i,D,a);t.value=50;const j=await C(i,I,a);t.value=80;const O=B[a.encoder],z=J(f.name,(O==null?void 0:O.extension)||"bin"),P=A(j,z,(O==null?void 0:O.mimeType)||"application/octet-stream"),U={file:P,size:P.size,width:I.width,height:I.height,blobUrl:q(P)},d={original:b,compressed:U,savingsBytes:b.size-U.size,savingsPercent:W(b.size,U.size),encoderType:a.encoder,encoderOptions:a.encoderOptions};return o.value=d,t.value=100,d}catch(b){if(b instanceof DOMException&&b.name==="AbortError")throw r.value="已取消",b;const h=b instanceof Error?b.message:"压缩失败";throw r.value=h,console.error("Compression error:",b),b}finally{n.value=!1,u.value=null}}function N(){u.value&&u.value.abort()}function k(){if(!o.value)return;const{compressed:f}=o.value,a=document.createElement("a");a.href=f.blobUrl,a.download=f.file.name,document.body.appendChild(a),a.click(),document.body.removeChild(a)}return{compress:R,cancel:N,downloadResult:k,isCompressing:n,progress:t,error:r,result:o,canCancel:w}}function K(){const n=e.ref("mozJPEG"),t=e.ref({}),r=e.computed(()=>B[n.value]),o=e.computed(()=>S[n.value]),u=e.computed(()=>G.map(E=>({type:E,label:B[E].label,mimeType:B[E].mimeType,extension:B[E].extension})));function p(E){n.value=E,t.value={...S[E]}}function m(E,T){t.value={...t.value,[E]:T}}function w(){t.value={...o.value}}return w(),{selectedEncoder:n,encoderOptions:t,currentMeta:r,currentDefaults:o,availableEncoders:u,selectEncoder:p,updateOption:m,resetOptions:w}}const ee={class:"image-compressor"},te={key:0,class:"preview-area"},ne={class:"preview-item"},oe=["src"],re={class:"preview-item"},ae=["src"],le={key:1,class:"compressing"},ie=["value"],se={key:2},ce={key:1,class:"encoder-settings"},de=["value"],me={key:0,class:"quality-control"},ue=["value"],fe={key:2,class:"actions"},pe=["disabled"],ge={key:3,class:"error"},be=((n,t)=>{const r=n.__vccOpts||n;for(const[o,u]of t)r[o]=u;return r})(e.defineComponent({__name:"ImageCompressor",props:{defaultEncoder:{},defaultOptions:{}},emits:["success","error","cancel"],setup(n,{emit:t}){const r=n,o=t,u=e.ref(null),p=e.ref(null),m=e.ref(""),w=e.ref(""),{compress:E,cancel:T,downloadResult:_,isCompressing:C,progress:R,error:N,result:k,canCancel:f}=Q(),{selectedEncoder:a,encoderOptions:i,availableEncoders:c,selectEncoder:g,updateOption:b}=K();r.defaultEncoder&&g(r.defaultEncoder),r.defaultOptions&&Object.entries(r.defaultOptions).forEach(([d,l])=>b(d,l));const h=e.computed(()=>p.value?V(p.value.size):""),D=e.computed(()=>k.value?V(k.value.compressed.size):""),I=e.computed(()=>["mozJPEG","webP","jxl","browserJPEG","wp2"].includes(a.value));function j(d){const l=d.target;l.files&&l.files[0]&&z(l.files[0])}function O(d){var l;(l=d.dataTransfer)!=null&&l.files[0]&&z(d.dataTransfer.files[0])}function z(d){d.type.startsWith("image/")&&(p.value=d,m.value=URL.createObjectURL(d),w.value&&(URL.revokeObjectURL(w.value),w.value=""))}function P(){g(a.value)}async function U(){if(p.value)try{const d={encoder:a.value,encoderOptions:{...i.value}},l=await E(p.value,d);w.value=l.compressed.blobUrl,o("success",l)}catch(d){if(d instanceof DOMException&&d.name==="AbortError"){o("cancel");return}o("error",d instanceof Error?d:new Error(String(d)))}}return e.watch(()=>p.value,(d,l)=>{d&&d!==l&&m.value&&URL.revokeObjectURL(m.value)}),(d,l)=>(e.openBlock(),e.createElementBlock("div",ee,[e.createElementVNode("div",{class:"upload-zone",onDrop:e.withModifiers(O,["prevent"]),onDragover:l[1]||(l[1]=e.withModifiers(()=>{},["prevent"]))},[e.createElementVNode("input",{type:"file",accept:"image/*",onChange:j,ref_key:"fileInput",ref:u,class:"file-input"},null,544),e.createElementVNode("div",{class:"upload-prompt",onClick:l[0]||(l[0]=y=>{var Y;return(Y=u.value)==null?void 0:Y.click()})},[e.renderSlot(d.$slots,"upload-prompt",{},()=>[l[6]||(l[6]=e.createElementVNode("p",null,"拖拽图片到此处,或点击上传",-1))],!0)])],32),m.value?(e.openBlock(),e.createElementBlock("div",te,[e.createElementVNode("div",ne,[l[7]||(l[7]=e.createElementVNode("h4",null,"原始图片",-1)),e.createElementVNode("img",{src:m.value,alt:"Original"},null,8,oe),e.createElementVNode("p",null,e.toDisplayString(h.value),1)]),e.createElementVNode("div",re,[l[8]||(l[8]=e.createElementVNode("h4",null,"压缩后",-1)),w.value?(e.openBlock(),e.createElementBlock("img",{key:0,src:w.value,alt:"Compressed"},null,8,ae)):e.unref(C)?(e.openBlock(),e.createElementBlock("div",le,[e.createElementVNode("progress",{value:e.unref(R),max:"100"},null,8,ie),e.createElementVNode("p",null,"压缩中... "+e.toDisplayString(e.unref(R))+"%",1)])):e.createCommentVNode("",!0),D.value?(e.openBlock(),e.createElementBlock("p",se,e.toDisplayString(D.value),1)):e.createCommentVNode("",!0)])])):e.createCommentVNode("",!0),m.value?(e.openBlock(),e.createElementBlock("div",ce,[l[9]||(l[9]=e.createElementVNode("label",null,"编码器:",-1)),e.withDirectives(e.createElementVNode("select",{"onUpdate:modelValue":l[2]||(l[2]=y=>e.isRef(a)?a.value=y:null),onChange:P},[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(e.unref(c),y=>(e.openBlock(),e.createElementBlock("option",{key:y.type,value:y.type},e.toDisplayString(y.label)+" ("+e.toDisplayString(y.extension)+") ",9,de))),128))],544),[[e.vModelSelect,e.unref(a)]]),I.value?(e.openBlock(),e.createElementBlock("div",me,[e.createElementVNode("label",null,[e.createTextVNode(" 质量: "+e.toDisplayString(e.unref(i).quality??75)+" ",1),e.createElementVNode("input",{type:"range",min:"0",max:"100",value:e.unref(i).quality??75,onInput:l[3]||(l[3]=y=>e.unref(b)("quality",Number(y.target.value))),"-------":"",REPLACE:""},null,40,ue)])])):e.createCommentVNode("",!0),e.renderSlot(d.$slots,"encoder-settings",{encoder:e.unref(a),options:e.unref(i)},void 0,!0)])):e.createCommentVNode("",!0),m.value?(e.openBlock(),e.createElementBlock("div",fe,[e.createElementVNode("button",{onClick:U,disabled:e.unref(C),class:"btn-primary"},e.toDisplayString(e.unref(C)?"压缩中...":"开始压缩"),9,pe),e.unref(f)?(e.openBlock(),e.createElementBlock("button",{key:0,onClick:l[4]||(l[4]=(...y)=>e.unref(T)&&e.unref(T)(...y)),class:"btn-secondary"}," 取消 ")):e.createCommentVNode("",!0),e.unref(k)?(e.openBlock(),e.createElementBlock("button",{key:1,onClick:l[5]||(l[5]=(...y)=>e.unref(_)&&e.unref(_)(...y)),class:"btn-success"}," 下载 (节省 "+e.toDisplayString(e.unref(k).savingsPercent)+"%) ",1)):e.createCommentVNode("",!0)])):e.createCommentVNode("",!0),e.unref(N)?(e.openBlock(),e.createElementBlock("div",ge,e.toDisplayString(e.unref(N)),1)):e.createCommentVNode("",!0)]))}}),[["__scopeId","data-v-86f2b482"]]),Ee=[{value:"lanczos3",label:"Lanczos3"},{value:"catrom",label:"Catrom"},{value:"mitchell",label:"Mitchell"},{value:"triangle",label:"Triangle"},{value:"vector",label:"Vector"}],we=[{value:"stretch",label:"拉伸"},{value:"contain",label:"适应"}],he=[{value:0,label:"不旋转"},{value:90,label:"顺时针 90°"},{value:180,label:"旋转 180°"},{value:270,label:"逆时针 90°"}];s.DEFAULT_ENCODER_OPTIONS=S,s.ENCODER_LIST=G,s.ENCODER_REGISTRY=B,s.FIT_METHODS=we,s.ImageCompressor=be,s.RESIZE_METHODS=Ee,s.ROTATE_OPTIONS=he,s.arrayBufferToFile=A,s.blobToImageData=M,s.calculateSavings=W,s.canDecodeImageType=F,s.createBlobUrl=q,s.formatBytes=V,s.generateCompressedFilename=J,s.getImageDimensions=Z,s.imageDataToBlob=L,s.revokeBlobUrl=$,s.sniffMimeType=x,s.useCompression=Q,s.useEncoderRegistry=K,s.useWorker=H,Object.defineProperty(s,Symbol.toStringTag,{value:"Module"})});
1
+ (function(u,e){typeof exports=="object"&&typeof module<"u"?e(exports,require("vue")):typeof define=="function"&&define.amd?define(["exports","vue"],e):(u=typeof globalThis<"u"?globalThis:u||self,e(u.VueImageCompressor={},u.Vue))})(this,function(u,e){"use strict";var I=typeof document<"u"?document.currentScript:null;const B={mozJPEG:{label:"MozJPEG",mimeType:"image/jpeg",extension:"jpg"},webP:{label:"WebP",mimeType:"image/webp",extension:"webp"},avif:{label:"AVIF",mimeType:"image/avif",extension:"avif"},jxl:{label:"JPEG XL",mimeType:"image/jxl",extension:"jxl"},oxiPNG:{label:"OxiPNG",mimeType:"image/png",extension:"png"},browserJPEG:{label:"Browser JPEG",mimeType:"image/jpeg",extension:"jpg"},browserPNG:{label:"Browser PNG",mimeType:"image/png",extension:"png"},browserGIF:{label:"Browser GIF",mimeType:"image/gif",extension:"gif"},qoi:{label:"QOI",mimeType:"image/qoi",extension:"qoi"},wp2:{label:"WebP2",mimeType:"image/webp2",extension:"wp2"}},x={mozJPEG:{quality:75,baseline:!1,arithmetic:!1,progressive:!0,optimize_coding:!0,smoothing:0,color_space:3,quant_table:3,trellis_multipass:!1,trellis_opt_zero:!1,trellis_opt_table:!1,trellis_loops:1,auto_subsample:!0,chroma_subsample:2,separate_chroma_quality:!1,chroma_quality:75},webP:{quality:75,target_size:0,target_PSNR:0,method:4,sns_strength:50,filter_strength:60,filter_sharpness:0,filter_type:1,partitions:0,segments:4,pass:1,show_compressed:0,preprocessing:0,autofilter:0,partition_limit:0,alpha_compression:1,alpha_filtering:1,alpha_quality:100,lossless:0,exact:0,use_delta_palette:0,vlnr:0,near_lossless:60},avif:{cqLevel:33,denoiseLevel:0,cqAlphaLevel:-1,tileRows:0,tileCols:0,speed:6,subsample:1,chromaDeltaQ:!1,sharpness:0,tune:0},jxl:{effort:7,quality:75,progressive:!1,targetPsize:0},oxiPNG:{level:2},browserJPEG:{quality:.75},browserPNG:{},browserGIF:{},qoi:{},wp2:{quality:75}},A=Object.keys(B);async function q(r){const n=await createImageBitmap(r),a=document.createElement("canvas");a.width=n.width,a.height=n.height;const o=a.getContext("2d");return o.drawImage(n,0,0),o.getImageData(0,0,a.width,a.height)}async function N(r,n,a){const o=document.createElement("canvas");return o.width=r.width,o.height=r.height,o.getContext("2d").putImageData(r,0,0),new Promise(i=>{o.toBlob(s=>i(s),n,a)})}function Z(r){return new Promise((n,a)=>{const o=new Image;o.onload=()=>{n({width:o.width,height:o.height}),URL.revokeObjectURL(o.src)},o.onerror=a,o.src=URL.createObjectURL(r)})}async function W(r){const n=new Uint8Array(await r.slice(0,4).arrayBuffer());return n[0]===137&&n[1]===80?"image/png":n[0]===255&&n[1]===216?"image/jpeg":n[0]===71&&n[1]===73?"image/gif":n[0]===82&&n[1]===73?"image/webp":n[0]===0&&n[1]===0?"image/avif":r.type||"image/jpeg"}function J(r){return new Promise(n=>{const a=new Image;a.onload=()=>n(!0),a.onerror=()=>n(!1),a.src=`data:${r};base64,`})}function L(r,n,a){return new File([r],n,{type:a})}function M(r,n=2){if(r===0)return"0 Bytes";const a=1024,o=n<0?0:n,t=["Bytes","KB","MB","GB"],i=Math.floor(Math.log(r)/Math.log(a));return parseFloat((r/Math.pow(a,i)).toFixed(o))+" "+t[i]}function S(r,n){if(r===0)return 0;const a=Math.round((r-n)/r*100);return Math.max(0,a)}function G(r,n){return`${r.replace(/\.[^.]+$/,"")}_compressed.${n}`}function V(r){return URL.createObjectURL(r)}function $(r){URL.revokeObjectURL(r)}const X=1e4;function ee(r){let n=0;const a=new Map;r.addEventListener("message",t=>{const{id:i,result:s,error:p}=t.data,m=a.get(i);m&&(a.delete(i),p?m.reject(new Error(p)):m.resolve(s))}),r.addEventListener("error",t=>{console.error("Worker error:",{message:t.message,filename:t.filename,lineno:t.lineno,colno:t.colno,error:t.error}),a.forEach(i=>i.reject(t)),a.clear()});function o(t,...i){return new Promise((s,p)=>{const m=++n;a.set(m,{resolve:s,reject:p}),r.postMessage({id:m,method:t,args:i})})}return{avifDecode:t=>o("avifDecode",t),jxlDecode:t=>o("jxlDecode",t),qoiDecode:t=>o("qoiDecode",t),webpDecode:t=>o("webpDecode",t),wp2Decode:t=>o("wp2Decode",t),avifEncode:(t,i)=>o("avifEncode",t,i),jxlEncode:(t,i)=>o("jxlEncode",t,i),mozjpegEncode:(t,i)=>o("mozjpegEncode",t,i),oxipngEncode:(t,i)=>o("oxipngEncode",t,i),qoiEncode:(t,i)=>o("qoiEncode",t,i),webpEncode:(t,i)=>o("webpEncode",t,i),wp2Encode:(t,i)=>o("wp2Encode",t,i),rotate:(t,i)=>o("rotate",t,i),quantize:(t,i)=>o("quantize",t,i),resize:(t,i)=>o("resize",t,i)}}function H(){const r=e.ref(!1),n=e.ref(!1),a=e.ref(null);let o=null,t=null;function i(){if(!o)try{n.value=!0,o=new Worker(new URL("/assets/imageWorker-Qv3pCULy.js",typeof document>"u"&&typeof location>"u"?require("url").pathToFileURL(__filename).href:typeof document>"u"?location.href:I&&I.tagName.toUpperCase()==="SCRIPT"&&I.src||new URL("vue-image-compressor.umd.cjs",document.baseURI).href),{type:"module"}),r.value=!0,a.value=null}catch(h){a.value=h instanceof Error?h:new Error("Failed to start worker"),console.error("Worker start failed:",h)}finally{n.value=!1}}function s(){t&&(clearTimeout(t),t=null),o&&(o.terminate(),o=null,r.value=!1)}function p(){t&&clearTimeout(t),t=window.setTimeout(()=>{s()},X)}async function m(){if(o||i(),!o)throw new Error("Worker failed to initialize");return ee(o)}async function y(h,T){if(h.aborted)throw new DOMException("AbortError","AbortError");return clearTimeout(t),new Promise((C,O)=>{const D=()=>{s(),O(new DOMException("AbortError","AbortError"))};h.addEventListener("abort",D),T().then(C).catch(O).finally(()=>{h.removeEventListener("abort",D),p()})})}return e.onUnmounted(()=>{s()}),{isReady:r,isLoading:n,error:a,worker:o,getWorkerApi:m,executeTask:y,terminateWorker:s}}function Q(){const r=e.ref(!1),n=e.ref(0),a=e.ref(null),o=e.ref(null),t=e.ref(null),{getWorkerApi:i,executeTask:s}=H(),p=e.computed(()=>r.value&&!!t.value);async function m(E,l){const c=await W(l),f=await J(c),w=await i();if(!f){if(c==="image/avif")return s(E,()=>w.avifDecode(l));if(c==="image/webp")return s(E,()=>w.webpDecode(l));if(c==="image/jxl")return s(E,()=>w.jxlDecode(l));if(c==="image/webp2")return s(E,()=>w.wp2Decode(l));if(c==="image/qoi")return s(E,()=>w.qoiDecode(l))}return q(l)}async function y(E,l,c){if(c===0)return l;const f=await i();return s(E,()=>f.rotate(l,{rotate:c}))}async function h(E,l,c){var b,k;let f=l;const w=await i();return(b=c.resize)!=null&&b.enabled&&(f=await s(E,()=>w.resize(f,{width:c.resize.width||f.width,height:c.resize.height||f.height,method:c.resize.method||"lanczos3",fitMethod:c.resize.fitMethod||"stretch",premultiply:!0,linearRGB:!0}))),(k=c.quantize)!=null&&k.enabled&&(f=await s(E,()=>w.quantize(f,{numColors:c.quantize.numColors||256,dither:c.quantize.dither||1}))),f}async function T(E,l,c){const f=await i(),w=c.encoder;if(w==="browserJPEG")return(await N(l,"image/jpeg",c.encoderOptions.quality)).arrayBuffer();if(w==="browserPNG")return(await N(l,"image/png")).arrayBuffer();if(w==="browserGIF")throw new Error("Browser GIF encoding not supported directly");const k={mozJPEG:f.mozjpegEncode,webP:f.webpEncode,avif:f.avifEncode,jxl:f.jxlEncode,oxiPNG:f.oxipngEncode,qoi:f.qoiEncode,wp2:f.wp2Encode}[w];if(!k)throw new Error(`Unknown encoder: ${w}`);return(await s(E,()=>k.call(f,l,c.encoderOptions))).buffer}async function C(E,l){var w;t.value=new AbortController;const c=t.value.signal;r.value=!0,n.value=0,a.value=null;const f=o.value;o.value=null,(w=f==null?void 0:f.compressed)!=null&&w.blobUrl&&$(f.compressed.blobUrl);try{n.value=10;const b={file:E,size:E.size,width:0,height:0,blobUrl:V(E)},k=await m(c,E);b.width=k.width,b.height=k.height,n.value=25;const P=await y(c,k,l.rotate||0);n.value=35;const z=await h(c,P,l);n.value=50;const F=await T(c,z,l);n.value=80;const v=B[l.encoder],j=G(E.name,(v==null?void 0:v.extension)||"bin"),R=L(F,j,(v==null?void 0:v.mimeType)||"application/octet-stream"),U={file:R,size:R.size,width:z.width,height:z.height,blobUrl:V(R)},g={original:b,compressed:U,savingsBytes:b.size-U.size,savingsPercent:S(b.size,U.size),encoderType:l.encoder,encoderOptions:l.encoderOptions};return o.value=g,n.value=100,g}catch(b){if(b instanceof DOMException&&b.name==="AbortError")throw a.value="已取消",b;const k=b instanceof Error?b.message:"压缩失败";throw a.value=k,console.error("Compression error:",b),b}finally{r.value=!1,t.value=null}}function O(){t.value&&t.value.abort()}function D(){if(!o.value)return;const{compressed:E}=o.value,l=document.createElement("a");l.href=E.blobUrl,l.download=E.file.name,document.body.appendChild(l),l.click(),document.body.removeChild(l)}return{compress:C,cancel:O,downloadResult:D,isCompressing:r,progress:n,error:a,result:o,canCancel:p}}function K(){const r=e.ref("mozJPEG"),n=e.ref({}),a=e.computed(()=>B[r.value]),o=e.computed(()=>x[r.value]),t=e.computed(()=>A.map(m=>({type:m,label:B[m].label,mimeType:B[m].mimeType,extension:B[m].extension})));function i(m){r.value=m,n.value={...x[m]}}function s(m,y){n.value={...n.value,[m]:y}}function p(){n.value={...o.value}}return p(),{selectedEncoder:r,encoderOptions:n,currentMeta:a,currentDefaults:o,availableEncoders:t,selectEncoder:i,updateOption:s,resetOptions:p}}const te={class:"image-compressor"},oe={key:0,class:"preview-area"},ne={class:"preview-item"},re=["src"],ae={class:"preview-item"},ie=["src"],se={key:1,class:"compressing"},le=["value"],ce={key:2},de={key:1,class:"encoder-settings"},me=["value"],ue={key:0,class:"quality-control"},fe=["value"],pe={key:2,class:"actions"},ge=["disabled"],Ee={key:3,class:"error"},we=((r,n)=>{const a=r.__vccOpts||r;for(const[o,t]of n)a[o]=t;return a})(e.defineComponent({__name:"ImageCompressor",props:{defaultEncoder:{},defaultOptions:{}},emits:["success","error","cancel"],setup(r,{emit:n}){const a=r,o=n,t=e.ref(null),i=e.ref(null),s=e.ref(""),p=e.ref(""),{compress:m,cancel:y,downloadResult:h,isCompressing:T,progress:C,error:O,result:D,canCancel:E}=Q(),{selectedEncoder:l,encoderOptions:c,availableEncoders:f,selectEncoder:w,updateOption:b}=K();a.defaultEncoder&&w(a.defaultEncoder),a.defaultOptions&&Object.entries(a.defaultOptions).forEach(([g,d])=>b(g,d));const k=e.computed(()=>i.value?M(i.value.size):""),P=e.computed(()=>D.value?M(D.value.compressed.size):""),z=e.computed(()=>["mozJPEG","webP","jxl","browserJPEG","wp2"].includes(l.value));function F(g){const d=g.target;d.files&&d.files[0]&&j(d.files[0])}function v(g){var d;(d=g.dataTransfer)!=null&&d.files[0]&&j(g.dataTransfer.files[0])}function j(g){g.type.startsWith("image/")&&(s.value&&URL.revokeObjectURL(s.value),p.value&&(URL.revokeObjectURL(p.value),p.value=""),i.value=g,s.value=URL.createObjectURL(g))}function R(){w(l.value)}async function U(){if(i.value)try{const g={encoder:l.value,encoderOptions:{...c.value}},d=await m(i.value,g);p.value=d.compressed.blobUrl,o("success",d)}catch(g){if(g instanceof DOMException&&g.name==="AbortError"){o("cancel");return}o("error",g instanceof Error?g:new Error(String(g)))}}return(g,d)=>(e.openBlock(),e.createElementBlock("div",te,[e.createElementVNode("div",{class:"upload-zone",onDrop:e.withModifiers(v,["prevent"]),onDragover:d[1]||(d[1]=e.withModifiers(()=>{},["prevent"]))},[e.createElementVNode("input",{type:"file",accept:"image/*",onChange:F,ref_key:"fileInput",ref:t,class:"file-input"},null,544),e.createElementVNode("div",{class:"upload-prompt",onClick:d[0]||(d[0]=_=>{var Y;return(Y=t.value)==null?void 0:Y.click()})},[e.renderSlot(g.$slots,"upload-prompt",{},()=>[d[6]||(d[6]=e.createElementVNode("p",null,"拖拽图片到此处,或点击上传",-1))],!0)])],32),s.value?(e.openBlock(),e.createElementBlock("div",oe,[e.createElementVNode("div",ne,[d[7]||(d[7]=e.createElementVNode("h4",null,"原始图片",-1)),e.createElementVNode("img",{src:s.value,alt:"Original"},null,8,re),e.createElementVNode("p",null,e.toDisplayString(k.value),1)]),e.createElementVNode("div",ae,[d[8]||(d[8]=e.createElementVNode("h4",null,"压缩后",-1)),p.value?(e.openBlock(),e.createElementBlock("img",{key:0,src:p.value,alt:"Compressed"},null,8,ie)):e.unref(T)?(e.openBlock(),e.createElementBlock("div",se,[e.createElementVNode("progress",{value:e.unref(C),max:"100"},null,8,le),e.createElementVNode("p",null,"压缩中... "+e.toDisplayString(e.unref(C))+"%",1)])):e.createCommentVNode("",!0),P.value?(e.openBlock(),e.createElementBlock("p",ce,e.toDisplayString(P.value),1)):e.createCommentVNode("",!0)])])):e.createCommentVNode("",!0),s.value?(e.openBlock(),e.createElementBlock("div",de,[d[9]||(d[9]=e.createElementVNode("label",null,"编码器:",-1)),e.withDirectives(e.createElementVNode("select",{"onUpdate:modelValue":d[2]||(d[2]=_=>e.isRef(l)?l.value=_:null),onChange:R},[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(e.unref(f),_=>(e.openBlock(),e.createElementBlock("option",{key:_.type,value:_.type},e.toDisplayString(_.label)+" ("+e.toDisplayString(_.extension)+") ",9,me))),128))],544),[[e.vModelSelect,e.unref(l)]]),z.value?(e.openBlock(),e.createElementBlock("div",ue,[e.createElementVNode("label",null,[e.createTextVNode(" 质量: "+e.toDisplayString(e.unref(c).quality??75)+" ",1),e.createElementVNode("input",{type:"range",min:"0",max:"100",value:e.unref(c).quality??75,onInput:d[3]||(d[3]=_=>e.unref(b)("quality",Number(_.target.value)))},null,40,fe)])])):e.createCommentVNode("",!0),e.renderSlot(g.$slots,"encoder-settings",{encoder:e.unref(l),options:e.unref(c)},void 0,!0)])):e.createCommentVNode("",!0),s.value?(e.openBlock(),e.createElementBlock("div",pe,[e.createElementVNode("button",{onClick:U,disabled:e.unref(T),class:"btn-primary"},e.toDisplayString(e.unref(T)?"压缩中...":"开始压缩"),9,ge),e.unref(E)?(e.openBlock(),e.createElementBlock("button",{key:0,onClick:d[4]||(d[4]=(..._)=>e.unref(y)&&e.unref(y)(..._)),class:"btn-secondary"}," 取消 ")):e.createCommentVNode("",!0),e.unref(D)?(e.openBlock(),e.createElementBlock("button",{key:1,onClick:d[5]||(d[5]=(..._)=>e.unref(h)&&e.unref(h)(..._)),class:"btn-success"}," 下载 (节省 "+e.toDisplayString(e.unref(D).savingsPercent)+"%) ",1)):e.createCommentVNode("",!0)])):e.createCommentVNode("",!0),e.unref(O)?(e.openBlock(),e.createElementBlock("div",Ee,e.toDisplayString(e.unref(O)),1)):e.createCommentVNode("",!0)]))}}),[["__scopeId","data-v-d7e6eec9"]]),be=[{value:"lanczos3",label:"Lanczos3"},{value:"catrom",label:"Catrom"},{value:"mitchell",label:"Mitchell"},{value:"triangle",label:"Triangle"},{value:"vector",label:"Vector"}],he=[{value:"stretch",label:"拉伸"},{value:"contain",label:"适应"}],ye=[{value:0,label:"不旋转"},{value:90,label:"顺时针 90°"},{value:180,label:"旋转 180°"},{value:270,label:"逆时针 90°"}],ke={mozJPEG:"mozjpegEncode",webP:"webpEncode",avif:"avifEncode",jxl:"jxlEncode",oxiPNG:"oxipngEncode",qoi:"qoiEncode",wp2:"wp2Encode"};function _e(){return new Worker(new URL("/assets/imageWorker-Qv3pCULy.js",typeof document>"u"&&typeof location>"u"?require("url").pathToFileURL(__filename).href:typeof document>"u"?location.href:I&&I.tagName.toUpperCase()==="SCRIPT"&&I.src||new URL("vue-image-compressor.umd.cjs",document.baseURI).href),{type:"module"})}function Te(r,n,a,o){return new Promise((t,i)=>{const s=_e();let p=!1;function m(){p||(p=!0,s.terminate())}s.addEventListener("message",T=>{const{result:C,error:O}=T.data;m(),O?i(new Error(O)):t(C.buffer)}),s.addEventListener("error",T=>{m(),i(T.error||T)});const y=()=>{m(),i(new DOMException("AbortError","AbortError"))};o&&o.addEventListener("abort",y,{once:!0});const h=ke[r];if(!h){m(),i(new Error(`Unknown encoder: ${r}`));return}s.postMessage({id:1,method:h,args:[n,a]})})}async function Oe(r,n={}){const{signal:a}=n;if(a!=null&&a.aborted)throw new DOMException("AbortError","AbortError");const o=n.encoder||"mozJPEG",t={...x[o],...n.encoderOptions},i=B[o],s=await q(r);if(a!=null&&a.aborted)throw new DOMException("AbortError","AbortError");let p;if(o==="browserJPEG")p=await(await N(s,"image/jpeg",t.quality)).arrayBuffer();else if(o==="browserPNG")p=await(await N(s,"image/png")).arrayBuffer();else{if(o==="browserGIF")throw new Error("Browser GIF encoding not supported directly");p=await Te(o,s,t,a)}if(a!=null&&a.aborted)throw new DOMException("AbortError","AbortError");const m=G(r.name,i.extension),y=L(p,m,i.mimeType);return{file:y,originalSize:r.size,compressedSize:y.size,savingsBytes:r.size-y.size,savingsPercent:S(r.size,y.size),width:s.width,height:s.height,encoderType:o,encoderOptions:t}}u.DEFAULT_ENCODER_OPTIONS=x,u.ENCODER_LIST=A,u.ENCODER_REGISTRY=B,u.FIT_METHODS=he,u.ImageCompressor=we,u.RESIZE_METHODS=be,u.ROTATE_OPTIONS=ye,u.arrayBufferToFile=L,u.blobToImageData=q,u.calculateSavings=S,u.canDecodeImageType=J,u.compressFile=Oe,u.createBlobUrl=V,u.formatBytes=M,u.generateCompressedFilename=G,u.getImageDimensions=Z,u.imageDataToBlob=N,u.revokeBlobUrl=$,u.sniffMimeType=W,u.useCompression=Q,u.useEncoderRegistry=K,u.useWorker=H,Object.defineProperty(u,Symbol.toStringTag,{value:"Module"})});
@@ -2,5 +2,19 @@
2
2
  * Emscripten WASM 模块初始化工具
3
3
  * 对标 Squoosh: src/features/worker-utils/index.ts
4
4
  */
5
- export declare function initEmscriptenModule<T extends EmscriptenWasm.Module>(moduleFactory: EmscriptenWasm.ModuleFactory<T>): Promise<T>;
5
+ declare namespace EmscriptenWasm {
6
+ interface Module {
7
+ noInitialRun?: boolean;
8
+ }
9
+ interface ModuleFactory<T extends Module> {
10
+ (opts?: {
11
+ noInitialRun?: boolean;
12
+ locateFile?: (path: string) => string;
13
+ }): Promise<T>;
14
+ }
15
+ }
16
+ export declare function initEmscriptenModule<T extends EmscriptenWasm.Module>(moduleFactory: EmscriptenWasm.ModuleFactory<T>, opts?: {
17
+ locateFile?: (path: string) => string;
18
+ }): Promise<T>;
6
19
  export declare function blobToArrayBuffer(blob: Blob): Promise<ArrayBuffer>;
20
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vue3-image-compressor",
3
- "version": "1.0.4",
3
+ "version": "1.0.7",
4
4
  "description": "Vue3 + TypeScript 图片压缩组件,基于 Squoosh 核心原理",
5
5
  "type": "module",
6
6
  "main": "./dist/vue-image-compressor.umd.cjs",
@@ -19,6 +19,7 @@
19
19
  ],
20
20
  "scripts": {
21
21
  "build": "vue-tsc && vite build",
22
+ "build:demo": "vite build --config vite.config.demo.ts",
22
23
  "dev": "vite",
23
24
  "preview": "vite preview"
24
25
  },
@@ -57,8 +57,6 @@
57
57
  max="100"
58
58
  :value="encoderOptions.quality ?? 75"
59
59
  @input="updateOption('quality', Number(($event.target as HTMLInputElement).value))"
60
- ------- REPLACE
61
-
62
60
  />
63
61
  </label>
64
62
  </div>
@@ -98,7 +96,7 @@
98
96
  </template>
99
97
 
100
98
  <script setup lang="ts">
101
- import { ref, computed, watch } from 'vue';
99
+ import { ref, computed } from 'vue';
102
100
  import { useCompression } from '../composables/useCompression';
103
101
  import { useEncoderRegistry } from '../composables/useEncoderRegistry';
104
102
  import type { CompressionOptions } from '../types/compression';
@@ -175,13 +173,19 @@ function onDrop(event: DragEvent) {
175
173
 
176
174
  function handleFile(file: File) {
177
175
  if (!file.type.startsWith('image/')) return;
178
- currentFile.value = file;
179
- originalUrl.value = URL.createObjectURL(file);
176
+
177
+ // 清理旧的原始图片 URL
178
+ if (originalUrl.value) {
179
+ URL.revokeObjectURL(originalUrl.value);
180
+ }
180
181
  // 清理旧的压缩结果
181
182
  if (compressedUrl.value) {
182
183
  URL.revokeObjectURL(compressedUrl.value);
183
184
  compressedUrl.value = '';
184
185
  }
186
+
187
+ currentFile.value = file;
188
+ originalUrl.value = URL.createObjectURL(file);
185
189
  }
186
190
 
187
191
  function onEncoderChange() {
@@ -209,16 +213,6 @@ async function startCompress() {
209
213
  emit('error', err instanceof Error ? err : new Error(String(err)));
210
214
  }
211
215
  }
212
-
213
- // 清理
214
- watch(
215
- () => currentFile.value,
216
- (oldVal, newVal) => {
217
- if (oldVal && oldVal !== newVal && originalUrl.value) {
218
- URL.revokeObjectURL(originalUrl.value);
219
- }
220
- }
221
- );
222
216
  </script>
223
217
 
224
218
  <style scoped>
@@ -10,13 +10,72 @@ import type { WorkerApi } from '../types/worker';
10
10
  // Worker 空闲超时时间(ms)
11
11
  const WORKER_TIMEOUT = 10000;
12
12
 
13
+ /**
14
+ * 创建基于 postMessage 的 Worker Proxy
15
+ * 将 Worker 包装为 Promise-based 的 API 对象
16
+ */
17
+ function createWorkerProxy(worker: Worker): WorkerApi {
18
+ let callId = 0;
19
+ const pending = new Map<number, { resolve: (v: any) => void; reject: (e: any) => void }>();
20
+
21
+ worker.addEventListener('message', (event) => {
22
+ const { id, result, error } = event.data;
23
+ const p = pending.get(id);
24
+ if (p) {
25
+ pending.delete(id);
26
+ if (error) {
27
+ p.reject(new Error(error));
28
+ } else {
29
+ p.resolve(result);
30
+ }
31
+ }
32
+ });
33
+
34
+ worker.addEventListener('error', (err) => {
35
+ console.error('Worker error:', {
36
+ message: err.message,
37
+ filename: err.filename,
38
+ lineno: err.lineno,
39
+ colno: err.colno,
40
+ error: err.error,
41
+ });
42
+ pending.forEach((p) => p.reject(err));
43
+ pending.clear();
44
+ });
45
+
46
+ function callMethod(method: string, ...args: any[]): Promise<any> {
47
+ return new Promise((resolve, reject) => {
48
+ const id = ++callId;
49
+ pending.set(id, { resolve, reject });
50
+ worker.postMessage({ id, method, args });
51
+ });
52
+ }
53
+
54
+ return {
55
+ avifDecode: (blob) => callMethod('avifDecode', blob),
56
+ jxlDecode: (blob) => callMethod('jxlDecode', blob),
57
+ qoiDecode: (blob) => callMethod('qoiDecode', blob),
58
+ webpDecode: (blob) => callMethod('webpDecode', blob),
59
+ wp2Decode: (blob) => callMethod('wp2Decode', blob),
60
+ avifEncode: (data, opts) => callMethod('avifEncode', data, opts),
61
+ jxlEncode: (data, opts) => callMethod('jxlEncode', data, opts),
62
+ mozjpegEncode: (data, opts) => callMethod('mozjpegEncode', data, opts),
63
+ oxipngEncode: (data, opts) => callMethod('oxipngEncode', data, opts),
64
+ qoiEncode: (data, opts) => callMethod('qoiEncode', data, opts),
65
+ webpEncode: (data, opts) => callMethod('webpEncode', data, opts),
66
+ wp2Encode: (data, opts) => callMethod('wp2Encode', data, opts),
67
+ rotate: (data, opts) => callMethod('rotate', data, opts),
68
+ quantize: (data, opts) => callMethod('quantize', data, opts),
69
+ resize: (data, opts) => callMethod('resize', data, opts),
70
+ } as WorkerApi;
71
+ }
72
+
13
73
  export function useWorker() {
14
74
  const isReady = ref(false);
15
75
  const isLoading = ref(false);
16
76
  const error = ref<Error | null>(null);
17
77
 
18
78
  let worker: Worker | null = null;
19
- let workerApi: WorkerApi | null = null;
20
79
  let timeoutId: number | null = null;
21
80
 
22
81
  /**
@@ -27,8 +86,6 @@ export function useWorker() {
27
86
 
28
87
  try {
29
88
  isLoading.value = true;
30
- // 使用 Vite 的 ?worker 导入方式
31
- // 实际项目中需要配置 worker 文件路径
32
89
  worker = new Worker(
33
90
  new URL('../workers/imageWorker.ts', import.meta.url),
34
91
  { type: 'module' }
@@ -54,7 +111,6 @@ export function useWorker() {
54
111
  if (worker) {
55
112
  worker.terminate();
56
113
  worker = null;
57
- workerApi = null;
58
114
  isReady.value = false;
59
115
  }
60
116
  }
@@ -79,9 +135,7 @@ export function useWorker() {
79
135
  if (!worker) {
80
136
  throw new Error('Worker failed to initialize');
81
137
  }
82
- // 简化版:直接返回 worker 的 postMessage API
83
- // 实际使用 comlink 时:return wrap<WorkerApi>(worker);
84
- return worker as any;
138
+ return createWorkerProxy(worker);
85
139
  }
86
140
 
87
141
  /**
package/src/index.ts CHANGED
@@ -61,3 +61,9 @@ export {
61
61
  createBlobUrl,
62
62
  revokeBlobUrl,
63
63
  } from './utils/file';
64
+
65
+ export { compressFile } from './utils/compressFile';
66
+ export type {
67
+ CompressFileOptions,
68
+ CompressFileResult,
69
+ } from './utils/compressFile';
@@ -0,0 +1,38 @@
1
+ declare module 'codecs/mozjpeg/enc/mozjpeg_enc' {
2
+ export enum MozJpegColorSpace {
3
+ GRAYSCALE = 1,
4
+ RGB = 2,
5
+ YCbCr = 3,
6
+ }
7
+
8
+ export interface EncodeOptions {
9
+ quality: number;
10
+ baseline: boolean;
11
+ arithmetic: boolean;
12
+ progressive: boolean;
13
+ optimize_coding: boolean;
14
+ smoothing: number;
15
+ color_space: MozJpegColorSpace;
16
+ quant_table: number;
17
+ trellis_multipass: boolean;
18
+ trellis_opt_zero: boolean;
19
+ trellis_opt_table: boolean;
20
+ trellis_loops: number;
21
+ auto_subsample: boolean;
22
+ chroma_subsample: number;
23
+ separate_chroma_quality: boolean;
24
+ chroma_quality: number;
25
+ }
26
+
27
+ export interface MozJPEGModule extends EmscriptenWasm.Module {
28
+ encode(
29
+ data: BufferSource,
30
+ width: number,
31
+ height: number,
32
+ options: EncodeOptions,
33
+ ): Uint8Array;
34
+ }
35
+
36
+ const moduleFactory: EmscriptenWasm.ModuleFactory<MozJPEGModule>;
37
+ export default moduleFactory;
38
+ }
@@ -0,0 +1,201 @@
1
+ /**
2
+ * 纯函数式手动压缩
3
+ * 参数:File 对象,返回压缩后的 File 对象及丰富信息
4
+ * 不依赖 Vue 响应式,支持 AbortSignal 取消
5
+ */
6
+
7
+ import { blobToImageData, imageDataToBlob, arrayBufferToFile } from './image';
8
+ import { generateCompressedFilename, calculateSavings } from './file';
9
+ import { ENCODER_REGISTRY, DEFAULT_ENCODER_OPTIONS } from '../constants/encoders';
10
+ import type { EncoderType } from '../types/encoder';
11
+
12
+ export interface CompressFileOptions {
13
+ /** 编码器类型,默认 mozJPEG */
14
+ encoder?: EncoderType;
15
+ /** 编码器选项,不传则使用默认值 */
16
+ encoderOptions?: Record<string, any>;
17
+ /** 支持取消 */
18
+ signal?: AbortSignal;
19
+ }
20
+
21
+ export interface CompressFileResult {
22
+ /** 压缩后的 File 对象 */
23
+ file: File;
24
+ /** 原始文件大小(字节) */
25
+ originalSize: number;
26
+ /** 压缩后文件大小(字节) */
27
+ compressedSize: number;
28
+ /** 节省字节数 */
29
+ savingsBytes: number;
30
+ /** 节省百分比 */
31
+ savingsPercent: number;
32
+ /** 图片宽度 */
33
+ width: number;
34
+ /** 图片高度 */
35
+ height: number;
36
+ /** 使用的编码器类型 */
37
+ encoderType: string;
38
+ /** 实际使用的编码器选项 */
39
+ encoderOptions: Record<string, any>;
40
+ }
41
+
42
+ const ENCODER_METHOD_MAP: Record<string, string> = {
43
+ mozJPEG: 'mozjpegEncode',
44
+ webP: 'webpEncode',
45
+ avif: 'avifEncode',
46
+ jxl: 'jxlEncode',
47
+ oxiPNG: 'oxipngEncode',
48
+ qoi: 'qoiEncode',
49
+ wp2: 'wp2Encode',
50
+ };
51
+
52
+ function createWorker(): Worker {
53
+ return new Worker(
54
+ new URL('../workers/imageWorker.ts', import.meta.url),
55
+ { type: 'module' }
56
+ );
57
+ }
58
+
59
+ /**
60
+ * 通过 Worker 调用 WASM 编码器
61
+ */
62
+ function encodeViaWorker(
63
+ encoder: string,
64
+ imageData: ImageData,
65
+ encoderOptions: Record<string, any>,
66
+ signal?: AbortSignal
67
+ ): Promise<ArrayBuffer> {
68
+ return new Promise((resolve, reject) => {
69
+ const worker = createWorker();
70
+ let settled = false;
71
+
72
+ function cleanup() {
73
+ if (!settled) {
74
+ settled = true;
75
+ worker.terminate();
76
+ }
77
+ }
78
+
79
+ worker.addEventListener('message', (event) => {
80
+ const { result, error } = event.data;
81
+ cleanup();
82
+ if (error) {
83
+ reject(new Error(error));
84
+ } else {
85
+ resolve((result as Uint8Array).buffer as ArrayBuffer);
86
+ }
87
+ });
88
+
89
+ worker.addEventListener('error', (err) => {
90
+ cleanup();
91
+ reject(err.error || err);
92
+ });
93
+
94
+ const onAbort = () => {
95
+ cleanup();
96
+ reject(new DOMException('AbortError', 'AbortError'));
97
+ };
98
+
99
+ if (signal) {
100
+ signal.addEventListener('abort', onAbort, { once: true });
101
+ }
102
+
103
+ const method = ENCODER_METHOD_MAP[encoder];
104
+ if (!method) {
105
+ cleanup();
106
+ reject(new Error(`Unknown encoder: ${encoder}`));
107
+ return;
108
+ }
109
+
110
+ worker.postMessage({ id: 1, method, args: [imageData, encoderOptions] });
111
+ });
112
+ }
113
+
114
+ /**
115
+ * 手动压缩图片
116
+ * @param file 原始图片文件
117
+ * @param options 压缩选项
118
+ * @returns 压缩结果,包含 File 对象及元信息
119
+ *
120
+ * @example
121
+ * ```ts
122
+ * const result = await compressFile(file, {
123
+ * encoder: 'mozJPEG',
124
+ * encoderOptions: { quality: 75 },
125
+ * });
126
+ * console.log(`压缩前: ${result.originalSize}, 压缩后: ${result.compressedSize}`);
127
+ * ```
128
+ */
129
+ export async function compressFile(
130
+ file: File,
131
+ options: CompressFileOptions = {}
132
+ ): Promise<CompressFileResult> {
133
+ const { signal } = options;
134
+
135
+ if (signal?.aborted) {
136
+ throw new DOMException('AbortError', 'AbortError');
137
+ }
138
+
139
+ const encoder: EncoderType = options.encoder || 'mozJPEG';
140
+ const encoderOptions = {
141
+ ...DEFAULT_ENCODER_OPTIONS[encoder],
142
+ ...options.encoderOptions,
143
+ };
144
+ const meta = ENCODER_REGISTRY[encoder];
145
+
146
+ // 1. 解码(主线程)
147
+ const imageData = await blobToImageData(file);
148
+
149
+ if (signal?.aborted) {
150
+ throw new DOMException('AbortError', 'AbortError');
151
+ }
152
+
153
+ // 2. 编码
154
+ let encodedBuffer: ArrayBuffer;
155
+
156
+ if (encoder === 'browserJPEG') {
157
+ const blob = await imageDataToBlob(
158
+ imageData,
159
+ 'image/jpeg',
160
+ encoderOptions.quality
161
+ );
162
+ encodedBuffer = await blob.arrayBuffer();
163
+ } else if (encoder === 'browserPNG') {
164
+ const blob = await imageDataToBlob(imageData, 'image/png');
165
+ encodedBuffer = await blob.arrayBuffer();
166
+ } else if (encoder === 'browserGIF') {
167
+ throw new Error('Browser GIF encoding not supported directly');
168
+ } else {
169
+ // WASM 编码器通过 Worker
170
+ encodedBuffer = await encodeViaWorker(
171
+ encoder,
172
+ imageData,
173
+ encoderOptions,
174
+ signal
175
+ );
176
+ }
177
+
178
+ if (signal?.aborted) {
179
+ throw new DOMException('AbortError', 'AbortError');
180
+ }
181
+
182
+ // 3. 组装结果
183
+ const filename = generateCompressedFilename(file.name, meta.extension);
184
+ const compressedFile = arrayBufferToFile(
185
+ encodedBuffer,
186
+ filename,
187
+ meta.mimeType
188
+ );
189
+
190
+ return {
191
+ file: compressedFile,
192
+ originalSize: file.size,
193
+ compressedSize: compressedFile.size,
194
+ savingsBytes: file.size - compressedFile.size,
195
+ savingsPercent: calculateSavings(file.size, compressedFile.size),
196
+ width: imageData.width,
197
+ height: imageData.height,
198
+ encoderType: encoder,
199
+ encoderOptions,
200
+ };
201
+ }
package/src/utils/file.ts CHANGED
@@ -19,7 +19,8 @@ export function formatBytes(bytes: number, decimals = 2): string {
19
19
  */
20
20
  export function calculateSavings(originalSize: number, compressedSize: number): number {
21
21
  if (originalSize === 0) return 0;
22
- return Math.round(((originalSize - compressedSize) / originalSize) * 100);
22
+ const saving = Math.round(((originalSize - compressedSize) / originalSize) * 100);
23
+ return Math.max(0, saving);
23
24
  }
24
25
 
25
26
  /**