apexify.js 5.1.0 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +263 -38
- package/README.md +248 -1109
- package/dist/cjs/Canvas/ApexPainter.d.ts +182 -204
- package/dist/cjs/Canvas/ApexPainter.d.ts.map +1 -1
- package/dist/cjs/Canvas/ApexPainter.js +482 -1286
- package/dist/cjs/Canvas/ApexPainter.js.map +1 -1
- package/dist/cjs/Canvas/extended/CanvasCreator.d.ts +33 -0
- package/dist/cjs/Canvas/extended/CanvasCreator.d.ts.map +1 -0
- package/dist/cjs/Canvas/extended/CanvasCreator.js +223 -0
- package/dist/cjs/Canvas/extended/CanvasCreator.js.map +1 -0
- package/dist/cjs/Canvas/extended/ChartCreator.d.ts +26 -0
- package/dist/cjs/Canvas/extended/ChartCreator.d.ts.map +1 -0
- package/dist/cjs/Canvas/extended/ChartCreator.js +50 -0
- package/dist/cjs/Canvas/extended/ChartCreator.js.map +1 -0
- package/dist/cjs/Canvas/extended/GIFCreator.d.ts +43 -0
- package/dist/cjs/Canvas/extended/GIFCreator.d.ts.map +1 -0
- package/dist/cjs/Canvas/extended/GIFCreator.js +157 -0
- package/dist/cjs/Canvas/extended/GIFCreator.js.map +1 -0
- package/dist/cjs/Canvas/extended/ImageCreator.d.ts +83 -0
- package/dist/cjs/Canvas/extended/ImageCreator.d.ts.map +1 -0
- package/dist/cjs/Canvas/extended/ImageCreator.js +479 -0
- package/dist/cjs/Canvas/extended/ImageCreator.js.map +1 -0
- package/dist/cjs/Canvas/extended/TextCreator.d.ts +35 -0
- package/dist/cjs/Canvas/extended/TextCreator.d.ts.map +1 -0
- package/dist/cjs/Canvas/extended/TextCreator.js +98 -0
- package/dist/cjs/Canvas/extended/TextCreator.js.map +1 -0
- package/dist/cjs/Canvas/extended/VideoCreator.d.ts +370 -0
- package/dist/cjs/Canvas/extended/VideoCreator.d.ts.map +1 -0
- package/dist/cjs/Canvas/extended/VideoCreator.js +478 -0
- package/dist/cjs/Canvas/extended/VideoCreator.js.map +1 -0
- package/dist/cjs/Canvas/utils/Background/bg.d.ts +1 -1
- package/dist/cjs/Canvas/utils/Background/bg.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Background/bg.js +43 -7
- package/dist/cjs/Canvas/utils/Background/bg.js.map +1 -1
- package/dist/cjs/Canvas/utils/Charts/barchart.d.ts +230 -0
- package/dist/cjs/Canvas/utils/Charts/barchart.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/Charts/barchart.js +1891 -0
- package/dist/cjs/Canvas/utils/Charts/barchart.js.map +1 -0
- package/dist/cjs/Canvas/utils/Charts/comparisonchart.d.ts +103 -0
- package/dist/cjs/Canvas/utils/Charts/comparisonchart.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/Charts/comparisonchart.js +368 -0
- package/dist/cjs/Canvas/utils/Charts/comparisonchart.js.map +1 -0
- package/dist/cjs/Canvas/utils/Charts/horizontalbarchart.d.ts +178 -0
- package/dist/cjs/Canvas/utils/Charts/horizontalbarchart.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/Charts/horizontalbarchart.js +1389 -0
- package/dist/cjs/Canvas/utils/Charts/horizontalbarchart.js.map +1 -0
- package/dist/cjs/Canvas/utils/Charts/index.d.ts +45 -0
- package/dist/cjs/Canvas/utils/Charts/index.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/Charts/index.js +17 -0
- package/dist/cjs/Canvas/utils/Charts/index.js.map +1 -0
- package/dist/cjs/Canvas/utils/Charts/linechart.d.ts +216 -0
- package/dist/cjs/Canvas/utils/Charts/linechart.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/Charts/linechart.js +1761 -0
- package/dist/cjs/Canvas/utils/Charts/linechart.js.map +1 -0
- package/dist/cjs/Canvas/utils/Charts/piechart.d.ts +167 -0
- package/dist/cjs/Canvas/utils/Charts/piechart.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/Charts/piechart.js +794 -0
- package/dist/cjs/Canvas/utils/Charts/piechart.js.map +1 -0
- package/dist/cjs/Canvas/utils/General/batchOperations.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/General/batchOperations.js +3 -4
- package/dist/cjs/Canvas/utils/General/batchOperations.js.map +1 -1
- package/dist/cjs/Canvas/utils/General/general functions.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/General/general functions.js +62 -33
- package/dist/cjs/Canvas/utils/General/general functions.js.map +1 -1
- package/dist/cjs/Canvas/utils/General/imageStitching.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/General/imageStitching.js +3 -6
- package/dist/cjs/Canvas/utils/General/imageStitching.js.map +1 -1
- package/dist/cjs/Canvas/utils/Image/imageMasking.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Image/imageMasking.js +5 -12
- package/dist/cjs/Canvas/utils/Image/imageMasking.js.map +1 -1
- package/dist/cjs/Canvas/utils/Image/imageProperties.d.ts +4 -4
- package/dist/cjs/Canvas/utils/Image/imageProperties.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Image/imageProperties.js +44 -9
- package/dist/cjs/Canvas/utils/Image/imageProperties.js.map +1 -1
- package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.d.ts +5 -0
- package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.js +48 -5
- package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.js.map +1 -1
- package/dist/cjs/Canvas/utils/Texts/textProperties.d.ts +1 -1
- package/dist/cjs/Canvas/utils/Texts/textProperties.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Texts/textProperties.js +48 -5
- package/dist/cjs/Canvas/utils/Texts/textProperties.js.map +1 -1
- package/dist/cjs/Canvas/utils/Video/videoHelpers.d.ts +489 -0
- package/dist/cjs/Canvas/utils/Video/videoHelpers.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/Video/videoHelpers.js +1835 -0
- package/dist/cjs/Canvas/utils/Video/videoHelpers.js.map +1 -0
- package/dist/cjs/Canvas/utils/errorUtils.d.ts +15 -0
- package/dist/cjs/Canvas/utils/errorUtils.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/errorUtils.js +26 -0
- package/dist/cjs/Canvas/utils/errorUtils.js.map +1 -0
- package/dist/cjs/Canvas/utils/types.d.ts +17 -178
- package/dist/cjs/Canvas/utils/types.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/types.js.map +1 -1
- package/dist/cjs/Canvas/utils/utils.d.ts +4 -3
- package/dist/cjs/Canvas/utils/utils.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/utils.js +40 -6
- package/dist/cjs/Canvas/utils/utils.js.map +1 -1
- package/dist/cjs/index.d.ts +1 -8
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +14 -45
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/Canvas/ApexPainter.d.ts +182 -204
- package/dist/esm/Canvas/ApexPainter.d.ts.map +1 -1
- package/dist/esm/Canvas/ApexPainter.js +482 -1286
- package/dist/esm/Canvas/ApexPainter.js.map +1 -1
- package/dist/esm/Canvas/extended/CanvasCreator.d.ts +33 -0
- package/dist/esm/Canvas/extended/CanvasCreator.d.ts.map +1 -0
- package/dist/esm/Canvas/extended/CanvasCreator.js +223 -0
- package/dist/esm/Canvas/extended/CanvasCreator.js.map +1 -0
- package/dist/esm/Canvas/extended/ChartCreator.d.ts +26 -0
- package/dist/esm/Canvas/extended/ChartCreator.d.ts.map +1 -0
- package/dist/esm/Canvas/extended/ChartCreator.js +50 -0
- package/dist/esm/Canvas/extended/ChartCreator.js.map +1 -0
- package/dist/esm/Canvas/extended/GIFCreator.d.ts +43 -0
- package/dist/esm/Canvas/extended/GIFCreator.d.ts.map +1 -0
- package/dist/esm/Canvas/extended/GIFCreator.js +157 -0
- package/dist/esm/Canvas/extended/GIFCreator.js.map +1 -0
- package/dist/esm/Canvas/extended/ImageCreator.d.ts +83 -0
- package/dist/esm/Canvas/extended/ImageCreator.d.ts.map +1 -0
- package/dist/esm/Canvas/extended/ImageCreator.js +479 -0
- package/dist/esm/Canvas/extended/ImageCreator.js.map +1 -0
- package/dist/esm/Canvas/extended/TextCreator.d.ts +35 -0
- package/dist/esm/Canvas/extended/TextCreator.d.ts.map +1 -0
- package/dist/esm/Canvas/extended/TextCreator.js +98 -0
- package/dist/esm/Canvas/extended/TextCreator.js.map +1 -0
- package/dist/esm/Canvas/extended/VideoCreator.d.ts +370 -0
- package/dist/esm/Canvas/extended/VideoCreator.d.ts.map +1 -0
- package/dist/esm/Canvas/extended/VideoCreator.js +478 -0
- package/dist/esm/Canvas/extended/VideoCreator.js.map +1 -0
- package/dist/esm/Canvas/utils/Background/bg.d.ts +1 -1
- package/dist/esm/Canvas/utils/Background/bg.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Background/bg.js +43 -7
- package/dist/esm/Canvas/utils/Background/bg.js.map +1 -1
- package/dist/esm/Canvas/utils/Charts/barchart.d.ts +230 -0
- package/dist/esm/Canvas/utils/Charts/barchart.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/Charts/barchart.js +1891 -0
- package/dist/esm/Canvas/utils/Charts/barchart.js.map +1 -0
- package/dist/esm/Canvas/utils/Charts/comparisonchart.d.ts +103 -0
- package/dist/esm/Canvas/utils/Charts/comparisonchart.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/Charts/comparisonchart.js +368 -0
- package/dist/esm/Canvas/utils/Charts/comparisonchart.js.map +1 -0
- package/dist/esm/Canvas/utils/Charts/horizontalbarchart.d.ts +178 -0
- package/dist/esm/Canvas/utils/Charts/horizontalbarchart.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/Charts/horizontalbarchart.js +1389 -0
- package/dist/esm/Canvas/utils/Charts/horizontalbarchart.js.map +1 -0
- package/dist/esm/Canvas/utils/Charts/index.d.ts +45 -0
- package/dist/esm/Canvas/utils/Charts/index.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/Charts/index.js +17 -0
- package/dist/esm/Canvas/utils/Charts/index.js.map +1 -0
- package/dist/esm/Canvas/utils/Charts/linechart.d.ts +216 -0
- package/dist/esm/Canvas/utils/Charts/linechart.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/Charts/linechart.js +1761 -0
- package/dist/esm/Canvas/utils/Charts/linechart.js.map +1 -0
- package/dist/esm/Canvas/utils/Charts/piechart.d.ts +167 -0
- package/dist/esm/Canvas/utils/Charts/piechart.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/Charts/piechart.js +794 -0
- package/dist/esm/Canvas/utils/Charts/piechart.js.map +1 -0
- package/dist/esm/Canvas/utils/General/batchOperations.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/General/batchOperations.js +3 -4
- package/dist/esm/Canvas/utils/General/batchOperations.js.map +1 -1
- package/dist/esm/Canvas/utils/General/general functions.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/General/general functions.js +62 -33
- package/dist/esm/Canvas/utils/General/general functions.js.map +1 -1
- package/dist/esm/Canvas/utils/General/imageStitching.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/General/imageStitching.js +3 -6
- package/dist/esm/Canvas/utils/General/imageStitching.js.map +1 -1
- package/dist/esm/Canvas/utils/Image/imageMasking.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Image/imageMasking.js +5 -12
- package/dist/esm/Canvas/utils/Image/imageMasking.js.map +1 -1
- package/dist/esm/Canvas/utils/Image/imageProperties.d.ts +4 -4
- package/dist/esm/Canvas/utils/Image/imageProperties.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Image/imageProperties.js +44 -9
- package/dist/esm/Canvas/utils/Image/imageProperties.js.map +1 -1
- package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.d.ts +5 -0
- package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.js +48 -5
- package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.js.map +1 -1
- package/dist/esm/Canvas/utils/Texts/textProperties.d.ts +1 -1
- package/dist/esm/Canvas/utils/Texts/textProperties.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Texts/textProperties.js +48 -5
- package/dist/esm/Canvas/utils/Texts/textProperties.js.map +1 -1
- package/dist/esm/Canvas/utils/Video/videoHelpers.d.ts +489 -0
- package/dist/esm/Canvas/utils/Video/videoHelpers.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/Video/videoHelpers.js +1835 -0
- package/dist/esm/Canvas/utils/Video/videoHelpers.js.map +1 -0
- package/dist/esm/Canvas/utils/errorUtils.d.ts +15 -0
- package/dist/esm/Canvas/utils/errorUtils.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/errorUtils.js +26 -0
- package/dist/esm/Canvas/utils/errorUtils.js.map +1 -0
- package/dist/esm/Canvas/utils/types.d.ts +17 -178
- package/dist/esm/Canvas/utils/types.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/types.js.map +1 -1
- package/dist/esm/Canvas/utils/utils.d.ts +4 -3
- package/dist/esm/Canvas/utils/utils.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/utils.js +40 -6
- package/dist/esm/Canvas/utils/utils.js.map +1 -1
- package/dist/esm/index.d.ts +1 -8
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +14 -45
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/package.json +118 -82
- package/dist/cjs/Canvas/utils/Charts/charts.d.ts +0 -13
- package/dist/cjs/Canvas/utils/Charts/charts.d.ts.map +0 -1
- package/dist/cjs/Canvas/utils/Charts/charts.js +0 -466
- package/dist/cjs/Canvas/utils/Charts/charts.js.map +0 -1
- package/dist/esm/Canvas/utils/Charts/charts.d.ts +0 -13
- package/dist/esm/Canvas/utils/Charts/charts.d.ts.map +0 -1
- package/dist/esm/Canvas/utils/Charts/charts.js +0 -466
- package/dist/esm/Canvas/utils/Charts/charts.js.map +0 -1
- package/lib/Canvas/ApexPainter.ts +0 -5414
- package/lib/Canvas/utils/Background/bg.ts +0 -285
- package/lib/Canvas/utils/Charts/charts.ts +0 -548
- package/lib/Canvas/utils/Custom/advancedLines.ts +0 -387
- package/lib/Canvas/utils/Custom/customLines.ts +0 -206
- package/lib/Canvas/utils/General/batchOperations.ts +0 -103
- package/lib/Canvas/utils/General/conversion.ts +0 -34
- package/lib/Canvas/utils/General/general functions.ts +0 -726
- package/lib/Canvas/utils/General/imageCompression.ts +0 -316
- package/lib/Canvas/utils/General/imageStitching.ts +0 -252
- package/lib/Canvas/utils/Image/imageEffects.ts +0 -175
- package/lib/Canvas/utils/Image/imageFilters.ts +0 -356
- package/lib/Canvas/utils/Image/imageMasking.ts +0 -335
- package/lib/Canvas/utils/Image/imageProperties.ts +0 -587
- package/lib/Canvas/utils/Image/professionalImageFilters.ts +0 -391
- package/lib/Canvas/utils/Image/simpleProfessionalFilters.ts +0 -229
- package/lib/Canvas/utils/Patterns/enhancedPatternRenderer.ts +0 -455
- package/lib/Canvas/utils/Shapes/shapes.ts +0 -528
- package/lib/Canvas/utils/Texts/enhancedTextRenderer.ts +0 -716
- package/lib/Canvas/utils/Texts/textPathRenderer.ts +0 -320
- package/lib/Canvas/utils/Texts/textProperties.ts +0 -231
- package/lib/Canvas/utils/types.ts +0 -983
- package/lib/Canvas/utils/utils.ts +0 -135
- package/lib/index.ts +0 -81
- package/lib/utils.ts +0 -5
|
@@ -6,7 +6,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.ApexPainter = void 0;
|
|
7
7
|
const canvas_1 = require("@napi-rs/canvas");
|
|
8
8
|
const gifencoder_1 = __importDefault(require("gifencoder"));
|
|
9
|
-
const stream_1 = require("stream");
|
|
10
9
|
const child_process_1 = require("child_process");
|
|
11
10
|
const util_1 = require("util");
|
|
12
11
|
const axios_1 = __importDefault(require("axios"));
|
|
@@ -14,48 +13,88 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
14
13
|
const path_1 = __importDefault(require("path"));
|
|
15
14
|
const execAsync = (0, util_1.promisify)(child_process_1.exec);
|
|
16
15
|
const utils_1 = require("./utils/utils");
|
|
17
|
-
const
|
|
18
|
-
const
|
|
16
|
+
const CanvasCreator_1 = require("./extended/CanvasCreator");
|
|
17
|
+
const ImageCreator_1 = require("./extended/ImageCreator");
|
|
18
|
+
const TextCreator_1 = require("./extended/TextCreator");
|
|
19
|
+
const GIFCreator_1 = require("./extended/GIFCreator");
|
|
20
|
+
const ChartCreator_1 = require("./extended/ChartCreator");
|
|
21
|
+
const VideoCreator_1 = require("./extended/VideoCreator");
|
|
22
|
+
const videoHelpers_1 = require("./utils/Video/videoHelpers");
|
|
19
23
|
class ApexPainter {
|
|
20
24
|
format;
|
|
21
25
|
saveCounter = 1;
|
|
26
|
+
// Extended handlers
|
|
27
|
+
canvasCreator;
|
|
28
|
+
imageCreator;
|
|
29
|
+
textCreator;
|
|
30
|
+
gifCreator;
|
|
31
|
+
chartCreator;
|
|
32
|
+
videoCreator;
|
|
33
|
+
videoHelpers;
|
|
22
34
|
constructor({ type } = { type: 'buffer' }) {
|
|
23
35
|
this.format = { type: type || 'buffer' };
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
36
|
+
// Initialize extended handlers
|
|
37
|
+
this.canvasCreator = new CanvasCreator_1.CanvasCreator();
|
|
38
|
+
this.imageCreator = new ImageCreator_1.ImageCreator();
|
|
39
|
+
this.textCreator = new TextCreator_1.TextCreator();
|
|
40
|
+
this.gifCreator = new GIFCreator_1.GIFCreator();
|
|
41
|
+
this.chartCreator = new ChartCreator_1.ChartCreator();
|
|
42
|
+
this.videoCreator = new VideoCreator_1.VideoCreator();
|
|
43
|
+
// Set up dependencies for CanvasCreator
|
|
44
|
+
this.canvasCreator.setExtractVideoFrame((videoSource, frameNumber, timeSeconds, outputFormat, quality) => this.#extractVideoFrame(videoSource, frameNumber ?? 0, timeSeconds, outputFormat ?? 'jpg', quality ?? 2));
|
|
45
|
+
// Set up dependencies for VideoCreator
|
|
46
|
+
this.videoCreator.setDependencies({
|
|
47
|
+
checkFFmpegAvailable: () => this.#checkFFmpegAvailable(),
|
|
48
|
+
getFFmpegInstallInstructions: () => this.#getFFmpegInstallInstructions(),
|
|
49
|
+
getVideoInfo: (videoSource, skipFFmpegCheck) => this.getVideoInfo(videoSource, skipFFmpegCheck),
|
|
50
|
+
extractVideoFrame: (videoSource, frameNumber, timeSeconds, outputFormat, quality) => this.#extractVideoFrame(videoSource, frameNumber ?? 0, timeSeconds, outputFormat ?? 'jpg', quality ?? 2),
|
|
51
|
+
extractFrames: (videoSource, options) => this.extractFrames(videoSource, options),
|
|
52
|
+
extractAllFrames: (videoSource, options) => this.extractAllFrames(videoSource, options)
|
|
53
|
+
});
|
|
54
|
+
// Initialize VideoHelpers
|
|
55
|
+
this.videoHelpers = new videoHelpers_1.VideoHelpers({
|
|
56
|
+
checkFFmpegAvailable: () => this.#checkFFmpegAvailable(),
|
|
57
|
+
getFFmpegInstallInstructions: () => this.#getFFmpegInstallInstructions(),
|
|
58
|
+
getVideoInfo: (videoSource, skipFFmpegCheck) => this.getVideoInfo(videoSource, skipFFmpegCheck),
|
|
59
|
+
extractVideoFrame: (videoSource, frameNumber, timeSeconds, outputFormat, quality) => this.#extractVideoFrame(videoSource, frameNumber ?? 0, timeSeconds, outputFormat ?? 'jpg', quality ?? 2),
|
|
60
|
+
createVideo: (options) => this.createVideo(options)
|
|
61
|
+
});
|
|
62
|
+
// Set up helper methods for VideoCreator - use VideoHelpers for last 6 methods
|
|
63
|
+
this.videoCreator.setHelperMethods({
|
|
64
|
+
generateVideoThumbnail: (videoSource, options, videoInfo) => this.#generateVideoThumbnail(videoSource, options, videoInfo),
|
|
65
|
+
convertVideo: (videoSource, options) => this.#convertVideo(videoSource, options),
|
|
66
|
+
trimVideo: (videoSource, options) => this.#trimVideo(videoSource, options),
|
|
67
|
+
extractAudio: (videoSource, options) => this.#extractAudio(videoSource, options),
|
|
68
|
+
addWatermarkToVideo: (videoSource, options) => this.#addWatermarkToVideo(videoSource, options),
|
|
69
|
+
changeVideoSpeed: (videoSource, options) => this.#changeVideoSpeed(videoSource, options),
|
|
70
|
+
generateVideoPreview: (videoSource, options, videoInfo) => this.#generateVideoPreview(videoSource, options, videoInfo),
|
|
71
|
+
applyVideoEffects: (videoSource, options) => this.#applyVideoEffects(videoSource, options),
|
|
72
|
+
mergeVideos: (options) => this.#mergeVideos(options),
|
|
73
|
+
replaceVideoSegment: (videoSource, options) => this.#replaceVideoSegment(videoSource, options),
|
|
74
|
+
rotateVideo: (videoSource, options) => this.#rotateVideo(videoSource, options),
|
|
75
|
+
cropVideo: (videoSource, options) => this.#cropVideo(videoSource, options),
|
|
76
|
+
compressVideo: (videoSource, options) => this.#compressVideo(videoSource, options),
|
|
77
|
+
addTextToVideo: (videoSource, options) => this.#addTextToVideo(videoSource, options),
|
|
78
|
+
addFadeToVideo: (videoSource, options) => this.#addFadeToVideo(videoSource, options),
|
|
79
|
+
reverseVideo: (videoSource, options) => this.#reverseVideo(videoSource, options),
|
|
80
|
+
createVideoLoop: (videoSource, options) => this.#createVideoLoop(videoSource, options),
|
|
81
|
+
batchProcessVideos: (options) => this.#batchProcessVideos(options),
|
|
82
|
+
detectVideoScenes: (videoSource, options) => this.#detectVideoScenes(videoSource, options),
|
|
83
|
+
stabilizeVideo: (videoSource, options) => this.#stabilizeVideo(videoSource, options),
|
|
84
|
+
colorCorrectVideo: (videoSource, options) => this.#colorCorrectVideo(videoSource, options),
|
|
85
|
+
addPictureInPicture: (videoSource, options) => this.#addPictureInPicture(videoSource, options),
|
|
86
|
+
createSplitScreen: (options) => this.#createSplitScreen(options),
|
|
87
|
+
createTimeLapseVideo: (videoSource, options) => this.#createTimeLapseVideo(videoSource, options),
|
|
88
|
+
muteVideo: (videoSource, options) => this.#muteVideo(videoSource, options),
|
|
89
|
+
adjustVideoVolume: (videoSource, options) => this.#adjustVideoVolume(videoSource, options),
|
|
90
|
+
createVideoFromFrames: (options) => this.#createVideoFromFrames(options),
|
|
91
|
+
freezeVideoFrame: (videoSource, options, onProgress) => this.videoHelpers.freezeVideoFrame(videoSource, options, onProgress),
|
|
92
|
+
exportVideoPreset: (videoSource, options, onProgress) => this.videoHelpers.exportVideoPreset(videoSource, options, onProgress),
|
|
93
|
+
normalizeVideoAudio: (videoSource, options, onProgress) => this.videoHelpers.normalizeVideoAudio(videoSource, options, onProgress),
|
|
94
|
+
applyLUTToVideo: (videoSource, options, onProgress) => this.videoHelpers.applyLUTToVideo(videoSource, options, onProgress),
|
|
95
|
+
addVideoTransition: (videoSource, options, onProgress) => this.videoHelpers.addVideoTransition(videoSource, options, onProgress),
|
|
96
|
+
addAnimatedTextToVideo: (videoSource, options, onProgress) => this.videoHelpers.addAnimatedTextToVideo(videoSource, options, onProgress)
|
|
97
|
+
});
|
|
59
98
|
}
|
|
60
99
|
/**
|
|
61
100
|
* Creates a canvas with the given configuration.
|
|
@@ -88,200 +127,8 @@ class ApexPainter {
|
|
|
88
127
|
* const buffer = result.buffer;
|
|
89
128
|
* ```
|
|
90
129
|
*/
|
|
91
|
-
/**
|
|
92
|
-
* Validates canvas configuration.
|
|
93
|
-
* @private
|
|
94
|
-
* @param canvas - Canvas configuration to validate
|
|
95
|
-
*/
|
|
96
|
-
#validateCanvasConfig(canvas) {
|
|
97
|
-
if (!canvas) {
|
|
98
|
-
throw new Error("createCanvas: canvas configuration is required.");
|
|
99
|
-
}
|
|
100
|
-
if (canvas.width !== undefined && (typeof canvas.width !== 'number' || canvas.width <= 0)) {
|
|
101
|
-
throw new Error("createCanvas: width must be a positive number.");
|
|
102
|
-
}
|
|
103
|
-
if (canvas.height !== undefined && (typeof canvas.height !== 'number' || canvas.height <= 0)) {
|
|
104
|
-
throw new Error("createCanvas: height must be a positive number.");
|
|
105
|
-
}
|
|
106
|
-
if (canvas.opacity !== undefined && (typeof canvas.opacity !== 'number' || canvas.opacity < 0 || canvas.opacity > 1)) {
|
|
107
|
-
throw new Error("createCanvas: opacity must be a number between 0 and 1.");
|
|
108
|
-
}
|
|
109
|
-
if (canvas.zoom?.scale !== undefined && (typeof canvas.zoom.scale !== 'number' || canvas.zoom.scale <= 0)) {
|
|
110
|
-
throw new Error("createCanvas: zoom.scale must be a positive number.");
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
130
|
async createCanvas(canvas) {
|
|
114
|
-
|
|
115
|
-
// Validate canvas configuration
|
|
116
|
-
this.#validateCanvasConfig(canvas);
|
|
117
|
-
// Handle inherit sizing
|
|
118
|
-
if (canvas.customBg?.inherit) {
|
|
119
|
-
let p = canvas.customBg.source;
|
|
120
|
-
if (!/^https?:\/\//i.test(p))
|
|
121
|
-
p = path_1.default.join(process.cwd(), p);
|
|
122
|
-
try {
|
|
123
|
-
const img = await (0, canvas_1.loadImage)(p);
|
|
124
|
-
canvas.width = img.width;
|
|
125
|
-
canvas.height = img.height;
|
|
126
|
-
}
|
|
127
|
-
catch (e) {
|
|
128
|
-
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
129
|
-
throw new Error(`createCanvas: Failed to load image for inherit sizing: ${errorMessage}`);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
// Handle video background inherit sizing
|
|
133
|
-
if (canvas.videoBg) {
|
|
134
|
-
try {
|
|
135
|
-
const frameBuffer = await this.#extractVideoFrame(canvas.videoBg.source, canvas.videoBg.frame ?? 0, canvas.videoBg.time, canvas.videoBg.format || 'jpg', canvas.videoBg.quality || 2);
|
|
136
|
-
if (frameBuffer) {
|
|
137
|
-
const img = await (0, canvas_1.loadImage)(frameBuffer);
|
|
138
|
-
if (!canvas.width)
|
|
139
|
-
canvas.width = img.width;
|
|
140
|
-
if (!canvas.height)
|
|
141
|
-
canvas.height = img.height;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
catch (e) {
|
|
145
|
-
console.warn('createCanvas: Failed to extract video frame for sizing, using defaults');
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
// 2) Use final width/height after inherit
|
|
149
|
-
const width = canvas.width ?? 500;
|
|
150
|
-
const height = canvas.height ?? 500;
|
|
151
|
-
const { x = 0, y = 0, rotation = 0, borderRadius = 0, borderPosition = 'all', opacity = 1, colorBg, customBg, gradientBg, videoBg, patternBg, noiseBg, blendMode, zoom, stroke, shadow, blur } = canvas;
|
|
152
|
-
// Validate background configuration
|
|
153
|
-
const bgSources = [
|
|
154
|
-
canvas.colorBg ? 'colorBg' : null,
|
|
155
|
-
canvas.gradientBg ? 'gradientBg' : null,
|
|
156
|
-
canvas.customBg ? 'customBg' : null
|
|
157
|
-
].filter(Boolean);
|
|
158
|
-
if (bgSources.length > 1) {
|
|
159
|
-
throw new Error(`createCanvas: only one of colorBg, gradientBg, or customBg can be used. You provided: ${bgSources.join(', ')}`);
|
|
160
|
-
}
|
|
161
|
-
const cv = (0, canvas_1.createCanvas)(width, height);
|
|
162
|
-
const ctx = cv.getContext('2d');
|
|
163
|
-
if (!ctx)
|
|
164
|
-
throw new Error('Unable to get 2D context');
|
|
165
|
-
ctx.globalAlpha = opacity;
|
|
166
|
-
// ---- BACKGROUND (clipped) ----
|
|
167
|
-
ctx.save();
|
|
168
|
-
(0, utils_1.applyRotation)(ctx, rotation, x, y, width, height);
|
|
169
|
-
(0, utils_1.buildPath)(ctx, x, y, width, height, borderRadius, borderPosition);
|
|
170
|
-
ctx.clip();
|
|
171
|
-
(0, utils_1.applyCanvasZoom)(ctx, width, height, zoom);
|
|
172
|
-
ctx.translate(x, y);
|
|
173
|
-
if (typeof blendMode === 'string') {
|
|
174
|
-
ctx.globalCompositeOperation = blendMode;
|
|
175
|
-
}
|
|
176
|
-
// Draw background - videoBg takes priority, then customBg, then gradientBg, then colorBg
|
|
177
|
-
if (videoBg) {
|
|
178
|
-
try {
|
|
179
|
-
// For videoBg, always use PNG format to ensure compatibility with loadImage
|
|
180
|
-
// The rgb24 pixel format for JPEG can cause issues with loadImage
|
|
181
|
-
const frameBuffer = await this.#extractVideoFrame(videoBg.source, videoBg.frame ?? 0, videoBg.time, 'png', // Force PNG format for videoBg to ensure proper color rendering
|
|
182
|
-
2);
|
|
183
|
-
if (frameBuffer && frameBuffer.length > 0) {
|
|
184
|
-
// Try loading from buffer first, if that fails, save to temp file and load from file
|
|
185
|
-
// This is a workaround for potential buffer compatibility issues with loadImage
|
|
186
|
-
let videoImg;
|
|
187
|
-
try {
|
|
188
|
-
videoImg = await (0, canvas_1.loadImage)(frameBuffer);
|
|
189
|
-
}
|
|
190
|
-
catch (bufferError) {
|
|
191
|
-
// If loading from buffer fails, try saving to temp file and loading from file
|
|
192
|
-
const tempFramePath = path_1.default.join(process.cwd(), '.temp-frames', `video-bg-temp-${Date.now()}.png`);
|
|
193
|
-
const frameDir = path_1.default.dirname(tempFramePath);
|
|
194
|
-
if (!fs_1.default.existsSync(frameDir)) {
|
|
195
|
-
fs_1.default.mkdirSync(frameDir, { recursive: true });
|
|
196
|
-
}
|
|
197
|
-
fs_1.default.writeFileSync(tempFramePath, frameBuffer);
|
|
198
|
-
videoImg = await (0, canvas_1.loadImage)(tempFramePath);
|
|
199
|
-
// Cleanup temp file after loading
|
|
200
|
-
if (fs_1.default.existsSync(tempFramePath)) {
|
|
201
|
-
fs_1.default.unlinkSync(tempFramePath);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
if (videoImg && videoImg.width > 0 && videoImg.height > 0) {
|
|
205
|
-
ctx.globalAlpha = videoBg.opacity ?? 1;
|
|
206
|
-
// Draw the video frame to fill the entire canvas
|
|
207
|
-
ctx.drawImage(videoImg, 0, 0, width, height);
|
|
208
|
-
ctx.globalAlpha = opacity;
|
|
209
|
-
}
|
|
210
|
-
else {
|
|
211
|
-
throw new Error(`Extracted video frame has invalid dimensions: ${videoImg?.width}x${videoImg?.height}`);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
else {
|
|
215
|
-
throw new Error('Frame extraction returned empty buffer');
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
catch (e) {
|
|
219
|
-
const errorMsg = e instanceof Error ? e.message : 'Unknown error';
|
|
220
|
-
// Re-throw FFmpeg installation errors so user sees installation guide
|
|
221
|
-
if (errorMsg.includes('FFMPEG NOT FOUND') || errorMsg.includes('FFmpeg')) {
|
|
222
|
-
throw e;
|
|
223
|
-
}
|
|
224
|
-
// Re-throw other errors instead of silently failing with black background
|
|
225
|
-
throw new Error(`createCanvas: videoBg extraction failed: ${errorMsg}`);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
else if (customBg) {
|
|
229
|
-
// Draw custom background with filters and opacity support
|
|
230
|
-
await (0, utils_1.customBackground)(ctx, { ...canvas, blur });
|
|
231
|
-
// Apply filters to background if specified
|
|
232
|
-
if (customBg.filters && customBg.filters.length > 0) {
|
|
233
|
-
const tempCanvas = (0, canvas_1.createCanvas)(width, height);
|
|
234
|
-
const tempCtx = tempCanvas.getContext('2d');
|
|
235
|
-
if (tempCtx) {
|
|
236
|
-
tempCtx.drawImage(cv, 0, 0);
|
|
237
|
-
await (0, utils_1.applySimpleProfessionalFilters)(tempCtx, customBg.filters, width, height);
|
|
238
|
-
ctx.clearRect(0, 0, width, height);
|
|
239
|
-
ctx.globalAlpha = customBg.opacity ?? 1;
|
|
240
|
-
ctx.drawImage(tempCanvas, 0, 0);
|
|
241
|
-
ctx.globalAlpha = opacity;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
else if (customBg.opacity !== undefined && customBg.opacity !== 1) {
|
|
245
|
-
ctx.globalAlpha = customBg.opacity;
|
|
246
|
-
await (0, utils_1.customBackground)(ctx, { ...canvas, blur });
|
|
247
|
-
ctx.globalAlpha = opacity;
|
|
248
|
-
}
|
|
249
|
-
else {
|
|
250
|
-
await (0, utils_1.customBackground)(ctx, { ...canvas, blur });
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
else if (gradientBg) {
|
|
254
|
-
await (0, utils_1.drawBackgroundGradient)(ctx, { ...canvas, blur });
|
|
255
|
-
}
|
|
256
|
-
else {
|
|
257
|
-
// Default to black background if no background is specified
|
|
258
|
-
await (0, utils_1.drawBackgroundColor)(ctx, { ...canvas, blur, colorBg: colorBg ?? '#000' });
|
|
259
|
-
}
|
|
260
|
-
if (patternBg)
|
|
261
|
-
await enhancedPatternRenderer_1.EnhancedPatternRenderer.renderPattern(ctx, cv, patternBg);
|
|
262
|
-
if (noiseBg)
|
|
263
|
-
(0, utils_1.applyNoise)(ctx, width, height, noiseBg.intensity ?? 0.05);
|
|
264
|
-
ctx.restore();
|
|
265
|
-
// Apply shadow effect
|
|
266
|
-
if (shadow) {
|
|
267
|
-
ctx.save();
|
|
268
|
-
(0, utils_1.buildPath)(ctx, x, y, width, height, borderRadius, borderPosition);
|
|
269
|
-
(0, utils_1.applyShadow)(ctx, shadow, x, y, width, height);
|
|
270
|
-
ctx.restore();
|
|
271
|
-
}
|
|
272
|
-
// Apply stroke effect
|
|
273
|
-
if (stroke) {
|
|
274
|
-
ctx.save();
|
|
275
|
-
(0, utils_1.buildPath)(ctx, x, y, width, height, borderRadius, borderPosition);
|
|
276
|
-
(0, utils_1.applyStroke)(ctx, stroke, x, y, width, height);
|
|
277
|
-
ctx.restore();
|
|
278
|
-
}
|
|
279
|
-
return { buffer: cv.toBuffer('image/png'), canvas };
|
|
280
|
-
}
|
|
281
|
-
catch (error) {
|
|
282
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
283
|
-
throw new Error(`createCanvas failed: ${errorMessage}`);
|
|
284
|
-
}
|
|
131
|
+
return this.canvasCreator.createCanvas(canvas);
|
|
285
132
|
}
|
|
286
133
|
/**
|
|
287
134
|
* Draws one or more images (or shapes) on an existing canvas buffer.
|
|
@@ -325,376 +172,8 @@ class ApexPainter {
|
|
|
325
172
|
* ], canvasBuffer);
|
|
326
173
|
* ```
|
|
327
174
|
*/
|
|
328
|
-
/**
|
|
329
|
-
* Validates image/shape properties array.
|
|
330
|
-
* @private
|
|
331
|
-
* @param images - Image properties to validate
|
|
332
|
-
*/
|
|
333
|
-
#validateImageArray(images) {
|
|
334
|
-
const list = Array.isArray(images) ? images : [images];
|
|
335
|
-
if (list.length === 0) {
|
|
336
|
-
throw new Error("createImage: At least one image/shape is required.");
|
|
337
|
-
}
|
|
338
|
-
for (const ip of list) {
|
|
339
|
-
this.#validateImageProperties(ip);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
175
|
async createImage(images, canvasBuffer) {
|
|
343
|
-
|
|
344
|
-
// Validate inputs
|
|
345
|
-
if (!canvasBuffer) {
|
|
346
|
-
throw new Error("createImage: canvasBuffer is required.");
|
|
347
|
-
}
|
|
348
|
-
this.#validateImageArray(images);
|
|
349
|
-
const list = Array.isArray(images) ? images : [images];
|
|
350
|
-
// Load base canvas buffer
|
|
351
|
-
const base = Buffer.isBuffer(canvasBuffer)
|
|
352
|
-
? await (0, canvas_1.loadImage)(canvasBuffer)
|
|
353
|
-
: await (0, canvas_1.loadImage)(canvasBuffer.buffer);
|
|
354
|
-
const cv = (0, canvas_1.createCanvas)(base.width, base.height);
|
|
355
|
-
const ctx = cv.getContext("2d");
|
|
356
|
-
if (!ctx)
|
|
357
|
-
throw new Error("Unable to get 2D rendering context");
|
|
358
|
-
// Paint bg
|
|
359
|
-
ctx.drawImage(base, 0, 0);
|
|
360
|
-
// Draw each image/shape on canvas
|
|
361
|
-
for (const ip of list) {
|
|
362
|
-
await this.#drawImageBitmap(ctx, ip);
|
|
363
|
-
}
|
|
364
|
-
// Return updated buffer
|
|
365
|
-
return cv.toBuffer("image/png");
|
|
366
|
-
}
|
|
367
|
-
catch (error) {
|
|
368
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
369
|
-
throw new Error(`createImage failed: ${errorMessage}`);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
/**
|
|
373
|
-
* Draws a single bitmap or shape with independent shadow & stroke.
|
|
374
|
-
* @private
|
|
375
|
-
* @param ctx - Canvas 2D context
|
|
376
|
-
* @param ip - Image properties
|
|
377
|
-
*/
|
|
378
|
-
async #drawImageBitmap(ctx, ip) {
|
|
379
|
-
const { source, x, y, width, height, inherit, fit = "fill", align = "center", rotation = 0, opacity = 1, blur = 0, borderRadius = 0, borderPosition = "all", shadow, stroke, boxBackground, shape, filters, filterIntensity = 1, filterOrder = 'post', mask, clipPath, distortion, meshWarp, effects } = ip;
|
|
380
|
-
this.#validateImageProperties(ip);
|
|
381
|
-
// Check if source is a shape
|
|
382
|
-
if ((0, utils_1.isShapeSource)(source)) {
|
|
383
|
-
await this.#drawShape(ctx, source, x, y, width ?? 100, height ?? 100, {
|
|
384
|
-
...shape,
|
|
385
|
-
rotation,
|
|
386
|
-
opacity,
|
|
387
|
-
blur,
|
|
388
|
-
borderRadius,
|
|
389
|
-
borderPosition,
|
|
390
|
-
shadow,
|
|
391
|
-
stroke,
|
|
392
|
-
boxBackground,
|
|
393
|
-
filters
|
|
394
|
-
});
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
// Handle image sources
|
|
398
|
-
const img = await (0, utils_1.loadImageCached)(source);
|
|
399
|
-
// Resolve this image's destination box
|
|
400
|
-
const boxW = (inherit && !width) ? img.width : (width ?? img.width);
|
|
401
|
-
const boxH = (inherit && !height) ? img.height : (height ?? img.height);
|
|
402
|
-
const box = { x, y, w: boxW, h: boxH };
|
|
403
|
-
ctx.save();
|
|
404
|
-
// Rotate around the box center; affects shadow, background, bitmap, stroke uniformly
|
|
405
|
-
(0, utils_1.applyRotation)(ctx, rotation, box.x, box.y, box.w, box.h);
|
|
406
|
-
// 1) Shadow (independent) — supports gradient or color
|
|
407
|
-
(0, utils_1.applyShadow)(ctx, box, shadow);
|
|
408
|
-
// 2) Optional box background (under bitmap, inside clip) — color or gradient
|
|
409
|
-
(0, utils_1.drawBoxBackground)(ctx, box, boxBackground, borderRadius, borderPosition);
|
|
410
|
-
// 3) Clip to image border radius or custom clip path, then draw the bitmap with blur/opacity and fit/align
|
|
411
|
-
ctx.save();
|
|
412
|
-
if (clipPath && clipPath.length >= 3) {
|
|
413
|
-
(0, utils_1.applyClipPath)(ctx, clipPath);
|
|
414
|
-
}
|
|
415
|
-
else {
|
|
416
|
-
(0, utils_1.buildPath)(ctx, box.x, box.y, box.w, box.h, borderRadius, borderPosition);
|
|
417
|
-
ctx.clip();
|
|
418
|
-
}
|
|
419
|
-
const { dx, dy, dw, dh, sx, sy, sw, sh } = (0, utils_1.fitInto)(box.x, box.y, box.w, box.h, img.width, img.height, fit, align);
|
|
420
|
-
const prevAlpha = ctx.globalAlpha;
|
|
421
|
-
ctx.globalAlpha = opacity ?? 1;
|
|
422
|
-
if ((blur ?? 0) > 0)
|
|
423
|
-
ctx.filter = `blur(${blur}px)`;
|
|
424
|
-
// Apply professional image filters BEFORE drawing if filterOrder is 'pre'
|
|
425
|
-
if (filters && filters.length > 0 && filterOrder === 'pre') {
|
|
426
|
-
const adjustedFilters = filters.map(f => ({
|
|
427
|
-
...f,
|
|
428
|
-
intensity: f.intensity !== undefined ? f.intensity * filterIntensity : (f.intensity ?? 1) * filterIntensity,
|
|
429
|
-
value: f.value !== undefined ? f.value * filterIntensity : f.value,
|
|
430
|
-
radius: f.radius !== undefined ? f.radius * filterIntensity : f.radius
|
|
431
|
-
}));
|
|
432
|
-
await (0, utils_1.applySimpleProfessionalFilters)(ctx, adjustedFilters, dw, dh);
|
|
433
|
-
}
|
|
434
|
-
// Apply distortion if specified (before drawing)
|
|
435
|
-
if (distortion) {
|
|
436
|
-
if (distortion.type === 'perspective' && distortion.points && distortion.points.length === 4) {
|
|
437
|
-
(0, utils_1.applyPerspectiveDistortion)(ctx, img, distortion.points, dx, dy, dw, dh);
|
|
438
|
-
ctx.filter = "none";
|
|
439
|
-
ctx.globalAlpha = prevAlpha;
|
|
440
|
-
ctx.restore();
|
|
441
|
-
ctx.restore();
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
else if (distortion.type === 'bulge' || distortion.type === 'pinch') {
|
|
445
|
-
const centerX = dx + dw / 2;
|
|
446
|
-
const centerY = dy + dh / 2;
|
|
447
|
-
const radius = Math.min(dw, dh) / 2;
|
|
448
|
-
const intensity = (distortion.intensity ?? 0.5) * (distortion.type === 'pinch' ? -1 : 1);
|
|
449
|
-
(0, utils_1.applyBulgeDistortion)(ctx, img, centerX, centerY, radius, intensity, dx, dy, dw, dh);
|
|
450
|
-
ctx.filter = "none";
|
|
451
|
-
ctx.globalAlpha = prevAlpha;
|
|
452
|
-
ctx.restore();
|
|
453
|
-
ctx.restore();
|
|
454
|
-
return;
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
// Apply mesh warp if specified
|
|
458
|
-
if (meshWarp && meshWarp.controlPoints) {
|
|
459
|
-
(0, utils_1.applyMeshWarp)(ctx, img, meshWarp.gridX ?? 10, meshWarp.gridY ?? 10, meshWarp.controlPoints, dx, dy, dw, dh);
|
|
460
|
-
ctx.filter = "none";
|
|
461
|
-
ctx.globalAlpha = prevAlpha;
|
|
462
|
-
ctx.restore();
|
|
463
|
-
ctx.restore();
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
|
-
// Draw image with or without masking
|
|
467
|
-
if (mask) {
|
|
468
|
-
await (0, utils_1.applyImageMask)(ctx, img, mask.source, mask.mode ?? 'alpha', dx, dy, dw, dh);
|
|
469
|
-
}
|
|
470
|
-
else {
|
|
471
|
-
ctx.drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh);
|
|
472
|
-
}
|
|
473
|
-
ctx.filter = "none";
|
|
474
|
-
ctx.globalAlpha = prevAlpha;
|
|
475
|
-
ctx.restore();
|
|
476
|
-
// Apply professional image filters AFTER drawing if filterOrder is 'post'
|
|
477
|
-
if (filters && filters.length > 0 && filterOrder === 'post') {
|
|
478
|
-
ctx.save();
|
|
479
|
-
const imageData = ctx.getImageData(box.x, box.y, box.w, box.h);
|
|
480
|
-
const tempCanvas = (0, canvas_1.createCanvas)(box.w, box.h);
|
|
481
|
-
const tempCtx = tempCanvas.getContext('2d');
|
|
482
|
-
if (tempCtx) {
|
|
483
|
-
tempCtx.putImageData(imageData, 0, 0);
|
|
484
|
-
const adjustedFilters = filters.map(f => ({
|
|
485
|
-
...f,
|
|
486
|
-
intensity: f.intensity !== undefined ? f.intensity * filterIntensity : (f.intensity ?? 1) * filterIntensity,
|
|
487
|
-
value: f.value !== undefined ? f.value * filterIntensity : f.value,
|
|
488
|
-
radius: f.radius !== undefined ? f.radius * filterIntensity : f.radius
|
|
489
|
-
}));
|
|
490
|
-
await (0, utils_1.applySimpleProfessionalFilters)(tempCtx, adjustedFilters, box.w, box.h);
|
|
491
|
-
ctx.clearRect(box.x, box.y, box.w, box.h);
|
|
492
|
-
ctx.drawImage(tempCanvas, box.x, box.y);
|
|
493
|
-
}
|
|
494
|
-
ctx.restore();
|
|
495
|
-
}
|
|
496
|
-
// Apply effects stack
|
|
497
|
-
if (effects) {
|
|
498
|
-
ctx.save();
|
|
499
|
-
const effectsCtx = ctx;
|
|
500
|
-
if (effects.vignette) {
|
|
501
|
-
(0, utils_1.applyVignette)(effectsCtx, effects.vignette.intensity, effects.vignette.size, box.w, box.h);
|
|
502
|
-
}
|
|
503
|
-
if (effects.lensFlare) {
|
|
504
|
-
(0, utils_1.applyLensFlare)(effectsCtx, box.x + effects.lensFlare.x, box.y + effects.lensFlare.y, effects.lensFlare.intensity, box.w, box.h);
|
|
505
|
-
}
|
|
506
|
-
if (effects.chromaticAberration) {
|
|
507
|
-
const imageData = ctx.getImageData(box.x, box.y, box.w, box.h);
|
|
508
|
-
const tempCanvas = (0, canvas_1.createCanvas)(box.w, box.h);
|
|
509
|
-
const tempCtx = tempCanvas.getContext('2d');
|
|
510
|
-
if (tempCtx) {
|
|
511
|
-
tempCtx.putImageData(imageData, 0, 0);
|
|
512
|
-
(0, utils_1.applyChromaticAberration)(tempCtx, effects.chromaticAberration.intensity, box.w, box.h);
|
|
513
|
-
ctx.clearRect(box.x, box.y, box.w, box.h);
|
|
514
|
-
ctx.drawImage(tempCanvas, box.x, box.y);
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
if (effects.filmGrain) {
|
|
518
|
-
const imageData = ctx.getImageData(box.x, box.y, box.w, box.h);
|
|
519
|
-
const tempCanvas = (0, canvas_1.createCanvas)(box.w, box.h);
|
|
520
|
-
const tempCtx = tempCanvas.getContext('2d');
|
|
521
|
-
if (tempCtx) {
|
|
522
|
-
tempCtx.putImageData(imageData, 0, 0);
|
|
523
|
-
(0, utils_1.applyFilmGrain)(tempCtx, effects.filmGrain.intensity, box.w, box.h);
|
|
524
|
-
ctx.clearRect(box.x, box.y, box.w, box.h);
|
|
525
|
-
ctx.drawImage(tempCanvas, box.x, box.y);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
ctx.restore();
|
|
529
|
-
}
|
|
530
|
-
// 4) Stroke (independent) — supports gradient or color
|
|
531
|
-
(0, utils_1.applyStroke)(ctx, box, stroke);
|
|
532
|
-
ctx.restore();
|
|
533
|
-
}
|
|
534
|
-
/**
|
|
535
|
-
* Draws a shape with all effects (shadow, stroke, filters, etc.).
|
|
536
|
-
* @private
|
|
537
|
-
* @param ctx - Canvas 2D context
|
|
538
|
-
* @param shapeType - Type of shape to draw
|
|
539
|
-
* @param x - X position
|
|
540
|
-
* @param y - Y position
|
|
541
|
-
* @param width - Shape width
|
|
542
|
-
* @param height - Shape height
|
|
543
|
-
* @param options - Shape drawing options
|
|
544
|
-
*/
|
|
545
|
-
async #drawShape(ctx, shapeType, x, y, width, height, options) {
|
|
546
|
-
const box = { x, y, w: width, h: height };
|
|
547
|
-
ctx.save();
|
|
548
|
-
// Apply rotation
|
|
549
|
-
if (options.rotation) {
|
|
550
|
-
(0, utils_1.applyRotation)(ctx, options.rotation, box.x, box.y, box.w, box.h);
|
|
551
|
-
}
|
|
552
|
-
// Apply opacity
|
|
553
|
-
if (options.opacity !== undefined) {
|
|
554
|
-
ctx.globalAlpha = options.opacity;
|
|
555
|
-
}
|
|
556
|
-
// Apply blur
|
|
557
|
-
if (options.blur && options.blur > 0) {
|
|
558
|
-
ctx.filter = `blur(${options.blur}px)`;
|
|
559
|
-
}
|
|
560
|
-
// 1) Custom Shadow for complex shapes (heart, star)
|
|
561
|
-
if (options.shadow && this.#isComplexShape(shapeType)) {
|
|
562
|
-
this.#applyShapeShadow(ctx, shapeType, x, y, width, height, options.shadow, {
|
|
563
|
-
radius: options.radius,
|
|
564
|
-
sides: options.sides,
|
|
565
|
-
innerRadius: options.innerRadius,
|
|
566
|
-
outerRadius: options.outerRadius
|
|
567
|
-
});
|
|
568
|
-
}
|
|
569
|
-
else if (options.shadow) {
|
|
570
|
-
// Use standard shadow for simple shapes
|
|
571
|
-
(0, utils_1.applyShadow)(ctx, box, options.shadow);
|
|
572
|
-
}
|
|
573
|
-
// 2) Optional box background
|
|
574
|
-
if (options.boxBackground) {
|
|
575
|
-
(0, utils_1.drawBoxBackground)(ctx, box, options.boxBackground, options.borderRadius, options.borderPosition);
|
|
576
|
-
}
|
|
577
|
-
// 3) Draw the shape
|
|
578
|
-
ctx.save();
|
|
579
|
-
if (options.borderRadius) {
|
|
580
|
-
(0, utils_1.buildPath)(ctx, box.x, box.y, box.w, box.h, options.borderRadius, options.borderPosition);
|
|
581
|
-
ctx.clip();
|
|
582
|
-
}
|
|
583
|
-
// Apply professional filters BEFORE drawing the shape
|
|
584
|
-
if (options.filters && options.filters.length > 0) {
|
|
585
|
-
await (0, utils_1.applySimpleProfessionalFilters)(ctx, options.filters, width, height);
|
|
586
|
-
}
|
|
587
|
-
(0, utils_1.drawShape)(ctx, shapeType, x, y, width, height, {
|
|
588
|
-
fill: options.fill,
|
|
589
|
-
color: options.color,
|
|
590
|
-
gradient: options.gradient,
|
|
591
|
-
radius: options.radius,
|
|
592
|
-
sides: options.sides,
|
|
593
|
-
innerRadius: options.innerRadius,
|
|
594
|
-
outerRadius: options.outerRadius
|
|
595
|
-
});
|
|
596
|
-
ctx.restore();
|
|
597
|
-
// 4) Custom Stroke for complex shapes (heart, star)
|
|
598
|
-
if (options.stroke && this.#isComplexShape(shapeType)) {
|
|
599
|
-
this.#applyShapeStroke(ctx, shapeType, x, y, width, height, options.stroke, {
|
|
600
|
-
radius: options.radius,
|
|
601
|
-
sides: options.sides,
|
|
602
|
-
innerRadius: options.innerRadius,
|
|
603
|
-
outerRadius: options.outerRadius
|
|
604
|
-
});
|
|
605
|
-
}
|
|
606
|
-
else if (options.stroke) {
|
|
607
|
-
// Use standard stroke for simple shapes
|
|
608
|
-
(0, utils_1.applyStroke)(ctx, box, options.stroke);
|
|
609
|
-
}
|
|
610
|
-
// Reset filters and alpha
|
|
611
|
-
ctx.filter = "none";
|
|
612
|
-
ctx.globalAlpha = 1;
|
|
613
|
-
ctx.restore();
|
|
614
|
-
}
|
|
615
|
-
/**
|
|
616
|
-
* Checks if shape needs custom shadow/stroke (heart, star).
|
|
617
|
-
* @private
|
|
618
|
-
* @param shapeType - Type of shape
|
|
619
|
-
* @returns True if shape is complex and needs custom effects
|
|
620
|
-
*/
|
|
621
|
-
#isComplexShape(shapeType) {
|
|
622
|
-
return shapeType === 'heart' || shapeType === 'star';
|
|
623
|
-
}
|
|
624
|
-
/**
|
|
625
|
-
* Applies custom shadow for complex shapes (heart, star).
|
|
626
|
-
* @private
|
|
627
|
-
* @param ctx - Canvas 2D context
|
|
628
|
-
* @param shapeType - Type of shape
|
|
629
|
-
* @param x - X position
|
|
630
|
-
* @param y - Y position
|
|
631
|
-
* @param width - Shape width
|
|
632
|
-
* @param height - Shape height
|
|
633
|
-
* @param shadow - Shadow options
|
|
634
|
-
* @param shapeOptions - Shape-specific options
|
|
635
|
-
*/
|
|
636
|
-
#applyShapeShadow(ctx, shapeType, x, y, width, height, shadow, shapeProps) {
|
|
637
|
-
const { color = "rgba(0,0,0,1)", gradient, opacity = 0.4, offsetX = 0, offsetY = 0, blur = 20 } = shadow;
|
|
638
|
-
ctx.save();
|
|
639
|
-
ctx.globalAlpha = opacity;
|
|
640
|
-
if (blur > 0)
|
|
641
|
-
ctx.filter = `blur(${blur}px)`;
|
|
642
|
-
// Set shadow color or gradient
|
|
643
|
-
if (gradient) {
|
|
644
|
-
const gfill = (0, utils_1.createGradientFill)(ctx, gradient, { x: x + offsetX, y: y + offsetY, w: width, h: height });
|
|
645
|
-
ctx.fillStyle = gfill;
|
|
646
|
-
}
|
|
647
|
-
else {
|
|
648
|
-
ctx.fillStyle = color;
|
|
649
|
-
}
|
|
650
|
-
// Create shadow path
|
|
651
|
-
(0, utils_1.createShapePath)(ctx, shapeType, x + offsetX, y + offsetY, width, height, shapeProps);
|
|
652
|
-
ctx.fill();
|
|
653
|
-
ctx.filter = "none";
|
|
654
|
-
ctx.globalAlpha = 1;
|
|
655
|
-
ctx.restore();
|
|
656
|
-
}
|
|
657
|
-
/**
|
|
658
|
-
* Applies custom stroke for complex shapes (heart, star).
|
|
659
|
-
* @private
|
|
660
|
-
* @param ctx - Canvas 2D context
|
|
661
|
-
* @param shapeType - Type of shape
|
|
662
|
-
* @param x - X position
|
|
663
|
-
* @param y - Y position
|
|
664
|
-
* @param width - Shape width
|
|
665
|
-
* @param height - Shape height
|
|
666
|
-
* @param stroke - Stroke options
|
|
667
|
-
* @param shapeOptions - Shape-specific options
|
|
668
|
-
*/
|
|
669
|
-
#applyShapeStroke(ctx, shapeType, x, y, width, height, stroke, shapeProps) {
|
|
670
|
-
const { color = "#000", gradient, width: strokeWidth = 2, position = 0, blur = 0, opacity = 1, style = 'solid' } = stroke;
|
|
671
|
-
ctx.save();
|
|
672
|
-
if (blur > 0)
|
|
673
|
-
ctx.filter = `blur(${blur}px)`;
|
|
674
|
-
ctx.globalAlpha = opacity;
|
|
675
|
-
// Set stroke color or gradient
|
|
676
|
-
if (gradient) {
|
|
677
|
-
const gstroke = (0, utils_1.createGradientFill)(ctx, gradient, { x, y, w: width, h: height });
|
|
678
|
-
ctx.strokeStyle = gstroke;
|
|
679
|
-
}
|
|
680
|
-
else {
|
|
681
|
-
ctx.strokeStyle = color;
|
|
682
|
-
}
|
|
683
|
-
ctx.lineWidth = strokeWidth;
|
|
684
|
-
// Apply stroke style
|
|
685
|
-
this.#applyShapeStrokeStyle(ctx, style, strokeWidth);
|
|
686
|
-
// Create stroke path
|
|
687
|
-
(0, utils_1.createShapePath)(ctx, shapeType, x, y, width, height, shapeProps);
|
|
688
|
-
// Handle complex stroke styles
|
|
689
|
-
if (style === 'groove' || style === 'ridge' || style === 'double') {
|
|
690
|
-
this.#applyComplexShapeStroke(ctx, style, strokeWidth, color, gradient);
|
|
691
|
-
}
|
|
692
|
-
else {
|
|
693
|
-
ctx.stroke();
|
|
694
|
-
}
|
|
695
|
-
ctx.filter = "none";
|
|
696
|
-
ctx.globalAlpha = 1;
|
|
697
|
-
ctx.restore();
|
|
176
|
+
return this.imageCreator.createImage(images, canvasBuffer);
|
|
698
177
|
}
|
|
699
178
|
/**
|
|
700
179
|
* Creates text on an existing canvas buffer with enhanced styling options.
|
|
@@ -750,61 +229,8 @@ class ApexPainter {
|
|
|
750
229
|
* ], canvasBuffer);
|
|
751
230
|
* ```
|
|
752
231
|
*/
|
|
753
|
-
/**
|
|
754
|
-
* Validates text properties array.
|
|
755
|
-
* @private
|
|
756
|
-
* @param textArray - Text properties to validate
|
|
757
|
-
*/
|
|
758
|
-
#validateTextArray(textArray) {
|
|
759
|
-
const textList = Array.isArray(textArray) ? textArray : [textArray];
|
|
760
|
-
if (textList.length === 0) {
|
|
761
|
-
throw new Error("createText: At least one text object is required.");
|
|
762
|
-
}
|
|
763
|
-
for (const textProps of textList) {
|
|
764
|
-
this.#validateTextProperties(textProps);
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
232
|
async createText(textArray, canvasBuffer) {
|
|
768
|
-
|
|
769
|
-
// Validate inputs
|
|
770
|
-
if (!canvasBuffer) {
|
|
771
|
-
throw new Error("createText: canvasBuffer is required.");
|
|
772
|
-
}
|
|
773
|
-
this.#validateTextArray(textArray);
|
|
774
|
-
// Ensure textArray is an array
|
|
775
|
-
const textList = Array.isArray(textArray) ? textArray : [textArray];
|
|
776
|
-
// Load existing canvas buffer
|
|
777
|
-
let existingImage;
|
|
778
|
-
if (Buffer.isBuffer(canvasBuffer)) {
|
|
779
|
-
existingImage = await (0, canvas_1.loadImage)(canvasBuffer);
|
|
780
|
-
}
|
|
781
|
-
else if (canvasBuffer && canvasBuffer.buffer) {
|
|
782
|
-
existingImage = await (0, canvas_1.loadImage)(canvasBuffer.buffer);
|
|
783
|
-
}
|
|
784
|
-
else {
|
|
785
|
-
throw new Error('Invalid canvasBuffer provided. It should be a Buffer or CanvasResults object with a buffer');
|
|
786
|
-
}
|
|
787
|
-
if (!existingImage) {
|
|
788
|
-
throw new Error('Unable to load image from buffer');
|
|
789
|
-
}
|
|
790
|
-
// Create new canvas with same dimensions
|
|
791
|
-
const canvas = (0, canvas_1.createCanvas)(existingImage.width, existingImage.height);
|
|
792
|
-
const ctx = canvas.getContext("2d");
|
|
793
|
-
if (!ctx) {
|
|
794
|
-
throw new Error("Unable to get 2D rendering context");
|
|
795
|
-
}
|
|
796
|
-
// Draw existing image as background
|
|
797
|
-
ctx.drawImage(existingImage, 0, 0);
|
|
798
|
-
// Render each text object with enhanced features
|
|
799
|
-
for (const textProps of textList) {
|
|
800
|
-
await this.#renderEnhancedText(ctx, textProps);
|
|
801
|
-
}
|
|
802
|
-
return canvas.toBuffer("image/png");
|
|
803
|
-
}
|
|
804
|
-
catch (error) {
|
|
805
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
806
|
-
throw new Error(`createText failed: ${errorMessage}`);
|
|
807
|
-
}
|
|
233
|
+
return this.textCreator.createText(textArray, canvasBuffer);
|
|
808
234
|
}
|
|
809
235
|
/**
|
|
810
236
|
* Validates custom line options.
|
|
@@ -853,135 +279,11 @@ class ApexPainter {
|
|
|
853
279
|
return canvas.toBuffer("image/png");
|
|
854
280
|
}
|
|
855
281
|
catch (error) {
|
|
856
|
-
|
|
857
|
-
throw new Error(`createCustom failed: ${errorMessage}`);
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
/**
|
|
861
|
-
* Validates GIF options and frames.
|
|
862
|
-
* @private
|
|
863
|
-
* @param gifFrames - GIF frames to validate
|
|
864
|
-
* @param options - GIF options to validate
|
|
865
|
-
*/
|
|
866
|
-
#validateGIFOptions(gifFrames, options) {
|
|
867
|
-
if (!gifFrames || gifFrames.length === 0) {
|
|
868
|
-
throw new Error("createGIF: At least one frame is required.");
|
|
869
|
-
}
|
|
870
|
-
for (const frame of gifFrames) {
|
|
871
|
-
if (!frame.background) {
|
|
872
|
-
throw new Error("createGIF: Each frame must have a background property.");
|
|
873
|
-
}
|
|
874
|
-
if (typeof frame.duration !== 'number' || frame.duration < 0) {
|
|
875
|
-
throw new Error("createGIF: Each frame duration must be a non-negative number.");
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
if (options.outputFormat === "file" && !options.outputFile) {
|
|
879
|
-
throw new Error("createGIF: outputFile is required when outputFormat is 'file'.");
|
|
880
|
-
}
|
|
881
|
-
if (options.repeat !== undefined && (typeof options.repeat !== "number" || options.repeat < 0)) {
|
|
882
|
-
throw new Error("createGIF: repeat must be a non-negative number or undefined.");
|
|
883
|
-
}
|
|
884
|
-
if (options.quality !== undefined && (typeof options.quality !== "number" || options.quality < 1 || options.quality > 20)) {
|
|
885
|
-
throw new Error("createGIF: quality must be a number between 1 and 20 or undefined.");
|
|
282
|
+
throw new Error(`createCustom failed: ${(0, utils_1.getErrorMessage)(error)}`);
|
|
886
283
|
}
|
|
887
284
|
}
|
|
888
285
|
async createGIF(gifFrames, options) {
|
|
889
|
-
|
|
890
|
-
this.#validateGIFOptions(gifFrames, options);
|
|
891
|
-
async function resizeImage(image, targetWidth, targetHeight) {
|
|
892
|
-
const canvas = (0, canvas_1.createCanvas)(targetWidth, targetHeight);
|
|
893
|
-
const ctx = canvas.getContext("2d");
|
|
894
|
-
ctx.drawImage(image, 0, 0, targetWidth, targetHeight);
|
|
895
|
-
return canvas;
|
|
896
|
-
}
|
|
897
|
-
function createOutputStream(outputFile) {
|
|
898
|
-
return fs_1.default.createWriteStream(outputFile);
|
|
899
|
-
}
|
|
900
|
-
function createBufferStream() {
|
|
901
|
-
const bufferStream = new stream_1.PassThrough();
|
|
902
|
-
const chunks = [];
|
|
903
|
-
bufferStream.on('data', (chunk) => {
|
|
904
|
-
chunks.push(chunk);
|
|
905
|
-
});
|
|
906
|
-
// Properly extend the stream object
|
|
907
|
-
const extendedStream = bufferStream;
|
|
908
|
-
extendedStream.getBuffer = function () {
|
|
909
|
-
return Buffer.concat(chunks);
|
|
910
|
-
};
|
|
911
|
-
extendedStream.chunks = chunks;
|
|
912
|
-
return extendedStream;
|
|
913
|
-
}
|
|
914
|
-
// Validation is done in #validateGIFOptions
|
|
915
|
-
const canvasWidth = options.width || 1200;
|
|
916
|
-
const canvasHeight = options.height || 1200;
|
|
917
|
-
const encoder = new gifencoder_1.default(canvasWidth, canvasHeight);
|
|
918
|
-
// Use buffer stream for buffer/base64/attachment, file stream only for 'file' format
|
|
919
|
-
const useBufferStream = options.outputFormat !== "file";
|
|
920
|
-
const outputStream = useBufferStream ? createBufferStream() : (options.outputFile ? createOutputStream(options.outputFile) : createBufferStream());
|
|
921
|
-
encoder.createReadStream().pipe(outputStream);
|
|
922
|
-
encoder.start();
|
|
923
|
-
encoder.setRepeat(options.repeat || 0);
|
|
924
|
-
encoder.setQuality(options.quality || 10);
|
|
925
|
-
const canvas = (0, canvas_1.createCanvas)(canvasWidth, canvasHeight);
|
|
926
|
-
const ctx = canvas.getContext("2d");
|
|
927
|
-
if (!ctx)
|
|
928
|
-
throw new Error("Unable to get 2D context");
|
|
929
|
-
for (const frame of gifFrames) {
|
|
930
|
-
const image = await (0, canvas_1.loadImage)(frame.background);
|
|
931
|
-
const resizedImage = await resizeImage(image, canvasWidth, canvasHeight);
|
|
932
|
-
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
933
|
-
ctx.drawImage(resizedImage, 0, 0);
|
|
934
|
-
if (options.watermark?.enable) {
|
|
935
|
-
const watermark = await (0, canvas_1.loadImage)(options.watermark.url);
|
|
936
|
-
ctx.drawImage(watermark, 10, canvasHeight - watermark.height - 10);
|
|
937
|
-
}
|
|
938
|
-
if (options.textOverlay) {
|
|
939
|
-
ctx.font = `${options.textOverlay.fontSize || 20}px Arial`;
|
|
940
|
-
ctx.fillStyle = options.textOverlay.fontColor || "white";
|
|
941
|
-
ctx.fillText(options.textOverlay.text, options.textOverlay.x || 10, options.textOverlay.y || 30);
|
|
942
|
-
}
|
|
943
|
-
encoder.setDelay(frame.duration);
|
|
944
|
-
encoder.addFrame(ctx);
|
|
945
|
-
}
|
|
946
|
-
encoder.finish();
|
|
947
|
-
if (options.outputFormat === "file") {
|
|
948
|
-
outputStream.end();
|
|
949
|
-
await new Promise((resolve) => outputStream.on("finish", () => resolve()));
|
|
950
|
-
}
|
|
951
|
-
else if (options.outputFormat === "base64") {
|
|
952
|
-
// Wait for stream to finish before getting buffer
|
|
953
|
-
await new Promise((resolve) => {
|
|
954
|
-
outputStream.on("end", () => resolve());
|
|
955
|
-
outputStream.end();
|
|
956
|
-
});
|
|
957
|
-
if ('getBuffer' in outputStream && typeof outputStream.getBuffer === 'function') {
|
|
958
|
-
return outputStream.getBuffer().toString("base64");
|
|
959
|
-
}
|
|
960
|
-
throw new Error("createGIF: Unable to get buffer for base64 output.");
|
|
961
|
-
}
|
|
962
|
-
else if (options.outputFormat === "attachment") {
|
|
963
|
-
const gifStream = encoder.createReadStream();
|
|
964
|
-
return [{ attachment: gifStream, name: "gif.js" }];
|
|
965
|
-
}
|
|
966
|
-
else if (options.outputFormat === "buffer") {
|
|
967
|
-
// Wait for stream to finish before getting buffer
|
|
968
|
-
await new Promise((resolve) => {
|
|
969
|
-
outputStream.on("end", () => resolve());
|
|
970
|
-
outputStream.end();
|
|
971
|
-
});
|
|
972
|
-
if ('getBuffer' in outputStream && typeof outputStream.getBuffer === 'function') {
|
|
973
|
-
return outputStream.getBuffer();
|
|
974
|
-
}
|
|
975
|
-
throw new Error("createGIF: Unable to get buffer for buffer output.");
|
|
976
|
-
}
|
|
977
|
-
else {
|
|
978
|
-
throw new Error("Invalid output format. Supported formats are 'file', 'base64', 'attachment', and 'buffer'.");
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
catch (error) {
|
|
982
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
983
|
-
throw new Error(`createGIF failed: ${errorMessage}`);
|
|
984
|
-
}
|
|
286
|
+
return this.gifCreator.createGIF(gifFrames, options);
|
|
985
287
|
}
|
|
986
288
|
/**
|
|
987
289
|
* Validates resize options.
|
|
@@ -1010,8 +312,7 @@ class ApexPainter {
|
|
|
1010
312
|
return await (0, utils_1.resizingImg)(resizeOptions);
|
|
1011
313
|
}
|
|
1012
314
|
catch (error) {
|
|
1013
|
-
|
|
1014
|
-
throw new Error(`resize failed: ${errorMessage}`);
|
|
315
|
+
throw new Error(`resize failed: ${(0, utils_1.getErrorMessage)(error)}`);
|
|
1015
316
|
}
|
|
1016
317
|
}
|
|
1017
318
|
/**
|
|
@@ -1038,8 +339,7 @@ class ApexPainter {
|
|
|
1038
339
|
return await (0, utils_1.converter)(source, newExtension);
|
|
1039
340
|
}
|
|
1040
341
|
catch (error) {
|
|
1041
|
-
|
|
1042
|
-
throw new Error(`imgConverter failed: ${errorMessage}`);
|
|
342
|
+
throw new Error(`imgConverter failed: ${(0, utils_1.getErrorMessage)(error)}`);
|
|
1043
343
|
}
|
|
1044
344
|
}
|
|
1045
345
|
/**
|
|
@@ -1062,8 +362,7 @@ class ApexPainter {
|
|
|
1062
362
|
return await (0, utils_1.imgEffects)(source, filters);
|
|
1063
363
|
}
|
|
1064
364
|
catch (error) {
|
|
1065
|
-
|
|
1066
|
-
throw new Error(`effects failed: ${errorMessage}`);
|
|
365
|
+
throw new Error(`effects failed: ${(0, utils_1.getErrorMessage)(error)}`);
|
|
1067
366
|
}
|
|
1068
367
|
}
|
|
1069
368
|
/**
|
|
@@ -1086,8 +385,7 @@ class ApexPainter {
|
|
|
1086
385
|
return await (0, utils_1.applyColorFilters)(source, filterColor, opacity);
|
|
1087
386
|
}
|
|
1088
387
|
catch (error) {
|
|
1089
|
-
|
|
1090
|
-
throw new Error(`colorsFilter failed: ${errorMessage}`);
|
|
388
|
+
throw new Error(`colorsFilter failed: ${(0, utils_1.getErrorMessage)(error)}`);
|
|
1091
389
|
}
|
|
1092
390
|
}
|
|
1093
391
|
async colorAnalysis(source) {
|
|
@@ -1098,8 +396,7 @@ class ApexPainter {
|
|
|
1098
396
|
return await (0, utils_1.detectColors)(source);
|
|
1099
397
|
}
|
|
1100
398
|
catch (error) {
|
|
1101
|
-
|
|
1102
|
-
throw new Error(`colorAnalysis failed: ${errorMessage}`);
|
|
399
|
+
throw new Error(`colorAnalysis failed: ${(0, utils_1.getErrorMessage)(error)}`);
|
|
1103
400
|
}
|
|
1104
401
|
}
|
|
1105
402
|
async colorsRemover(source, colorToRemove) {
|
|
@@ -1118,8 +415,7 @@ class ApexPainter {
|
|
|
1118
415
|
return await (0, utils_1.removeColor)(source, colorToRemove);
|
|
1119
416
|
}
|
|
1120
417
|
catch (error) {
|
|
1121
|
-
|
|
1122
|
-
throw new Error(`colorsRemover failed: ${errorMessage}`);
|
|
418
|
+
throw new Error(`colorsRemover failed: ${(0, utils_1.getErrorMessage)(error)}`);
|
|
1123
419
|
}
|
|
1124
420
|
}
|
|
1125
421
|
async removeBackground(imageURL, apiKey) {
|
|
@@ -1133,8 +429,7 @@ class ApexPainter {
|
|
|
1133
429
|
return await (0, utils_1.bgRemoval)(imageURL, apiKey);
|
|
1134
430
|
}
|
|
1135
431
|
catch (error) {
|
|
1136
|
-
|
|
1137
|
-
throw new Error(`removeBackground failed: ${errorMessage}`);
|
|
432
|
+
throw new Error(`removeBackground failed: ${(0, utils_1.getErrorMessage)(error)}`);
|
|
1138
433
|
}
|
|
1139
434
|
}
|
|
1140
435
|
/**
|
|
@@ -1167,9 +462,7 @@ class ApexPainter {
|
|
|
1167
462
|
this.#validateBlendInputs(layers, baseImageBuffer);
|
|
1168
463
|
const baseImage = await (0, canvas_1.loadImage)(baseImageBuffer);
|
|
1169
464
|
const canvas = (0, canvas_1.createCanvas)(baseImage.width, baseImage.height);
|
|
1170
|
-
const ctx =
|
|
1171
|
-
if (!ctx)
|
|
1172
|
-
throw new Error("Unable to get 2D context");
|
|
465
|
+
const ctx = (0, utils_1.getCanvasContext)(canvas);
|
|
1173
466
|
ctx.globalCompositeOperation = defaultBlendMode;
|
|
1174
467
|
ctx.drawImage(baseImage, 0, 0);
|
|
1175
468
|
for (const layer of layers) {
|
|
@@ -1183,79 +476,7 @@ class ApexPainter {
|
|
|
1183
476
|
return canvas.toBuffer('image/png');
|
|
1184
477
|
}
|
|
1185
478
|
catch (error) {
|
|
1186
|
-
|
|
1187
|
-
throw new Error(`blend failed: ${errorMessage}`);
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
/**
|
|
1191
|
-
* Validates chart inputs.
|
|
1192
|
-
* @private
|
|
1193
|
-
* @param data - Chart data to validate
|
|
1194
|
-
* @param type - Chart type configuration to validate
|
|
1195
|
-
*/
|
|
1196
|
-
#validateChartInputs(data, type) {
|
|
1197
|
-
if (!data || typeof data !== 'object' || Object.keys(data).length === 0) {
|
|
1198
|
-
throw new Error("createChart: data object with datasets is required.");
|
|
1199
|
-
}
|
|
1200
|
-
if (!type || typeof type !== 'object') {
|
|
1201
|
-
throw new Error("createChart: type configuration object is required.");
|
|
1202
|
-
}
|
|
1203
|
-
if (!type.chartType || typeof type.chartType !== 'string') {
|
|
1204
|
-
throw new Error("createChart: type.chartType must be a string.");
|
|
1205
|
-
}
|
|
1206
|
-
if (typeof type.chartNumber !== 'number' || type.chartNumber < 1) {
|
|
1207
|
-
throw new Error("createChart: type.chartNumber must be a positive number.");
|
|
1208
|
-
}
|
|
1209
|
-
const validChartTypes = ['bar', 'line', 'pie'];
|
|
1210
|
-
if (!validChartTypes.includes(type.chartType.toLowerCase())) {
|
|
1211
|
-
throw new Error(`createChart: Invalid chartType. Supported: ${validChartTypes.join(', ')}`);
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
async createChart(data, type) {
|
|
1215
|
-
try {
|
|
1216
|
-
this.#validateChartInputs(data, type);
|
|
1217
|
-
const { chartType, chartNumber } = type;
|
|
1218
|
-
switch (chartType.toLowerCase()) {
|
|
1219
|
-
case 'bar':
|
|
1220
|
-
switch (chartNumber) {
|
|
1221
|
-
case 1:
|
|
1222
|
-
const barResult = await (0, utils_1.verticalBarChart)(data);
|
|
1223
|
-
if (!barResult) {
|
|
1224
|
-
throw new Error("createChart: Failed to generate bar chart.");
|
|
1225
|
-
}
|
|
1226
|
-
return barResult;
|
|
1227
|
-
case 2:
|
|
1228
|
-
throw new Error('Type 2 is still under development.');
|
|
1229
|
-
default:
|
|
1230
|
-
throw new Error('Invalid chart number for chart type "bar".');
|
|
1231
|
-
}
|
|
1232
|
-
case 'line':
|
|
1233
|
-
switch (chartNumber) {
|
|
1234
|
-
case 1:
|
|
1235
|
-
// LineChart expects DataPoint[][] where DataPoint has { label: string; y: number }
|
|
1236
|
-
// Type assertion needed because there are two different DataPoint interfaces
|
|
1237
|
-
return await (0, utils_1.lineChart)(data);
|
|
1238
|
-
case 2:
|
|
1239
|
-
throw new Error('Type 2 is still under development.');
|
|
1240
|
-
default:
|
|
1241
|
-
throw new Error('Invalid chart number for chart type "line".');
|
|
1242
|
-
}
|
|
1243
|
-
case 'pie':
|
|
1244
|
-
switch (chartNumber) {
|
|
1245
|
-
case 1:
|
|
1246
|
-
return await (0, utils_1.pieChart)(data);
|
|
1247
|
-
case 2:
|
|
1248
|
-
throw new Error('Type 2 is still under development.');
|
|
1249
|
-
default:
|
|
1250
|
-
throw new Error('Invalid chart number for chart type "pie".');
|
|
1251
|
-
}
|
|
1252
|
-
default:
|
|
1253
|
-
throw new Error(`Unsupported chart type "${chartType}".`);
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
catch (error) {
|
|
1257
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1258
|
-
throw new Error(`createChart failed: ${errorMessage}`);
|
|
479
|
+
throw new Error(`blend failed: ${(0, utils_1.getErrorMessage)(error)}`);
|
|
1259
480
|
}
|
|
1260
481
|
}
|
|
1261
482
|
/**
|
|
@@ -1288,8 +509,7 @@ class ApexPainter {
|
|
|
1288
509
|
}
|
|
1289
510
|
}
|
|
1290
511
|
catch (error) {
|
|
1291
|
-
|
|
1292
|
-
throw new Error(`cropImage failed: ${errorMessage}`);
|
|
512
|
+
throw new Error(`cropImage failed: ${(0, utils_1.getErrorMessage)(error)}`);
|
|
1293
513
|
}
|
|
1294
514
|
}
|
|
1295
515
|
_ffmpegAvailable = null;
|
|
@@ -1493,7 +713,7 @@ class ApexPainter {
|
|
|
1493
713
|
return result;
|
|
1494
714
|
}
|
|
1495
715
|
catch (error) {
|
|
1496
|
-
const errorMessage =
|
|
716
|
+
const errorMessage = (0, utils_1.getErrorMessage)(error);
|
|
1497
717
|
// Re-throw FFmpeg installation errors
|
|
1498
718
|
if (errorMessage.includes('FFMPEG NOT FOUND') || errorMessage.includes('FFmpeg')) {
|
|
1499
719
|
throw error;
|
|
@@ -1630,7 +850,7 @@ class ApexPainter {
|
|
|
1630
850
|
}
|
|
1631
851
|
}
|
|
1632
852
|
catch (error) {
|
|
1633
|
-
const errorMessage =
|
|
853
|
+
const errorMessage = (0, utils_1.getErrorMessage)(error);
|
|
1634
854
|
// Re-throw FFmpeg installation errors so user sees installation guide
|
|
1635
855
|
if (errorMessage.includes('FFMPEG NOT FOUND') || errorMessage.includes('FFmpeg')) {
|
|
1636
856
|
throw error;
|
|
@@ -1760,7 +980,7 @@ class ApexPainter {
|
|
|
1760
980
|
}
|
|
1761
981
|
}
|
|
1762
982
|
catch (error) {
|
|
1763
|
-
const errorMessage =
|
|
983
|
+
const errorMessage = (0, utils_1.getErrorMessage)(error);
|
|
1764
984
|
// Re-throw FFmpeg installation errors so user sees installation guide
|
|
1765
985
|
if (errorMessage.includes('FFMPEG NOT FOUND') || errorMessage.includes('FFmpeg')) {
|
|
1766
986
|
throw error;
|
|
@@ -1774,226 +994,7 @@ class ApexPainter {
|
|
|
1774
994
|
* @returns Results based on the operation requested
|
|
1775
995
|
*/
|
|
1776
996
|
async createVideo(options) {
|
|
1777
|
-
|
|
1778
|
-
const ffmpegAvailable = await this.#checkFFmpegAvailable();
|
|
1779
|
-
if (!ffmpegAvailable) {
|
|
1780
|
-
const errorMessage = '❌ FFMPEG NOT FOUND\n' +
|
|
1781
|
-
'Video processing features require FFmpeg to be installed on your system.\n' +
|
|
1782
|
-
this.#getFFmpegInstallInstructions();
|
|
1783
|
-
throw new Error(errorMessage);
|
|
1784
|
-
}
|
|
1785
|
-
// Get video info if requested or needed
|
|
1786
|
-
let videoInfo = null;
|
|
1787
|
-
if (options.getInfo || options.extractFrame?.frame || options.generateThumbnail || options.generatePreview) {
|
|
1788
|
-
videoInfo = await this.getVideoInfo(options.source, true);
|
|
1789
|
-
}
|
|
1790
|
-
// Handle getInfo
|
|
1791
|
-
if (options.getInfo) {
|
|
1792
|
-
return videoInfo || await this.getVideoInfo(options.source, true);
|
|
1793
|
-
}
|
|
1794
|
-
// Handle extractFrame (creates canvas)
|
|
1795
|
-
if (options.extractFrame) {
|
|
1796
|
-
const frameBuffer = await this.#extractVideoFrame(options.source, options.extractFrame.frame ?? 0, options.extractFrame.time, options.extractFrame.outputFormat || 'png', options.extractFrame.quality || 2);
|
|
1797
|
-
if (!frameBuffer || frameBuffer.length === 0) {
|
|
1798
|
-
throw new Error('Failed to extract video frame');
|
|
1799
|
-
}
|
|
1800
|
-
const frameImage = await (0, canvas_1.loadImage)(frameBuffer);
|
|
1801
|
-
const videoWidth = frameImage.width;
|
|
1802
|
-
const videoHeight = frameImage.height;
|
|
1803
|
-
const width = options.extractFrame.width ?? videoWidth;
|
|
1804
|
-
const height = options.extractFrame.height ?? videoHeight;
|
|
1805
|
-
const canvas = (0, canvas_1.createCanvas)(width, height);
|
|
1806
|
-
const ctx = canvas.getContext('2d');
|
|
1807
|
-
if (!ctx) {
|
|
1808
|
-
throw new Error('Unable to get 2D context');
|
|
1809
|
-
}
|
|
1810
|
-
ctx.drawImage(frameImage, 0, 0, width, height);
|
|
1811
|
-
return {
|
|
1812
|
-
buffer: canvas.toBuffer('image/png'),
|
|
1813
|
-
canvas: { width, height }
|
|
1814
|
-
};
|
|
1815
|
-
}
|
|
1816
|
-
// Handle extractFrames (multiple frames at specific times or intervals)
|
|
1817
|
-
if (options.extractFrames) {
|
|
1818
|
-
if (options.extractFrames.times) {
|
|
1819
|
-
// Extract frames at specific times
|
|
1820
|
-
const frames = [];
|
|
1821
|
-
for (const time of options.extractFrames.times) {
|
|
1822
|
-
const frame = await this.#extractVideoFrame(options.source, 0, time, options.extractFrames.outputFormat || 'jpg', options.extractFrames.quality || 2);
|
|
1823
|
-
if (frame) {
|
|
1824
|
-
frames.push(frame);
|
|
1825
|
-
}
|
|
1826
|
-
}
|
|
1827
|
-
return frames;
|
|
1828
|
-
}
|
|
1829
|
-
else if (options.extractFrames.interval) {
|
|
1830
|
-
// Extract frames at intervals
|
|
1831
|
-
return await this.extractFrames(options.source, {
|
|
1832
|
-
interval: options.extractFrames.interval,
|
|
1833
|
-
outputFormat: options.extractFrames.outputFormat || 'jpg',
|
|
1834
|
-
frameSelection: options.extractFrames.frameSelection,
|
|
1835
|
-
outputDirectory: options.extractFrames.outputDirectory
|
|
1836
|
-
});
|
|
1837
|
-
}
|
|
1838
|
-
}
|
|
1839
|
-
// Handle extractAllFrames
|
|
1840
|
-
if (options.extractAllFrames) {
|
|
1841
|
-
return await this.extractAllFrames(options.source, {
|
|
1842
|
-
outputFormat: options.extractAllFrames.outputFormat,
|
|
1843
|
-
outputDirectory: options.extractAllFrames.outputDirectory,
|
|
1844
|
-
quality: options.extractAllFrames.quality,
|
|
1845
|
-
prefix: options.extractAllFrames.prefix,
|
|
1846
|
-
startTime: options.extractAllFrames.startTime,
|
|
1847
|
-
endTime: options.extractAllFrames.endTime
|
|
1848
|
-
});
|
|
1849
|
-
}
|
|
1850
|
-
// Handle generateThumbnail
|
|
1851
|
-
if (options.generateThumbnail) {
|
|
1852
|
-
return await this.#generateVideoThumbnail(options.source, options.generateThumbnail, videoInfo);
|
|
1853
|
-
}
|
|
1854
|
-
// Handle convert
|
|
1855
|
-
if (options.convert) {
|
|
1856
|
-
return await this.#convertVideo(options.source, options.convert);
|
|
1857
|
-
}
|
|
1858
|
-
// Handle trim
|
|
1859
|
-
if (options.trim) {
|
|
1860
|
-
return await this.#trimVideo(options.source, options.trim);
|
|
1861
|
-
}
|
|
1862
|
-
// Handle extractAudio
|
|
1863
|
-
if (options.extractAudio) {
|
|
1864
|
-
return await this.#extractAudio(options.source, options.extractAudio);
|
|
1865
|
-
}
|
|
1866
|
-
// Handle addWatermark
|
|
1867
|
-
if (options.addWatermark) {
|
|
1868
|
-
return await this.#addWatermarkToVideo(options.source, options.addWatermark);
|
|
1869
|
-
}
|
|
1870
|
-
// Handle changeSpeed
|
|
1871
|
-
if (options.changeSpeed) {
|
|
1872
|
-
return await this.#changeVideoSpeed(options.source, options.changeSpeed);
|
|
1873
|
-
}
|
|
1874
|
-
// Handle generatePreview
|
|
1875
|
-
if (options.generatePreview) {
|
|
1876
|
-
return await this.#generateVideoPreview(options.source, options.generatePreview, videoInfo);
|
|
1877
|
-
}
|
|
1878
|
-
// Handle applyEffects
|
|
1879
|
-
if (options.applyEffects) {
|
|
1880
|
-
return await this.#applyVideoEffects(options.source, options.applyEffects);
|
|
1881
|
-
}
|
|
1882
|
-
// Handle merge
|
|
1883
|
-
if (options.merge) {
|
|
1884
|
-
return await this.#mergeVideos(options.merge);
|
|
1885
|
-
}
|
|
1886
|
-
// Handle rotate
|
|
1887
|
-
if (options.rotate) {
|
|
1888
|
-
return await this.#rotateVideo(options.source, options.rotate);
|
|
1889
|
-
}
|
|
1890
|
-
// Handle crop
|
|
1891
|
-
if (options.crop) {
|
|
1892
|
-
return await this.#cropVideo(options.source, options.crop);
|
|
1893
|
-
}
|
|
1894
|
-
// Handle compress
|
|
1895
|
-
if (options.compress) {
|
|
1896
|
-
return await this.#compressVideo(options.source, options.compress);
|
|
1897
|
-
}
|
|
1898
|
-
// Handle addText
|
|
1899
|
-
if (options.addText) {
|
|
1900
|
-
return await this.#addTextToVideo(options.source, options.addText);
|
|
1901
|
-
}
|
|
1902
|
-
// Handle addFade
|
|
1903
|
-
if (options.addFade) {
|
|
1904
|
-
return await this.#addFadeToVideo(options.source, options.addFade);
|
|
1905
|
-
}
|
|
1906
|
-
// Handle reverse
|
|
1907
|
-
if (options.reverse) {
|
|
1908
|
-
return await this.#reverseVideo(options.source, options.reverse);
|
|
1909
|
-
}
|
|
1910
|
-
// Handle createLoop
|
|
1911
|
-
if (options.createLoop) {
|
|
1912
|
-
return await this.#createVideoLoop(options.source, options.createLoop);
|
|
1913
|
-
}
|
|
1914
|
-
// Handle batch
|
|
1915
|
-
if (options.batch) {
|
|
1916
|
-
return await this.#batchProcessVideos(options.batch);
|
|
1917
|
-
}
|
|
1918
|
-
// Handle detectScenes
|
|
1919
|
-
if (options.detectScenes) {
|
|
1920
|
-
return await this.#detectVideoScenes(options.source, options.detectScenes);
|
|
1921
|
-
}
|
|
1922
|
-
// Handle stabilize
|
|
1923
|
-
if (options.stabilize) {
|
|
1924
|
-
return await this.#stabilizeVideo(options.source, options.stabilize);
|
|
1925
|
-
}
|
|
1926
|
-
// Handle colorCorrect
|
|
1927
|
-
if (options.colorCorrect) {
|
|
1928
|
-
return await this.#colorCorrectVideo(options.source, options.colorCorrect);
|
|
1929
|
-
}
|
|
1930
|
-
// Handle pictureInPicture
|
|
1931
|
-
if (options.pictureInPicture) {
|
|
1932
|
-
return await this.#addPictureInPicture(options.source, options.pictureInPicture);
|
|
1933
|
-
}
|
|
1934
|
-
// Handle splitScreen
|
|
1935
|
-
if (options.splitScreen) {
|
|
1936
|
-
return await this.#createSplitScreen(options.splitScreen);
|
|
1937
|
-
}
|
|
1938
|
-
// Handle createTimeLapse
|
|
1939
|
-
if (options.createTimeLapse) {
|
|
1940
|
-
return await this.#createTimeLapseVideo(options.source, options.createTimeLapse);
|
|
1941
|
-
}
|
|
1942
|
-
// Handle mute
|
|
1943
|
-
if (options.mute) {
|
|
1944
|
-
return await this.#muteVideo(options.source, options.mute);
|
|
1945
|
-
}
|
|
1946
|
-
// Handle adjustVolume
|
|
1947
|
-
if (options.adjustVolume) {
|
|
1948
|
-
return await this.#adjustVideoVolume(options.source, options.adjustVolume);
|
|
1949
|
-
}
|
|
1950
|
-
// Handle detectFormat
|
|
1951
|
-
if (options.detectFormat) {
|
|
1952
|
-
const info = await this.getVideoInfo(options.source, true);
|
|
1953
|
-
// Try to get codec from ffprobe
|
|
1954
|
-
let codec = 'unknown';
|
|
1955
|
-
try {
|
|
1956
|
-
const frameDir = path_1.default.join(process.cwd(), '.temp-frames');
|
|
1957
|
-
let videoPath;
|
|
1958
|
-
if (Buffer.isBuffer(options.source)) {
|
|
1959
|
-
const tempPath = path_1.default.join(frameDir, `temp-video-${Date.now()}.mp4`);
|
|
1960
|
-
fs_1.default.writeFileSync(tempPath, options.source);
|
|
1961
|
-
videoPath = tempPath;
|
|
1962
|
-
}
|
|
1963
|
-
else {
|
|
1964
|
-
let resolvedPath = options.source;
|
|
1965
|
-
if (!/^https?:\/\//i.test(resolvedPath)) {
|
|
1966
|
-
resolvedPath = path_1.default.join(process.cwd(), resolvedPath);
|
|
1967
|
-
}
|
|
1968
|
-
videoPath = resolvedPath;
|
|
1969
|
-
}
|
|
1970
|
-
const escapedPath = videoPath.replace(/"/g, '\\"');
|
|
1971
|
-
const { stdout } = await execAsync(`ffprobe -v error -select_streams v:0 -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 "${escapedPath}"`, { timeout: 10000, maxBuffer: 1024 * 1024 });
|
|
1972
|
-
codec = stdout.toString().trim() || 'unknown';
|
|
1973
|
-
}
|
|
1974
|
-
catch {
|
|
1975
|
-
codec = 'unknown';
|
|
1976
|
-
}
|
|
1977
|
-
return {
|
|
1978
|
-
format: info?.format || 'unknown',
|
|
1979
|
-
codec: codec,
|
|
1980
|
-
container: info?.format || 'unknown',
|
|
1981
|
-
width: info?.width,
|
|
1982
|
-
height: info?.height,
|
|
1983
|
-
fps: info?.fps,
|
|
1984
|
-
bitrate: info?.bitrate,
|
|
1985
|
-
duration: info?.duration
|
|
1986
|
-
};
|
|
1987
|
-
}
|
|
1988
|
-
throw new Error('No video operation specified');
|
|
1989
|
-
}
|
|
1990
|
-
catch (error) {
|
|
1991
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1992
|
-
if (errorMessage.includes('FFMPEG NOT FOUND') || errorMessage.includes('FFmpeg')) {
|
|
1993
|
-
throw error;
|
|
1994
|
-
}
|
|
1995
|
-
throw new Error(`createVideo failed: ${errorMessage}`);
|
|
1996
|
-
}
|
|
997
|
+
return this.videoCreator.createVideo(options);
|
|
1997
998
|
}
|
|
1998
999
|
/**
|
|
1999
1000
|
* Generate video thumbnail (grid of frames)
|
|
@@ -2024,10 +1025,7 @@ class ApexPainter {
|
|
|
2024
1025
|
const thumbnailWidth = frameWidth * grid.cols;
|
|
2025
1026
|
const thumbnailHeight = frameHeight * grid.rows;
|
|
2026
1027
|
const canvas = (0, canvas_1.createCanvas)(thumbnailWidth, thumbnailHeight);
|
|
2027
|
-
const ctx =
|
|
2028
|
-
if (!ctx) {
|
|
2029
|
-
throw new Error('Unable to get 2D context');
|
|
2030
|
-
}
|
|
1028
|
+
const ctx = (0, utils_1.getCanvasContext)(canvas);
|
|
2031
1029
|
// Draw frames in grid
|
|
2032
1030
|
for (let i = 0; i < frames.length; i++) {
|
|
2033
1031
|
const row = Math.floor(i / grid.cols);
|
|
@@ -2551,6 +1549,172 @@ class ApexPainter {
|
|
|
2551
1549
|
throw error;
|
|
2552
1550
|
}
|
|
2553
1551
|
}
|
|
1552
|
+
/**
|
|
1553
|
+
* Replace segment in video with segment from another video
|
|
1554
|
+
* @private
|
|
1555
|
+
*/
|
|
1556
|
+
async #replaceVideoSegment(mainVideoSource, options) {
|
|
1557
|
+
const frameDir = path_1.default.join(process.cwd(), '.temp-frames');
|
|
1558
|
+
if (!fs_1.default.existsSync(frameDir)) {
|
|
1559
|
+
fs_1.default.mkdirSync(frameDir, { recursive: true });
|
|
1560
|
+
}
|
|
1561
|
+
const timestamp = Date.now();
|
|
1562
|
+
const tempFiles = [];
|
|
1563
|
+
let shouldCleanupMain = false;
|
|
1564
|
+
let shouldCleanupReplacement = false;
|
|
1565
|
+
// Prepare main video
|
|
1566
|
+
let mainVideoPath;
|
|
1567
|
+
if (Buffer.isBuffer(mainVideoSource)) {
|
|
1568
|
+
mainVideoPath = path_1.default.join(frameDir, `main-video-${timestamp}.mp4`);
|
|
1569
|
+
fs_1.default.writeFileSync(mainVideoPath, mainVideoSource);
|
|
1570
|
+
shouldCleanupMain = true;
|
|
1571
|
+
tempFiles.push(mainVideoPath);
|
|
1572
|
+
}
|
|
1573
|
+
else {
|
|
1574
|
+
let resolvedPath = mainVideoSource;
|
|
1575
|
+
if (!/^https?:\/\//i.test(resolvedPath)) {
|
|
1576
|
+
resolvedPath = path_1.default.join(process.cwd(), resolvedPath);
|
|
1577
|
+
}
|
|
1578
|
+
if (!fs_1.default.existsSync(resolvedPath)) {
|
|
1579
|
+
throw new Error(`Main video file not found: ${mainVideoSource}`);
|
|
1580
|
+
}
|
|
1581
|
+
mainVideoPath = resolvedPath;
|
|
1582
|
+
}
|
|
1583
|
+
// Validate that either replacementVideo or replacementFrames is provided
|
|
1584
|
+
if (!options.replacementVideo && !options.replacementFrames) {
|
|
1585
|
+
throw new Error('Either replacementVideo or replacementFrames must be provided');
|
|
1586
|
+
}
|
|
1587
|
+
if (options.replacementVideo && options.replacementFrames) {
|
|
1588
|
+
throw new Error('Cannot specify both replacementVideo and replacementFrames');
|
|
1589
|
+
}
|
|
1590
|
+
// Get main video info to validate times
|
|
1591
|
+
const mainVideoInfo = await this.getVideoInfo(mainVideoPath, true);
|
|
1592
|
+
if (!mainVideoInfo) {
|
|
1593
|
+
throw new Error('Failed to get main video information');
|
|
1594
|
+
}
|
|
1595
|
+
if (options.targetStartTime < 0 || options.targetEndTime > mainVideoInfo.duration) {
|
|
1596
|
+
throw new Error(`Target time range (${options.targetStartTime}-${options.targetEndTime}s) is outside video duration (${mainVideoInfo.duration}s)`);
|
|
1597
|
+
}
|
|
1598
|
+
if (options.targetStartTime >= options.targetEndTime) {
|
|
1599
|
+
throw new Error('targetStartTime must be less than targetEndTime');
|
|
1600
|
+
}
|
|
1601
|
+
const targetDuration = options.targetEndTime - options.targetStartTime;
|
|
1602
|
+
const escapedMainPath = mainVideoPath.replace(/"/g, '\\"');
|
|
1603
|
+
try {
|
|
1604
|
+
// Step 1: Extract part before the segment to replace
|
|
1605
|
+
const part1Path = path_1.default.join(frameDir, `part1-${timestamp}.mp4`);
|
|
1606
|
+
tempFiles.push(part1Path);
|
|
1607
|
+
if (options.targetStartTime > 0) {
|
|
1608
|
+
const escapedPart1 = part1Path.replace(/"/g, '\\"');
|
|
1609
|
+
const part1Command = `ffmpeg -i "${escapedMainPath}" -t ${options.targetStartTime} -c copy -y "${escapedPart1}"`;
|
|
1610
|
+
await execAsync(part1Command, { timeout: 300000, maxBuffer: 10 * 1024 * 1024 });
|
|
1611
|
+
}
|
|
1612
|
+
// Step 2: Create replacement segment (from video or frames)
|
|
1613
|
+
const replacementSegmentPath = path_1.default.join(frameDir, `replacement-segment-${timestamp}.mp4`);
|
|
1614
|
+
tempFiles.push(replacementSegmentPath);
|
|
1615
|
+
if (options.replacementVideo) {
|
|
1616
|
+
// Extract replacement segment from replacement video
|
|
1617
|
+
let replacementVideoPath;
|
|
1618
|
+
if (Buffer.isBuffer(options.replacementVideo)) {
|
|
1619
|
+
replacementVideoPath = path_1.default.join(frameDir, `replacement-video-${timestamp}.mp4`);
|
|
1620
|
+
fs_1.default.writeFileSync(replacementVideoPath, options.replacementVideo);
|
|
1621
|
+
shouldCleanupReplacement = true;
|
|
1622
|
+
tempFiles.push(replacementVideoPath);
|
|
1623
|
+
}
|
|
1624
|
+
else {
|
|
1625
|
+
let resolvedPath = options.replacementVideo;
|
|
1626
|
+
if (!/^https?:\/\//i.test(resolvedPath)) {
|
|
1627
|
+
resolvedPath = path_1.default.join(process.cwd(), resolvedPath);
|
|
1628
|
+
}
|
|
1629
|
+
if (!fs_1.default.existsSync(resolvedPath)) {
|
|
1630
|
+
throw new Error(`Replacement video file not found: ${options.replacementVideo}`);
|
|
1631
|
+
}
|
|
1632
|
+
replacementVideoPath = resolvedPath;
|
|
1633
|
+
}
|
|
1634
|
+
const replacementStartTime = options.replacementStartTime || 0;
|
|
1635
|
+
const replacementDuration = options.replacementDuration || targetDuration;
|
|
1636
|
+
const escapedReplacementPath = replacementVideoPath.replace(/"/g, '\\"');
|
|
1637
|
+
const escapedSegment = replacementSegmentPath.replace(/"/g, '\\"');
|
|
1638
|
+
const segmentCommand = `ffmpeg -i "${escapedReplacementPath}" -ss ${replacementStartTime} -t ${replacementDuration} -c copy -y "${escapedSegment}"`;
|
|
1639
|
+
await execAsync(segmentCommand, { timeout: 300000, maxBuffer: 10 * 1024 * 1024 });
|
|
1640
|
+
}
|
|
1641
|
+
else if (options.replacementFrames) {
|
|
1642
|
+
// Create video from frames
|
|
1643
|
+
const replacementFps = options.replacementFps || 30;
|
|
1644
|
+
await this.#createVideoFromFrames({
|
|
1645
|
+
frames: options.replacementFrames,
|
|
1646
|
+
outputPath: replacementSegmentPath,
|
|
1647
|
+
fps: replacementFps,
|
|
1648
|
+
format: 'mp4',
|
|
1649
|
+
quality: 'high'
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1652
|
+
// Step 3: Extract part after the segment to replace
|
|
1653
|
+
const part3Path = path_1.default.join(frameDir, `part3-${timestamp}.mp4`);
|
|
1654
|
+
tempFiles.push(part3Path);
|
|
1655
|
+
const remainingDuration = mainVideoInfo.duration - options.targetEndTime;
|
|
1656
|
+
if (remainingDuration > 0) {
|
|
1657
|
+
const escapedPart3 = part3Path.replace(/"/g, '\\"');
|
|
1658
|
+
const part3Command = `ffmpeg -i "${escapedMainPath}" -ss ${options.targetEndTime} -t ${remainingDuration} -c copy -y "${escapedPart3}"`;
|
|
1659
|
+
await execAsync(part3Command, { timeout: 300000, maxBuffer: 10 * 1024 * 1024 });
|
|
1660
|
+
}
|
|
1661
|
+
else {
|
|
1662
|
+
// If no remaining duration, part3 is empty
|
|
1663
|
+
}
|
|
1664
|
+
// Step 4: Create concat file and merge all parts
|
|
1665
|
+
const concatFile = path_1.default.join(frameDir, `concat-${timestamp}.txt`);
|
|
1666
|
+
tempFiles.push(concatFile);
|
|
1667
|
+
const concatParts = [];
|
|
1668
|
+
// Add part 1 if it exists and has content
|
|
1669
|
+
if (options.targetStartTime > 0 && fs_1.default.existsSync(part1Path) && fs_1.default.statSync(part1Path).size > 0) {
|
|
1670
|
+
concatParts.push(part1Path.replace(/\\/g, '/').replace(/'/g, "\\'"));
|
|
1671
|
+
}
|
|
1672
|
+
// Add replacement segment
|
|
1673
|
+
if (fs_1.default.existsSync(replacementSegmentPath) && fs_1.default.statSync(replacementSegmentPath).size > 0) {
|
|
1674
|
+
concatParts.push(replacementSegmentPath.replace(/\\/g, '/').replace(/'/g, "\\'"));
|
|
1675
|
+
}
|
|
1676
|
+
// Add part 3 if it exists and has content
|
|
1677
|
+
if (remainingDuration > 0 && fs_1.default.existsSync(part3Path) && fs_1.default.statSync(part3Path).size > 0) {
|
|
1678
|
+
concatParts.push(part3Path.replace(/\\/g, '/').replace(/'/g, "\\'"));
|
|
1679
|
+
}
|
|
1680
|
+
if (concatParts.length === 0) {
|
|
1681
|
+
throw new Error('No valid video segments to concatenate');
|
|
1682
|
+
}
|
|
1683
|
+
const concatContent = concatParts.map(p => `file '${p}'`).join('\n');
|
|
1684
|
+
fs_1.default.writeFileSync(concatFile, concatContent);
|
|
1685
|
+
// Step 5: Concatenate all parts
|
|
1686
|
+
const escapedConcatFile = concatFile.replace(/"/g, '\\"');
|
|
1687
|
+
const escapedOutputPath = options.outputPath.replace(/"/g, '\\"');
|
|
1688
|
+
const concatCommand = `ffmpeg -f concat -safe 0 -i "${escapedConcatFile}" -c copy -y "${escapedOutputPath}"`;
|
|
1689
|
+
await execAsync(concatCommand, { timeout: 600000, maxBuffer: 20 * 1024 * 1024 });
|
|
1690
|
+
// Cleanup temp files
|
|
1691
|
+
for (const tempFile of tempFiles) {
|
|
1692
|
+
if (fs_1.default.existsSync(tempFile)) {
|
|
1693
|
+
try {
|
|
1694
|
+
fs_1.default.unlinkSync(tempFile);
|
|
1695
|
+
}
|
|
1696
|
+
catch {
|
|
1697
|
+
// Ignore cleanup errors
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
return { outputPath: options.outputPath, success: true };
|
|
1702
|
+
}
|
|
1703
|
+
catch (error) {
|
|
1704
|
+
// Cleanup temp files on error
|
|
1705
|
+
for (const tempFile of tempFiles) {
|
|
1706
|
+
if (fs_1.default.existsSync(tempFile)) {
|
|
1707
|
+
try {
|
|
1708
|
+
fs_1.default.unlinkSync(tempFile);
|
|
1709
|
+
}
|
|
1710
|
+
catch {
|
|
1711
|
+
// Ignore cleanup errors
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
throw error;
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
2554
1718
|
/**
|
|
2555
1719
|
* Rotate/Flip video
|
|
2556
1720
|
* @private
|
|
@@ -3325,7 +2489,7 @@ class ApexPainter {
|
|
|
3325
2489
|
return await this.#changeVideoSpeed(videoSource, { speed, outputPath: options.outputPath });
|
|
3326
2490
|
}
|
|
3327
2491
|
/**
|
|
3328
|
-
* Mute video (remove audio)
|
|
2492
|
+
* Mute video (remove audio) - supports full mute or partial mute with time ranges
|
|
3329
2493
|
* @private
|
|
3330
2494
|
*/
|
|
3331
2495
|
async #muteVideo(videoSource, options) {
|
|
@@ -3353,7 +2517,36 @@ class ApexPainter {
|
|
|
3353
2517
|
}
|
|
3354
2518
|
const escapedVideoPath = videoPath.replace(/"/g, '\\"');
|
|
3355
2519
|
const escapedOutputPath = options.outputPath.replace(/"/g, '\\"');
|
|
3356
|
-
|
|
2520
|
+
// If no ranges specified, mute entire video
|
|
2521
|
+
if (!options.ranges || options.ranges.length === 0) {
|
|
2522
|
+
const command = `ffmpeg -i "${escapedVideoPath}" -c copy -an -y "${escapedOutputPath}"`;
|
|
2523
|
+
try {
|
|
2524
|
+
await execAsync(command, { timeout: 300000, maxBuffer: 10 * 1024 * 1024 });
|
|
2525
|
+
if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
|
|
2526
|
+
fs_1.default.unlinkSync(videoPath);
|
|
2527
|
+
}
|
|
2528
|
+
return { outputPath: options.outputPath, success: true };
|
|
2529
|
+
}
|
|
2530
|
+
catch (error) {
|
|
2531
|
+
if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
|
|
2532
|
+
fs_1.default.unlinkSync(videoPath);
|
|
2533
|
+
}
|
|
2534
|
+
throw error;
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
// Partial mute: mute specific time ranges
|
|
2538
|
+
// Get video info to determine duration
|
|
2539
|
+
const videoInfo = await this.getVideoInfo(videoPath, true);
|
|
2540
|
+
if (!videoInfo) {
|
|
2541
|
+
throw new Error('Failed to get video information for partial mute');
|
|
2542
|
+
}
|
|
2543
|
+
// Build audio filter for partial muting
|
|
2544
|
+
// Format: volume=enable='between(t,start,end)':volume=0
|
|
2545
|
+
const volumeFilters = options.ranges.map((range, index) => {
|
|
2546
|
+
return `volume=enable='between(t,${range.start},${range.end})':volume=0`;
|
|
2547
|
+
}).join(',');
|
|
2548
|
+
// Use complex filter to apply volume changes at specific times
|
|
2549
|
+
const command = `ffmpeg -i "${escapedVideoPath}" -af "${volumeFilters}" -c:v copy -y "${escapedOutputPath}"`;
|
|
3357
2550
|
try {
|
|
3358
2551
|
await execAsync(command, { timeout: 300000, maxBuffer: 10 * 1024 * 1024 });
|
|
3359
2552
|
if (shouldCleanupVideo && fs_1.default.existsSync(videoPath)) {
|
|
@@ -3413,6 +2606,170 @@ class ApexPainter {
|
|
|
3413
2606
|
throw error;
|
|
3414
2607
|
}
|
|
3415
2608
|
}
|
|
2609
|
+
/**
|
|
2610
|
+
* Create video from frames/images
|
|
2611
|
+
* @private
|
|
2612
|
+
*/
|
|
2613
|
+
async #createVideoFromFrames(options) {
|
|
2614
|
+
if (!options.frames || options.frames.length === 0) {
|
|
2615
|
+
throw new Error('createFromFrames: At least one frame is required');
|
|
2616
|
+
}
|
|
2617
|
+
const frameDir = path_1.default.join(process.cwd(), '.temp-frames');
|
|
2618
|
+
if (!fs_1.default.existsSync(frameDir)) {
|
|
2619
|
+
fs_1.default.mkdirSync(frameDir, { recursive: true });
|
|
2620
|
+
}
|
|
2621
|
+
const timestamp = Date.now();
|
|
2622
|
+
const fps = options.fps || 30;
|
|
2623
|
+
const format = options.format || 'mp4';
|
|
2624
|
+
const qualityPresets = {
|
|
2625
|
+
low: '-crf 28',
|
|
2626
|
+
medium: '-crf 23',
|
|
2627
|
+
high: '-crf 18',
|
|
2628
|
+
ultra: '-crf 15'
|
|
2629
|
+
};
|
|
2630
|
+
const qualityFlag = options.bitrate
|
|
2631
|
+
? `-b:v ${options.bitrate}k`
|
|
2632
|
+
: qualityPresets[options.quality || 'medium'];
|
|
2633
|
+
// Process frames: save buffers to temp files, resolve paths
|
|
2634
|
+
const framePaths = [];
|
|
2635
|
+
const tempFiles = [];
|
|
2636
|
+
const frameSequenceDir = path_1.default.join(frameDir, `frames-${timestamp}`);
|
|
2637
|
+
try {
|
|
2638
|
+
// Get first frame dimensions if resolution not specified
|
|
2639
|
+
let frameWidth;
|
|
2640
|
+
let frameHeight;
|
|
2641
|
+
if (options.resolution) {
|
|
2642
|
+
frameWidth = options.resolution.width;
|
|
2643
|
+
frameHeight = options.resolution.height;
|
|
2644
|
+
}
|
|
2645
|
+
else {
|
|
2646
|
+
// Load first frame to get dimensions
|
|
2647
|
+
const firstFrame = options.frames[0];
|
|
2648
|
+
let firstFramePath;
|
|
2649
|
+
if (Buffer.isBuffer(firstFrame)) {
|
|
2650
|
+
firstFramePath = path_1.default.join(frameDir, `frame-${timestamp}-0.png`);
|
|
2651
|
+
fs_1.default.writeFileSync(firstFramePath, firstFrame);
|
|
2652
|
+
tempFiles.push(firstFramePath);
|
|
2653
|
+
}
|
|
2654
|
+
else {
|
|
2655
|
+
let resolvedPath = firstFrame;
|
|
2656
|
+
if (!/^https?:\/\//i.test(resolvedPath)) {
|
|
2657
|
+
resolvedPath = path_1.default.join(process.cwd(), resolvedPath);
|
|
2658
|
+
}
|
|
2659
|
+
if (!fs_1.default.existsSync(resolvedPath)) {
|
|
2660
|
+
throw new Error(`Frame file not found: ${firstFrame}`);
|
|
2661
|
+
}
|
|
2662
|
+
firstFramePath = resolvedPath;
|
|
2663
|
+
}
|
|
2664
|
+
// Get dimensions using ffprobe or loadImage
|
|
2665
|
+
try {
|
|
2666
|
+
const { loadImage } = require('@napi-rs/canvas');
|
|
2667
|
+
const img = await loadImage(firstFramePath);
|
|
2668
|
+
frameWidth = img.width;
|
|
2669
|
+
frameHeight = img.height;
|
|
2670
|
+
}
|
|
2671
|
+
catch {
|
|
2672
|
+
// Fallback: try to get from ffprobe
|
|
2673
|
+
const escapedPath = firstFramePath.replace(/"/g, '\\"');
|
|
2674
|
+
try {
|
|
2675
|
+
const { stdout } = await execAsync(`ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of default=noprint_wrappers=1:nokey=1 "${escapedPath}"`, { timeout: 10000, maxBuffer: 1024 * 1024 });
|
|
2676
|
+
const [w, h] = stdout.toString().trim().split('\n').map(Number);
|
|
2677
|
+
if (w && h) {
|
|
2678
|
+
frameWidth = w;
|
|
2679
|
+
frameHeight = h;
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
catch {
|
|
2683
|
+
throw new Error('Could not determine frame dimensions. Please specify resolution.');
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
// Process all frames - save all to temp directory with sequential naming for reliable pattern matching
|
|
2688
|
+
if (!fs_1.default.existsSync(frameSequenceDir)) {
|
|
2689
|
+
fs_1.default.mkdirSync(frameSequenceDir, { recursive: true });
|
|
2690
|
+
}
|
|
2691
|
+
for (let i = 0; i < options.frames.length; i++) {
|
|
2692
|
+
const frame = options.frames[i];
|
|
2693
|
+
let frameBuffer;
|
|
2694
|
+
if (Buffer.isBuffer(frame)) {
|
|
2695
|
+
frameBuffer = frame;
|
|
2696
|
+
}
|
|
2697
|
+
else {
|
|
2698
|
+
let resolvedPath = frame;
|
|
2699
|
+
if (!/^https?:\/\//i.test(resolvedPath)) {
|
|
2700
|
+
resolvedPath = path_1.default.join(process.cwd(), resolvedPath);
|
|
2701
|
+
}
|
|
2702
|
+
if (!fs_1.default.existsSync(resolvedPath)) {
|
|
2703
|
+
throw new Error(`Frame file not found: ${frame}`);
|
|
2704
|
+
}
|
|
2705
|
+
frameBuffer = fs_1.default.readFileSync(resolvedPath);
|
|
2706
|
+
}
|
|
2707
|
+
// Save with sequential naming (frame-000000.png, frame-000001.png, etc.)
|
|
2708
|
+
const frameNumber = i.toString().padStart(6, '0');
|
|
2709
|
+
const framePath = path_1.default.join(frameSequenceDir, `frame-${frameNumber}.png`);
|
|
2710
|
+
fs_1.default.writeFileSync(framePath, frameBuffer);
|
|
2711
|
+
tempFiles.push(framePath);
|
|
2712
|
+
framePaths.push(framePath);
|
|
2713
|
+
}
|
|
2714
|
+
// Use image2 pattern input for reliable frame sequence
|
|
2715
|
+
const patternPath = path_1.default.join(frameSequenceDir, 'frame-%06d.png').replace(/\\/g, '/');
|
|
2716
|
+
const escapedPattern = patternPath.replace(/"/g, '\\"');
|
|
2717
|
+
const escapedOutputPath = options.outputPath.replace(/"/g, '\\"');
|
|
2718
|
+
const resolutionFlag = frameWidth && frameHeight
|
|
2719
|
+
? `-vf scale=${frameWidth}:${frameHeight}:force_original_aspect_ratio=decrease,pad=${frameWidth}:${frameHeight}:(ow-iw)/2:(oh-ih)/2`
|
|
2720
|
+
: '';
|
|
2721
|
+
// Use image2 demuxer with pattern for frame sequence
|
|
2722
|
+
const command = `ffmpeg -framerate ${fps} -i "${escapedPattern}" ${resolutionFlag} ${qualityFlag} -pix_fmt yuv420p -y "${escapedOutputPath}"`;
|
|
2723
|
+
await execAsync(command, {
|
|
2724
|
+
timeout: 600000, // 10 minute timeout for large frame sequences
|
|
2725
|
+
maxBuffer: 10 * 1024 * 1024
|
|
2726
|
+
});
|
|
2727
|
+
// Cleanup temp files and directory
|
|
2728
|
+
for (const tempFile of tempFiles) {
|
|
2729
|
+
if (fs_1.default.existsSync(tempFile)) {
|
|
2730
|
+
try {
|
|
2731
|
+
fs_1.default.unlinkSync(tempFile);
|
|
2732
|
+
}
|
|
2733
|
+
catch {
|
|
2734
|
+
// Ignore cleanup errors
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
// Remove frame sequence directory
|
|
2739
|
+
if (fs_1.default.existsSync(frameSequenceDir)) {
|
|
2740
|
+
try {
|
|
2741
|
+
fs_1.default.rmSync(frameSequenceDir, { recursive: true, force: true });
|
|
2742
|
+
}
|
|
2743
|
+
catch {
|
|
2744
|
+
// Ignore cleanup errors
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
return { outputPath: options.outputPath, success: true };
|
|
2748
|
+
}
|
|
2749
|
+
catch (error) {
|
|
2750
|
+
// Cleanup temp files on error
|
|
2751
|
+
for (const tempFile of tempFiles) {
|
|
2752
|
+
if (fs_1.default.existsSync(tempFile)) {
|
|
2753
|
+
try {
|
|
2754
|
+
fs_1.default.unlinkSync(tempFile);
|
|
2755
|
+
}
|
|
2756
|
+
catch {
|
|
2757
|
+
// Ignore cleanup errors
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
// Remove frame sequence directory on error
|
|
2762
|
+
if (fs_1.default.existsSync(frameSequenceDir)) {
|
|
2763
|
+
try {
|
|
2764
|
+
fs_1.default.rmSync(frameSequenceDir, { recursive: true, force: true });
|
|
2765
|
+
}
|
|
2766
|
+
catch {
|
|
2767
|
+
// Ignore cleanup errors
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
throw error;
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
3416
2773
|
/**
|
|
3417
2774
|
* Extracts a frame at a specific time in seconds
|
|
3418
2775
|
* @param videoSource - Video source (path, URL, or Buffer)
|
|
@@ -3565,7 +2922,7 @@ class ApexPainter {
|
|
|
3565
2922
|
return frames;
|
|
3566
2923
|
}
|
|
3567
2924
|
catch (error) {
|
|
3568
|
-
const errorMessage =
|
|
2925
|
+
const errorMessage = (0, utils_1.getErrorMessage)(error);
|
|
3569
2926
|
if (errorMessage.includes('FFMPEG NOT FOUND') || errorMessage.includes('FFmpeg')) {
|
|
3570
2927
|
throw error;
|
|
3571
2928
|
}
|
|
@@ -3632,8 +2989,7 @@ class ApexPainter {
|
|
|
3632
2989
|
return canvas.toBuffer("image/png");
|
|
3633
2990
|
}
|
|
3634
2991
|
catch (error) {
|
|
3635
|
-
|
|
3636
|
-
throw new Error(`masking failed: ${errorMessage}`);
|
|
2992
|
+
throw new Error(`masking failed: ${(0, utils_1.getErrorMessage)(error)}`);
|
|
3637
2993
|
}
|
|
3638
2994
|
}
|
|
3639
2995
|
/**
|
|
@@ -3702,8 +3058,7 @@ class ApexPainter {
|
|
|
3702
3058
|
return canvas.toBuffer("image/png");
|
|
3703
3059
|
}
|
|
3704
3060
|
catch (error) {
|
|
3705
|
-
|
|
3706
|
-
throw new Error(`gradientBlend failed: ${errorMessage}`);
|
|
3061
|
+
throw new Error(`gradientBlend failed: ${(0, utils_1.getErrorMessage)(error)}`);
|
|
3707
3062
|
}
|
|
3708
3063
|
}
|
|
3709
3064
|
/**
|
|
@@ -3829,8 +3184,7 @@ class ApexPainter {
|
|
|
3829
3184
|
return options?.gif ? undefined : buffers;
|
|
3830
3185
|
}
|
|
3831
3186
|
catch (error) {
|
|
3832
|
-
|
|
3833
|
-
throw new Error(`animate failed: ${errorMessage}`);
|
|
3187
|
+
throw new Error(`animate failed: ${(0, utils_1.getErrorMessage)(error)}`);
|
|
3834
3188
|
}
|
|
3835
3189
|
}
|
|
3836
3190
|
/**
|
|
@@ -3843,8 +3197,7 @@ class ApexPainter {
|
|
|
3843
3197
|
return await (0, utils_1.batchOperations)(this, operations);
|
|
3844
3198
|
}
|
|
3845
3199
|
catch (error) {
|
|
3846
|
-
|
|
3847
|
-
throw new Error(`batch failed: ${errorMessage}`);
|
|
3200
|
+
throw new Error(`batch failed: ${(0, utils_1.getErrorMessage)(error)}`);
|
|
3848
3201
|
}
|
|
3849
3202
|
}
|
|
3850
3203
|
/**
|
|
@@ -3857,8 +3210,7 @@ class ApexPainter {
|
|
|
3857
3210
|
return await (0, utils_1.chainOperations)(this, operations);
|
|
3858
3211
|
}
|
|
3859
3212
|
catch (error) {
|
|
3860
|
-
|
|
3861
|
-
throw new Error(`chain failed: ${errorMessage}`);
|
|
3213
|
+
throw new Error(`chain failed: ${(0, utils_1.getErrorMessage)(error)}`);
|
|
3862
3214
|
}
|
|
3863
3215
|
}
|
|
3864
3216
|
/**
|
|
@@ -3875,8 +3227,7 @@ class ApexPainter {
|
|
|
3875
3227
|
return await (0, utils_1.stitchImages)(images, options);
|
|
3876
3228
|
}
|
|
3877
3229
|
catch (error) {
|
|
3878
|
-
|
|
3879
|
-
throw new Error(`stitchImages failed: ${errorMessage}`);
|
|
3230
|
+
throw new Error(`stitchImages failed: ${(0, utils_1.getErrorMessage)(error)}`);
|
|
3880
3231
|
}
|
|
3881
3232
|
}
|
|
3882
3233
|
/**
|
|
@@ -3896,8 +3247,7 @@ class ApexPainter {
|
|
|
3896
3247
|
return await (0, utils_1.createCollage)(images, layout);
|
|
3897
3248
|
}
|
|
3898
3249
|
catch (error) {
|
|
3899
|
-
|
|
3900
|
-
throw new Error(`createCollage failed: ${errorMessage}`);
|
|
3250
|
+
throw new Error(`createCollage failed: ${(0, utils_1.getErrorMessage)(error)}`);
|
|
3901
3251
|
}
|
|
3902
3252
|
}
|
|
3903
3253
|
/**
|
|
@@ -3914,8 +3264,7 @@ class ApexPainter {
|
|
|
3914
3264
|
return await (0, utils_1.compressImage)(image, options);
|
|
3915
3265
|
}
|
|
3916
3266
|
catch (error) {
|
|
3917
|
-
|
|
3918
|
-
throw new Error(`compress failed: ${errorMessage}`);
|
|
3267
|
+
throw new Error(`compress failed: ${(0, utils_1.getErrorMessage)(error)}`);
|
|
3919
3268
|
}
|
|
3920
3269
|
}
|
|
3921
3270
|
/**
|
|
@@ -3932,8 +3281,7 @@ class ApexPainter {
|
|
|
3932
3281
|
return await (0, utils_1.extractPalette)(image, options);
|
|
3933
3282
|
}
|
|
3934
3283
|
catch (error) {
|
|
3935
|
-
|
|
3936
|
-
throw new Error(`extractPalette failed: ${errorMessage}`);
|
|
3284
|
+
throw new Error(`extractPalette failed: ${(0, utils_1.getErrorMessage)(error)}`);
|
|
3937
3285
|
}
|
|
3938
3286
|
}
|
|
3939
3287
|
/**
|
|
@@ -3996,8 +3344,7 @@ class ApexPainter {
|
|
|
3996
3344
|
}
|
|
3997
3345
|
}
|
|
3998
3346
|
catch (error) {
|
|
3999
|
-
|
|
4000
|
-
throw new Error(`outPut failed: ${errorMessage}`);
|
|
3347
|
+
throw new Error(`outPut failed: ${(0, utils_1.getErrorMessage)(error)}`);
|
|
4001
3348
|
}
|
|
4002
3349
|
}
|
|
4003
3350
|
/**
|
|
@@ -4145,8 +3492,7 @@ class ApexPainter {
|
|
|
4145
3492
|
};
|
|
4146
3493
|
}
|
|
4147
3494
|
catch (error) {
|
|
4148
|
-
|
|
4149
|
-
throw new Error(`save failed: ${errorMessage}`);
|
|
3495
|
+
throw new Error(`save failed: ${(0, utils_1.getErrorMessage)(error)}`);
|
|
4150
3496
|
}
|
|
4151
3497
|
}
|
|
4152
3498
|
/**
|
|
@@ -4185,174 +3531,24 @@ class ApexPainter {
|
|
|
4185
3531
|
return results;
|
|
4186
3532
|
}
|
|
4187
3533
|
catch (error) {
|
|
4188
|
-
|
|
4189
|
-
throw new Error(`saveMultiple failed: ${errorMessage}`);
|
|
3534
|
+
throw new Error(`saveMultiple failed: ${(0, utils_1.getErrorMessage)(error)}`);
|
|
4190
3535
|
}
|
|
4191
3536
|
}
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
*/
|
|
4195
|
-
resetSaveCounter() {
|
|
4196
|
-
this.saveCounter = 1;
|
|
4197
|
-
}
|
|
4198
|
-
/**
|
|
4199
|
-
* Applies stroke style to shape context
|
|
4200
|
-
* @private
|
|
4201
|
-
* @param ctx - Canvas 2D context
|
|
4202
|
-
* @param style - Stroke style type
|
|
4203
|
-
* @param width - Stroke width for calculating dash patterns
|
|
4204
|
-
*/
|
|
4205
|
-
#applyShapeStrokeStyle(ctx, style, width) {
|
|
4206
|
-
switch (style) {
|
|
4207
|
-
case 'solid':
|
|
4208
|
-
ctx.setLineDash([]);
|
|
4209
|
-
ctx.lineCap = 'butt';
|
|
4210
|
-
ctx.lineJoin = 'miter';
|
|
4211
|
-
break;
|
|
4212
|
-
case 'dashed':
|
|
4213
|
-
ctx.setLineDash([width * 3, width * 2]);
|
|
4214
|
-
ctx.lineCap = 'butt';
|
|
4215
|
-
ctx.lineJoin = 'miter';
|
|
4216
|
-
break;
|
|
4217
|
-
case 'dotted':
|
|
4218
|
-
ctx.setLineDash([width, width]);
|
|
4219
|
-
ctx.lineCap = 'round';
|
|
4220
|
-
ctx.lineJoin = 'round';
|
|
4221
|
-
break;
|
|
4222
|
-
case 'groove':
|
|
4223
|
-
case 'ridge':
|
|
4224
|
-
case 'double':
|
|
4225
|
-
ctx.setLineDash([]);
|
|
4226
|
-
ctx.lineCap = 'butt';
|
|
4227
|
-
ctx.lineJoin = 'miter';
|
|
4228
|
-
break;
|
|
4229
|
-
default:
|
|
4230
|
-
ctx.setLineDash([]);
|
|
4231
|
-
ctx.lineCap = 'butt';
|
|
4232
|
-
ctx.lineJoin = 'miter';
|
|
4233
|
-
break;
|
|
4234
|
-
}
|
|
4235
|
-
}
|
|
4236
|
-
/**
|
|
4237
|
-
* Applies complex shape stroke styles that require multiple passes
|
|
4238
|
-
* @private
|
|
4239
|
-
* @param ctx - Canvas 2D context
|
|
4240
|
-
* @param style - Complex stroke style type
|
|
4241
|
-
* @param width - Stroke width
|
|
4242
|
-
* @param color - Base stroke color
|
|
4243
|
-
* @param gradient - Optional gradient
|
|
4244
|
-
*/
|
|
4245
|
-
#applyComplexShapeStroke(ctx, style, width, color, gradient) {
|
|
4246
|
-
const halfWidth = width / 2;
|
|
4247
|
-
switch (style) {
|
|
4248
|
-
case 'groove':
|
|
4249
|
-
// Groove: dark outer, light inner
|
|
4250
|
-
ctx.lineWidth = halfWidth;
|
|
4251
|
-
// Outer dark stroke
|
|
4252
|
-
if (gradient) {
|
|
4253
|
-
const gstroke = (0, utils_1.createGradientFill)(ctx, gradient, { x: 0, y: 0, w: 100, h: 100 });
|
|
4254
|
-
ctx.strokeStyle = gstroke;
|
|
4255
|
-
}
|
|
4256
|
-
else {
|
|
4257
|
-
ctx.strokeStyle = this.#darkenColor(color, 0.3);
|
|
4258
|
-
}
|
|
4259
|
-
ctx.stroke();
|
|
4260
|
-
// Inner light stroke
|
|
4261
|
-
ctx.lineWidth = halfWidth;
|
|
4262
|
-
if (gradient) {
|
|
4263
|
-
const gstroke = (0, utils_1.createGradientFill)(ctx, gradient, { x: 0, y: 0, w: 100, h: 100 });
|
|
4264
|
-
ctx.strokeStyle = gstroke;
|
|
4265
|
-
}
|
|
4266
|
-
else {
|
|
4267
|
-
ctx.strokeStyle = this.#lightenColor(color, 0.3);
|
|
4268
|
-
}
|
|
4269
|
-
ctx.stroke();
|
|
4270
|
-
break;
|
|
4271
|
-
case 'ridge':
|
|
4272
|
-
// Ridge: light outer, dark inner
|
|
4273
|
-
ctx.lineWidth = halfWidth;
|
|
4274
|
-
// Outer light stroke
|
|
4275
|
-
if (gradient) {
|
|
4276
|
-
const gstroke = (0, utils_1.createGradientFill)(ctx, gradient, { x: 0, y: 0, w: 100, h: 100 });
|
|
4277
|
-
ctx.strokeStyle = gstroke;
|
|
4278
|
-
}
|
|
4279
|
-
else {
|
|
4280
|
-
ctx.strokeStyle = this.#lightenColor(color, 0.3);
|
|
4281
|
-
}
|
|
4282
|
-
ctx.stroke();
|
|
4283
|
-
// Inner dark stroke
|
|
4284
|
-
ctx.lineWidth = halfWidth;
|
|
4285
|
-
if (gradient) {
|
|
4286
|
-
const gstroke = (0, utils_1.createGradientFill)(ctx, gradient, { x: 0, y: 0, w: 100, h: 100 });
|
|
4287
|
-
ctx.strokeStyle = gstroke;
|
|
4288
|
-
}
|
|
4289
|
-
else {
|
|
4290
|
-
ctx.strokeStyle = this.#darkenColor(color, 0.3);
|
|
4291
|
-
}
|
|
4292
|
-
ctx.stroke();
|
|
4293
|
-
break;
|
|
4294
|
-
case 'double':
|
|
4295
|
-
// Double: two parallel strokes
|
|
4296
|
-
ctx.lineWidth = halfWidth;
|
|
4297
|
-
// First stroke (outer)
|
|
4298
|
-
if (gradient) {
|
|
4299
|
-
const gstroke = (0, utils_1.createGradientFill)(ctx, gradient, { x: 0, y: 0, w: 100, h: 100 });
|
|
4300
|
-
ctx.strokeStyle = gstroke;
|
|
4301
|
-
}
|
|
4302
|
-
else {
|
|
4303
|
-
ctx.strokeStyle = color;
|
|
4304
|
-
}
|
|
4305
|
-
ctx.stroke();
|
|
4306
|
-
// Second stroke (inner)
|
|
4307
|
-
ctx.lineWidth = halfWidth;
|
|
4308
|
-
if (gradient) {
|
|
4309
|
-
const gstroke = (0, utils_1.createGradientFill)(ctx, gradient, { x: 0, y: 0, w: 100, h: 100 });
|
|
4310
|
-
ctx.strokeStyle = gstroke;
|
|
4311
|
-
}
|
|
4312
|
-
else {
|
|
4313
|
-
ctx.strokeStyle = color;
|
|
4314
|
-
}
|
|
4315
|
-
ctx.stroke();
|
|
4316
|
-
break;
|
|
4317
|
-
}
|
|
3537
|
+
async createChart(chartType, data, options = {}) {
|
|
3538
|
+
return await this.chartCreator.createChart(chartType, data, options);
|
|
4318
3539
|
}
|
|
4319
3540
|
/**
|
|
4320
|
-
*
|
|
4321
|
-
*
|
|
4322
|
-
*
|
|
4323
|
-
* @param
|
|
4324
|
-
* @returns
|
|
3541
|
+
* Creates a comparison chart with two charts side by side or top/bottom.
|
|
3542
|
+
* Each chart can be of any type (pie, bar, horizontalBar, line, donut) with its own data and config.
|
|
3543
|
+
*
|
|
3544
|
+
* @param options - Comparison chart configuration
|
|
3545
|
+
* @returns Promise<Buffer> - Comparison chart image buffer
|
|
4325
3546
|
*/
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
if (color.startsWith('#')) {
|
|
4329
|
-
const hex = color.slice(1);
|
|
4330
|
-
const num = parseInt(hex, 16);
|
|
4331
|
-
const r = Math.max(0, Math.floor((num >> 16) * (1 - factor)));
|
|
4332
|
-
const g = Math.max(0, Math.floor(((num >> 8) & 0x00FF) * (1 - factor)));
|
|
4333
|
-
const b = Math.max(0, Math.floor((num & 0x0000FF) * (1 - factor)));
|
|
4334
|
-
return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`;
|
|
4335
|
-
}
|
|
4336
|
-
return color; // Return original for non-hex colors
|
|
3547
|
+
async createComparisonChart(options) {
|
|
3548
|
+
return this.chartCreator.createComparisonChart(options);
|
|
4337
3549
|
}
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
* @private
|
|
4341
|
-
* @param color - Color string
|
|
4342
|
-
* @param factor - Lightening factor (0-1)
|
|
4343
|
-
* @returns Lightened color string
|
|
4344
|
-
*/
|
|
4345
|
-
#lightenColor(color, factor) {
|
|
4346
|
-
// Simple lightening for hex colors
|
|
4347
|
-
if (color.startsWith('#')) {
|
|
4348
|
-
const hex = color.slice(1);
|
|
4349
|
-
const num = parseInt(hex, 16);
|
|
4350
|
-
const r = Math.min(255, Math.floor((num >> 16) + (255 - (num >> 16)) * factor));
|
|
4351
|
-
const g = Math.min(255, Math.floor(((num >> 8) & 0x00FF) + (255 - ((num >> 8) & 0x00FF)) * factor));
|
|
4352
|
-
const b = Math.min(255, Math.floor((num & 0x0000FF) + (255 - (num & 0x0000FF)) * factor));
|
|
4353
|
-
return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`;
|
|
4354
|
-
}
|
|
4355
|
-
return color; // Return original for non-hex colors
|
|
3550
|
+
resetSaveCounter() {
|
|
3551
|
+
this.saveCounter = 1;
|
|
4356
3552
|
}
|
|
4357
3553
|
}
|
|
4358
3554
|
exports.ApexPainter = ApexPainter;
|