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.
- package/README.md +6 -0
- package/dist/assets/imageWorker-Qv3pCULy.js +1042 -0
- package/dist/codecs/avif/enc/avif_enc.js +16 -0
- package/dist/codecs/avif/enc/avif_enc.wasm +0 -0
- package/dist/codecs/mozjpeg/enc/mozjpeg_enc.js +16 -0
- package/dist/codecs/mozjpeg/enc/mozjpeg_enc.wasm +0 -0
- package/dist/codecs/webp/dec/webp_dec.js +16 -0
- package/dist/codecs/webp/dec/webp_dec.wasm +0 -0
- package/dist/codecs/webp/enc/webp_enc.js +16 -0
- package/dist/codecs/webp/enc/webp_enc.wasm +0 -0
- package/dist/components/ImageCompressor.vue.d.ts +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/style.css +1 -1
- package/dist/utils/compressFile.d.ts +46 -0
- package/dist/vue-image-compressor.js +508 -382
- package/dist/vue-image-compressor.umd.cjs +1 -1
- package/dist/workers/utils/emscripten.d.ts +15 -1
- package/package.json +2 -1
- package/src/components/ImageCompressor.vue +9 -15
- package/src/composables/useWorker.ts +61 -7
- package/src/index.ts +6 -0
- package/src/types/mozjpeg.d.ts +38 -0
- package/src/utils/compressFile.ts +201 -0
- package/src/utils/file.ts +2 -1
- package/src/workers/imageWorker.ts +126 -30
- package/src/workers/utils/emscripten.ts +12 -1
- package/dist/assets/imageWorker-DyeUTFOy.js +0 -67
|
@@ -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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
179
|
-
|
|
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
|
-
|
|
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
|
@@ -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
|
-
|
|
22
|
+
const saving = Math.round(((originalSize - compressedSize) / originalSize) * 100);
|
|
23
|
+
return Math.max(0, saving);
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
/**
|