mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-11 03:00:26 +01:00
feat(diagram-render): __downscaleRaster for print-resolution image normalization
Data-URI rasters re-encode in their own format (JPEG stays JPEG at q0.9 — PNG-encoding photos bloats them) at an explicit target pixel width. Used by make-pdf's pre-pass for the 300dpi content-box ceiling (eng-review D4). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a311cb1ec5
commit
69bf3b07a1
3 files changed
+35
-3
No files matched your search
+2
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "gstack-diagram-render",
|
||||
"sha256": "9b1da250ef93f176bfb7b96c6eb735645a879702539bb6a58c378273d90aca23",
|
||||
"bytes": 9644924,
|
||||
"sha256": "0ee91aef5a8da85c8941c26ebf2991bbeba82412644bb070d5c5dd2e23538b81",
|
||||
"bytes": 9645503,
|
||||
"bunVersion": "1.3.13",
|
||||
"deps": {
|
||||
"@excalidraw/excalidraw": "0.18.0",
|
||||
|
||||
+1
-1
@@ -5128,7 +5128,7 @@ TTD mermaid definition render errror: ${$.message}`,"color: yellow"),CC("ai","me
|
||||
`)}}}else Q=eb(Xlu(D.link||""));let G=this.state.activeEmbeddable?.element===D&&this.state.activeEmbeddable?.state==="active",W=this.state.activeEmbeddable?.element===D&&this.state.activeEmbeddable?.state==="hover";return d("div",{className:j3("excalidraw__embeddable-container",{"is-hovered":W}),style:{transform:M?`translate(${Z-this.state.offsetLeft}px, ${X-this.state.offsetTop}px) scale(${B})`:"none",display:M?"block":"none",opacity:$Ru(D,_Q(D,this.scene.getNonDeletedElementsMap()),this.elementsPendingErasure,null,this.state.openDialog?.name==="elementLinkSelector"?hju:1),"--embeddable-radius":`${_O(Math.min(D.width,D.height),D)}px`},children:c0("div",{className:"excalidraw__embeddable-container__inner",style:{width:M?`${D.width}px`:0,height:M?`${D.height}px`:0,transform:M?`rotate(${D.angle}rad)`:"none",pointerEvents:G?_b.enabled:_b.disabled},children:[W&&d("div",{className:"excalidraw__embeddable-hint",children:r0("buttons.embeddableInteractionButton")}),d("div",{className:"excalidraw__embeddable__outer",style:{padding:`${D.strokeWidth}px`},children:($Q(D)?this.props.renderEmbeddable?.(D,this.state):null)??d("iframe",{ref:(q)=>this.cacheEmbeddableRef(D,q),className:"excalidraw__embeddable",srcDoc:Q?.type==="document"?Q.srcdoc(this.state.theme):void 0,src:Q?.type!=="document"?Q?.link??"":void 0,scrolling:"no",referrerPolicy:"no-referrer-when-downgrade",title:"Excalidraw Embedded Content",allow:"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture",allowFullScreen:!0,sandbox:`${Q?.sandbox?.allowSameOrigin?"allow-same-origin":""} allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox allow-presentation allow-downloads`})})]})},D.id)})})}toggleOverscrollBehavior(B){document.documentElement.style.overscrollBehaviorX=B.type==="pointerenter"?"none":"auto"}render(){let B=this.scene.getSelectedElements(this.state),{renderTopRightUI:C,renderCustomStats:F}=this.props,U=this.scene.getSceneNonce(),{elementsMap:D,visibleElements:Z}=this.renderer.getRenderableElements({sceneNonce:U,zoom:this.state.zoom,offsetLeft:this.state.offsetLeft,offsetTop:this.state.offsetTop,scrollX:this.state.scrollX,scrollY:this.state.scrollY,height:this.state.height,width:this.state.width,editingTextElement:this.state.editingTextElement,newElementId:this.state.newElement?.id,pendingImageElementId:this.state.pendingImageElementId});this.visibleElements=Z;let X=this.scene.getNonDeletedElementsMap(),M="setPointerCapture"in HTMLElement.prototype?!1:this.state.selectionElement||this.state.newElement||this.state.selectedElementsAreBeingDragged||this.state.resizingElement||this.state.activeTool.type==="laser"&&this.state.cursorButton==="down",K=B[0];return d("div",{className:j3("excalidraw excalidraw-container",{"excalidraw--view-mode":this.state.viewModeEnabled||this.state.openDialog?.name==="elementLinkSelector","excalidraw--mobile":this.device.editor.isMobile}),style:{"--ui-pointerEvents":M?_b.disabled:_b.enabled,"--right-sidebar-width":"302px"},ref:this.excalidrawContainerRef,onDrop:this.handleAppOnDrop,tabIndex:0,onKeyDown:this.props.handleKeyboardGlobally?void 0:this.onKeyDown,onPointerEnter:this.toggleOverscrollBehavior,onPointerLeave:this.toggleOverscrollBehavior,children:d(nU1.Provider,{value:this,children:d(sU1.Provider,{value:this.props,children:d(N$u.Provider,{value:this.excalidrawContainerValue,children:d(I$u.Provider,{value:this.device,children:d(x$u.Provider,{value:this.setAppState,children:d(T$u.Provider,{value:this.state,children:c0(k$u.Provider,{value:this.scene.getNonDeletedElements(),children:[c0(v$u.Provider,{value:this.actionManager,children:[d(KY9,{canvas:this.canvas,appState:this.state,files:this.files,setAppState:this.setAppState,actionManager:this.actionManager,elements:this.scene.getNonDeletedElements(),onLockToggle:this.toggleLock,onPenModeToggle:this.togglePenMode,onHandToolToggle:this.onHandToolToggle,langCode:CL0().code,renderTopRightUI:C,renderCustomStats:F,showExitZenModeBtn:typeof this.props?.zenModeEnabled>"u"&&this.state.zenModeEnabled,UIOptions:this.props.UIOptions,onExportImage:this.onExportImage,renderWelcomeScreen:!this.state.isLoading&&this.state.showWelcomeScreen&&this.state.activeTool.type==="selection"&&!this.state.zenModeEnabled&&!this.scene.getElementsIncludingDeleted().length,app:this,isCollaborating:this.props.isCollaborating,generateLinkForSelection:this.props.generateLinkForSelection,children:this.props.children}),d("div",{className:"excalidraw-textEditorContainer"}),d("div",{className:"excalidraw-contextMenuContainer"}),d("div",{className:"excalidraw-eye-dropper-container"}),d(cY9,{trails:[this.laserTrails,this.eraserTrail]}),B.length===1&&this.state.openDialog?.name!=="elementLinkSelector"&&this.state.showHyperlinkPopup&&d(iQ9,{element:K,elementsMap:X,setAppState:this.setAppState,onLinkOpen:this.props.onLinkOpen,setToast:this.setToast,updateEmbedValidationStatus:this.updateEmbedValidationStatus},K.id),this.props.aiEnabled!==!1&&BLine truncated
|
||||
|
||||
`),C)}}addTextFromPaste(B,C=!1){let{x:F,y:U}=EC({clientX:this.lastViewportPosition.x,clientY:this.lastViewportPosition.y},this.state),D={x:F,y:U,strokeColor:this.state.currentItemStrokeColor,backgroundColor:this.state.currentItemBackgroundColor,fillStyle:this.state.currentItemFillStyle,strokeWidth:this.state.currentItemStrokeWidth,strokeStyle:this.state.currentItemStrokeStyle,roundness:null,roughness:this.state.currentItemRoughness,opacity:this.state.currentItemOpacity,text:B,fontSize:this.state.currentItemFontSize,fontFamily:this.state.currentItemFontFamily,textAlign:E20,verticalAlign:dH0,locked:!1},Z=y8({fontSize:D.fontSize,fontFamily:D.fontFamily}),X=ii(D.fontFamily),[M,,K]=_x3(this.state),Q=Math.max(Math.min((K-M)*0.5,800),200),G=10,W=U,q=C?[B]:B.split(`
|
||||
`),P=q.reduce((w,N,k)=>{let x=ob(N).trim();if(x.length){let v=this.getTopLayerFrameAtSceneCoords({x:F,y:W}),R=MM(x,Z,X),j=R.width>Q,l=j?yi(x,Z,Q):x;R=j?MM(l,Z,X):R;let y=F-R.width/2,h=W-R.height/2,_=iL({...D,x:y,y:h,text:l,originalText:x,lineHeight:X,autoResize:!j,frameId:v?v.id:null});w.push(_),W+=_.height+G}else q[k-1]?.trim()&&(W+=Z20(D.fontSize,X)+G);return w},[]);P.length!==0&&(this.scene.insertElements(P),this.setState({selectedElementIds:pB(Object.fromEntries(P.map((w)=>[w.id,!0])),this.state)}),!C&&P.length>1&&DA1===!1&&!this.device.editor.isMobile&&(this.setToast({message:r0("toast.pasteAsSingleElement",{shortcut:N3("CtrlOrCmd+Shift+V")}),duration:5000}),DA1=!0),this.store.shouldCaptureIncrement())}handleTextWysiwyg(B,{isExistingElement:C=!1}){let F=this.scene.getElementsMapIncludingDeleted(),U=(D,Z)=>{this.scene.replaceAllElements([...this.scene.getElementsIncludingDeleted().map((X)=>X.id===B.id&&M6(X)?Z5(X,{originalText:D,isDeleted:Z??X.isDeleted,...Klu(X,sZ(X,F),F,D)}):X)])};Eq9({id:B.id,canvas:this.canvas,getViewportCoords:(D,Z)=>{let{x:X,y:M}=iO({sceneX:D,sceneY:Z},this.state);return[X-this.state.offsetLeft,M-this.state.offsetTop]},onChange:HU((D)=>{U(D,!1),ks0(B)&&yQ(B,this.scene.getNonDeletedElementsMap())}),onSubmit:HU(({viaKeyboard:D,nextOriginalText:Z})=>{let X=!Z.trim();if(U(Z,X),!X&&D){let M=B.containerId?B.containerId:B.id;wg.flushSync(()=>{this.setState((K)=>({selectedElementIds:pB({...K.selectedElementIds,[M]:!0},K)}))})}X&&tRu(this.scene.getNonDeletedElements(),[B]),(!X||C)&&this.store.shouldCaptureIncrement(),wg.flushSync(()=>{this.setState({newElement:null,editingTextElement:null})}),this.state.activeTool.locked&&UN(this.interactiveCanvas,this.state),this.focusContainer()}),element:B,excalidrawContainer:this.excalidrawContainerRef.current,app:this,autoSelect:!this.device.isTouchScreen}),this.deselectElements(),U(B.originalText,!1)}deselectElements(){this.setState({selectedElementIds:pB({},this.state),selectedGroupIds:{},editingGroupId:null,activeEmbeddable:null})}getTextElementAtPosition(B,C){let F=this.getElementAtPosition(B,C,{includeBoundTextElement:!0});return F&&M6(F)&&!F.isDeleted?F:null}getElementAtPosition(B,C,F){let U=this.getElementsAtPosition(B,C,F?.includeBoundTextElement,F?.includeLockedElements);if(U.length>1){if(F?.preferSelected){for(let Z=U.length-1;Z>-1;Z--)if(this.state.selectedElementIds[U[Z].id])return U[Z]}let D=U[U.length-1];return db({x:B,y:C,element:D,shape:wv(D,this.scene.getNonDeletedElementsMap()),threshold:this.getElementHitThreshold()/2,frameNameBound:w2(D)?this.frameNameBoundsCache.get(D):null})?D:U[U.length-2]}return U.length===1?U[0]:null}getElementsAtPosition(B,C,F=!1,U=!1){let D=[],Z=this.scene.getNonDeletedElementsMap();return(F&&U?this.scene.getNonDeletedElements():this.scene.getNonDeletedElements().filter((X)=>(U||!X.locked)&&(F||!(M6(X)&&X.containerId)))).filter((X)=>this.hitElement(B,C,X)).filter((X)=>{let M=_Q(X,Z);return M&&this.state.frameRendering.enabled&&this.state.frameRendering.clip?$lu({x:B,y:C},M,Z):!0}).filter((X)=>hl(X)?(D.push(X),!1):!0).concat(D)}getElementHitThreshold(){return mN3/this.state.zoom.value}hitElement(B,C,F,U=!0){if(U&&this.state.selectedElementIds[F.id]&&blu([F],this.state)){let D=Kx3(F,this.scene.getNonDeletedElementsMap(),gB(F)?0:this.getElementHitThreshold());if(pH0(v0(B,C),D))return!0}return yRu(B,C,_Ru(F,this.scene.getNonDeletedElementsMap()))?!0:db({x:B,y:C,element:F,shape:wv(F,this.scene.getNonDeletedElementsMap()),threshold:this.getElementHitThreshold(),frameNameBound:w2(F)?this.frameNameBoundsCache.get(F):null})}getTextBindableContainerAtPosition(B,C){let F=this.scene.getNonDeletedElements(),U=this.scene.getSelectedElements(this.state);if(U.length===1)return hi(U[0],!1)?U[0]:null;let D=null;for(let Z=F.length-1;Z>=0;--Z){if(F[Z].isDeleted)continue;let[X,M,K,Q]=b2(F[Z],this.scene.getNonDeletedElementsMap());if(L5(F[Z])&&db({x:B,y:C,element:F[Z],shape:wv(F[Z],this.scene.getNonDeletedElementsMap()),threshold:this.getElementHitThreshold()})){D=F[Z];break}else if(X<B&&B<K&&M<C&&C<Q){D=F[Z];break}}return hi(D,!1)?D:null}handleHoverSelectedLinearElement(B,C,F){let U=this.scene.getNonDeletedElementsMap(),D=L6.getElement(B.elementId,U);if(D)if(this.state.selectedLinearElement){let Z=-1,X=null;db({x:C,y:F,element:D,shape:wv(D,this.scene.getNonDeletedElementsMap())})?(Z=L6.getPointIndexUnderCursor(D,U,this.state.zoom,C,F),X=L6.getSegmentMidpointHitCoords(B,{x:C,y:F},this.state,this.scene.getNonDeletedElementsMap()),(P6(D)?Z===0||Z===D.points.length-1:Z>=0)||X?_8(this.interactiveCanvas,O8.POINTER):this.hitElement(C,F,D)&&_8(this.interactiveCanvas,O8.MOVE)):this.hitElement(C,F,D)&&(!P6(D)||!(D.startBinding||D.endBinding))&&_8(this.interactiveCanvas,O8.MOVE),this.state.selectedLinearElement.hoverPointIndex!==Z&&this.setState({selectedLinearElement:{...this.state.selectedLinearElement,hoverPointIndex:Z}}),L6.arePointsEqual(this.state.selectedLinearElement.segmentMidPointHoveredCoords,X)||this.setState({selectedLinearElLine truncated
|
||||
`),P=q.reduce((w,N,k)=>{let x=ob(N).trim();if(x.length){let v=this.getTopLayerFrameAtSceneCoords({x:F,y:W}),R=MM(x,Z,X),j=R.width>Q,l=j?yi(x,Z,Q):x;R=j?MM(l,Z,X):R;let y=F-R.width/2,h=W-R.height/2,_=iL({...D,x:y,y:h,text:l,originalText:x,lineHeight:X,autoResize:!j,frameId:v?v.id:null});w.push(_),W+=_.height+G}else q[k-1]?.trim()&&(W+=Z20(D.fontSize,X)+G);return w},[]);P.length!==0&&(this.scene.insertElements(P),this.setState({selectedElementIds:pB(Object.fromEntries(P.map((w)=>[w.id,!0])),this.state)}),!C&&P.length>1&&DA1===!1&&!this.device.editor.isMobile&&(this.setToast({message:r0("toast.pasteAsSingleElement",{shortcut:N3("CtrlOrCmd+Shift+V")}),duration:5000}),DA1=!0),this.store.shouldCaptureIncrement())}handleTextWysiwyg(B,{isExistingElement:C=!1}){let F=this.scene.getElementsMapIncludingDeleted(),U=(D,Z)=>{this.scene.replaceAllElements([...this.scene.getElementsIncludingDeleted().map((X)=>X.id===B.id&&M6(X)?Z5(X,{originalText:D,isDeleted:Z??X.isDeleted,...Klu(X,sZ(X,F),F,D)}):X)])};Eq9({id:B.id,canvas:this.canvas,getViewportCoords:(D,Z)=>{let{x:X,y:M}=iO({sceneX:D,sceneY:Z},this.state);return[X-this.state.offsetLeft,M-this.state.offsetTop]},onChange:HU((D)=>{U(D,!1),ks0(B)&&yQ(B,this.scene.getNonDeletedElementsMap())}),onSubmit:HU(({viaKeyboard:D,nextOriginalText:Z})=>{let X=!Z.trim();if(U(Z,X),!X&&D){let M=B.containerId?B.containerId:B.id;wg.flushSync(()=>{this.setState((K)=>({selectedElementIds:pB({...K.selectedElementIds,[M]:!0},K)}))})}X&&tRu(this.scene.getNonDeletedElements(),[B]),(!X||C)&&this.store.shouldCaptureIncrement(),wg.flushSync(()=>{this.setState({newElement:null,editingTextElement:null})}),this.state.activeTool.locked&&UN(this.interactiveCanvas,this.state),this.focusContainer()}),element:B,excalidrawContainer:this.excalidrawContainerRef.current,app:this,autoSelect:!this.device.isTouchScreen}),this.deselectElements(),U(B.originalText,!1)}deselectElements(){this.setState({selectedElementIds:pB({},this.state),selectedGroupIds:{},editingGroupId:null,activeEmbeddable:null})}getTextElementAtPosition(B,C){let F=this.getElementAtPosition(B,C,{includeBoundTextElement:!0});return F&&M6(F)&&!F.isDeleted?F:null}getElementAtPosition(B,C,F){let U=this.getElementsAtPosition(B,C,F?.includeBoundTextElement,F?.includeLockedElements);if(U.length>1){if(F?.preferSelected){for(let Z=U.length-1;Z>-1;Z--)if(this.state.selectedElementIds[U[Z].id])return U[Z]}let D=U[U.length-1];return db({x:B,y:C,element:D,shape:wv(D,this.scene.getNonDeletedElementsMap()),threshold:this.getElementHitThreshold()/2,frameNameBound:w2(D)?this.frameNameBoundsCache.get(D):null})?D:U[U.length-2]}return U.length===1?U[0]:null}getElementsAtPosition(B,C,F=!1,U=!1){let D=[],Z=this.scene.getNonDeletedElementsMap();return(F&&U?this.scene.getNonDeletedElements():this.scene.getNonDeletedElements().filter((X)=>(U||!X.locked)&&(F||!(M6(X)&&X.containerId)))).filter((X)=>this.hitElement(B,C,X)).filter((X)=>{let M=_Q(X,Z);return M&&this.state.frameRendering.enabled&&this.state.frameRendering.clip?$lu({x:B,y:C},M,Z):!0}).filter((X)=>hl(X)?(D.push(X),!1):!0).concat(D)}getElementHitThreshold(){return mN3/this.state.zoom.value}hitElement(B,C,F,U=!0){if(U&&this.state.selectedElementIds[F.id]&&blu([F],this.state)){let D=Kx3(F,this.scene.getNonDeletedElementsMap(),gB(F)?0:this.getElementHitThreshold());if(pH0(v0(B,C),D))return!0}return yRu(B,C,_Ru(F,this.scene.getNonDeletedElementsMap()))?!0:db({x:B,y:C,element:F,shape:wv(F,this.scene.getNonDeletedElementsMap()),threshold:this.getElementHitThreshold(),frameNameBound:w2(F)?this.frameNameBoundsCache.get(F):null})}getTextBindableContainerAtPosition(B,C){let F=this.scene.getNonDeletedElements(),U=this.scene.getSelectedElements(this.state);if(U.length===1)return hi(U[0],!1)?U[0]:null;let D=null;for(let Z=F.length-1;Z>=0;--Z){if(F[Z].isDeleted)continue;let[X,M,K,Q]=b2(F[Z],this.scene.getNonDeletedElementsMap());if(L5(F[Z])&&db({x:B,y:C,element:F[Z],shape:wv(F[Z],this.scene.getNonDeletedElementsMap()),threshold:this.getElementHitThreshold()})){D=F[Z];break}else if(X<B&&B<K&&M<C&&C<Q){D=F[Z];break}}return hi(D,!1)?D:null}handleHoverSelectedLinearElement(B,C,F){let U=this.scene.getNonDeletedElementsMap(),D=L6.getElement(B.elementId,U);if(D)if(this.state.selectedLinearElement){let Z=-1,X=null;db({x:C,y:F,element:D,shape:wv(D,this.scene.getNonDeletedElementsMap())})?(Z=L6.getPointIndexUnderCursor(D,U,this.state.zoom,C,F),X=L6.getSegmentMidpointHitCoords(B,{x:C,y:F},this.state,this.scene.getNonDeletedElementsMap()),(P6(D)?Z===0||Z===D.points.length-1:Z>=0)||X?_8(this.interactiveCanvas,O8.POINTER):this.hitElement(C,F,D)&&_8(this.interactiveCanvas,O8.MOVE)):this.hitElement(C,F,D)&&(!P6(D)||!(D.startBinding||D.endBinding))&&_8(this.interactiveCanvas,O8.MOVE),this.state.selectedLinearElement.hoverPointIndex!==Z&&this.setState({selectedLinearElement:{...this.state.selectedLinearElement,hoverPointIndex:Z}}),L6.arePointsEqual(this.state.selectedLinearElement.segmentMidPointHoveredCoords,X)||this.setState({selectedLinearElLine truncated
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -34,6 +34,7 @@ declare global {
|
||||
__mermaidToExcalidraw: (text: string) => Promise<string>;
|
||||
__excalidrawToSvg: (sceneJson: string) => Promise<string>;
|
||||
__rasterize: (svgText: string, targetWidthPx: number) => Promise<string>;
|
||||
__downscaleRaster: (dataUri: string, targetWidthPx: number, mime: string) => Promise<string>;
|
||||
__mountForScreenshot: (svgText: string, targetWidthPx: number) => string;
|
||||
__probeImage: (src: string) => Promise<string>;
|
||||
EXCALIDRAW_ASSET_PATH?: string;
|
||||
@@ -152,6 +153,37 @@ window.__mountForScreenshot = (svgText: string, targetWidthPx: number): string =
|
||||
return `mounted:${targetWidthPx}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Downscale a raster image (data URI) to targetWidthPx, preserving aspect.
|
||||
* Re-encodes in the requested mime — JPEG photos stay JPEG (q0.9); PNG-encoding
|
||||
* a photo would bloat it past the original. Data URIs are same-origin, so the
|
||||
* canvas never taints.
|
||||
*/
|
||||
window.__downscaleRaster = async (
|
||||
dataUri: string,
|
||||
targetWidthPx: number,
|
||||
mime: string,
|
||||
): Promise<string> => {
|
||||
if (!(targetWidthPx > 0 && targetWidthPx <= 10000)) {
|
||||
throw new Error(`targetWidthPx out of range: ${targetWidthPx}`);
|
||||
}
|
||||
const img = new Image();
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
img.onload = () => resolve();
|
||||
img.onerror = () => reject(new Error("image decode failed"));
|
||||
img.src = dataUri;
|
||||
});
|
||||
const scale = targetWidthPx / (img.naturalWidth || targetWidthPx);
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = Math.round(img.naturalWidth * scale);
|
||||
canvas.height = Math.round(img.naturalHeight * scale);
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) throw new Error("2d canvas context unavailable");
|
||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
const outMime = mime === "image/jpeg" ? "image/jpeg" : "image/png";
|
||||
return outMime === "image/jpeg" ? canvas.toDataURL(outMime, 0.9) : canvas.toDataURL(outMime);
|
||||
};
|
||||
|
||||
/** Probe intrinsic dimensions of an image (data URI or URL). Returns JSON. */
|
||||
window.__probeImage = async (src: string): Promise<string> => {
|
||||
const img = new Image();
|
||||
|
||||
Reference in new issue
Block a user