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

Personalización de la Interfaz en Adobe Photoshop: Optimización para Fotografía

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