{"id":30,"date":"2025-11-03T15:05:40","date_gmt":"2025-11-03T15:05:40","guid":{"rendered":"https:\/\/dte.meshti.cl\/?page_id=30"},"modified":"2025-11-03T16:56:18","modified_gmt":"2025-11-03T16:56:18","slug":"verificador-de-boletas","status":"publish","type":"page","link":"https:\/\/dte.meshti.cl\/index.php\/verificador-de-boletas\/","title":{"rendered":"Verificador de Boletas"},"content":{"rendered":"\n<div id=\"verificador-boletas\" style=\"font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial;margin:16px;\">\n  <h2>Verificador de Boletas (PDF417)<\/h2>\n  <p>Sube una imagen que contenga el c\u00f3digo PDF417 o toma una foto desde la c\u00e1mara.<\/p>\n\n  <div style=\"display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px;\">\n    <input type=\"file\" id=\"fileInput\" accept=\"image\/*\" \/>\n    <button id=\"btnCamera\">Abrir c\u00e1mara (foto)<\/button>\n    <button id=\"btnParseRaw\" style=\"display:none;\">Intentar parsear RAW<\/button>\n    <button id=\"btnDownload\" style=\"display:none;\">Descargar JSON<\/button>\n  <\/div>\n\n  <div id=\"cameraContainer\" style=\"display:none;margin-bottom:8px;\">\n    <video id=\"video\" autoplay playsinline style=\"max-width:100%;border:1px solid #ddd;border-radius:6px;\"><\/video>\n    <div style=\"margin-top:6px;\">\n      <button id=\"captureBtn\">Tomar foto<\/button>\n      <button id=\"closeCamBtn\">Cerrar c\u00e1mara<\/button>\n    <\/div>\n  <\/div>\n\n  <div style=\"display:flex;gap:16px;flex-wrap:wrap;\">\n    <div style=\"flex:1;min-width:280px;\">\n      <h3>Resultado decodificado<\/h3>\n      <pre id=\"rawOutput\" style=\"white-space:pre-wrap;background:#f8f8f8;padding:8px;border-radius:6px;min-height:120px;\"><\/pre>\n    <\/div>\n\n    <div style=\"flex:1;min-width:280px;\">\n      <h3>Campos extra\u00eddos<\/h3>\n      <table id=\"fieldsTable\" style=\"width:100%;border-collapse:collapse;\">\n        <thead><tr><th style=\"text-align:left;padding:6px;border-bottom:1px solid #ddd\">Campo<\/th><th style=\"text-align:left;padding:6px;border-bottom:1px solid #ddd\">Valor<\/th><\/tr><\/thead>\n        <tbody><\/tbody>\n      <\/table>\n    <\/div>\n  <\/div>\n\n  <p style=\"margin-top:12px;color:#666;font-size:0.95em;\">\n    Nota: este m\u00f3dulo decodifica en el navegador. Para validar contra servicios del SII necesitar\u00e1s un backend que maneje autenticaci\u00f3n y certificados.\n  <\/p>\n<\/div>\n\n<!-- Librer\u00eda de PDF417 (cliente) -->\n<script src=\"https:\/\/unpkg.com\/pdf417-js@0.1.7\/dist\/pdf417.min.js\"><\/script>\n\n<script>\n\/*\n  M\u00f3dulo: Decodificador PDF417 + parser b\u00e1sico\n  - Pega todo en un bloque HTML de WordPress.\n  - Adaptalo para conectar a WS\/SIIs cuando tengas backend.\n*\/\n\n(() => {\n  const fileInput = document.getElementById('fileInput');\n  const rawOutput = document.getElementById('rawOutput');\n  const fieldsTableBody = document.querySelector('#fieldsTable tbody');\n  const btnCamera = document.getElementById('btnCamera');\n  const cameraContainer = document.getElementById('cameraContainer');\n  const video = document.getElementById('video');\n  const captureBtn = document.getElementById('captureBtn');\n  const closeCamBtn = document.getElementById('closeCamBtn');\n  const btnDownload = document.getElementById('btnDownload');\n  const btnParseRaw = document.getElementById('btnParseRaw');\n\n  let lastParsed = null;\n  let streamRef = null;\n\n  \/\/ Util: limpiar UI\n  function clearUI() {\n    rawOutput.textContent = '';\n    fieldsTableBody.innerHTML = '';\n    btnDownload.style.display = 'none';\n    btnParseRaw.style.display = 'none';\n    lastParsed = null;\n  }\n\n  \/\/ Util: mostrar campos en la tabla\n  function showFields(obj) {\n    fieldsTableBody.innerHTML = '';\n    const keys = Object.keys(obj || {});\n    if (!keys.length) {\n      const tr = document.createElement('tr');\n      tr.innerHTML = `<td colspan=\"2\" style=\"padding:8px;color:#666\">No se extrajeron campos reconocibles.<\/td>`;\n      fieldsTableBody.appendChild(tr);\n      return;\n    }\n    keys.forEach(k => {\n      const tr = document.createElement('tr');\n      tr.innerHTML = `<td style=\"padding:6px;border-bottom:1px solid #f0f0f0;width:35%\">${k}<\/td><td style=\"padding:6px;border-bottom:1px solid #f0f0f0\">${escapeHtml(String(obj[k] ?? ''))}<\/td>`;\n      fieldsTableBody.appendChild(tr);\n    });\n  }\n\n  \/\/ Escape b\u00e1sico para inyecci\u00f3n de HTML en la tabla\n  function escapeHtml(str) {\n    return str.replace(\/[&<>\"'`]\/g, s => ({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;','`':'&#96;'}[s]));\n  }\n\n  \/\/ Funci\u00f3n principal: recibe ImageData o canvas y trata de decodificar PDF417\n  async function decodeFromCanvas(canvas) {\n    clearUI();\n    try {\n      const ctx = canvas.getContext('2d');\n      const imgData = ctx.getImageData(0,0,canvas.width,canvas.height);\n      \/\/ pdf417-js devuelve un array de objetos decodificados\n      const decoded = PDF417.decode(imgData);\n      if (!decoded || decoded.length === 0) {\n        rawOutput.textContent = 'No se encontr\u00f3 PDF417 en la imagen.';\n        return null;\n      }\n      \/\/ Tomamos el primer resultado\n      const text = decoded[0].data;\n      rawOutput.textContent = text;\n      btnParseRaw.style.display = 'inline-block';\n      \/\/ Intentamos parsear autom\u00e1ticamente\n      const parsed = tryParseAsXMLorKV(text);\n      lastParsed = parsed;\n      showFields(parsed.extracted || {});\n      btnDownload.style.display = 'inline-block';\n      return { raw: text, parsed: parsed };\n    } catch (err) {\n      rawOutput.textContent = 'Error al decodificar: ' + err.message;\n      return null;\n    }\n  }\n\n  \/\/ Handler: archivo subido\n  fileInput.addEventListener('change', async (e) => {\n    if (!e.target.files || !e.target.files[0]) return;\n    const file = e.target.files[0];\n    const img = new Image();\n    img.src = URL.createObjectURL(file);\n    img.onload = async () => {\n      const canvas = document.createElement('canvas');\n      \/\/ reescalamos si muy grande para evitar l\u00edmites\n      const maxDim = 1600;\n      let w = img.width, h = img.height;\n      if (Math.max(w,h) > maxDim) {\n        const ratio = maxDim \/ Math.max(w,h);\n        w = Math.round(w*ratio);\n        h = Math.round(h*ratio);\n      }\n      canvas.width = w; canvas.height = h;\n      const ctx = canvas.getContext('2d');\n      ctx.drawImage(img, 0, 0, w, h);\n      await decodeFromCanvas(canvas);\n      URL.revokeObjectURL(img.src);\n    };\n    img.onerror = () => {\n      rawOutput.textContent = 'No se pudo cargar la imagen.';\n    };\n  });\n\n  \/\/ C\u00e1mara: abrir\n  btnCamera.addEventListener('click', async () => {\n    clearUI();\n    cameraContainer.style.display = 'block';\n    try {\n      const s = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' }, audio: false });\n      streamRef = s;\n      video.srcObject = s;\n    } catch (err) {\n      cameraContainer.style.display = 'none';\n      alert('No se pudo acceder a la c\u00e1mara: ' + err.message);\n    }\n  });\n\n  captureBtn.addEventListener('click', async () => {\n    if (!video.videoWidth) {\n      alert('La c\u00e1mara a\u00fan no est\u00e1 lista.');\n      return;\n    }\n    const canvas = document.createElement('canvas');\n    \/\/ ajustar a resoluci\u00f3n del video\n    canvas.width = video.videoWidth;\n    canvas.height = video.videoHeight;\n    const ctx = canvas.getContext('2d');\n    ctx.drawImage(video, 0, 0, canvas.width, canvas.height);\n    await decodeFromCanvas(canvas);\n  });\n\n  closeCamBtn.addEventListener('click', () => {\n    if (streamRef) {\n      streamRef.getTracks().forEach(t => t.stop());\n      streamRef = null;\n    }\n    cameraContainer.style.display = 'none';\n  });\n\n  \/\/ Intentar parsear raw si no se extrajo\n  btnParseRaw.addEventListener('click', () => {\n    if (!rawOutput.textContent) return alert('No hay texto decodificado.');\n    const parsed = tryParseAsXMLorKV(rawOutput.textContent);\n    lastParsed = parsed;\n    showFields(parsed.extracted || {});\n  });\n\n  \/\/ Descargar JSON\n  btnDownload.addEventListener('click', () => {\n    if (!lastParsed) return alert('No hay datos para descargar.');\n    const blob = new Blob([JSON.stringify(lastParsed, null, 2)], { type: 'application\/json' });\n    const url = URL.createObjectURL(blob);\n    const a = document.createElement('a');\n    a.href = url;\n    a.download = 'verificador_boleta.json';\n    document.body.appendChild(a);\n    a.click();\n    a.remove();\n    URL.revokeObjectURL(url);\n  });\n\n  \/\/ Parser robusto: intenta XML -> key:value heur\u00edstico -> regex de DTE si aplica\n  function tryParseAsXMLorKV(text) {\n    const result = { mode: null, raw: text, extracted: {} };\n\n    \/\/ 1) Intentar parseo como XML\n    try {\n      const parser = new DOMParser();\n      const xml = parser.parseFromString(text, 'text\/xml');\n      \/\/ comprobar si es XML v\u00e1lido\n      if (xml && xml.getElementsByTagName('parsererror').length === 0) {\n        result.mode = 'xml';\n        const extracted = extractFieldsFromXML(xml);\n        result.extracted = extracted;\n        return result;\n      }\n    } catch (e) {\n      \/\/ continuar\n    }\n\n    \/\/ 2) Intentar parseo por pares clave=valor (ej: \"RUTEmisor:1234|Folio:1|...\")\n    if (text.includes(':') && (text.includes('|') || text.includes(';') || text.includes(','))) {\n      result.mode = 'kv';\n      const kv = parseKV(text);\n      result.extracted = postProcessExtracted(kv);\n      return result;\n    }\n\n    \/\/ 3) Buscar patrones t\u00edpicos: RUT (12345678-9), Fecha ISO, Monto\n    result.mode = 'heuristic';\n    const heur = heuristicExtract(text);\n    result.extracted = postProcessExtracted(heur);\n    return result;\n  }\n\n  \/\/ Extrae campos desde XML con tags conocidos (intentar acomodo a etiquetas SII)\n  function extractFieldsFromXML(xmlDoc) {\n    const map = {};\n    \/\/ Campos t\u00edpicos que buscamos (intentar muchas etiquetas posibles)\n    const tryTags = {\n      version: ['Version', 'version', 'VersionTimbre', 'versionTimbre'],\n      rutEmisor: ['RUTEmisor','RutEmisor','RutEmisorDTE','EmisorRut','RUTEmisorDTE'],\n      tipoDocumento: ['TipoDTE','TipoDocumento','Tipo','TpoDoc'],\n      folio: ['Folio','folio','Numero','NroFolio'],\n      fechaEmision: ['FechaEmision','fechaEmision','Fecha','FechaEmi'],\n      rutReceptor: ['RUTRecep','RutReceptor','RUTReceptor','ReceptorRut'],\n      razonSocial: ['RznSocRecep','RazonSocial','Razon','RazonSocialReceptor','RznSoc'],\n      montoTotal: ['MntTotal','MontoTotal','Total','MntNeto'],\n      descripcionItem: ['Descripcion','Desc','Detalle','DescripcionItem'],\n      fechaHoraTimbre: ['FechaHoraTimbre','FechaTimbrado','FechaGeneracion'],\n      CAF: ['CAF','CodigoCAF','CodigoAutorizacion'],\n      algoritmoFirma: ['Algoritmo','AlgFirma','AlgoritmoFirma'],\n      firmaDigital: ['Firma','FirmaDigital','Signature']\n    };\n    for (const key in tryTags) {\n      const tags = tryTags[key];\n      for (const tag of tags) {\n        const nodes = xmlDoc.getElementsByTagName(tag);\n        if (nodes && nodes.length) {\n          map[key] = nodes[0].textContent.trim();\n          break;\n        }\n      }\n    }\n    \/\/ Si no extrajo razonSocial, intentar obtener <RUTRecep> y buscar atributo 'RznSoc' en nodos relacionados\n    \/\/ Buscar primer item de detalle\n    const itemDesc = xmlDoc.getElementsByTagName('Descripcion')[0] || xmlDoc.getElementsByTagName('Detalle')[0];\n    if (itemDesc && itemDesc.textContent) {\n      map.descripcionItem = map.descripcionItem || itemDesc.textContent.trim().slice(0,40);\n    }\n    return postProcessExtracted(map);\n  }\n\n  \/\/ Parser de pares key:value separado por delimitadores\n  function parseKV(text) {\n    const out = {};\n    \/\/ Normalizar separadores comunes\n    const sep = text.includes('|') ? '|' : (text.includes(';') ? ';' : (text.includes(',') ? ',' : '\\n'));\n    const parts = text.split(sep).map(s => s.trim()).filter(Boolean);\n    parts.forEach(p => {\n      const sep2 = p.includes(':') ? ':' : (p.includes('=') ? '=' : null);\n      if (!sep2) return;\n      const [k,v] = p.split(sep2).map(x => x.trim());\n      if (k && v) out[k.replace(\/\\s+\/g,'_')] = v;\n    });\n    return out;\n  }\n\n  \/\/ Heur\u00edsticas: buscar RUTs, fechas, montos\n  function heuristicExtract(text) {\n    const out = {};\n    const rutRegex = \/(\\d{1,3}(?:\\.\\d{3})*-\\d|[0-9]{7,8}-[0-9Kk])\/g;\n    const dateRegex = \/\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\/g;\n    const simpleDate = \/\\d{4}-\\d{2}-\\d{2}\/g;\n    const money = \/(\\d{1,3}(?:[.,]\\d{3})*(?:[.,]\\d{2})?)\/g;\n\n    const rutMatch = text.match(rutRegex);\n    if (rutMatch) out.rutEmisor = rutMatch[0];\n\n    const dateMatch = text.match(dateRegex) || text.match(simpleDate);\n    if (dateMatch) out.fechaEmision = dateMatch[0];\n\n    const moneyMatch = text.match(money);\n    if (moneyMatch) out.montoTotal = moneyMatch[moneyMatch.length-1].replace(\/[.,]\/g,'');\n\n    \/\/ Folio: buscar \"Folio\" seguido de n\u00famero\n    const folioMatch = text.match(\/Folio\\s*[:\\-]?\\s*(\\d{1,10})\/i);\n    if (folioMatch) out.folio = folioMatch[1];\n\n    return out;\n  }\n\n  \/\/ Post-procesar campos (normalizar nombres y validar RUT)\n  function postProcessExtracted(obj) {\n    const out = {};\n    \/\/ Normalizar claves\n    const mapping = {\n      rutEmisor: ['rutEmisor','RUTEmisor','RutEmisor','RUTEmisorDTE','EmisorRut'],\n      rutReceptor: ['rutReceptor','RUTReceptor','RutReceptor','RUTRecep','RutRecep'],\n      montoTotal: ['montoTotal','MntTotal','MontoTotal','Total','monto','Mnt'],\n      folio: ['folio','Folio','FolioDocumento','NroFolio'],\n      fechaEmision: ['fechaEmision','FechaEmision','Fecha','FechaEmi'],\n      razonSocial: ['razonSocial','RazonSocial','RznSoc','RznSocRecep']\n    };\n\n    \/\/ invertir mapping para lookup\n    const invert = {};\n    for (const k in mapping) mapping[k].forEach(n => invert[n.toLowerCase()] = k);\n\n    for (const k in obj) {\n      const lk = k.toLowerCase();\n      const target = invert[lk] || lk;\n      out[target] = obj[k];\n    }\n\n    \/\/ Validaciones b\u00e1sicas\n    if (out.rutEmisor) out.rutEmisor_valid = validarRUT(out.rutEmisor);\n    if (out.rutReceptor) out.rutReceptor_valid = validarRUT(out.rutReceptor);\n    return out;\n  }\n\n  \/\/ Validaci\u00f3n RUT chileno (tolerante a puntos y may\u00fasculas)\n  function validarRUT(rut) {\n    if (!rut) return false;\n    try {\n      const clean = rut.replace(\/\\.\/g,'').replace(\/\\s+\/g,'').toUpperCase();\n      const parts = clean.split('-');\n      if (parts.length !== 2) return false;\n      const num = parts[0];\n      const dv = parts[1];\n      let sum = 0, mul = 2;\n      for (let i = num.length-1; i >=0; i--) {\n        sum += parseInt(num.charAt(i),10) * mul;\n        mul = mul === 7 ? 2 : mul + 1;\n      }\n      const res = 11 - (sum % 11);\n      const dvCalc = res === 11 ? '0' : (res === 10 ? 'K' : String(res));\n      return dvCalc === dv;\n    } catch (e) {\n      return false;\n    }\n  }\n\n  \/\/ Mostrar dataset inicial vac\u00edo\n  clearUI();\n\n  \/\/ Al terminar parseado con \u00e9xito, habilitar descarga\n  \/\/ Observador simple: si lastParsed se setea, mostrar bot\u00f3n descargar\n  const observer = new MutationObserver(() => {\n    if (lastParsed) btnDownload.style.display = 'inline-block';\n  });\n  observer.observe(rawOutput, { childList: true, subtree: true });\n\n  \/\/ NOTA: aqu\u00ed podemos exponer funciones p\u00fablicas si quieres extender luego.\n  window.VerificadorBoletas = {\n    tryParseAsXMLorKV,\n    decodeFromCanvas\n  };\n\n})();\n<\/script>\n\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Verificador de Boletas (PDF417) Sube una imagen que contenga el c\u00f3digo PDF417 o toma una foto desde la c\u00e1mara. Abrir c\u00e1mara (foto) Intentar parsear RAW Descargar JSON Tomar foto Cerrar c\u00e1mara Resultado decodificado Campos extra\u00eddos Campo Valor Nota: este m\u00f3dulo decodifica en el navegador. Para validar contra servicios del SII necesitar\u00e1s un backend que maneje [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-30","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/dte.meshti.cl\/index.php\/wp-json\/wp\/v2\/pages\/30","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/dte.meshti.cl\/index.php\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/dte.meshti.cl\/index.php\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/dte.meshti.cl\/index.php\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/dte.meshti.cl\/index.php\/wp-json\/wp\/v2\/comments?post=30"}],"version-history":[{"count":5,"href":"https:\/\/dte.meshti.cl\/index.php\/wp-json\/wp\/v2\/pages\/30\/revisions"}],"predecessor-version":[{"id":38,"href":"https:\/\/dte.meshti.cl\/index.php\/wp-json\/wp\/v2\/pages\/30\/revisions\/38"}],"wp:attachment":[{"href":"https:\/\/dte.meshti.cl\/index.php\/wp-json\/wp\/v2\/media?parent=30"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}