-
Notifications
You must be signed in to change notification settings - Fork 5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Experimental pdf exports v2 #639
base: main
Are you sure you want to change the base?
Changes from all commits
e50de6e
9a48c87
da54650
56153b3
b302396
b04c13b
c050a94
a72fc4f
66c6b7e
b44f8a3
e5ca32c
398c47b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -40,17 +40,68 @@ export function image(element: ImageElement, base64content: string): Content { | |
}; | ||
} | ||
|
||
function preprocessSvg(element: ImageElement, svg: string): string { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Canvas requires there to be a width and height on the svg element. Otherwise it wont draw it. |
||
if (element.mimeType !== 'image/svg+xml') { | ||
return svg; | ||
} | ||
|
||
// Convert the svg base64 to the actual svg | ||
const svgString = atob(svg); | ||
|
||
// We lied in the past about the mimetype, so we need to check if the svg is actually an svg and otherwise return the original string | ||
if (!svgString.includes('<svg')) { | ||
element.mimeType = svgString.includes('PNG') ? 'image/png' : 'image/jpeg'; | ||
return svg; | ||
} | ||
|
||
// Parse the svg string | ||
const parser = new DOMParser(); | ||
const doc = parser.parseFromString(svgString, 'image/svg+xml'); | ||
const svgElement = doc.documentElement; | ||
|
||
// Check if a size is set and if not set it to the size from the viewbox | ||
if (!svgElement.getAttribute('width') || !svgElement.getAttribute('height')) { | ||
const viewBox = svgElement.getAttribute('viewBox')?.split(' '); | ||
if (viewBox) { | ||
svgElement.setAttribute('width', viewBox[2]); | ||
svgElement.setAttribute('height', viewBox[3]); | ||
} else { | ||
svgElement.setAttribute('width', element.width.toString()); | ||
svgElement.setAttribute('height', element.height.toString()); | ||
} | ||
} | ||
|
||
// Convert the svg element to a string | ||
const serializer = new XMLSerializer(); | ||
const svgStringProcessed = serializer.serializeToString(svgElement); | ||
|
||
// Convert the svg string to base64 | ||
return btoa(svgStringProcessed); | ||
} | ||
|
||
export function conv2png(element: ImageElement, base64content: string): string { | ||
// If we have a svg image we need to preprocess it for the canvas to work | ||
if (element.mimeType === 'image/svg+xml') { | ||
base64content = preprocessSvg(element, base64content); | ||
} | ||
|
||
// If we now end up with a png or jpeg image we can return it early | ||
if (element.mimeType === 'image/png' || element.mimeType === 'image/jpeg') { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the short-circuit for files which lied about being an svg. |
||
return base64content; | ||
} | ||
|
||
const img = new Image(); | ||
img.src = 'data:' + element.mimeType + ';base64,' + base64content; | ||
|
||
const canvas = document.createElement('canvas'); | ||
[canvas.width, canvas.height] = [element.width, element.height]; | ||
|
||
const ctx = canvas.getContext('2d') ?? new CanvasRenderingContext2D(); | ||
ctx.drawImage(img, 0, 0, element.width, element.height); | ||
const ctx = canvas.getContext('2d'); | ||
ctx?.drawImage(img, 0, 0, element.width, element.height); | ||
|
||
const dataUrl = canvas.toDataURL('image/png'); | ||
|
||
return canvas.toDataURL('image/png').split(',')[1]; | ||
return dataUrl.split(',')[1]; | ||
} | ||
|
||
export function textContent( | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
/* | ||
* Copyright 2024 Nordeck IT + Consulting GmbH | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
import { WidgetApi } from '@matrix-widget-toolkit/api'; | ||
import { from, fromEvent, map, Observable, switchMap, take, tap } from 'rxjs'; | ||
import { WhiteboardDocumentExport, WhiteboardInstance } from '../../../state'; | ||
import { conv2png } from '../pdf/utils'; | ||
|
||
export function createWhiteboardPdf(params: { | ||
whiteboardInstance: WhiteboardInstance; | ||
roomName: string; | ||
authorName: string; | ||
widgetApi: WidgetApi; | ||
}): Observable<Blob> { | ||
const whiteboardExport = params.whiteboardInstance.export(params.widgetApi); | ||
|
||
// Use local mode for tests | ||
if (window.Worker && import.meta.env.MODE !== 'test') { | ||
const worker = new Worker(new URL('./pdf.worker.ts', import.meta.url), { | ||
type: 'module', | ||
}); | ||
worker.onmessageerror = (e) => { | ||
console.error('Worker error', e); | ||
worker.terminate(); | ||
}; | ||
|
||
// Post the whiteboard instance to the worker and then return the blob that the worker sends back to us when it's done. | ||
// We must return an observable that emits the blob and then completes. | ||
// Ensure we also terminate the worker when we're done with it. | ||
// Also note that we need the whiteboardExport which is a promise and the resolved value must be passed to the worker. | ||
return from(whiteboardExport).pipe( | ||
switchMap((exportData) => { | ||
// Convert all images which are not png or jpeg to png | ||
exportData = convertAllImagesToPNG(exportData); | ||
|
||
// Pass the whiteboard export data to the worker and wait for the worker to send back the blob. | ||
// If the worker sends back an error, we should throw it and stop the worker. | ||
// If the worker sends back the blob, we should return it. We should also stop the worker. | ||
|
||
// Create an observable that emits the blob when the worker sends it back. | ||
const stream = fromEvent<MessageEvent>(worker, 'message'); | ||
|
||
if (import.meta.hot) { | ||
// Delay the post message to the worker to ensure the worker is ready to receive it. | ||
new Promise((resolve) => setTimeout(resolve, 2000)) | ||
.then(() => { | ||
console.log('Posting message to worker'); | ||
worker.postMessage(exportData); | ||
return; | ||
}) | ||
.catch((e) => { | ||
console.error('Error posting message to worker', e); | ||
worker.terminate(); | ||
}); | ||
} else { | ||
// Note that we cant directly return the stream as it needs to be Observable<Blob> and not Observable<MessageEvent<Blob> | ||
console.log('Posting message to worker'); | ||
worker.postMessage(exportData); | ||
} | ||
return stream; | ||
}), | ||
map((e) => e.data), | ||
take(1), | ||
tap({ | ||
finalize: () => { | ||
// terminate the worker | ||
worker.terminate(); | ||
}, | ||
}), | ||
); | ||
} else { | ||
// Async import pdf.local.ts and thn call renderPDF with the whiteboard export data which is a promise too. | ||
return from(import('./pdf.local')).pipe( | ||
switchMap(({ renderPDF }) => | ||
from(whiteboardExport).pipe( | ||
switchMap((exportData) => { | ||
// Convert all images which are not png or jpeg to png | ||
exportData = convertAllImagesToPNG(exportData); | ||
return renderPDF(exportData); | ||
}), | ||
), | ||
), | ||
take(1), | ||
); | ||
} | ||
} | ||
|
||
function convertAllImagesToPNG( | ||
exportData: WhiteboardDocumentExport, | ||
): WhiteboardDocumentExport { | ||
// Convert all images which are not png or jpeg to png | ||
exportData.whiteboard.slides.forEach((slide) => { | ||
slide.elements.forEach((element) => { | ||
if (element.type === 'image' && element.mimeType !== 'image/png') { | ||
const file = exportData.whiteboard.files?.find( | ||
(f) => f.mxc === element.mxc, | ||
); | ||
if (file) { | ||
const data = conv2png(element, file.data); | ||
file.data = data; | ||
element.mimeType = 'image/png'; | ||
} | ||
} | ||
}); | ||
}); | ||
|
||
// Sanity check in debug mode. Report error if any image is not converted to png or jpeg. | ||
if (import.meta.env.MODE === 'development') { | ||
exportData.whiteboard.slides.forEach((slide) => { | ||
slide.elements.forEach((element) => { | ||
if (element.type === 'image' && element.mimeType !== 'image/png') { | ||
console.error('Image not converted to png or jpeg', element); | ||
} | ||
}); | ||
}); | ||
} | ||
|
||
return exportData; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is required as we require vitejs/vite-plugin-react-swc@beb09db