mediaguru 1.0.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 (48) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/CODE_OF_CONDUCT.md +41 -0
  3. package/CONTRIBUTING.md +50 -0
  4. package/LICENSE +21 -0
  5. package/README.md +193 -0
  6. package/RELEASE.md +38 -0
  7. package/SECURITY.md +24 -0
  8. package/bin/mediaguru.js +2 -0
  9. package/dist/cli/interactive.d.ts +1 -0
  10. package/dist/cli/interactive.js +647 -0
  11. package/dist/core/batch/index.d.ts +16 -0
  12. package/dist/core/batch/index.js +66 -0
  13. package/dist/core/compress/index.d.ts +17 -0
  14. package/dist/core/compress/index.js +96 -0
  15. package/dist/core/config/index.d.ts +14 -0
  16. package/dist/core/config/index.js +56 -0
  17. package/dist/core/export/index.d.ts +12 -0
  18. package/dist/core/export/index.js +82 -0
  19. package/dist/core/image/index.d.ts +44 -0
  20. package/dist/core/image/index.js +206 -0
  21. package/dist/core/ocr/index.d.ts +14 -0
  22. package/dist/core/ocr/index.js +53 -0
  23. package/dist/core/pdf/index.d.ts +34 -0
  24. package/dist/core/pdf/index.js +121 -0
  25. package/dist/core/qr/index.d.ts +12 -0
  26. package/dist/core/qr/index.js +37 -0
  27. package/dist/core/screenshot/index.d.ts +12 -0
  28. package/dist/core/screenshot/index.js +46 -0
  29. package/dist/core/server/index.d.ts +5 -0
  30. package/dist/core/server/index.js +101 -0
  31. package/dist/core/text2img/index.d.ts +14 -0
  32. package/dist/core/text2img/index.js +64 -0
  33. package/dist/index.d.ts +1 -0
  34. package/dist/index.js +429 -0
  35. package/dist/plugins/index.d.ts +41 -0
  36. package/dist/plugins/index.js +61 -0
  37. package/dist/tests/test.d.ts +1 -0
  38. package/dist/tests/test.js +108 -0
  39. package/dist/tests/test_playwright.d.ts +1 -0
  40. package/dist/tests/test_playwright.js +60 -0
  41. package/dist/utils/branding.d.ts +6 -0
  42. package/dist/utils/branding.js +21 -0
  43. package/dist/utils/file.d.ts +7 -0
  44. package/dist/utils/file.js +29 -0
  45. package/dist/utils/templates.d.ts +9 -0
  46. package/dist/utils/templates.js +347 -0
  47. package/mediaguru-1.0.0.tgz +0 -0
  48. package/package.json +51 -0
@@ -0,0 +1,66 @@
1
+ import { glob } from 'glob';
2
+ import path from 'path';
3
+ import { ImageEngine } from '../image/index.js';
4
+ import { PdfEngine } from '../pdf/index.js';
5
+ export class BatchEngine {
6
+ /**
7
+ * Run batch conversion on images matching a glob pattern
8
+ */
9
+ static async convertImages(pattern, targetFormat) {
10
+ const files = await glob(pattern, { windowsPathsNoEscape: true });
11
+ const results = [];
12
+ for (const file of files) {
13
+ try {
14
+ const outPath = await ImageEngine.convert(file, targetFormat);
15
+ results.push({
16
+ sourceFile: file,
17
+ outputFile: outPath,
18
+ success: true,
19
+ });
20
+ }
21
+ catch (err) {
22
+ results.push({
23
+ sourceFile: file,
24
+ success: false,
25
+ error: err.message || String(err),
26
+ });
27
+ }
28
+ }
29
+ return results;
30
+ }
31
+ /**
32
+ * Run batch PDF generation on markdown or HTML files matching a glob pattern
33
+ */
34
+ static async convertToPdf(pattern) {
35
+ const files = await glob(pattern, { windowsPathsNoEscape: true });
36
+ const results = [];
37
+ for (const file of files) {
38
+ const ext = path.extname(file).toLowerCase();
39
+ try {
40
+ let outPath = '';
41
+ if (ext === '.md') {
42
+ outPath = await PdfEngine.markdownToPdf(file);
43
+ }
44
+ else if (ext === '.html' || ext === '.htm') {
45
+ outPath = await PdfEngine.htmlToPdf(file);
46
+ }
47
+ else {
48
+ throw new Error(`Unsupported extension for PDF generation: ${ext}`);
49
+ }
50
+ results.push({
51
+ sourceFile: file,
52
+ outputFile: outPath,
53
+ success: true,
54
+ });
55
+ }
56
+ catch (err) {
57
+ results.push({
58
+ sourceFile: file,
59
+ success: false,
60
+ error: err.message || String(err),
61
+ });
62
+ }
63
+ }
64
+ return results;
65
+ }
66
+ }
@@ -0,0 +1,17 @@
1
+ export interface CompressionResult {
2
+ filePath: string;
3
+ originalSize: number;
4
+ compressedSize: number;
5
+ savedBytes: number;
6
+ percentage: number;
7
+ }
8
+ export declare class CompressEngine {
9
+ /**
10
+ * Compresses a single image and returns statistics
11
+ */
12
+ static compressImage(imagePath: string, outputPath?: string): Promise<CompressionResult>;
13
+ /**
14
+ * Compresses all images in a folder recursively
15
+ */
16
+ static compressFolder(folderPath: string, outputDir?: string): Promise<CompressionResult[]>;
17
+ }
@@ -0,0 +1,96 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import sharp from 'sharp';
4
+ import { ConfigManager } from '../config/index.js';
5
+ export class CompressEngine {
6
+ /**
7
+ * Compresses a single image and returns statistics
8
+ */
9
+ static async compressImage(imagePath, outputPath) {
10
+ const config = ConfigManager.load();
11
+ const quality = config.compressionQuality;
12
+ const outDir = ConfigManager.getOutputFolder();
13
+ const originalStats = fs.statSync(imagePath);
14
+ const originalSize = originalStats.size;
15
+ const ext = path.extname(imagePath).toLowerCase();
16
+ const baseName = path.basename(imagePath);
17
+ const targetPath = outputPath || path.join(outDir, `compressed_${baseName}`);
18
+ // Ensure output dir exists
19
+ if (!fs.existsSync(path.dirname(targetPath))) {
20
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
21
+ }
22
+ let pipe = sharp(imagePath);
23
+ if (ext === '.jpg' || ext === '.jpeg') {
24
+ pipe = pipe.jpeg({ quality, progressive: true });
25
+ }
26
+ else if (ext === '.png') {
27
+ pipe = pipe.png({ quality, compressionLevel: 9, palette: true });
28
+ }
29
+ else if (ext === '.webp') {
30
+ pipe = pipe.webp({ quality, effort: 6 });
31
+ }
32
+ else {
33
+ // For unsupported image types, copy directly
34
+ fs.copyFileSync(imagePath, targetPath);
35
+ return {
36
+ filePath: targetPath,
37
+ originalSize,
38
+ compressedSize: originalSize,
39
+ savedBytes: 0,
40
+ percentage: 0,
41
+ };
42
+ }
43
+ await pipe.toFile(targetPath);
44
+ const compressedStats = fs.statSync(targetPath);
45
+ const compressedSize = compressedStats.size;
46
+ const savedBytes = originalSize - compressedSize;
47
+ const percentage = originalSize > 0 ? (savedBytes / originalSize) * 100 : 0;
48
+ return {
49
+ filePath: targetPath,
50
+ originalSize,
51
+ compressedSize,
52
+ savedBytes,
53
+ percentage,
54
+ };
55
+ }
56
+ /**
57
+ * Compresses all images in a folder recursively
58
+ */
59
+ static async compressFolder(folderPath, outputDir) {
60
+ const targetDir = outputDir || path.join(ConfigManager.getOutputFolder(), 'compressed_folder');
61
+ const results = [];
62
+ const supportedExts = ['.png', '.jpg', '.jpeg', '.webp'];
63
+ async function walk(dir, currentDest) {
64
+ if (!fs.existsSync(currentDest)) {
65
+ fs.mkdirSync(currentDest, { recursive: true });
66
+ }
67
+ const files = fs.readdirSync(dir);
68
+ for (const file of files) {
69
+ const fullPath = path.join(dir, file);
70
+ const destPath = path.join(currentDest, file);
71
+ const stat = fs.statSync(fullPath);
72
+ if (stat.isDirectory()) {
73
+ await walk(fullPath, destPath);
74
+ }
75
+ else {
76
+ const ext = path.extname(file).toLowerCase();
77
+ if (supportedExts.includes(ext)) {
78
+ try {
79
+ const res = await CompressEngine.compressImage(fullPath, destPath);
80
+ results.push(res);
81
+ }
82
+ catch (err) {
83
+ console.error(`Failed to compress ${file}:`, err);
84
+ }
85
+ }
86
+ else {
87
+ // Non-image files are copied over to maintain folder integrity
88
+ fs.copyFileSync(fullPath, destPath);
89
+ }
90
+ }
91
+ }
92
+ }
93
+ await walk(folderPath, targetDir);
94
+ return results;
95
+ }
96
+ }
@@ -0,0 +1,14 @@
1
+ export interface AppConfig {
2
+ defaultImageFormat: 'png' | 'jpg' | 'jpeg' | 'webp' | 'svg';
3
+ pdfEngine: 'playwright' | 'local';
4
+ compressionQuality: number;
5
+ screenshotResolution: string;
6
+ outputFolder: string;
7
+ }
8
+ export declare class ConfigManager {
9
+ private static cachedConfig;
10
+ static load(): AppConfig;
11
+ static save(config: AppConfig): void;
12
+ static update(updates: Partial<AppConfig>): AppConfig;
13
+ static getOutputFolder(): string;
14
+ }
@@ -0,0 +1,56 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ const DEFAULT_CONFIG = {
5
+ defaultImageFormat: 'webp',
6
+ pdfEngine: 'playwright',
7
+ compressionQuality: 80,
8
+ screenshotResolution: '1280x720',
9
+ outputFolder: './output',
10
+ };
11
+ const CONFIG_FILE = path.join(os.homedir(), '.mediagururc.json');
12
+ export class ConfigManager {
13
+ static cachedConfig = null;
14
+ static load() {
15
+ if (this.cachedConfig)
16
+ return this.cachedConfig;
17
+ try {
18
+ if (fs.existsSync(CONFIG_FILE)) {
19
+ const data = fs.readFileSync(CONFIG_FILE, 'utf-8');
20
+ const parsed = JSON.parse(data);
21
+ this.cachedConfig = { ...DEFAULT_CONFIG, ...parsed };
22
+ }
23
+ else {
24
+ this.cachedConfig = { ...DEFAULT_CONFIG };
25
+ this.save(this.cachedConfig);
26
+ }
27
+ }
28
+ catch (e) {
29
+ this.cachedConfig = { ...DEFAULT_CONFIG };
30
+ }
31
+ return this.cachedConfig || DEFAULT_CONFIG;
32
+ }
33
+ static save(config) {
34
+ try {
35
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
36
+ this.cachedConfig = config;
37
+ }
38
+ catch (e) {
39
+ console.error('Failed to save config file:', e);
40
+ }
41
+ }
42
+ static update(updates) {
43
+ const current = this.load();
44
+ const updated = { ...current, ...updates };
45
+ this.save(updated);
46
+ return updated;
47
+ }
48
+ static getOutputFolder() {
49
+ const config = this.load();
50
+ const folder = config.outputFolder;
51
+ if (!fs.existsSync(folder)) {
52
+ fs.mkdirSync(folder, { recursive: true });
53
+ }
54
+ return folder;
55
+ }
56
+ }
@@ -0,0 +1,12 @@
1
+ export declare class ExportEngine {
2
+ /**
3
+ * Exports the current configuration in the specified format
4
+ */
5
+ static exportConfig(format: 'json' | 'markdown' | 'csv', opts?: {
6
+ outputPath?: string;
7
+ }): Promise<string>;
8
+ /**
9
+ * Generically converts a JSON file containing an array of objects to CSV or Markdown
10
+ */
11
+ static convertJsonData(jsonFilePath: string, format: 'csv' | 'markdown', outputPath?: string): Promise<string>;
12
+ }
@@ -0,0 +1,82 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { createObjectCsvWriter } from 'csv-writer';
4
+ import { ConfigManager } from '../config/index.js';
5
+ export class ExportEngine {
6
+ /**
7
+ * Exports the current configuration in the specified format
8
+ */
9
+ static async exportConfig(format, opts = {}) {
10
+ const config = ConfigManager.load();
11
+ const outDir = ConfigManager.getOutputFolder();
12
+ const targetPath = opts.outputPath || path.join(outDir, `mediaguru_config.${format}`);
13
+ if (!fs.existsSync(outDir)) {
14
+ fs.mkdirSync(outDir, { recursive: true });
15
+ }
16
+ if (format === 'json') {
17
+ fs.writeFileSync(targetPath, JSON.stringify(config, null, 2), 'utf-8');
18
+ }
19
+ else if (format === 'markdown') {
20
+ const md = `# MediaGuru Configuration Export\n\n` +
21
+ `| Setting | Value |\n` +
22
+ `| --- | --- |\n` +
23
+ `| **Default Image Format** | \`${config.defaultImageFormat}\` |\n` +
24
+ `| **PDF Engine** | \`${config.pdfEngine}\` |\n` +
25
+ `| **Compression Quality** | \`${config.compressionQuality}%\` |\n` +
26
+ `| **Screenshot Resolution** | \`${config.screenshotResolution}\` |\n` +
27
+ `| **Output Folder** | \`${config.outputFolder}\` |\n` +
28
+ `\n*Generated by MediaGuru*\n`;
29
+ fs.writeFileSync(targetPath, md, 'utf-8');
30
+ }
31
+ else {
32
+ // CSV
33
+ const csvWriter = createObjectCsvWriter({
34
+ path: targetPath,
35
+ header: [
36
+ { id: 'setting', title: 'Setting' },
37
+ { id: 'value', title: 'Value' },
38
+ ],
39
+ });
40
+ const records = Object.entries(config).map(([key, val]) => ({
41
+ setting: key,
42
+ value: String(val),
43
+ }));
44
+ await csvWriter.writeRecords(records);
45
+ }
46
+ return targetPath;
47
+ }
48
+ /**
49
+ * Generically converts a JSON file containing an array of objects to CSV or Markdown
50
+ */
51
+ static async convertJsonData(jsonFilePath, format, outputPath) {
52
+ const rawData = fs.readFileSync(jsonFilePath, 'utf-8');
53
+ const data = JSON.parse(rawData);
54
+ const items = Array.isArray(data) ? data : [data];
55
+ if (items.length === 0) {
56
+ throw new Error('JSON data contains no objects to export.');
57
+ }
58
+ const outDir = ConfigManager.getOutputFolder();
59
+ const baseName = path.basename(jsonFilePath, path.extname(jsonFilePath));
60
+ const targetPath = outputPath || path.join(outDir, `${baseName}_converted.${format}`);
61
+ if (format === 'markdown') {
62
+ const keys = Object.keys(items[0]);
63
+ let md = `\n| ${keys.join(' | ')} |\n`;
64
+ md += `| ${keys.map(() => '---').join(' | ')} |\n`;
65
+ for (const item of items) {
66
+ const row = keys.map(k => String(item[k] !== undefined ? item[k] : ''));
67
+ md += `| ${row.join(' | ')} |\n`;
68
+ }
69
+ fs.writeFileSync(targetPath, md, 'utf-8');
70
+ }
71
+ else {
72
+ // CSV
73
+ const keys = Object.keys(items[0]);
74
+ const csvWriter = createObjectCsvWriter({
75
+ path: targetPath,
76
+ header: keys.map(k => ({ id: k, title: k })),
77
+ });
78
+ await csvWriter.writeRecords(items);
79
+ }
80
+ return targetPath;
81
+ }
82
+ }
@@ -0,0 +1,44 @@
1
+ export interface ImageEngineOptions {
2
+ outputDir?: string;
3
+ outputPath?: string;
4
+ quality?: number;
5
+ }
6
+ export interface IBgRemovalEngine {
7
+ removeBackground(inputPath: string, outputPath: string): Promise<void>;
8
+ }
9
+ /**
10
+ * Local background removal engine:
11
+ * Uses color-threshold masking to remove solid/near-solid backdrops (chroma keying)
12
+ * using sharp's pixel manipulation or simple masks.
13
+ */
14
+ export declare class LocalBgRemovalEngine implements IBgRemovalEngine {
15
+ removeBackground(inputPath: string, outputPath: string): Promise<void>;
16
+ }
17
+ /**
18
+ * API-powered background removal engine:
19
+ * Calls remove.bg or another API endpoint with standard multipart form upload.
20
+ */
21
+ export declare class ApiBgRemovalEngine implements IBgRemovalEngine {
22
+ private apiKey;
23
+ private apiEndpoint;
24
+ constructor(apiKey?: string, endpoint?: string);
25
+ removeBackground(inputPath: string, outputPath: string): Promise<void>;
26
+ }
27
+ export declare class ImageEngine {
28
+ /**
29
+ * Resizes an image file
30
+ */
31
+ static resize(imagePath: string, sizeStr: string, opts?: ImageEngineOptions): Promise<string>;
32
+ /**
33
+ * Converts an image format
34
+ */
35
+ static convert(imagePath: string, format: string, opts?: ImageEngineOptions): Promise<string>;
36
+ /**
37
+ * Overlay a watermark onto an image
38
+ */
39
+ static watermark(imagePath: string, watermarkPath: string, opts?: ImageEngineOptions): Promise<string>;
40
+ /**
41
+ * Background Removal Engine runner
42
+ */
43
+ static removeBg(imagePath: string, engineType?: 'local' | 'api', opts?: ImageEngineOptions): Promise<string>;
44
+ }
@@ -0,0 +1,206 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import sharp from 'sharp';
4
+ import { resolveOutputPath } from '../../utils/file.js';
5
+ import { ConfigManager } from '../config/index.js';
6
+ /**
7
+ * Local background removal engine:
8
+ * Uses color-threshold masking to remove solid/near-solid backdrops (chroma keying)
9
+ * using sharp's pixel manipulation or simple masks.
10
+ */
11
+ export class LocalBgRemovalEngine {
12
+ async removeBackground(inputPath, outputPath) {
13
+ // A clean, solid-backdrop color boundary removal using sharp channel extraction.
14
+ // It makes the dominant edge colors transparent.
15
+ const image = sharp(inputPath);
16
+ const metadata = await image.metadata();
17
+ if (!metadata.width || !metadata.height) {
18
+ throw new Error('Could not read image metadata for background removal.');
19
+ }
20
+ // Read raw pixel channels to perform chroma thresholding
21
+ const { data, info } = await image
22
+ .raw()
23
+ .toBuffer({ resolveWithObject: true });
24
+ // Let's assume the top-left pixel is the background color (common heuristic)
25
+ const rBg = data[0];
26
+ const gBg = data[1];
27
+ const bBg = data[2];
28
+ const threshold = 40; // color distance threshold
29
+ const transparentData = Buffer.alloc(info.width * info.height * 4);
30
+ let srcIdx = 0;
31
+ let destIdx = 0;
32
+ const hasAlpha = info.channels === 4;
33
+ for (let y = 0; y < info.height; y++) {
34
+ for (let x = 0; x < info.width; x++) {
35
+ const r = data[srcIdx];
36
+ const g = data[srcIdx + 1];
37
+ const b = data[srcIdx + 2];
38
+ const a = hasAlpha ? data[srcIdx + 3] : 255;
39
+ // Calculate Euclidean distance in RGB space
40
+ const distance = Math.sqrt(Math.pow(r - rBg, 2) +
41
+ Math.pow(g - gBg, 2) +
42
+ Math.pow(b - bBg, 2));
43
+ if (distance < threshold) {
44
+ // Make background transparent
45
+ transparentData[destIdx] = r;
46
+ transparentData[destIdx + 1] = g;
47
+ transparentData[destIdx + 2] = b;
48
+ transparentData[destIdx + 3] = 0;
49
+ }
50
+ else {
51
+ // Keep original pixel
52
+ transparentData[destIdx] = r;
53
+ transparentData[destIdx + 1] = g;
54
+ transparentData[destIdx + 2] = b;
55
+ transparentData[destIdx + 3] = a;
56
+ }
57
+ srcIdx += info.channels;
58
+ destIdx += 4;
59
+ }
60
+ }
61
+ await sharp(transparentData, {
62
+ raw: {
63
+ width: info.width,
64
+ height: info.height,
65
+ channels: 4,
66
+ },
67
+ })
68
+ .png()
69
+ .toFile(outputPath);
70
+ }
71
+ }
72
+ /**
73
+ * API-powered background removal engine:
74
+ * Calls remove.bg or another API endpoint with standard multipart form upload.
75
+ */
76
+ export class ApiBgRemovalEngine {
77
+ apiKey;
78
+ apiEndpoint;
79
+ constructor(apiKey, endpoint) {
80
+ this.apiKey = apiKey || process.env.REMOVE_BG_API_KEY || '';
81
+ this.apiEndpoint = endpoint || 'https://api.remove.bg/v1.0/removebg';
82
+ }
83
+ async removeBackground(inputPath, outputPath) {
84
+ if (!this.apiKey) {
85
+ throw new Error('API key is not configured. Please set REMOVE_BG_API_KEY in your config or environment.');
86
+ }
87
+ const fileData = fs.readFileSync(inputPath);
88
+ const formData = new FormData();
89
+ formData.append('image_file', new Blob([fileData]), path.basename(inputPath));
90
+ formData.append('size', 'auto');
91
+ const response = await fetch(this.apiEndpoint, {
92
+ method: 'POST',
93
+ headers: {
94
+ 'X-Api-Key': this.apiKey,
95
+ },
96
+ body: formData,
97
+ });
98
+ if (!response.ok) {
99
+ const errText = await response.text();
100
+ throw new Error(`remove.bg API error (${response.status}): ${errText}`);
101
+ }
102
+ const arrayBuffer = await response.arrayBuffer();
103
+ fs.writeFileSync(outputPath, Buffer.from(arrayBuffer));
104
+ }
105
+ }
106
+ export class ImageEngine {
107
+ /**
108
+ * Resizes an image file
109
+ */
110
+ static async resize(imagePath, sizeStr, opts = {}) {
111
+ let width = null;
112
+ let height = null;
113
+ if (sizeStr.includes('x')) {
114
+ const parts = sizeStr.split('x');
115
+ width = parseInt(parts[0], 10);
116
+ height = parseInt(parts[1], 10);
117
+ }
118
+ else {
119
+ width = parseInt(sizeStr, 10);
120
+ }
121
+ if (isNaN(width) || (height !== null && isNaN(height))) {
122
+ throw new Error('Invalid size format. Use "800x600" or "800"');
123
+ }
124
+ const outDir = opts.outputDir || ConfigManager.getOutputFolder();
125
+ const config = ConfigManager.load();
126
+ const format = config.defaultImageFormat;
127
+ const targetPath = opts.outputPath || resolveOutputPath(imagePath, outDir, format, `_resized_${sizeStr}`);
128
+ let transformer = sharp(imagePath);
129
+ if (width && height) {
130
+ transformer = transformer.resize(width, height, { fit: 'fill' });
131
+ }
132
+ else if (width) {
133
+ transformer = transformer.resize(width);
134
+ }
135
+ await transformer.toFile(targetPath);
136
+ return targetPath;
137
+ }
138
+ /**
139
+ * Converts an image format
140
+ */
141
+ static async convert(imagePath, format, opts = {}) {
142
+ const supported = ['png', 'jpg', 'jpeg', 'webp', 'svg'];
143
+ const fmt = format.toLowerCase().replace('.', '');
144
+ if (!supported.includes(fmt)) {
145
+ throw new Error(`Unsupported format: ${format}. Supported are: ${supported.join(', ')}`);
146
+ }
147
+ if (fmt === 'svg') {
148
+ throw new Error('Converting raster images to SVG vector is not supported natively. SVG can only be converted to raster images.');
149
+ }
150
+ const outDir = opts.outputDir || ConfigManager.getOutputFolder();
151
+ const targetPath = opts.outputPath || resolveOutputPath(imagePath, outDir, fmt);
152
+ const q = opts.quality || ConfigManager.load().compressionQuality;
153
+ let process = sharp(imagePath);
154
+ if (fmt === 'png')
155
+ process = process.png({ quality: q });
156
+ else if (fmt === 'webp')
157
+ process = process.webp({ quality: q });
158
+ else if (fmt === 'jpg' || fmt === 'jpeg')
159
+ process = process.jpeg({ quality: q });
160
+ await process.toFile(targetPath);
161
+ return targetPath;
162
+ }
163
+ /**
164
+ * Overlay a watermark onto an image
165
+ */
166
+ static async watermark(imagePath, watermarkPath, opts = {}) {
167
+ const outDir = opts.outputDir || ConfigManager.getOutputFolder();
168
+ const config = ConfigManager.load();
169
+ const format = config.defaultImageFormat;
170
+ const targetPath = opts.outputPath || resolveOutputPath(imagePath, outDir, format, '_watermarked');
171
+ // Load watermark metadata to scale it if needed
172
+ const mainMeta = await sharp(imagePath).metadata();
173
+ const wmMeta = await sharp(watermarkPath).metadata();
174
+ if (!mainMeta.width || !mainMeta.height || !wmMeta.width || !wmMeta.height) {
175
+ throw new Error('Unable to read image metadata.');
176
+ }
177
+ // Scale watermark to be max 25% of the main image width
178
+ const targetWmWidth = Math.floor(mainMeta.width * 0.25);
179
+ const scaledWm = await sharp(watermarkPath)
180
+ .resize(targetWmWidth)
181
+ .toBuffer();
182
+ await sharp(imagePath)
183
+ .composite([{
184
+ input: scaledWm,
185
+ gravity: 'southeast', // Default bottom-right placement
186
+ }])
187
+ .toFile(targetPath);
188
+ return targetPath;
189
+ }
190
+ /**
191
+ * Background Removal Engine runner
192
+ */
193
+ static async removeBg(imagePath, engineType = 'local', opts = {}) {
194
+ const outDir = opts.outputDir || ConfigManager.getOutputFolder();
195
+ const targetPath = opts.outputPath || resolveOutputPath(imagePath, outDir, 'png', '_nobg');
196
+ let engine;
197
+ if (engineType === 'api') {
198
+ engine = new ApiBgRemovalEngine();
199
+ }
200
+ else {
201
+ engine = new LocalBgRemovalEngine();
202
+ }
203
+ await engine.removeBackground(imagePath, targetPath);
204
+ return targetPath;
205
+ }
206
+ }
@@ -0,0 +1,14 @@
1
+ export interface OcrEngineOptions {
2
+ exportFormat?: 'txt' | 'markdown' | 'json';
3
+ outputDir?: string;
4
+ outputPath?: string;
5
+ }
6
+ export declare class OcrEngine {
7
+ /**
8
+ * Extracts text from an image using Tesseract OCR
9
+ */
10
+ static extract(imagePath: string, opts?: OcrEngineOptions): Promise<{
11
+ text: string;
12
+ exportedPath?: string;
13
+ }>;
14
+ }
@@ -0,0 +1,53 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { createWorker } from 'tesseract.js';
4
+ import { ConfigManager } from '../config/index.js';
5
+ export class OcrEngine {
6
+ /**
7
+ * Extracts text from an image using Tesseract OCR
8
+ */
9
+ static async extract(imagePath, opts = {}) {
10
+ // Initialize Tesseract worker
11
+ const worker = await createWorker('eng');
12
+ let text = '';
13
+ try {
14
+ const { data } = await worker.recognize(imagePath);
15
+ text = data.text;
16
+ }
17
+ finally {
18
+ await worker.terminate();
19
+ }
20
+ if (!opts.exportFormat) {
21
+ return { text };
22
+ }
23
+ // Handle exports
24
+ const format = opts.exportFormat.toLowerCase();
25
+ const outDir = opts.outputDir || ConfigManager.getOutputFolder();
26
+ const baseName = path.basename(imagePath, path.extname(imagePath));
27
+ const targetPath = opts.outputPath || path.join(outDir, `${baseName}_ocr.${format}`);
28
+ if (!fs.existsSync(outDir)) {
29
+ fs.mkdirSync(outDir, { recursive: true });
30
+ }
31
+ let exportContent = '';
32
+ if (format === 'json') {
33
+ exportContent = JSON.stringify({
34
+ source: path.basename(imagePath),
35
+ extractedAt: new Date().toISOString(),
36
+ text: text,
37
+ }, null, 2);
38
+ }
39
+ else if (format === 'markdown') {
40
+ exportContent = `# OCR Extraction Report\n\n` +
41
+ `**Source File:** \`${path.basename(imagePath)}\`\n` +
42
+ `**Extracted At:** ${new Date().toLocaleString()}\n\n` +
43
+ `## Extracted Content\n\n` +
44
+ `\`\`\`text\n${text}\n\`\`\`\n`;
45
+ }
46
+ else {
47
+ // Default to plain txt
48
+ exportContent = text;
49
+ }
50
+ fs.writeFileSync(targetPath, exportContent, 'utf-8');
51
+ return { text, exportedPath: targetPath };
52
+ }
53
+ }