From 6c1feeb4fc9bf1364815122b3b8ca20d85bab5d8 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Fri, 10 Jan 2025 13:29:03 -0500 Subject: [PATCH 1/4] Update to Fabric 6 --- index.d.ts | 4 +-- index.js | 4 +-- package.json | 2 +- sources/fabric.js | 39 ++++++++++------------------ sources/fabric/fabricFrameSources.js | 21 +++++++-------- sources/videoFrameSource.js | 4 +-- 6 files changed, 30 insertions(+), 44 deletions(-) diff --git a/index.d.ts b/index.d.ts index 6895c815..83527dd7 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,4 +1,4 @@ -import type { fabric as Fabric } from 'fabric'; +import type * as Fabric from 'fabric/node'; /** * Edit and render videos. @@ -283,7 +283,7 @@ declare namespace Editly { interface VideoPostProcessingFunctionArgs { canvas: Fabric.Canvas; - image: Fabric.Image; + image: Fabric.FabricImage; fabric: typeof Fabric, progress: number; time: number; diff --git a/index.js b/index.js index af3299b2..9a6df3e4 100644 --- a/index.js +++ b/index.js @@ -7,7 +7,7 @@ import { nanoid } from 'nanoid'; import { testFf } from './ffmpeg.js'; import { parseFps, multipleOf2, assertFileValid, checkTransition } from './util.js'; -import { createFabricCanvas, rgbaToFabricImage, getNodeCanvasFromFabricCanvas } from './sources/fabric.js'; +import { createFabricCanvas, rgbaToFabricImage } from './sources/fabric.js'; import { createFrameSource } from './sources/frameSource.js'; import parseConfig from './parseConfig.js'; import GlTransitions from './glTransitions.js'; @@ -434,7 +434,7 @@ async function renderSingleFrame({ const fabricImage = await rgbaToFabricImage({ width, height, rgba }); canvas.add(fabricImage); canvas.renderAll(); - const internalCanvas = getNodeCanvasFromFabricCanvas(canvas); + const internalCanvas = canvas.getNodeCanvas(); await fsExtra.writeFile(outPath, internalCanvas.toBuffer('image/png')); canvas.clear(); canvas.dispose(); diff --git a/package.json b/package.json index 24c2897e..72ce7305 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "canvas": "^2.11.2", "compare-versions": "^6.1.0", "execa": "^6.1.0", - "fabric": "^5.2.4", + "fabric": "^6.5.4", "file-type": "^19.1.0", "file-url": "^4.0.0", "fs-extra": "^11.2.0", diff --git a/sources/fabric.js b/sources/fabric.js index 600d9b2f..9b19a00b 100644 --- a/sources/fabric.js +++ b/sources/fabric.js @@ -1,8 +1,9 @@ -import { fabric } from 'fabric'; +import * as fabric from 'fabric/node'; import { createCanvas, ImageData } from 'canvas'; - import { boxBlurImage } from '../BoxBlur.js'; +export { registerFont } from 'canvas'; + // Fabric is used as a fundament for compositing layers in editly export function canvasToRgba(ctx) { @@ -23,14 +24,8 @@ export function canvasToRgba(ctx) { return Buffer.from(imageData.data); } -export function getNodeCanvasFromFabricCanvas(fabricCanvas) { - // https://github.com/fabricjs/fabric.js/blob/26e1a5b55cbeeffb59845337ced3f3f91d533d7d/src/static_canvas.class.js - // https://github.com/fabricjs/fabric.js/issues/3885 - return fabric.util.getNodeCanvas(fabricCanvas.lowerCanvasEl); -} - export function fabricCanvasToRgba(fabricCanvas) { - const internalCanvas = getNodeCanvasFromFabricCanvas(fabricCanvas); + const internalCanvas = fabricCanvas.getNodeCanvas(); const ctx = internalCanvas.getContext('2d'); // require('fs').writeFileSync(`${Math.floor(Math.random() * 1e12)}.png`, internalCanvas.toBuffer('image/png')); @@ -64,19 +59,19 @@ export function toUint8ClampedArray(buffer) { return data; } -export function fabricCanvasToFabricImage(fabricCanvas) { - const canvas = getNodeCanvasFromFabricCanvas(fabricCanvas); - return new fabric.Image(canvas); -} - export async function rgbaToFabricImage({ width, height, rgba }) { const canvas = createCanvas(width, height); + + // FIXME: Fabric tries to add a class to this, but DOM is not defined. Because node? + // https://github.com/fabricjs/fabric.js/issues/10032 + canvas.classList = new Set(); + const ctx = canvas.getContext('2d'); // https://developer.mozilla.org/en-US/docs/Web/API/ImageData/ImageData // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/putImageData ctx.putImageData(new ImageData(toUint8ClampedArray(rgba), width, height), 0, 0); // https://stackoverflow.com/questions/58209996/unable-to-render-tiff-images-and-add-it-as-a-fabric-object - return new fabric.Image(canvas); + return new fabric.FabricImage(canvas); } export async function createFabricFrameSource(func, { width, height, ...rest }) { @@ -111,23 +106,15 @@ export async function createCustomCanvasFrameSource({ width, height, params }) { }; } -export function registerFont(...args) { - fabric.nodeCanvas.registerFont(...args); -} - export async function blurImage({ mutableImg, width, height }) { mutableImg.setOptions({ scaleX: width / mutableImg.width, scaleY: height / mutableImg.height }); - const fabricCanvas = createFabricCanvas({ width, height }); - fabricCanvas.add(mutableImg); - fabricCanvas.renderAll(); - - const internalCanvas = getNodeCanvasFromFabricCanvas(fabricCanvas); - const ctx = internalCanvas.getContext('2d'); + const canvas = mutableImg.toCanvasElement(); + const ctx = canvas.getContext('2d'); const blurAmount = Math.min(100, Math.max(width, height) / 10); // More than 100 seems to cause issues const passes = 1; boxBlurImage(ctx, width, height, blurAmount, false, passes); - return new fabric.Image(internalCanvas); + return new fabric.FabricImage(canvas); } diff --git a/sources/fabric/fabricFrameSources.js b/sources/fabric/fabricFrameSources.js index f40c3a36..cc70da9c 100644 --- a/sources/fabric/fabricFrameSources.js +++ b/sources/fabric/fabricFrameSources.js @@ -1,4 +1,4 @@ -import { fabric } from 'fabric'; +import * as fabric from 'fabric/node'; import fileUrl from 'file-url'; import { getRandomGradient, getRandomColors } from '../../colors.js'; @@ -10,7 +10,7 @@ import { blurImage } from '../fabric.js'; const defaultFontFamily = 'sans-serif'; -const loadImage = async (pathOrUrl) => new Promise((resolve) => fabric.util.loadImage(isUrl(pathOrUrl) ? pathOrUrl : fileUrl(pathOrUrl), resolve)); +const loadImage = (pathOrUrl) => fabric.util.loadImage(isUrl(pathOrUrl) ? pathOrUrl : fileUrl(pathOrUrl)); function getZoomParams({ progress, zoomDirection, zoomAmount }) { let scaleFactor = 1; @@ -37,7 +37,7 @@ export async function imageFrameSource({ verbose, params, width, height }) { const imgData = await loadImage(path); - const createImg = () => new fabric.Image(imgData, { + const createImg = () => new fabric.FabricImage(imgData, { originX: 'center', originY: 'center', left: width / 2, @@ -189,7 +189,6 @@ export async function linearGradientFrameSource({ width, height, params }) { export async function subtitleFrameSource({ width, height, params }) { const { text, textColor = '#ffffff', backgroundColor = 'rgba(0,0,0,0.3)', fontFamily = defaultFontFamily, delay = 0, speed = 1 } = params; - async function onRender(progress, canvas) { const easedProgress = easeOutExpo(Math.max(0, Math.min((progress - delay) * speed, 1))); @@ -234,7 +233,7 @@ export async function imageOverlayFrameSource({ params, width, height }) { const { left, top, originX, originY } = getPositionProps({ position, width, height }); - const img = new fabric.Image(imgData, { + const img = new fabric.FabricImage(imgData, { originX, originY, left, @@ -285,7 +284,7 @@ export async function titleFrameSource({ width, height, params }) { }); // We need the text as an image in order to scale it - const textImage = await new Promise((r) => textBox.cloneAsImage(r)); + const textImage = textBox.cloneAsImage(); const { left, top, originX, originY } = getPositionProps({ position, width, height }); @@ -320,7 +319,7 @@ export async function newsTitleFrameSource({ width, height, params }) { const paddingV = 0.07 * min; const paddingH = 0.03 * min; - const textBox = new fabric.Text(text, { + const textBox = new fabric.FabricText(text, { top, left: paddingV + (easedTextProgress - 1) * width, fill: textColor, @@ -367,10 +366,10 @@ async function getFadedObject({ object, progress }) { ], })); - const gradientMaskImg = await new Promise((r) => rect.cloneAsImage(r)); - const fadedImage = await new Promise((r) => object.cloneAsImage(r)); + const gradientMaskImg = rect.cloneAsImage(); + const fadedImage = object.cloneAsImage(); - fadedImage.filters.push(new fabric.Image.filters.BlendImage({ + fadedImage.filters.push(new fabric.FabricImage.filters.BlendImage({ image: gradientMaskImg, mode: 'multiply', })); @@ -386,7 +385,7 @@ export async function slideInTextFrameSource({ width, height, params: { position const { left, top, originX, originY } = getPositionProps({ position, width, height }); - const textBox = new fabric.Text(text, { + const textBox = new fabric.FabricText(text, { fill: color, fontFamily, fontSize: fontSizeAbs, diff --git a/sources/videoFrameSource.js b/sources/videoFrameSource.js index d3a55e41..57b8bdb3 100644 --- a/sources/videoFrameSource.js +++ b/sources/videoFrameSource.js @@ -1,6 +1,6 @@ import { execa } from 'execa'; import assert from 'assert'; -import { fabric } from 'fabric'; +import * as fabric from 'fabric/node'; import { getFfmpegCommonArgs } from '../ffmpeg.js'; import { readFileStreams } from '../util.js'; @@ -217,7 +217,7 @@ export default async ({ width: canvasWidth, height: canvasHeight, channels, fram }); if (resizeMode === 'contain-blur') { - const mutableImg = await new Promise((r) => img.cloneAsImage(r)); + const mutableImg = img.cloneAsImage(); const blurredImg = await blurImage({ mutableImg, width: requestedWidth, height: requestedHeight }); blurredImg.setOptions({ left, From ebf10419bc9218335f9a370c4d5962cd2eb49c86 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Fri, 10 Jan 2025 17:10:31 -0500 Subject: [PATCH 2/4] Fix missing media in example --- examples/fabricImagePostProcessing.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/fabricImagePostProcessing.js b/examples/fabricImagePostProcessing.js index ac061ad8..f13c1f0a 100644 --- a/examples/fabricImagePostProcessing.js +++ b/examples/fabricImagePostProcessing.js @@ -7,10 +7,10 @@ editly({ clips: [{ duration: 4, layers: [ - { type: 'video', path: './assets/lofoten.mp4', cutFrom: 0, cutTo: 4 }, + { type: 'video', path: './assets/kohlipe1.mp4', cutFrom: 0, cutTo: 4 }, { type: 'video', - path: './assets/hiking.mp4', + path: './assets/kohlipe2.mp4', cutFrom: 0, cutTo: 4, resizeMode: 'cover', From d52b0dad235578e9cc1463bd9dce699d76ded5cf Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Sun, 12 Jan 2025 16:44:14 -0500 Subject: [PATCH 3/4] Use eslint-plugin-import-exports-imports-resolver to fix lint error --- .eslintrc.cjs | 3 +++ package.json | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 274d1336..9c76c7e7 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -7,6 +7,9 @@ module.exports = { node: true, }, settings: { + 'import/resolver': { + [require.resolve('eslint-plugin-import-exports-imports-resolver')]: {}, + }, 'import/extensions': ['.js'], }, rules: { diff --git a/package.json b/package.json index 72ce7305..05bc100b 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "@types/fabric": "^5.2.4", "eslint": "^8.22.0", "eslint-config-airbnb-base": "^15.0.0", - "eslint-plugin-import": "^2.29.1" + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-import-exports-imports-resolver": "^1.0.1" } } From a7f8daa319eca58e5dbc05ed074ba445a41289f7 Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Mon, 13 Jan 2025 10:00:50 -0500 Subject: [PATCH 4/4] Try building all packages from source --- .github/workflows/test.yml | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 51fb952d..16095409 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,18 @@ jobs: DISPLAY: :0 steps: - if: runner.os == 'macOS' - run: brew install ffmpeg + run: | + brew install \ + cairo \ + ffmpeg \ + giflib \ + jpeg \ + libpng \ + librsvg \ + pango \ + pixman \ + pkg-config \ + python-setuptools - if: runner.os == 'Linux' run: | sudo apt-get update && sudo apt-get install -y \ @@ -37,12 +48,12 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 22 - cache: "yarn" + cache: "npm" cache-dependency-path: ./package.json - - run: yarn install + - run: npm install --build-from-source - run: npm run lint - run: npm run test - uses: actions/upload-artifact@v4 with: - name: video + name: video-${{ runner.os }} path: editly-out.mp4