g360-cli 1.15.8 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -4
- package/package.json +2 -1
- package/src/assets/pptx/design-tokens.js +124 -0
- package/src/assets/pptx/layouts/base.js +441 -0
- package/src/assets/pptx/layouts/index.js +19 -0
- package/src/assets/pptx/scripts/analyze-app.js +358 -0
- package/src/assets/pptx/scripts/image-size.js +56 -0
- package/src/assets/pptx/templates/app-manual.js +397 -0
- package/src/assets/pptx/themes/base.js +8 -0
- package/src/assets/pptx/themes/cipsa.js +4 -0
- package/src/assets/pptx/themes/g360.js +4 -0
- package/src/assets/pptx/themes/index.js +87 -0
- package/src/cli.js +11 -0
- package/src/commands/pptx.js +91 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layouts re-exportados desde base.js para compatibilidad.
|
|
3
|
+
*/
|
|
4
|
+
export {
|
|
5
|
+
coverSlide,
|
|
6
|
+
screenshotSlide,
|
|
7
|
+
screenshotFrame,
|
|
8
|
+
featureSlide,
|
|
9
|
+
workflowSlide,
|
|
10
|
+
architectureSlide,
|
|
11
|
+
checklistSlide,
|
|
12
|
+
kpiSlide,
|
|
13
|
+
limitsSlide,
|
|
14
|
+
modulesTable,
|
|
15
|
+
card,
|
|
16
|
+
textBlock,
|
|
17
|
+
addFooter,
|
|
18
|
+
addSectionHeader,
|
|
19
|
+
} from './base.js';
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analisis de app G360 — extrae features, screens y flujos de la estructura estandarizada.
|
|
3
|
+
* Aprovecha las convenciones FLET-NAMING-CONVENTIONS.md para parseo deterministico.
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'fs-extra';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Clases UI conocidas → nombre legible para el usuario.
|
|
10
|
+
*/
|
|
11
|
+
const UI_CLASS_MAP = {
|
|
12
|
+
'KpiCard': 'Indicadores KPI',
|
|
13
|
+
'Dashboard': 'Dashboard',
|
|
14
|
+
'WarehouseCard': 'Tarjeta de Almacen',
|
|
15
|
+
'SearchOverlay': 'Buscador Flotante',
|
|
16
|
+
'ExportModal': 'Exportar a Excel',
|
|
17
|
+
'SkuDetailModal': 'Detalle de SKU',
|
|
18
|
+
'TrasladosModal': 'Traslados entre Almacenes',
|
|
19
|
+
'SinStockModal': 'Productos Sin Stock',
|
|
20
|
+
'LineaSection': 'Seccion por Linea',
|
|
21
|
+
'AppSidebar': 'Barra Lateral',
|
|
22
|
+
'LoadingOverlay': 'Indicador de Carga',
|
|
23
|
+
'HealthBadge': 'Estado de Salud',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Patrón para detectar metodos desde src/app.py.
|
|
28
|
+
*/
|
|
29
|
+
const METHOD_PATTERN = /def\s+(_(?:setup|build|on|fetch|load|save|update|show|hide|toggle|validate|format)[_\w]*)/g;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Patrón para detectar imports de clases UI.
|
|
33
|
+
*/
|
|
34
|
+
const IMPORT_UI_PATTERN = /from\s+src\.ui\.\w+\s+import\s+([\w,]+)/g;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Analiza la estructura de una app G360.
|
|
38
|
+
*/
|
|
39
|
+
export async function analyzeApp(projectDir) {
|
|
40
|
+
const result = {
|
|
41
|
+
name: '',
|
|
42
|
+
version: '',
|
|
43
|
+
description: '',
|
|
44
|
+
brand: 'g360',
|
|
45
|
+
skill: '',
|
|
46
|
+
framework: '',
|
|
47
|
+
features: [],
|
|
48
|
+
screens: [],
|
|
49
|
+
workflows: [],
|
|
50
|
+
modules: [],
|
|
51
|
+
screenshots: [],
|
|
52
|
+
hasSource1: false,
|
|
53
|
+
hasSource2: false,
|
|
54
|
+
hasExport: false,
|
|
55
|
+
hasAutoRefresh: false,
|
|
56
|
+
hasSearch: false,
|
|
57
|
+
hasModals: [],
|
|
58
|
+
templates: [],
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// 1. Leer skill.json
|
|
62
|
+
const skillPath = path.join(projectDir, 'skill.json');
|
|
63
|
+
if (await fs.pathExists(skillPath)) {
|
|
64
|
+
try {
|
|
65
|
+
const skill = await fs.readJson(skillPath);
|
|
66
|
+
result.name = skill.name || result.name;
|
|
67
|
+
result.description = skill.description || result.description;
|
|
68
|
+
result.brand = skill.brand || 'g360';
|
|
69
|
+
result.skill = skill.skill || skill.name || '';
|
|
70
|
+
result.framework = skill.framework || '';
|
|
71
|
+
result.version = skill.version || '';
|
|
72
|
+
result.events = skill.events || [];
|
|
73
|
+
} catch { /* ignorar */ }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 2. Leer manifest
|
|
77
|
+
const manifestPath = path.join(projectDir, 'g360-manifest.json');
|
|
78
|
+
if (await fs.pathExists(manifestPath)) {
|
|
79
|
+
try {
|
|
80
|
+
const manifest = await fs.readJson(manifestPath);
|
|
81
|
+
result.name = manifest.name || result.name;
|
|
82
|
+
result.version = manifest.version || result.version;
|
|
83
|
+
} catch { /* ignorar */ }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 3. Escanear src/ui/ para clases UI conocidas
|
|
87
|
+
const uiDir = path.join(projectDir, 'src', 'ui');
|
|
88
|
+
if (await fs.pathExists(uiDir)) {
|
|
89
|
+
const uiFiles = await fs.readdir(uiDir);
|
|
90
|
+
for (const file of uiFiles) {
|
|
91
|
+
if (file === '__pycache__' || file.endsWith('.pyc')) continue;
|
|
92
|
+
if (file === '__init__.py') continue;
|
|
93
|
+
const filePath = path.join(uiDir, file);
|
|
94
|
+
const st = await fs.stat(filePath).catch(() => null);
|
|
95
|
+
if (!st || !st.isFile()) continue;
|
|
96
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
97
|
+
const className = file.replace('.py', '');
|
|
98
|
+
const displayName = UI_CLASS_MAP[className] || className;
|
|
99
|
+
result.features.push({
|
|
100
|
+
name: className,
|
|
101
|
+
display: displayName,
|
|
102
|
+
file,
|
|
103
|
+
path: filePath,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 4. Escanear modals
|
|
109
|
+
const modalsDir = path.join(projectDir, 'src', 'ui', 'modals');
|
|
110
|
+
if (await fs.pathExists(modalsDir)) {
|
|
111
|
+
const modalFiles = await fs.readdir(modalsDir);
|
|
112
|
+
for (const file of modalFiles) {
|
|
113
|
+
if (file === '__pycache__' || file.endsWith('.pyc')) continue;
|
|
114
|
+
if (file === '__init__.py') continue;
|
|
115
|
+
const className = file.replace('.py', '');
|
|
116
|
+
const displayName = UI_CLASS_MAP[className] || className;
|
|
117
|
+
result.hasModals.push(className);
|
|
118
|
+
result.workflows.push({
|
|
119
|
+
name: className,
|
|
120
|
+
display: displayName,
|
|
121
|
+
steps: [`Abrir ${displayName}`, 'Interactuar con datos', 'Confirmar o cancelar'],
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 5. Detectar features desde src/app.py
|
|
127
|
+
const appPy = path.join(projectDir, 'src', 'app.py');
|
|
128
|
+
if (await fs.pathExists(appPy)) {
|
|
129
|
+
const content = await fs.readFile(appPy, 'utf-8');
|
|
130
|
+
|
|
131
|
+
// Detectar auto-refresh
|
|
132
|
+
if (content.includes('auto_refresh') || content.includes('_auto_refresh')) {
|
|
133
|
+
result.hasAutoRefresh = true;
|
|
134
|
+
}
|
|
135
|
+
// Detectar busqueda
|
|
136
|
+
if (content.includes('search') || content.includes('SearchOverlay')) {
|
|
137
|
+
result.hasSearch = true;
|
|
138
|
+
}
|
|
139
|
+
// Detectar source1
|
|
140
|
+
if (content.includes('download_source1') || content.includes('source1')) {
|
|
141
|
+
result.hasSource1 = true;
|
|
142
|
+
}
|
|
143
|
+
// Detectar export
|
|
144
|
+
if (content.includes('export') || content.includes('excel') || content.includes('openpyxl')) {
|
|
145
|
+
result.hasExport = true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Extraer metodos _on_* como flujos de usuario
|
|
149
|
+
const methods = content.match(METHOD_PATTERN) || [];
|
|
150
|
+
for (const match of methods) {
|
|
151
|
+
const methodName = match.replace('def ', '').trim();
|
|
152
|
+
if (methodName.startsWith('_on_')) {
|
|
153
|
+
const displayName = methodName
|
|
154
|
+
.replace('_on_', '')
|
|
155
|
+
.replace(/_/g, ' ')
|
|
156
|
+
.replace(/\b\w/g, l => l.toUpperCase());
|
|
157
|
+
result.templates.push({
|
|
158
|
+
type: 'interaction',
|
|
159
|
+
name: methodName,
|
|
160
|
+
display: displayName,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 6. Detección web: framework, módulos y capacidades desde package.json
|
|
167
|
+
await analyzeWebApp(projectDir, result);
|
|
168
|
+
|
|
169
|
+
// 7. Detectar Screenshots disponibles
|
|
170
|
+
const screenshotsDir = path.join(projectDir, 'assets', 'screenshots');
|
|
171
|
+
if (await fs.pathExists(screenshotsDir)) {
|
|
172
|
+
try {
|
|
173
|
+
const files = await fs.readdir(screenshotsDir);
|
|
174
|
+
result.screenshots = files.filter(f => /\.(png|jpg|jpeg|webp)$/i.test(f)).map(f => ({
|
|
175
|
+
filename: f,
|
|
176
|
+
path: path.join(screenshotsDir, f),
|
|
177
|
+
}));
|
|
178
|
+
} catch { /* ignorar */ }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 8. Construir features list
|
|
182
|
+
result.features = result.features.map(f => ({
|
|
183
|
+
...f,
|
|
184
|
+
screenshotIndex: result.screenshots.findIndex(s => s.filename.includes(f.name.toLowerCase().slice(0, 4))),
|
|
185
|
+
}));
|
|
186
|
+
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Descripciones legibles para rutas web conocidas.
|
|
192
|
+
*/
|
|
193
|
+
const WEB_ROUTE_MAP = {
|
|
194
|
+
dashboard: 'Vista principal: resumen operativo, KPIs y accesos a los módulos',
|
|
195
|
+
hoy: 'Agenda del día: prioridades, alertas y avance',
|
|
196
|
+
radar: 'Oportunidades priorizadas y ruta del día',
|
|
197
|
+
netos: 'Montos netos jerárquicos por periodo',
|
|
198
|
+
clientes: 'Directorio de clientes con filtros y búsqueda',
|
|
199
|
+
ficha: 'Ficha de detalle por cliente con historial y precios',
|
|
200
|
+
login: 'Autenticación y validación de acceso',
|
|
201
|
+
reportes: 'Reportes y exportación de datos',
|
|
202
|
+
stock: 'Consulta de existencias en tiempo real',
|
|
203
|
+
pedidos: 'Gestión de pedidos y vigencias',
|
|
204
|
+
config: 'Configuración y preferencias de la app',
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const WEB_COMPONENT_MAP = {
|
|
208
|
+
'app-root': 'Contenedor principal de la aplicación',
|
|
209
|
+
'stock-header': 'Encabezado con estado de conexión y acciones',
|
|
210
|
+
'stock-search': 'Búsqueda de productos con coincidencias en vivo',
|
|
211
|
+
'stock-alerts': 'Alertas de quiebres de stock y reposición',
|
|
212
|
+
'estado-panel': 'Panel de estado por almacén/producto',
|
|
213
|
+
'pulso-form': 'Formulario de registro de datos de campo',
|
|
214
|
+
'sin-catalogo-panel': 'Aviso de catálogo no disponible con acción de carga',
|
|
215
|
+
'login': 'Autenticación y validación de acceso',
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Detección para apps web: SvelteKit (routes), React (pages),
|
|
220
|
+
* Lit/Componentes (src/components) y capacidades desde package.json.
|
|
221
|
+
*/
|
|
222
|
+
async function analyzeWebApp(projectDir, result) {
|
|
223
|
+
const pkgPath = path.join(projectDir, 'package.json');
|
|
224
|
+
const pkg = (await fs.pathExists(pkgPath))
|
|
225
|
+
? await fs.readJson(pkgPath).catch(() => null)
|
|
226
|
+
: null;
|
|
227
|
+
const deps = { ...(pkg?.dependencies || {}), ...(pkg?.devDependencies || {}) };
|
|
228
|
+
const hasDep = (n) => Object.keys(deps).some((d) => d === n || d.startsWith(n));
|
|
229
|
+
|
|
230
|
+
if (pkg) {
|
|
231
|
+
result.name = result.name || pkg.name || '';
|
|
232
|
+
result.version = result.version || pkg.version || '';
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Framework
|
|
236
|
+
if (hasDep('@sveltejs/kit')) result.framework = result.framework || 'SvelteKit';
|
|
237
|
+
else if (hasDep('lit')) result.framework = result.framework || 'Lit';
|
|
238
|
+
else if (hasDep('react')) result.framework = result.framework || 'React';
|
|
239
|
+
else if (hasDep('vue')) result.framework = result.framework || 'Vue';
|
|
240
|
+
if (result.framework) result.type = 'web';
|
|
241
|
+
|
|
242
|
+
// Capabilidades desde dependencias
|
|
243
|
+
if (hasDep('exceljs') || hasDep('xlsx') || hasDep('sheetjs')) result.hasExport = true;
|
|
244
|
+
if (hasDep('@supabase/supabase-js')) result.hasSupabase = true;
|
|
245
|
+
if (hasDep('vite-plugin-pwa') || hasDep('workbox-window')) result.hasPwa = true;
|
|
246
|
+
if (hasDep('chart.js') || hasDep('recharts') || hasDep('echarts')) result.hasCharts = true;
|
|
247
|
+
if (hasDep('tailwindcss') || hasDep('@tailwindcss/vite')) result.hasTailwind = true;
|
|
248
|
+
|
|
249
|
+
// Módulos: SvelteKit routes (src/routes/**/+page.svelte, incluye subrutas dinámicas)
|
|
250
|
+
const routesDir = path.join(projectDir, 'src', 'routes');
|
|
251
|
+
if (await fs.pathExists(routesDir)) {
|
|
252
|
+
const entries = await fs.readdir(routesDir, { withFileTypes: true });
|
|
253
|
+
for (const entry of entries) {
|
|
254
|
+
if (!entry.isDirectory()) continue;
|
|
255
|
+
const routeDir = path.join(routesDir, entry.name);
|
|
256
|
+
const pageFile = await findPageSvelte(routeDir);
|
|
257
|
+
if (!pageFile) continue;
|
|
258
|
+
result.features.push({
|
|
259
|
+
name: entry.name,
|
|
260
|
+
display: capitalize(entry.name),
|
|
261
|
+
desc: WEB_ROUTE_MAP[entry.name] || `Sección ${entry.name} de la aplicación.`,
|
|
262
|
+
file: path.relative(projectDir, pageFile).split(path.sep).join('/'),
|
|
263
|
+
path: pageFile,
|
|
264
|
+
kind: 'route',
|
|
265
|
+
});
|
|
266
|
+
result.modules.push(entry.name);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Módulos: componentes web destacados (src/components/*.js) — solo si no hay routes
|
|
271
|
+
const componentsDir = path.join(projectDir, 'src', 'components');
|
|
272
|
+
if (result.features.length === 0 && await fs.pathExists(componentsDir)) {
|
|
273
|
+
const files = (await fs.readdir(componentsDir))
|
|
274
|
+
.filter((f) => /\.(js|ts)$/.test(f));
|
|
275
|
+
for (const file of files) {
|
|
276
|
+
const base = file.replace(/\.(js|ts)$/, '');
|
|
277
|
+
result.features.push({
|
|
278
|
+
name: base,
|
|
279
|
+
display: componentDisplayName(base),
|
|
280
|
+
desc: WEB_COMPONENT_MAP[base] || `Componente de interfaz ${base}.`,
|
|
281
|
+
file: `src/components/${file}`,
|
|
282
|
+
path: path.join(componentsDir, file),
|
|
283
|
+
kind: 'component',
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Buscador global: por nombre de componente/ruta o contenido de las páginas
|
|
289
|
+
if (result.features.some((f) => /search|busca/i.test(f.name))) {
|
|
290
|
+
result.hasSearch = true;
|
|
291
|
+
} else {
|
|
292
|
+
for (const f of result.features) {
|
|
293
|
+
if (f.path && /\.(svelte|js|ts)$/.test(f.path)) {
|
|
294
|
+
try {
|
|
295
|
+
const content = await fs.readFile(f.path, 'utf-8');
|
|
296
|
+
if (/search|buscador|lupa/i.test(content)) { result.hasSearch = true; break; }
|
|
297
|
+
} catch { /* ignorar */ }
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Flujos web: modales/buscador como workflows si hay PWA/Supabase (solo títulos)
|
|
303
|
+
if (result.features.length > 0 && result.hasPwa) {
|
|
304
|
+
result.workflows.push({
|
|
305
|
+
name: 'PWA',
|
|
306
|
+
display: 'Instalación PWA y modo offline',
|
|
307
|
+
steps: [
|
|
308
|
+
'Abrir la URL de la app en el navegador',
|
|
309
|
+
'Instalar desde el menú (Android/iOS)',
|
|
310
|
+
'Usar con cache offline cuando no haya señal',
|
|
311
|
+
],
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
if (result.hasSearch) {
|
|
315
|
+
result.workflows.push({
|
|
316
|
+
name: 'Busqueda',
|
|
317
|
+
display: 'Búsqueda global',
|
|
318
|
+
steps: ['Abrir el buscador', 'Escribir el criterio', 'Abrir el resultado'],
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function componentDisplayName(base) {
|
|
324
|
+
return base
|
|
325
|
+
.replace(/-([a-z])/g, (_, c) => ' ' + c.toUpperCase())
|
|
326
|
+
.replace(/\b\w/g, (l) => l.toUpperCase());
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function capitalize(s) {
|
|
330
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Busca recursivamente +page.svelte dentro de un directorio de ruta (subrutas dinámicas incluidas). */
|
|
334
|
+
async function findPageSvelte(dir, depth = 0) {
|
|
335
|
+
if (depth > 3) return null;
|
|
336
|
+
const direct = path.join(dir, '+page.svelte');
|
|
337
|
+
if (await fs.pathExists(direct)) return direct;
|
|
338
|
+
try {
|
|
339
|
+
const children = await fs.readdir(dir, { withFileTypes: true });
|
|
340
|
+
for (const child of children) {
|
|
341
|
+
if (!child.isDirectory()) continue;
|
|
342
|
+
const found = await findPageSvelte(path.join(dir, child.name), depth + 1);
|
|
343
|
+
if (found) return found;
|
|
344
|
+
}
|
|
345
|
+
} catch { /* ignorar */ }
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Mapeo de clase UI → tipo de slide recomendado.
|
|
351
|
+
*/
|
|
352
|
+
export function classifyFeature(feature) {
|
|
353
|
+
if (feature.name.includes('Kpi') || feature.name.includes('Card')) return 'kpi';
|
|
354
|
+
if (feature.name.includes('Modal')) return 'workflow';
|
|
355
|
+
if (feature.name.includes('Dashboard')) return 'screenshot';
|
|
356
|
+
if (feature.name.includes('Search')) return 'feature';
|
|
357
|
+
return 'feature';
|
|
358
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lector de dimensiones de imagen sin dependencias externas.
|
|
3
|
+
* Soporta PNG y JPEG (los formatos aceptados por pptxgenjs).
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Devuelve { width, height } leyendo los headers del archivo.
|
|
9
|
+
* @param {string} filePath
|
|
10
|
+
* @returns {{width:number,height:number}|null}
|
|
11
|
+
*/
|
|
12
|
+
export function getImageSize(filePath) {
|
|
13
|
+
let buf;
|
|
14
|
+
try {
|
|
15
|
+
buf = fs.readFileSync(filePath);
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
if (!buf || buf.length < 24) return null;
|
|
20
|
+
|
|
21
|
+
// PNG: firma 8 bytes + IHDR (largo 4 + tipo 4) -> ancho en 16, alto en 20 (BE)
|
|
22
|
+
if (buf.toString('ascii', 1, 4) === 'PNG') {
|
|
23
|
+
return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// JPEG: recorrer marcadores hasta SOF0-2 (0xC0/0xC1/0xC2)
|
|
27
|
+
if (buf[0] === 0xff && buf[1] === 0xd8) {
|
|
28
|
+
let off = 2;
|
|
29
|
+
while (off + 9 < buf.length) {
|
|
30
|
+
if (buf[off] !== 0xff) { off += 1; continue; }
|
|
31
|
+
const marker = buf[off + 1];
|
|
32
|
+
const isSOF = marker >= 0xc0 && marker <= 0xcf
|
|
33
|
+
&& marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc;
|
|
34
|
+
if (isSOF) {
|
|
35
|
+
return {
|
|
36
|
+
height: buf.readUInt16BE(off + 5),
|
|
37
|
+
width: buf.readUInt16BE(off + 7),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
off += 2 + buf.readUInt16BE(off + 2);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Calcula encaje proporcional (contain) de la imagen en el marco.
|
|
48
|
+
* @returns {{dw:number,dh:number}}
|
|
49
|
+
*/
|
|
50
|
+
export function containFit(width, height, frameW, frameH) {
|
|
51
|
+
if (!width || !height) return { dw: frameW, dh: frameH };
|
|
52
|
+
const ir = width / height;
|
|
53
|
+
const fr = frameW / frameH;
|
|
54
|
+
if (ir > fr) return { dw: frameW, dh: frameW / ir };
|
|
55
|
+
return { dw: frameH * ir, dh: frameH };
|
|
56
|
+
}
|