1+ /*\
2+ title: $:/plugins/sq/photopea/edit-photopea.js
3+ type: application/javascript
4+ module-type: widget
5+
6+ Edit-bitmap widget
7+
8+ \*/
9+ ( function ( ) {
10+
11+ /*jslint node: true, browser: true */
12+ /*global $tw: false */
13+
14+ let Widget = require ( "$:/core/modules/widgets/widget.js" ) . widget ;
15+ let Photopea = require ( "$:/plugins/sq/photopea/photopea.min.js" ) ;
16+ let DEFAULT_IMAGE_TYPE = "image/png" ;
17+
18+ let EditPhotopeaWidget = function ( parseTreeNode , options ) {
19+ this . initialise ( parseTreeNode , options ) ;
20+ } ;
21+
22+ function base64ToArrayBuffer ( base64 ) {
23+ let binaryString = atob ( base64 ) ,
24+ bytes = new Uint8Array ( binaryString . length ) ;
25+ for ( let i = 0 ; i < binaryString . length ; i ++ ) {
26+ bytes [ i ] = binaryString . charCodeAt ( i ) ;
27+ }
28+ return bytes . buffer ;
29+ }
30+
31+ function arrayBufferToBase64 ( buffer ) {
32+ let binary = '' ,
33+ bytes = new Uint8Array ( buffer ) ,
34+ len = bytes . byteLength ;
35+ for ( let i = 0 ; i < len ; i ++ ) {
36+ binary += String . fromCharCode ( bytes [ i ] ) ;
37+ }
38+ return window . btoa ( binary ) ;
39+ }
40+
41+ /*
42+ Inherit from the base widget class
43+ */
44+ EditPhotopeaWidget . prototype = new Widget ( ) ;
45+
46+ /*
47+ Render this widget into the DOM
48+ */
49+ EditPhotopeaWidget . prototype . render = function ( parent , nextSibling ) {
50+ let self = this ;
51+ this . window = self . document . parentWindow || self . document . defaultView ;
52+ // Save the parent dom node
53+ this . parentDomNode = parent ;
54+ // Compute our attributes
55+ this . computeAttributes ( ) ;
56+ // Execute our logic
57+ this . execute ( ) ;
58+ // Create the wrapper for the toolbar and render its content
59+ this . container = this . document . createElement ( "div" ) ;
60+ this . container . className = "photopea-container" ;
61+ this . container . style . height = "850px" ;
62+ // // Insert the elements into the DOM
63+ parent . insertBefore ( this . container , nextSibling ) ;
64+ this . renderChildren ( this . container , null ) ;
65+ this . domNodes . push ( this . container ) ;
66+ //set up the iframe and initalize Photopea
67+ this . init ( ) ;
68+ } ;
69+
70+ EditPhotopeaWidget . prototype . getImageType = function ( ) {
71+ let tiddler = this . wiki . getTiddler ( this . editTitle ) ,
72+ type = tiddler ?. fields ?. type || DEFAULT_IMAGE_TYPE ,
73+ extension = $tw . config . contentTypeInfo [ type ] . extension ;
74+ return extension . startsWith ( '.' ) ? extension . slice ( 1 ) : extension ;
75+ } ;
76+
77+ EditPhotopeaWidget . prototype . init = async function ( ) {
78+ let self = this ,
79+ iframeLoaded = false ,
80+ pendingSave = false ,
81+ pendingExit = false ,
82+ photopeaWindow ;
83+
84+ if ( ! $tw . browser ) {
85+ return ;
86+ }
87+
88+ function onMessage ( e ) {
89+ console . log ( e ) ;
90+ if ( Object . prototype . toString . call ( e . data ) === "[object ArrayBuffer]" ) {
91+ //save image
92+ self . saveChanges ( e . data ) ;
93+ }
94+ if ( e . data === "saved" && pendingSave ) {
95+ pendingSave = false ;
96+ if ( pendingExit ) {
97+ if ( self . exitActions ) {
98+ self . invokeActionString ( self . exitActions ) ;
99+ }
100+ }
101+ }
102+ }
103+ window . addEventListener ( "message" , onMessage ) ;
104+
105+ try {
106+ let language = self . wiki . getTiddlerText ( "$:/language" ) . split ( "-" ) [ 0 ] . slice ( "$:/languages/" . length ) ,
107+ pea = await Photopea . createEmbed ( this . container , {
108+ "environment" : {
109+ "customIO" : {
110+ "save" : `if(!app.activeDocument.saved)app.activeDocument.saveToOE("${ self . getImageType ( ) } ");` ,
111+ //TODO:, saveToOE has second part to the argument for quality that defaults to 0.7 https://www.photopea.com/learn/scripts#:~:text=Document.saveToOE(%22png%22)
112+ "exportAs" :true ,
113+ } ,
114+ "lang" : `${ language } ` ,
115+ "menus" : [ [ 0 , 0 , 0 , 0 , 0 , 1 , 0 , 0 , 1 ] , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ] ,
116+ /*"autosave" : 5*/
117+ }
118+ } , self . window ) ;
119+
120+ iframeLoaded = true ;
121+ photopeaWindow = self . container . getElementsByTagName ( "iframe" ) [ 0 ] . contentWindow ;
122+ if ( self . wiki . getTiddler ( self . editTitle ) ) {
123+ let base64String = self . wiki . getTiddlerText ( self . editTitle ) ;
124+ //send the image to the iframe
125+ photopeaWindow . postMessage ( base64ToArrayBuffer ( base64String ) , "*" ) ;
126+ }
127+ } catch ( err ) {
128+ let errorMessage = self . document . createElement ( "span" ) ;
129+ errorMessage . className = "tc-error" ;
130+ errorMessage . textContent = `Error loading Photopea: ${ err } ` ;
131+ self . container . append ( errorMessage ) ;
132+ } finally {
133+ self . addEventListener ( "tm-photopea-save" , function ( event ) {
134+ if ( iframeLoaded ) {
135+ pendingExit = true ;
136+ pendingSave = true ;
137+ let script = `app.activeDocument.saveToOE("${ self . getImageType ( ) } ");app.echoToOE("saved");` ;
138+ photopeaWindow . postMessage ( script , "*" ) ;
139+ //pea.runScript(script);
140+ } else {
141+ // make sure we can still exit
142+ if ( self . exitActions ) {
143+ self . invokeActionString ( self . exitActions ) ;
144+ }
145+ }
146+ } ) ;
147+ }
148+ } ;
149+
150+ /*
151+ Compute the internal state of the widget
152+ */
153+ EditPhotopeaWidget . prototype . execute = function ( ) {
154+ // Get our parameters
155+ this . editTitle = this . getAttribute ( "tiddler" , this . getVariable ( "currentTiddler" ) ) ;
156+ this . exitActions = this . getAttribute ( "exitactions" ) ;
157+ // Make the child widgets
158+ this . makeChildWidgets ( ) ;
159+ } ;
160+
161+ function getFileType ( arrayBuffer ) {
162+ // Convert the ArrayBuffer to a Uint8Array
163+ const bytes = new Uint8Array ( arrayBuffer ) ;
164+
165+ // Convert the first few bytes to a hexadecimal string
166+ const hexSignature = bytes . slice ( 0 , 8 ) . reduce ( ( acc , byte ) => acc + byte . toString ( 16 ) . padStart ( 2 , '0' ) , '' ) ;
167+
168+ // Known file signatures
169+ const signatures = {
170+ 'ffd8ffe0' : 'JPEG' , // JPEG file signature
171+ 'ffd8ffe1' : 'JPEG' , // Additional JPEG signature
172+ 'ffd8ffe2' : 'JPEG' , // Additional JPEG signature
173+ '89504e47' : 'PNG' , // PNG file signature
174+ '47494638' : 'GIF' , // GIF file signature
175+ '49492a00' : 'TIFF' , // TIFF (little-endian)
176+ '4d4d002a' : 'TIFF' , // TIFF (big-endian)
177+ '52494646' : 'WEBP' , // RIFF header for WEBP
178+ '00000020' : 'ISO Base Media' , // Base signature for ISO formats (AVIF included)
179+ } ;
180+
181+ // Match the signature for basic formats
182+ const basicMatch = signatures [ hexSignature . slice ( 0 , 8 ) ] ;
183+ if ( basicMatch ) {
184+ if ( basicMatch === 'ISO Base Media' ) {
185+ // Check additional bytes for AVIF
186+ const avifSignature = String . fromCharCode ( ...bytes . slice ( 4 , 12 ) ) ;
187+ if ( avifSignature . includes ( 'avif' ) ) {
188+ return '.avif' ;
189+ }
190+ }
191+ return `.${ basicMatch } ` . toLowerCase ( ) ;
192+ }
193+
194+ // SVG detection (based on XML declaration or <svg> tag)
195+ const text = new TextDecoder ( ) . decode ( arrayBuffer ) ;
196+ if ( text . trim ( ) . startsWith ( '<?xml' ) || text . includes ( '<svg' ) ) {
197+ return '.svg' ;
198+ }
199+
200+ return null ;
201+ } ;
202+
203+ /*
204+ Just refresh the toolbar
205+ */
206+ EditPhotopeaWidget . prototype . refresh = function ( changedTiddlers ) {
207+ return this . refreshChildren ( changedTiddlers ) ;
208+ } ;
209+
210+ EditPhotopeaWidget . prototype . saveChanges = function ( buffer ) {
211+ let tiddler = this . wiki . getTiddler ( this . editTitle ) || new $tw . Tiddler ( { title : this . editTitle , type : DEFAULT_IMAGE_TYPE } ) ,
212+ //type = tiddler.fields.type || DEFAULT_IMAGE_TYPE;
213+ buffertype = $tw . config . fileExtensionInfo [ getFileType ( buffer ) ] ?. type ,
214+ type = buffertype || tiddler . fields . type || DEFAULT_IMAGE_TYPE ,
215+ newContent = arrayBufferToBase64 ( buffer ) ;
216+
217+ if ( ! tiddler . fields . text || newContent != tiddler . fields . text ) {
218+ let update = { type : type , text : newContent } ;
219+ this . wiki . addTiddler ( new $tw . Tiddler ( this . wiki . getModificationFields ( ) , tiddler , update , this . wiki . getCreationFields ( ) ) ) ;
220+ }
221+ } ;
222+
223+ exports [ "edit-photopea" ] = EditPhotopeaWidget ;
224+
225+ } ) ( ) ;
0 commit comments