feat: image support#4011
Conversation
* master: (159 commits) fix: deselect elements on viewMode toggle (#3741) fix: allow pointer events for disable zen mode button (#3743) feat: expose fontfamily and refactor FONT_FAMILY (#3710) feat: Show active file name when saving to current file (#3733) feat: add hint around text editing (#3708) chore(deps-dev): bump ts-loader in /src/packages/excalidraw (#3716) chore(deps-dev): bump ts-loader in /src/packages/utils (#3712) chore(deps-dev): bump typescript in /src/packages/excalidraw (#3671) chore(deps-dev): bump @babel/plugin-transform-typescript (#3713) chore(deps-dev): bump @babel/preset-env in /src/packages/utils (#3714) chore(deps-dev): bump @babel/plugin-transform-async-to-generator (#3715) chore(deps): bump ws from 7.4.3 to 7.4.6 in /src/packages/excalidraw (#3665) chore(deps-dev): bump webpack in /src/packages/excalidraw (#3670) chore(deps): bump ws from 7.4.3 to 7.4.6 in /src/packages/utils (#3664) chore(deps-dev): bump autoprefixer in /src/packages/excalidraw (#3672) chore(deps-dev): bump webpack in /src/packages/utils (#3673) chore(deps-dev): bump @babel/preset-env in /src/packages/utils (#3675) feat: change library icon to be more clear (#3583) chore: Update translations from Crowdin (#3659) fix: use excal id so every element has unique id (#3696) ... # Conflicts: # package.json # src/components/App.tsx # src/data/restore.ts # src/element/collision.ts # src/element/types.ts # src/keys.ts # src/locales/en.json # src/renderer/renderElement.ts
|
Hi @dwelle, Since then, starting with this deployment the image does not appear. I wonder if this commit is the reason... If I add an imageElement from code and use addFiles to add the image to appState then it works for SVG files, but does not seem to work for jpg & png, etc. How is Insert image intended to work? As a minor side point, I also notice that while earlier I was able to add gifs as well from code, now the supported formats are limited to jpg, png, svg, binary. |
|
@zsviczian it's actually related to a previous commit where I started resizing images: I'll need to figure something out.
I thought I was allowing only those formats since the start, but maybe not. Anyway, I've added |
|
Should be fixed now. |
|
|
||
| - File data are encoded as DataURLs (base64) for portability reasons. | ||
|
|
||
| [ExcalidrawAPI](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#onLibraryChange): |
There was a problem hiding this comment.
onLibraryChange API didn't change?
# Conflicts: # src/actions/actionCanvas.tsx
|
@zsviczian TBH I don't know what that type is for. We use it only in The API isn't settled yet, which is why I'm holding off on these changes until we gather more data from early integrations. |
|
Alright people, let's do this 🚀 |


fix #19, replaces #3424 (more or less completely rewritten)
Refactor & thorough description TBD.
notes
When you import an image onto the scene, we do 2 things with the file:
Generate an
idfor the image so it can be referenced later. By default we use the SHA-1 hash of the file.Get the file's
DataURL(it looks like this:data:${mimeType};base64,${base64_of_the_image}).We use this when rendering the images on canvas (there aren't many way to do that. One of them is to create a
HTMLImageElementand draw it on canvas usingcontext.drawImage(). To create thatimgelement, we'll need to set itssrcto something. When dealing with local data, it can be set toDataURL).Another benefit of
DataURLis it doesn't need to be re-encoded when saving to JSON, which means we can just store it e.g. as part of the exported json (.excalidraw) and not care about decoding on restore. (Note that base64 is around 33% larger than binary encoding — so we if we cared about size, we'd need to dispense with JSON files and start exporting binary files).Image elements (
ExcalidrawImageElement) do not contain the image data itself for at least 2 reasons:We store the image id to the excalidraw element (
fileId), and the image'sDataURLtoApp.fileswhich is an object ofRecord<FileId, BinaryFileData>whereBinaryFileDatais an object containing theDataURLand some metadata for the file.For persisting the image files locally in the browser we use IndexedDB because LocalStorage is way too small (5MB). For now we still keep using LocalStorage for non-image state because it's faster to read from LocalStorage than from IDB (for cold starts). We may decide to migrate to IDB all the way later.
For server-persisted images, we upload them to Firebase Storage (an object store), both for collab and shareLink cases.
One other thing: while we use
DataURLwhen rendering, we don't want to create theimgelements on every render. While pretty fast, it's an async process (you need to set thesrcand wait onloadevent). So we keep aapp.imageCachewhich is aMap<FileId, HTMLImageElement>.When uploading images to server, we compress them (usually saves ~10-40% — of the binary size, not the
DataURLsize, because on server we are storing binaries) and encrypt them with the same key we use for the scene. The encoding format is described inencode.ts(seecompressData()andconcatBuffers()).ExcalidrawImageElement["status"]flag (initially set topending) is a "hacky" solution to determine whether the file is already persisted to storage.pendingstatus, and after the origin client uploads the images, it emits the elements again, but withsavedstatus).pending. This way we signal that they should be persisted again to the respective storage (Firebase/IDB).The solution is "hacky" because it's not quite correct that
pendingstatus means the file isn't persisted. It just means that the scene element itself is in unsaved state — we need not only the image DataURL be persisted, but also the elements'statusbe properly set tosavedand saved/synced with other clients, hence why we also use thestatusto prevent unload, and not the theFileManagerclass'#savedFilesmap.todos
ImageId→FileIdimage.statusisDeleted: trueupdate, and instead prune dead images on cold load (or other time), to guard against false positive race conditions and false negatives..keys()state.filesencryptDatafromfirebase.tsbefore merging
export to Excalidraw+Future
app.imageCachesize to guard against memory leaksreset to original aspect ratioto contextMenu for images