MEGA PROMPT: Generador TSP IDC — Inyeccion de Formulario a DOCX + PPTX

 # MEGA PROMPT: Generador TSP IDC — Inyeccion de Formulario a DOCX + PPTX


## CONTEXTO


Desarrollar una aplicacion PWA/HTA para egresados del Instituto de Educacion Superior Publico "Diseno y Comunicacion" (IDC) que permita completar un formulario web paso a paso y generar dos archivos descargables con los datos del formulario inyectados:


1. **Informe TSP en Word (.docx)** — documento academico completo (~25 paginas, 4 capitulos)

2. **Presentacion TSP en PowerPoint (.pptx)** — 15 diapositivas para sustentacion oral


---


## PARTE 1: ARQUITECTURA CRITICA — PLANTILLAS EMBEBIDAS COMO BASE64


### REGLA DE ORO #1: NUNCA usar `fetch()` para cargar plantillas


`fetch('InformeTSP.docx')` falla silenciosamente con `NetworkError when attempting to fetch resource` cuando se abre el archivo localmente (`file:///`). La unica solucion robusta es embeber las plantillas como variables base64 en archivos `.js` separados.


### Estructura de carga en HTML:


```html

<!-- 1. Cargar JSZip (desde CDN o local) -->

<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>


<!-- 2. Cargar plantillas embebidas ANTES del script principal -->

<script src="tpl-docx.js"></script>   <!-- const TPL_DOCX_B64 = '...' -->

<script src="tpl-pptx.js"></script>   <!-- const TPL_PPTX_B64 = '...' -->


<!-- 3. Script principal con la logica -->

<script>

  // TPL_DOCX_B64 y TPL_PPTX_B64 ya estan disponibles globalmente

  function generarWord(){ ... }

  function generarPPTX(){ ... }

</script>

```


### Script Python para generar los archivos de plantilla:


```python

import base64, os


def embed_template(input_path, output_path, var_name):

    """Convierte un archivo binario (.docx/.pptx) a un archivo .js con variable base64"""

    with open(input_path, 'rb') as f:

        b64 = base64.b64encode(f.read()).decode('ascii')

    

    with open(output_path, 'w') as f:

        f.write(f"/* {os.path.basename(input_path)} embebido como base64 */\n")

        f.write(f"const {var_name}='")

        for i in range(0, len(b64), 120):

            chunk = b64[i:i+120]

            f.write(chunk)

            if i + 120 < len(b64):

                f.write("' +\n '")

        f.write("';\n")

    

    print(f"✓ {output_path}: {len(b64):,} chars base64")


# Uso:

embed_template('InformeTSP.docx', 'tpl-docx.js', 'TPL_DOCX_B64')

embed_template('InformeTSP.pptx', 'tpl-pptx.js', 'TPL_PPTX_B64')

```


### Helper indispensable para decodificar:


```javascript

function b64ToU8(b64){

  const bin = atob(b64);

  const u8 = new Uint8Array(bin.length);

  for(let i = 0; i < bin.length; i++) u8[i] = bin.charCodeAt(i);

  return u8;

}

```


---


## PARTE 2: ERROR CRITICO ENCONTRADO — Basura JavaScript rompe TODOS los botones


### Sintoma:

Ningun boton funciona (ni "Rellenar con ejemplo", ni "Descargar Word", ni "Limpiar"). El JavaScript se rompe silenciosamente antes de registrar los event listeners.


### Causa raiz:

Al reemplazar `<script>const TPL_DOCX_B64="..."</script>` (base64 inline) por `<script src="tpl-docx.js"></script>`, el base64 anterior quedo como basura en la siguiente linea `<script>`, rompiendo TODO el JavaScript con un error de sintaxis silencioso.


### Antes (ROTO — 137,995 caracteres de basura):


```html

<script src="tpl-docx.js"></script>

<script>B1BLAwQUAAAACAAAACEAmOGOWfsBAACFBAAAEAAAAGRvY1Byb3Bz...137995 chars...

/* ===== HELPERS ===== */

const $=s=>document.querySelector(s);

```


### Despues (CORRECTO):


```html

<script src="tpl-docx.js"></script>

<script>

/* ===== HELPERS ===== */

const $=s=>document.querySelector(s);

```


### Verificacion post-fix:


```python

# El garbage base64 NO debe estar en el HTML inline

assert 'B1BLAwQUAAAACAAAACEAmOGOWfsB' not in html_content

# El JS real debe estar intacto

assert 'function generarWord()' in html_content

assert 'function rellenarDemo()' in html_content

```


---


## PARTE 3: INYECCION EN DOCX (.docx)


### Paso 1 — Analizar la plantilla y mapear indices posicionales


Abrir `InformeTSP.docx` (es un ZIP), extraer `word/document.xml`, y listar todos los textos `<w:t>` en orden:


```python

import zipfile, re


with zipfile.ZipFile('InformeTSP.docx') as z:

    xml = z.read('word/document.xml').decode('utf-8')

    texts = re.findall(r'<w:t[^>]*>([^<]*)</w:t>', xml)

    for i, t in enumerate(texts):

        if t.strip():

            print(f"[{i:3d}] {t.strip()[:100]}")

```


### Paso 2 — Mapa completo de reemplazos posicionales (38 campos)


```javascript

function procesarPlantilla(u8data, formData, rows){

  return JSZip.loadAsync(u8data).then(zip => {

    return zip.file('word/document.xml').async('string').then(xml => {

      const d = formData;

      const fnc = rows.funciones || [];

      const hal = rows.hallazgos || [];

      const pm  = rows.planMejora || [];

      

      const rep = new Map();

      const V = (v) => (v && String(v).trim()) ? String(v).trim() : '';

      const ph = (v, fb) => V(v) || fb;


      /* ═══ CARATULA [indices 5-16] ═══ */

      rep.set(5,  d.titulo || '');

      rep.set(12, d.autor || '');

      rep.set(13, d.correo1 || '');

      rep.set(14, d.asesor ? 'ASESOR: ' + d.asesor : '');

      rep.set(16, d.anio || '2026');


      /* ═══ DEDICATORIA + AGRADECIMIENTO [18, 20] ═══ */

      rep.set(18, ph(d.dedicatoria, '[Espacio para la dedicatoria]'));

      rep.set(20, ph(d.agradecimiento, '[Espacio para redactar los agradecimientos]'));


      /* ═══ RESUMEN + INTRODUCCION [51, 53, 55] ═══ */

      rep.set(51, ph(d.resumen, '[Resumen ejecutivo]'));

      rep.set(53, ph(d.palabrasClave, '[Palabras clave]'));

      rep.set(55, ph(d.introduccion, '[Introduccion]'));


      /* ═══ CAP.1 MARCO TEORICO [64, 66, 68] ═══ */

      rep.set(64, ph(d.basesTeoricas, '[Bases teoricas]'));

      rep.set(66, ph(d.antecedentes, '[Antecedentes]'));

      rep.set(68, ph(d.marcoConceptual, '[Marco conceptual]'));


      /* ═══ CAP.2 CONTEXTO LABORAL [75, 77, 78] ═══ */

      rep.set(75, ph(d.descripOrg, '[Descripcion de la organizacion]'));

      rep.set(77, ph(d.organigrama, '[Organigrama]'));

      rep.set(78, '[Ver anexo adjunto]');


      /* ═══ MANUAL DE FUNCIONES — Tabla dinamica [84-88] ═══ */

      const f0 = fnc[0] || {};

      rep.set(84, ph(f0.cargo, '[Cargo desempenado]'));

      rep.set(85, ph(f0.funcDesc, '[Descripcion de funciones]'));

      rep.set(86, ph(f0.periodo, '[Periodo]'));


      /* ═══ CAP.3 ACTIVIDAD PROFESIONAL [95-119] ═══ */

      rep.set(95, ph(d.target, '[Brief y audiencia objetivo]'));

      rep.set(96, d.target ? 'Target / Perfil del consumidor: ' + d.target : '[Target]');

      rep.set(97, d.forecasting ? 'Forecasting de tendencias: ' + d.forecasting : '[Forecasting]');

      rep.set(98, d.objComercial ? 'Objetivo comercial / creativo: ' + d.objComercial : '[Objetivo comercial]');

      rep.set(103, ph(d.concepto, '[Concepto creativo audiovisual]'));

      rep.set(105, ph(d.paleta, '[Paleta visual y direccion de color]'));

      rep.set(113, ph(d.fase1, '[Fase 1: Preproduccion]'));

      rep.set(115, ph(d.fase2, '[Fase 2: Rodaje]'));

      rep.set(117, ph(d.fase3, '[Fase 3: Posproduccion]'));

      rep.set(119, ph(d.fase4, '[Fase 4: Distribucion]'));


      /* ═══ CAP.4 EVALUACION [136, 143-170] ═══ */

      rep.set(136, ph(d.analisisProductos, '[Analisis de productos desarrollados]'));


      /* Hallazgos: 3 filas x 3 columnas, salto de 4 indices por fila */

      for(let i = 0; i < 3; i++){

        const h = hal[i] || {};

        const base = 143 + i * 4;

        rep.set(base,     ph(h.texto,    '[Hallazgo ' + (i+1) + ']'));

        rep.set(base + 1, ph(h.area,     '[Area]'));

        rep.set(base + 2, ph(h.impacto,  '[Impacto]'));

      }


      /* Plan de mejora: 3 filas x 4 columnas, salto de 4 indices por fila */

      for(let i = 0; i < 3; i++){

        const p = pm[i] || {};

        const base = 159 + i * 4;

        rep.set(base,     ph(p.proceso,   '[Proceso ' + (i+1) + ']'));

        rep.set(base + 1, ph(p.situacion,  '[Situacion actual]'));

        rep.set(base + 2, ph(p.mejora,     '[Mejora propuesta]'));

        rep.set(base + 3, ph(p.plazo,      '[Plazo]'));

      }


      /* ═══ CONCLUSIONES + RECOMENDACIONES [172-183] ═══ */

      rep.set(172, ph(d.conclusion1, '[Conclusion 1]'));

      rep.set(173, ph(d.conclusion2, '[Conclusion 2]'));

      rep.set(174, ph(d.conclusion3, '[Conclusion 3]'));

      rep.set(177, ph(d.recSector,        '[Recomendacion al sector]'));

      rep.set(179, ph(d.recEmpresa,       '[Recomendacion a la empresa]'));

      rep.set(181, ph(d.recProfesionales, '[Recomendacion a profesionales]'));

      rep.set(183, ph(d.recAcademia,      '[Recomendacion a la academia]'));


      /* ═══ APLICAR REEMPLAZOS POSICIONALES ═══ */

      let idx = 0;

      let newXml = xml.replace(/(<w:t[^>]*>)([^<]*)(<\/w:t>)/g, (m, open, body, close) => {

        const rText = rep.get(idx);

        idx++;

        return (rText !== undefined) ? (open + escXML(rText) + close) : m;

      });


      /* ═══ REEMPLAZO ESPECIAL: Referencias APA (bloque completo) ═══ */

      if(d.referencias && d.referencias.trim()){

        const refs = d.referencias.trim().split('\n').filter(l => l.trim());

        let refXml = '';

        refs.forEach(r => {

          refXml += '<w:p><w:pPr><w:spacing w:after="120" w:line="360" w:lineRule="auto"/>'

            + '<w:ind w:left="708" w:hanging="708"/><w:jc w:val="both"/></w:pPr>'

            + '<w:r><w:rPr><w:rFonts w:ascii="Times New Roman" w:hAnsi="Times New Roman"/>'

            + '<w:sz w:val="22"/><w:szCs w:val="22"/></w:rPr>'

            + '<w:t xml:space="preserve">' + escXML(r.trim()) + '</w:t></w:r></w:p>';

        });

        const refStart = newXml.indexOf('>Bruzzi, S.');

        if(refStart > 0){

          const pStart = newXml.lastIndexOf('<w:p ', refStart);

          const anexosIdx = newXml.indexOf('ANEXOS O APENDICES');

          const pEnd = newXml.lastIndexOf('</w:p>', anexosIdx) + 6;

          if(pStart > 0 && pEnd > pStart){

            newXml = newXml.substring(0, pStart) + refXml + newXml.substring(pEnd);

          }

        }

      }


      zip.file('word/document.xml', newXml);

      return zip.generateAsync({type:'uint8array', compression:'DEFLATE', compressionOptions:{level:6}});

    });

  });

}

```


### Helper escXML (escapar XML):


```javascript

function escXML(s){

  return String(s || '').replace(/&/g,'&amp;')

                        .replace(/</g,'&lt;')

                        .replace(/>/g,'&gt;')

                        .replace(/"/g,'&quot;')

                        .replace(/'/g,'&apos;');

}

```


---


## PARTE 4: INYECCION EN PPTX (.pptx)


### Estrategia identica: reemplazo posicional por slide


```javascript

function generarPPTX(){

  /* Validaciones minimas */

  const d = recolectar();

  const err = validarGeneracion(d);

  if(err){ mostrarToast('✕ ' + err, 'err'); return; }

  

  mostrarToast('Generando presentacion PPTX...', 'ai');

  

  const u8tpl = b64ToU8(TPL_PPTX_B64);

  

  if(typeof JSZip === 'undefined'){

    cargarJSZip(() => procesarPlantillaPPTX(u8tpl, d));

  } else {

    procesarPlantillaPPTX(u8tpl, d);

  }

}


function procesarPlantillaPPTX(u8tpl, d){

  JSZip.loadAsync(u8tpl).then(zip => {

    const repMap = pptxSlideReplacements(d, State.rows);

    

    /* Reemplazar textos en cada slide (1-15) */

    const slidePromises = [];

    for(let n = 1; n <= 15; n++){

      const file = zip.file('ppt/slides/slide' + n + '.xml');

      if(!file) continue;

      slidePromises.push(

        file.async('string').then(xml => {

          const newXml = pptxApplyReplacements(xml, repMap[n] || []);

          zip.file('ppt/slides/slide' + n + '.xml', newXml);

        })

      );

    }

    return Promise.all(slidePromises).then(() => zip);

  }).then(zip => {

    return zip.generateAsync({

      type: 'blob',

      mimeType: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',

      compression: 'DEFLATE',

      compressionOptions: {level: 6}

    });

  }).then(blob => {

    const a = document.createElement('a');

    a.href = URL.createObjectURL(blob);

    const ap = (d.autor || 'TSP').split(/\s+/)[0].replace(/[^a-zA-Z0-9_-]/g, '') || 'TSP';

    a.download = 'SustentacionTSP_ComAudiovisual_' + ap + '_' + (d.anio || '2026') + '.pptx';

    document.body.appendChild(a); a.click(); a.remove();

    setTimeout(() => URL.revokeObjectURL(a.href), 3000);

    mostrarToast('✓ Presentacion PPTX generada', 'ok');

  }).catch(e => {

    console.error(e);

    mostrarToast('✕ Error: ' + e.message, 'err');

  });

}


/* Reemplazo posicional en slides PPTX

   En PPTX los textos estan en <a:t>...</a:t> (DrawingML) */

function pptxApplyReplacements(xml, repls){

  const map = new Map(repls.map(r => [r[0], r[1]]));

  let i = 0;

  return xml.replace(/(<a:t[^>]*>)([^<]*)(<\/a:t>)/g, (m, open, body, close) => {

    const t = map.get(i);

    i++;

    return (t === undefined) ? m : (open + escXML(t) + close);

  });

}


/* Mapa de reemplazos por slide */

function pptxSlideReplacements(d, rows){

  const ph = (v, fb) => (v && !esPlaceholder(v)) ? String(v).trim() : fb;

  const ap = (d.autor || 'Autor').split(/\s+/).slice(0, 2).join(' ');

  const footer = ap + ' · TSP ComAudiovisual IDC · ' + (d.anio || '2026');

  

  return {

    1:  [[2, ph(d.titulo, '[Titulo del TSP]')],

         [3, (d.autor || '') + (d.autor2 ? ' · ' + d.autor2 : '')],

         [4, 'Asesor: ' + (d.asesor || '[Asesor]') + ' · ' + (d.semestre || '') + ' · Lima – Peru ' + (d.anio || '2026')]],

    2:  [[2, ph(d.dedicatoria, '[Dedicatoria]')], [3, footer]],

    3:  [[2, ph(d.agradecimiento, '[Agradecimiento]')], [4, footer]],

    4:  [[2, ph(d.resumen, '[Resumen ejecutivo]')],

         [3, 'Palabras clave: ' + ph(d.palabrasClave, '[...]')],

         [5, footer]],

    5:  [[2, ph(d.introduccion, '[Introduccion]')], [3, footer]],

    6:  [[2, ph(d.basesTeoricas, '[Bases teoricas]')],

         [3, ph(d.antecedentes, '[Antecedentes]')],

         [4, ph(d.marcoConceptual, '[Marco conceptual]')],

         [6, footer]],

    7:  [[2, ph(d.descripOrg, '[Descripcion de la organizacion]')],

         [3, ph(d.organigrama, '[Organigrama]')],

         [5, footer]],

    8:  [[2, ph(d.target, '[Brief y audiencia]')],

         [3, ph(d.forecasting, '[Forecasting]')],

         [4, ph(d.objComercial, '[Objetivo comercial]')],

         [6, footer]],

    9:  [[2, ph(d.concepto, '[Concepto creativo]')],

         [3, ph(d.paleta, '[Paleta visual]')],

         [4, ph(d.texturas, '[Texturas y materiales]')],

         [6, footer]],

    10: [[2, ph(d.fase1, '[Fase 1: Preproduccion]')],

         [3, ph(d.fase2, '[Fase 2: Rodaje]')],

         [5, footer]],

    11: [[2, ph(d.fase3, '[Fase 3: Posproduccion]')],

         [3, ph(d.fase4, '[Fase 4: Distribucion]')],

         [4, ph(d.evidencias, '[Evidencias]')],

         [6, footer]],

    12: [[2, ph(d.analisisProductos, '[Analisis de productos]')],

         [3, (rows.hallazgos || []).map(h => h.texto).join(' · ')],

         [5, footer]],

    13: [[2, ph(d.conclusion1, '[Conclusion 1]')],

         [3, ph(d.conclusion2, '[Conclusion 2]')],

         [4, ph(d.conclusion3, '[Conclusion 3]')],

         [6, footer]],

    14: [[2, ph(d.recSector, '[Rec. al sector]')],

         [3, ph(d.recEmpresa, '[Rec. a la empresa]')],

         [4, ph(d.recProfesionales, '[Rec. a profesionales]')],

         [5, ph(d.recAcademia, '[Rec. a la academia]')],

         [7, footer]],

    15: [[2, 'REFERENCIAS BIBLIOGRAFICAS'],

         [3, (d.referencias || '').split('\n').slice(0, 3).join('\n')],

         [5, footer]],

  };

}

```


---


## PARTE 5: VALIDACIONES PRE-GENERACION


```javascript

function validarGeneracion(d){

  if(!d.titulo || d.titulo.trim().length < 5)

    return 'El titulo del TSP es obligatorio (minimo 5 caracteres)';

  if(d.titulo.trim().split(/\s+/).length > 25)

    return 'El titulo no debe superar 25 palabras';

  if(!d.autor || d.autor.trim().split(/\s+/).length < 2)

    return 'Debe registrar al menos un autor con nombre completo';

  if(!d.asesor || !d.asesor.trim())

    return 'El nombre del asesor es obligatorio';

  if(!d.resumen || !d.resumen.trim())

    return 'El Resumen Ejecutivo es obligatorio';

  if(d.resumen.trim().split(/\s+/).length > 250)

    return 'El Resumen no debe superar 250 palabras';

  if(State.rows.funciones.filter(r => r.cargo || r.funcDesc).length < 1)

    return 'Debe registrar al menos una funcion en el Manual de Funciones';

  return null; // Todo OK

}

```


---


## PARTE 6: FORMULARIO HTML (40+ campos, 8 pasos)


```

PASO 1 — Caratula:        titulo, autor, dni, correo1, autor2, dni2, correo2, asesor, anio, semestre, linea

PASO 2 — Dedicatoria:     dedicatoria, agradecimiento

PASO 3 — Resumen:         resumen, palabrasClave, introduccion

PASO 4 — Cap.1:           basesTeoricas, antecedentes, marcoConceptual

PASO 5 — Cap.2:           descripOrg, organigrama + tabla funciones[{cargo, funcDesc, periodo}]

PASO 6 — Cap.3:           target, forecasting, objComercial, concepto, paleta, texturas, materiales, 

                          avios, tecnologia, fase1, fase2, fase3, fase4, evidencias

PASO 7 — Cap.4:           analisisProductos + tabla hallazgos[{num, texto, area, impacto}] 

                          + tabla planMejora[{proceso, situacion, mejora, plazo}]

PASO 8 — Cierre:          conclusion1, conclusion2, conclusion3, recSector, recEmpresa, 

                          recProfesionales, recAcademia, referencias

```


---


## PARTE 7: ESTRUCTURA DE ARCHIVOS FINAL


```

/output/

|-- index.html          # PWA principal (~305 KB limpio)

|-- tpl-docx.js         # InformeTSP.docx embebido en base64 (~173 KB)

|-- tpl-pptx.js         # InformeTSP.pptx embebido en base64

|-- sw.js               # Service Worker (opcional)

|-- manifest.json       # Manifest PWA (opcional)

|-- Logo_IDC.png        # Logo del IDC

`-- InformeTSP.docx     # Plantilla original (referencia)

```


---


## PARTE 8: REGLAS DE ORO


| # | Regla | Por que |

|---|-------|---------|

| 1 | **NUNCA `fetch()`** para plantillas | Falla con `file:///` -> embeber base64 en `.js` |

| 2 | **NUNCA buscar/reemplazar por texto** en XML | Usar indices posicionales de `<w:t>` / `<a:t>` |

| 3 | **SIEMPRE escapar XML** con `escXML()` | Evita romper el XML con `&`, `<`, `>` del usuario |

| 4 | **SIEMPRE preservar** archivos internos no modificados | No tocar `media/`, `theme/`, `styles.xml`, etc. |

| 5 | **SIEMPRE usar `DEFLATE`** al recomprimir el ZIP | Compresion requerida por Office |

| 6 | **SIEMPRE limpiar** basura al cambiar de estrategia | El base64 inline antiguo rompio TODO el JS |

| 7 | **SIEMPRE revocar** `URL.createObjectURL()` | `setTimeout(() => URL.revokeObjectURL(href), 3000)` |


---


## PARTE 9: CHECKLIST DE VERIFICACION


- [ ] DOCX: Analizar plantilla y mapear todos los indices `<w:t>`

- [ ] DOCX: Crear `tpl-docx.js` con base64 embebido

- [ ] DOCX: Implementar reemplazos posicionales con `Map()`

- [ ] DOCX: Implementar reemplazo de bloque para referencias APA

- [ ] PPTX: Analizar plantilla y mapear indices `<a:t>` por slide

- [ ] PPTX: Crear `tpl-pptx.js` con base64 embebido

- [ ] PPTX: Implementar `pptxApplyReplacements()` posicional

- [ ] CRITICO: Verificar que NO haya basura base64 en el `<script>` inline

- [ ] CRITICO: Verificar que `TPL_DOCX_B64` se carga desde `tpl-docx.js` externo

- [ ] Validaciones: Verificar campos obligatorios antes de generar

- [ ] Test: "Rellenar con ejemplo" -> llena todos los campos

- [ ] Test: "Descargar Word" -> genera .docx con datos inyectados

- [ ] Test: "Descargar PPTX" -> genera .pptx con datos inyectados

- [ ] Test: Verificar que estilos, imagenes y tablas se preservan intactos


---


## PARTE 10: FLUJO DE DATOS COMPLETO


```

+-----------------+     +------------------+

|  FORMULARIO HTML |---->|  recolectar()    |

|  (40+ campos)    |     |  (validaciones)  |

+-----------------+     +--------+---------+

                                 |

                    +------------+------------+

                    |            |            |

                    v            v            v

              +---------+  +---------+  +---------+

              | tpl-docx|  | tpl-pptx|  |   UI    |

              | .js     |  | .js     |  | toast   |

              +----+----+  +----+----+  +---------+

                   |            |

              +----+----+  +----+----+

              |b64ToU8()|  |b64ToU8()|

              +----+----+  +----+----+

                   |            |

              +----+----+  +----+----+

              |JSZip    |  |JSZip    |

              |loadAsync|  |loadAsync|

              +----+----+  +----+----+

                   |            |

              +----+---------+ +--+---------+

              |reemplazos    | |reemplazos  |

              |posicionales  | |por slide   |

              |Map() <w:t>   | |Map() <a:t> |

              +----+---------+ +----+-------+

                   |               |

              +----+---------+ +----+-------+

              |generateAsync | |generateAsync|

              |(uint8array)  | |(blob)       |

              +----+---------+ +----+-------+

                   |               |

              +----+---------+ +----+-------+

              |Blob +        | |Blob +      |

              |descarga      | |descarga    |

              |.docx         | |.pptx       |

              +--------------+ +-------------+

```


Comentarios

Entradas populares de este blog

Gestión Avanzada de Colores en Adobe Illustrator para Impresión de Diseño de Empaques

Compaginación de Documentos en Adobe InDesign para Impresión Profesional

Diseños Institucionales con Adobe Illustrator: Uso de Data Merge y Scripts