apexify.js 5.1.1 → 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.
Files changed (236) hide show
  1. package/CHANGELOG.md +240 -0
  2. package/README.md +248 -1105
  3. package/dist/cjs/Canvas/ApexPainter.d.ts +182 -204
  4. package/dist/cjs/Canvas/ApexPainter.d.ts.map +1 -1
  5. package/dist/cjs/Canvas/ApexPainter.js +482 -1286
  6. package/dist/cjs/Canvas/ApexPainter.js.map +1 -1
  7. package/dist/cjs/Canvas/extended/CanvasCreator.d.ts +33 -0
  8. package/dist/cjs/Canvas/extended/CanvasCreator.d.ts.map +1 -0
  9. package/dist/cjs/Canvas/extended/CanvasCreator.js +223 -0
  10. package/dist/cjs/Canvas/extended/CanvasCreator.js.map +1 -0
  11. package/dist/cjs/Canvas/extended/ChartCreator.d.ts +26 -0
  12. package/dist/cjs/Canvas/extended/ChartCreator.d.ts.map +1 -0
  13. package/dist/cjs/Canvas/extended/ChartCreator.js +50 -0
  14. package/dist/cjs/Canvas/extended/ChartCreator.js.map +1 -0
  15. package/dist/cjs/Canvas/extended/GIFCreator.d.ts +43 -0
  16. package/dist/cjs/Canvas/extended/GIFCreator.d.ts.map +1 -0
  17. package/dist/cjs/Canvas/extended/GIFCreator.js +157 -0
  18. package/dist/cjs/Canvas/extended/GIFCreator.js.map +1 -0
  19. package/dist/cjs/Canvas/extended/ImageCreator.d.ts +83 -0
  20. package/dist/cjs/Canvas/extended/ImageCreator.d.ts.map +1 -0
  21. package/dist/cjs/Canvas/extended/ImageCreator.js +479 -0
  22. package/dist/cjs/Canvas/extended/ImageCreator.js.map +1 -0
  23. package/dist/cjs/Canvas/extended/TextCreator.d.ts +35 -0
  24. package/dist/cjs/Canvas/extended/TextCreator.d.ts.map +1 -0
  25. package/dist/cjs/Canvas/extended/TextCreator.js +98 -0
  26. package/dist/cjs/Canvas/extended/TextCreator.js.map +1 -0
  27. package/dist/cjs/Canvas/extended/VideoCreator.d.ts +370 -0
  28. package/dist/cjs/Canvas/extended/VideoCreator.d.ts.map +1 -0
  29. package/dist/cjs/Canvas/extended/VideoCreator.js +478 -0
  30. package/dist/cjs/Canvas/extended/VideoCreator.js.map +1 -0
  31. package/dist/cjs/Canvas/utils/Background/bg.d.ts +1 -1
  32. package/dist/cjs/Canvas/utils/Background/bg.d.ts.map +1 -1
  33. package/dist/cjs/Canvas/utils/Background/bg.js +43 -7
  34. package/dist/cjs/Canvas/utils/Background/bg.js.map +1 -1
  35. package/dist/cjs/Canvas/utils/Charts/barchart.d.ts +230 -0
  36. package/dist/cjs/Canvas/utils/Charts/barchart.d.ts.map +1 -0
  37. package/dist/cjs/Canvas/utils/Charts/barchart.js +1891 -0
  38. package/dist/cjs/Canvas/utils/Charts/barchart.js.map +1 -0
  39. package/dist/cjs/Canvas/utils/Charts/comparisonchart.d.ts +103 -0
  40. package/dist/cjs/Canvas/utils/Charts/comparisonchart.d.ts.map +1 -0
  41. package/dist/cjs/Canvas/utils/Charts/comparisonchart.js +368 -0
  42. package/dist/cjs/Canvas/utils/Charts/comparisonchart.js.map +1 -0
  43. package/dist/cjs/Canvas/utils/Charts/horizontalbarchart.d.ts +178 -0
  44. package/dist/cjs/Canvas/utils/Charts/horizontalbarchart.d.ts.map +1 -0
  45. package/dist/cjs/Canvas/utils/Charts/horizontalbarchart.js +1389 -0
  46. package/dist/cjs/Canvas/utils/Charts/horizontalbarchart.js.map +1 -0
  47. package/dist/cjs/Canvas/utils/Charts/index.d.ts +45 -0
  48. package/dist/cjs/Canvas/utils/Charts/index.d.ts.map +1 -0
  49. package/dist/cjs/Canvas/utils/Charts/index.js +17 -0
  50. package/dist/cjs/Canvas/utils/Charts/index.js.map +1 -0
  51. package/dist/cjs/Canvas/utils/Charts/linechart.d.ts +216 -0
  52. package/dist/cjs/Canvas/utils/Charts/linechart.d.ts.map +1 -0
  53. package/dist/cjs/Canvas/utils/Charts/linechart.js +1761 -0
  54. package/dist/cjs/Canvas/utils/Charts/linechart.js.map +1 -0
  55. package/dist/cjs/Canvas/utils/Charts/piechart.d.ts +167 -0
  56. package/dist/cjs/Canvas/utils/Charts/piechart.d.ts.map +1 -0
  57. package/dist/cjs/Canvas/utils/Charts/piechart.js +794 -0
  58. package/dist/cjs/Canvas/utils/Charts/piechart.js.map +1 -0
  59. package/dist/cjs/Canvas/utils/General/batchOperations.d.ts.map +1 -1
  60. package/dist/cjs/Canvas/utils/General/batchOperations.js +3 -4
  61. package/dist/cjs/Canvas/utils/General/batchOperations.js.map +1 -1
  62. package/dist/cjs/Canvas/utils/General/general functions.d.ts.map +1 -1
  63. package/dist/cjs/Canvas/utils/General/general functions.js +62 -33
  64. package/dist/cjs/Canvas/utils/General/general functions.js.map +1 -1
  65. package/dist/cjs/Canvas/utils/General/imageStitching.d.ts.map +1 -1
  66. package/dist/cjs/Canvas/utils/General/imageStitching.js +3 -6
  67. package/dist/cjs/Canvas/utils/General/imageStitching.js.map +1 -1
  68. package/dist/cjs/Canvas/utils/Image/imageMasking.d.ts.map +1 -1
  69. package/dist/cjs/Canvas/utils/Image/imageMasking.js +5 -12
  70. package/dist/cjs/Canvas/utils/Image/imageMasking.js.map +1 -1
  71. package/dist/cjs/Canvas/utils/Image/imageProperties.d.ts +4 -4
  72. package/dist/cjs/Canvas/utils/Image/imageProperties.d.ts.map +1 -1
  73. package/dist/cjs/Canvas/utils/Image/imageProperties.js +44 -9
  74. package/dist/cjs/Canvas/utils/Image/imageProperties.js.map +1 -1
  75. package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.d.ts +5 -0
  76. package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.d.ts.map +1 -1
  77. package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.js +48 -5
  78. package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.js.map +1 -1
  79. package/dist/cjs/Canvas/utils/Texts/textProperties.d.ts +1 -1
  80. package/dist/cjs/Canvas/utils/Texts/textProperties.d.ts.map +1 -1
  81. package/dist/cjs/Canvas/utils/Texts/textProperties.js +48 -5
  82. package/dist/cjs/Canvas/utils/Texts/textProperties.js.map +1 -1
  83. package/dist/cjs/Canvas/utils/Video/videoHelpers.d.ts +489 -0
  84. package/dist/cjs/Canvas/utils/Video/videoHelpers.d.ts.map +1 -0
  85. package/dist/cjs/Canvas/utils/Video/videoHelpers.js +1835 -0
  86. package/dist/cjs/Canvas/utils/Video/videoHelpers.js.map +1 -0
  87. package/dist/cjs/Canvas/utils/errorUtils.d.ts +15 -0
  88. package/dist/cjs/Canvas/utils/errorUtils.d.ts.map +1 -0
  89. package/dist/cjs/Canvas/utils/errorUtils.js +26 -0
  90. package/dist/cjs/Canvas/utils/errorUtils.js.map +1 -0
  91. package/dist/cjs/Canvas/utils/types.d.ts +17 -178
  92. package/dist/cjs/Canvas/utils/types.d.ts.map +1 -1
  93. package/dist/cjs/Canvas/utils/types.js.map +1 -1
  94. package/dist/cjs/Canvas/utils/utils.d.ts +4 -3
  95. package/dist/cjs/Canvas/utils/utils.d.ts.map +1 -1
  96. package/dist/cjs/Canvas/utils/utils.js +40 -6
  97. package/dist/cjs/Canvas/utils/utils.js.map +1 -1
  98. package/dist/cjs/index.d.ts +1 -8
  99. package/dist/cjs/index.d.ts.map +1 -1
  100. package/dist/cjs/index.js +14 -45
  101. package/dist/cjs/index.js.map +1 -1
  102. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  103. package/dist/esm/Canvas/ApexPainter.d.ts +182 -204
  104. package/dist/esm/Canvas/ApexPainter.d.ts.map +1 -1
  105. package/dist/esm/Canvas/ApexPainter.js +482 -1286
  106. package/dist/esm/Canvas/ApexPainter.js.map +1 -1
  107. package/dist/esm/Canvas/extended/CanvasCreator.d.ts +33 -0
  108. package/dist/esm/Canvas/extended/CanvasCreator.d.ts.map +1 -0
  109. package/dist/esm/Canvas/extended/CanvasCreator.js +223 -0
  110. package/dist/esm/Canvas/extended/CanvasCreator.js.map +1 -0
  111. package/dist/esm/Canvas/extended/ChartCreator.d.ts +26 -0
  112. package/dist/esm/Canvas/extended/ChartCreator.d.ts.map +1 -0
  113. package/dist/esm/Canvas/extended/ChartCreator.js +50 -0
  114. package/dist/esm/Canvas/extended/ChartCreator.js.map +1 -0
  115. package/dist/esm/Canvas/extended/GIFCreator.d.ts +43 -0
  116. package/dist/esm/Canvas/extended/GIFCreator.d.ts.map +1 -0
  117. package/dist/esm/Canvas/extended/GIFCreator.js +157 -0
  118. package/dist/esm/Canvas/extended/GIFCreator.js.map +1 -0
  119. package/dist/esm/Canvas/extended/ImageCreator.d.ts +83 -0
  120. package/dist/esm/Canvas/extended/ImageCreator.d.ts.map +1 -0
  121. package/dist/esm/Canvas/extended/ImageCreator.js +479 -0
  122. package/dist/esm/Canvas/extended/ImageCreator.js.map +1 -0
  123. package/dist/esm/Canvas/extended/TextCreator.d.ts +35 -0
  124. package/dist/esm/Canvas/extended/TextCreator.d.ts.map +1 -0
  125. package/dist/esm/Canvas/extended/TextCreator.js +98 -0
  126. package/dist/esm/Canvas/extended/TextCreator.js.map +1 -0
  127. package/dist/esm/Canvas/extended/VideoCreator.d.ts +370 -0
  128. package/dist/esm/Canvas/extended/VideoCreator.d.ts.map +1 -0
  129. package/dist/esm/Canvas/extended/VideoCreator.js +478 -0
  130. package/dist/esm/Canvas/extended/VideoCreator.js.map +1 -0
  131. package/dist/esm/Canvas/utils/Background/bg.d.ts +1 -1
  132. package/dist/esm/Canvas/utils/Background/bg.d.ts.map +1 -1
  133. package/dist/esm/Canvas/utils/Background/bg.js +43 -7
  134. package/dist/esm/Canvas/utils/Background/bg.js.map +1 -1
  135. package/dist/esm/Canvas/utils/Charts/barchart.d.ts +230 -0
  136. package/dist/esm/Canvas/utils/Charts/barchart.d.ts.map +1 -0
  137. package/dist/esm/Canvas/utils/Charts/barchart.js +1891 -0
  138. package/dist/esm/Canvas/utils/Charts/barchart.js.map +1 -0
  139. package/dist/esm/Canvas/utils/Charts/comparisonchart.d.ts +103 -0
  140. package/dist/esm/Canvas/utils/Charts/comparisonchart.d.ts.map +1 -0
  141. package/dist/esm/Canvas/utils/Charts/comparisonchart.js +368 -0
  142. package/dist/esm/Canvas/utils/Charts/comparisonchart.js.map +1 -0
  143. package/dist/esm/Canvas/utils/Charts/horizontalbarchart.d.ts +178 -0
  144. package/dist/esm/Canvas/utils/Charts/horizontalbarchart.d.ts.map +1 -0
  145. package/dist/esm/Canvas/utils/Charts/horizontalbarchart.js +1389 -0
  146. package/dist/esm/Canvas/utils/Charts/horizontalbarchart.js.map +1 -0
  147. package/dist/esm/Canvas/utils/Charts/index.d.ts +45 -0
  148. package/dist/esm/Canvas/utils/Charts/index.d.ts.map +1 -0
  149. package/dist/esm/Canvas/utils/Charts/index.js +17 -0
  150. package/dist/esm/Canvas/utils/Charts/index.js.map +1 -0
  151. package/dist/esm/Canvas/utils/Charts/linechart.d.ts +216 -0
  152. package/dist/esm/Canvas/utils/Charts/linechart.d.ts.map +1 -0
  153. package/dist/esm/Canvas/utils/Charts/linechart.js +1761 -0
  154. package/dist/esm/Canvas/utils/Charts/linechart.js.map +1 -0
  155. package/dist/esm/Canvas/utils/Charts/piechart.d.ts +167 -0
  156. package/dist/esm/Canvas/utils/Charts/piechart.d.ts.map +1 -0
  157. package/dist/esm/Canvas/utils/Charts/piechart.js +794 -0
  158. package/dist/esm/Canvas/utils/Charts/piechart.js.map +1 -0
  159. package/dist/esm/Canvas/utils/General/batchOperations.d.ts.map +1 -1
  160. package/dist/esm/Canvas/utils/General/batchOperations.js +3 -4
  161. package/dist/esm/Canvas/utils/General/batchOperations.js.map +1 -1
  162. package/dist/esm/Canvas/utils/General/general functions.d.ts.map +1 -1
  163. package/dist/esm/Canvas/utils/General/general functions.js +62 -33
  164. package/dist/esm/Canvas/utils/General/general functions.js.map +1 -1
  165. package/dist/esm/Canvas/utils/General/imageStitching.d.ts.map +1 -1
  166. package/dist/esm/Canvas/utils/General/imageStitching.js +3 -6
  167. package/dist/esm/Canvas/utils/General/imageStitching.js.map +1 -1
  168. package/dist/esm/Canvas/utils/Image/imageMasking.d.ts.map +1 -1
  169. package/dist/esm/Canvas/utils/Image/imageMasking.js +5 -12
  170. package/dist/esm/Canvas/utils/Image/imageMasking.js.map +1 -1
  171. package/dist/esm/Canvas/utils/Image/imageProperties.d.ts +4 -4
  172. package/dist/esm/Canvas/utils/Image/imageProperties.d.ts.map +1 -1
  173. package/dist/esm/Canvas/utils/Image/imageProperties.js +44 -9
  174. package/dist/esm/Canvas/utils/Image/imageProperties.js.map +1 -1
  175. package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.d.ts +5 -0
  176. package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.d.ts.map +1 -1
  177. package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.js +48 -5
  178. package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.js.map +1 -1
  179. package/dist/esm/Canvas/utils/Texts/textProperties.d.ts +1 -1
  180. package/dist/esm/Canvas/utils/Texts/textProperties.d.ts.map +1 -1
  181. package/dist/esm/Canvas/utils/Texts/textProperties.js +48 -5
  182. package/dist/esm/Canvas/utils/Texts/textProperties.js.map +1 -1
  183. package/dist/esm/Canvas/utils/Video/videoHelpers.d.ts +489 -0
  184. package/dist/esm/Canvas/utils/Video/videoHelpers.d.ts.map +1 -0
  185. package/dist/esm/Canvas/utils/Video/videoHelpers.js +1835 -0
  186. package/dist/esm/Canvas/utils/Video/videoHelpers.js.map +1 -0
  187. package/dist/esm/Canvas/utils/errorUtils.d.ts +15 -0
  188. package/dist/esm/Canvas/utils/errorUtils.d.ts.map +1 -0
  189. package/dist/esm/Canvas/utils/errorUtils.js +26 -0
  190. package/dist/esm/Canvas/utils/errorUtils.js.map +1 -0
  191. package/dist/esm/Canvas/utils/types.d.ts +17 -178
  192. package/dist/esm/Canvas/utils/types.d.ts.map +1 -1
  193. package/dist/esm/Canvas/utils/types.js.map +1 -1
  194. package/dist/esm/Canvas/utils/utils.d.ts +4 -3
  195. package/dist/esm/Canvas/utils/utils.d.ts.map +1 -1
  196. package/dist/esm/Canvas/utils/utils.js +40 -6
  197. package/dist/esm/Canvas/utils/utils.js.map +1 -1
  198. package/dist/esm/index.d.ts +1 -8
  199. package/dist/esm/index.d.ts.map +1 -1
  200. package/dist/esm/index.js +14 -45
  201. package/dist/esm/index.js.map +1 -1
  202. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  203. package/package.json +234 -198
  204. package/dist/cjs/Canvas/utils/Charts/charts.d.ts +0 -13
  205. package/dist/cjs/Canvas/utils/Charts/charts.d.ts.map +0 -1
  206. package/dist/cjs/Canvas/utils/Charts/charts.js +0 -466
  207. package/dist/cjs/Canvas/utils/Charts/charts.js.map +0 -1
  208. package/dist/esm/Canvas/utils/Charts/charts.d.ts +0 -13
  209. package/dist/esm/Canvas/utils/Charts/charts.d.ts.map +0 -1
  210. package/dist/esm/Canvas/utils/Charts/charts.js +0 -466
  211. package/dist/esm/Canvas/utils/Charts/charts.js.map +0 -1
  212. package/lib/Canvas/ApexPainter.ts +0 -5414
  213. package/lib/Canvas/utils/Background/bg.ts +0 -285
  214. package/lib/Canvas/utils/Charts/charts.ts +0 -548
  215. package/lib/Canvas/utils/Custom/advancedLines.ts +0 -387
  216. package/lib/Canvas/utils/Custom/customLines.ts +0 -206
  217. package/lib/Canvas/utils/General/batchOperations.ts +0 -103
  218. package/lib/Canvas/utils/General/conversion.ts +0 -34
  219. package/lib/Canvas/utils/General/general functions.ts +0 -726
  220. package/lib/Canvas/utils/General/imageCompression.ts +0 -316
  221. package/lib/Canvas/utils/General/imageStitching.ts +0 -252
  222. package/lib/Canvas/utils/Image/imageEffects.ts +0 -175
  223. package/lib/Canvas/utils/Image/imageFilters.ts +0 -356
  224. package/lib/Canvas/utils/Image/imageMasking.ts +0 -335
  225. package/lib/Canvas/utils/Image/imageProperties.ts +0 -587
  226. package/lib/Canvas/utils/Image/professionalImageFilters.ts +0 -391
  227. package/lib/Canvas/utils/Image/simpleProfessionalFilters.ts +0 -229
  228. package/lib/Canvas/utils/Patterns/enhancedPatternRenderer.ts +0 -455
  229. package/lib/Canvas/utils/Shapes/shapes.ts +0 -528
  230. package/lib/Canvas/utils/Texts/enhancedTextRenderer.ts +0 -716
  231. package/lib/Canvas/utils/Texts/textPathRenderer.ts +0 -320
  232. package/lib/Canvas/utils/Texts/textProperties.ts +0 -231
  233. package/lib/Canvas/utils/types.ts +0 -983
  234. package/lib/Canvas/utils/utils.ts +0 -135
  235. package/lib/index.ts +0 -81
  236. 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 enhancedTextRenderer_1 = require("./utils/Texts/enhancedTextRenderer");
18
- const enhancedPatternRenderer_1 = require("./utils/Patterns/enhancedPatternRenderer");
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
- * Validates image properties for required fields.
27
- * @private
28
- * @param ip - Image properties to validate
29
- */
30
- #validateImageProperties(ip) {
31
- if (!ip.source || ip.x == null || ip.y == null) {
32
- throw new Error("createImage: source, x, and y are required.");
33
- }
34
- }
35
- /**
36
- * Validates text properties for required fields.
37
- * @private
38
- * @param textProps - Text properties to validate
39
- */
40
- #validateTextProperties(textProps) {
41
- if (!textProps.text || textProps.x == null || textProps.y == null) {
42
- throw new Error("createText: text, x, and y are required.");
43
- }
44
- }
45
- /**
46
- * Renders enhanced text using the new text renderer.
47
- * @private
48
- * @param ctx - Canvas 2D context
49
- * @param textProps - Text properties
50
- */
51
- async #renderEnhancedText(ctx, textProps) {
52
- // Check if text should be rendered on a path
53
- if (textProps.path && textProps.textOnPath) {
54
- (0, utils_1.renderTextOnPath)(ctx, textProps.text, textProps.path, textProps.path.offset ?? 0);
55
- }
56
- else {
57
- await enhancedTextRenderer_1.EnhancedTextRenderer.renderText(ctx, textProps);
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
- try {
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
- try {
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
- try {
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
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
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
- try {
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
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
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
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
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
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
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
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
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
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
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
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
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
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
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 = canvas.getContext('2d');
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
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
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
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
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 = error instanceof Error ? error.message : 'Unknown error occurred';
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 = error instanceof Error ? error.message : 'Unknown error occurred';
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 = error instanceof Error ? error.message : 'Unknown error occurred';
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
- try {
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 = canvas.getContext('2d');
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
- const command = `ffmpeg -i "${escapedVideoPath}" -c copy -an -y "${escapedOutputPath}"`;
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 = error instanceof Error ? error.message : 'Unknown error occurred';
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
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
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
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
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
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
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
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
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
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
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
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
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
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
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
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
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
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
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
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
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
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
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
- const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
4189
- throw new Error(`saveMultiple failed: ${errorMessage}`);
3534
+ throw new Error(`saveMultiple failed: ${(0, utils_1.getErrorMessage)(error)}`);
4190
3535
  }
4191
3536
  }
4192
- /**
4193
- * Reset the save counter (useful when using 'counter' naming).
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
- * Darkens a color by a factor
4321
- * @private
4322
- * @param color - Color string
4323
- * @param factor - Darkening factor (0-1)
4324
- * @returns Darkened color string
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
- #darkenColor(color, factor) {
4327
- // Simple darkening for hex colors
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
- * Lightens a color by a factor
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;