Guest User

svg_ttf_glyph_extractor.html

a guest
Jul 2nd, 2025
46
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
HTML 12.64 KB | Source Code | 0 0
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4.   <meta charset="UTF-8">
  5.   <title>Font Glyph Extractor</title>
  6.  
  7.   <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono&display=swap" rel="stylesheet">
  8.  
  9.   <style>
  10.     /* === SCROLLBAR === */
  11.     ::-webkit-scrollbar          { width:10px; height:10px; }
  12.     ::-webkit-scrollbar-track    { background:#1a1a1a; }
  13.     ::-webkit-scrollbar-thumb    { background:#444; border-radius:5px; }
  14.     ::-webkit-scrollbar-thumb:hover { background:#666; }
  15.  
  16.     /* === PAGE LAYOUT === */
  17.     html,body{ margin:0; padding:0; background:#111; color:#ddd;
  18.                font-family:'JetBrains Mono', monospace; overflow-y:scroll; }
  19.     h1{ margin:1.2em 0 0.6em; text-align:center; }
  20.  
  21.     /* === DROP-ZONE === */
  22.     #dropzone{
  23.       width:calc(100vw - 4em); max-width:1600px; margin:0 auto 1.6em;
  24.       padding:2em; box-sizing:border-box;
  25.       border:2px dashed #555; border-radius:8px; background:#1a1a1a;
  26.       text-align:center; cursor:pointer; transition:background .25s;
  27.     }
  28.     #dropzone.hover{ background:#222; }
  29.     #fileInput{ display:none; }
  30.  
  31.     /* === TOP CONTROLS === */
  32.     .controls{ width:100%; display:flex; justify-content:center; gap:1em; margin-bottom:1em; }
  33.     .controls button, .copy-btn{
  34.       background:#444; color:#fff; border:none; border-radius:4px;
  35.       padding:0.6em 1.2em; font-size:1em; cursor:pointer; transition:background .25s;
  36.     }
  37.     .controls button:hover, .copy-btn:hover{ background:#666; }
  38.  
  39.     /* === OUTPUT AREA === */
  40.     #outputs{ width:100%; }
  41.     .output-box{
  42.       position:relative; width:calc(100vw - 4em); max-width:1600px;
  43.       margin:0.8em auto; padding:1em 2em 2em; box-sizing:border-box;
  44.       background:#222; border:1px solid #333; border-radius:8px;
  45.     }
  46.     .output-box.dragging{ opacity:0.35; }
  47.     .output-box.dragover{ outline:2px dashed #888; }
  48.  
  49.     .output-box pre{ margin:0; overflow-x:auto; white-space:pre; }
  50.  
  51.     .copy-btn{
  52.       position:absolute; top:8px; right:8px;
  53.       padding:0.35em 0.7em; font-size:0.85em;
  54.     }
  55.   </style>
  56. </head>
  57.  
  58. <body>
  59.   <h1>Font Glyph Extractor</h1>
  60.  
  61.   <div id="dropzone">
  62.     <p>Drop <code>.ttf</code> or <code>.svg</code> files (multi-select OK) or click to browse</p>
  63.     <input type="file" id="fileInput" multiple>
  64.   </div>
  65.  
  66.   <div class="controls">
  67.     <button id="mergeAllBtn">Merge All</button>
  68.     <button id="clearBtn">Clear All</button>
  69.   </div>
  70.  
  71.   <div id="outputs"></div>
  72.  
  73.   <script src="https://cdn.jsdelivr.net/npm/opentype.js@latest/dist/opentype.min.js"></script>
  74.   <script>
  75.     /* ───────────────────────────────────────────────────────── helpers */
  76.     const INDENT = '    ';
  77.  
  78.     /* full CSS-3/SVG colour keyword list → rgb triplets */
  79.     const NAMED = {
  80.       aliceblue:'#f0f8ff', antiquewhite:'#faebd7', aqua:'#00ffff', aquamarine:'#7fffd4',
  81.       azure:'#f0ffff', beige:'#f5f5dc', bisque:'#ffe4c4', black:'#000000',
  82.       blanchedalmond:'#ffebcd', blue:'#0000ff', blueviolet:'#8a2be2', brown:'#a52a2a',
  83.       burlywood:'#deb887', cadetblue:'#5f9ea0', chartreuse:'#7fff00', chocolate:'#d2691e',
  84.       coral:'#ff7f50', cornflowerblue:'#6495ed', cornsilk:'#fff8dc', crimson:'#dc143c',
  85.       cyan:'#00ffff', darkblue:'#00008b', darkcyan:'#008b8b', darkgoldenrod:'#b8860b',
  86.       darkgray:'#a9a9a9', darkgreen:'#006400', darkgrey:'#a9a9a9', darkkhaki:'#bdb76b',
  87.       darkmagenta:'#8b008b', darkolivegreen:'#556b2f', darkorange:'#ff8c00',
  88.       darkorchid:'#9932cc', darkred:'#8b0000', darksalmon:'#e9967a',
  89.       darkseagreen:'#8fbc8f', darkslateblue:'#483d8b', darkslategray:'#2f4f4f',
  90.       darkslategrey:'#2f4f4f', darkturquoise:'#00ced1', darkviolet:'#9400d3',
  91.       deeppink:'#ff1493', deepskyblue:'#00bfff', dimgray:'#696969', dimgrey:'#696969',
  92.       dodgerblue:'#1e90ff', firebrick:'#b22222', floralwhite:'#fffaf0', forestgreen:'#228b22',
  93.       fuchsia:'#ff00ff', gainsboro:'#dcdcdc', ghostwhite:'#f8f8ff', gold:'#ffd700',
  94.       goldenrod:'#daa520', gray:'#808080', green:'#008000', greenyellow:'#adff2f',
  95.       grey:'#808080', honeydew:'#f0fff0', hotpink:'#ff69b4', indianred:'#cd5c5c',
  96.       indigo:'#4b0082', ivory:'#fffff0', khaki:'#f0e68c', lavender:'#e6e6fa',
  97.       lavenderblush:'#fff0f5', lawngreen:'#7cfc00', lemonchiffon:'#fffacd',
  98.       lightblue:'#add8e6', lightcoral:'#f08080', lightcyan:'#e0ffff',
  99.       lightgoldenrodyellow:'#fafad2', lightgray:'#d3d3d3', lightgreen:'#90ee90',
  100.       lightgrey:'#d3d3d3', lightpink:'#ffb6c1', lightsalmon:'#ffa07a',
  101.       lightseagreen:'#20b2aa', lightskyblue:'#87cefa', lightslategray:'#778899',
  102.       lightslategrey:'#778899', lightsteelblue:'#b0c4de', lightyellow:'#ffffe0',
  103.       lime:'#00ff00', limegreen:'#32cd32', linen:'#faf0e6', magenta:'#ff00ff',
  104.       maroon:'#800000', mediumaquamarine:'#66cdaa', mediumblue:'#0000cd',
  105.       mediumorchid:'#ba55d3', mediumpurple:'#9370db', mediumseagreen:'#3cb371',
  106.       mediumslateblue:'#7b68ee', mediumspringgreen:'#00fa9a',
  107.       mediumturquoise:'#48d1cc', mediumvioletred:'#c71585', midnightblue:'#191970',
  108.       mintcream:'#f5fffa', mistyrose:'#ffe4e1', moccasin:'#ffe4b5', navajowhite:'#ffdead',
  109.       navy:'#000080', oldlace:'#fdf5e6', olive:'#808000', olivedrab:'#6b8e23',
  110.       orange:'#ffa500', orangered:'#ff4500', orchid:'#da70d6', palegoldenrod:'#eee8aa',
  111.       palegreen:'#98fb98', paleturquoise:'#afeeee', palevioletred:'#db7093',
  112.       papayawhip:'#ffefd5', peachpuff:'#ffdab9', peru:'#cd853f', pink:'#ffc0cb',
  113.       plum:'#dda0dd', powderblue:'#b0e0e6', purple:'#800080', rebeccapurple:'#663399',
  114.       red:'#ff0000', rosybrown:'#bc8f8f', royalblue:'#4169e1', saddlebrown:'#8b4513',
  115.       salmon:'#fa8072', sandybrown:'#f4a460', seagreen:'#2e8b57', seashell:'#fff5ee',
  116.       sienna:'#a0522d', silver:'#c0c0c0', skyblue:'#87ceeb', slateblue:'#6a5acd',
  117.       slategray:'#708090', slategrey:'#708090', snow:'#fffafa', springgreen:'#00ff7f',
  118.       steelblue:'#4682b4', tan:'#d2b48c', teal:'#008080', thistle:'#d8bfd8',
  119.       tomato:'#ff6347', turquoise:'#40e0d0', violet:'#ee82ee', wheat:'#f5deb3',
  120.       white:'#ffffff', whitesmoke:'#f5f5f5', yellow:'#ffff00', yellowgreen:'#9acd32'
  121.     };
  122.  
  123.     function hexToRgb(h){
  124.       if(h.length===4) h='#'+[h[1],h[1],h[2],h[2],h[3],h[3]].join('');
  125.       const n=parseInt(h.slice(1),16);
  126.       return`rgb(${(n>>16)&255},${(n>>8)&255},${n&255})`;
  127.     }
  128.  
  129.     function cssToRGB(str){
  130.       if(!str) return 'rgb(unknown)';
  131.       str=str.trim().toLowerCase();
  132.       if(str.startsWith('#'))        return hexToRgb(str);
  133.       if(str.startsWith('rgb'))      return str.replace(/\s+/g,'');   // keep as-is
  134.       if(NAMED[str])                 return hexToRgb(NAMED[str]);
  135.       return 'rgb(unknown)';         // unsupported pattern e.g. currentColor
  136.     }
  137.  
  138.     /* climb element → root for fill attr / inline style */
  139.     function findFill(el){
  140.       while(el && el.nodeType===1){
  141.        let f=el.getAttribute('fill');
  142.         if(f && f!=='none') return f;
  143.         const style=el.getAttribute('style');
  144.         if(style){
  145.           const m=style.match(/fill\s*:\s*([^;]+)/i);
  146.           if(m && m[1] && m[1]!=='none') return m[1];
  147.         }
  148.         el=el.parentElement;
  149.       }
  150.       return null;
  151.     }
  152.  
  153.     /* ─────────────────────────────────────────────── DOM shortcuts */
  154.     const dropzone=document.getElementById('dropzone');
  155.     const fileInput=document.getElementById('fileInput');
  156.     const outputs=document.getElementById('outputs');
  157.  
  158.     /* ────────────────────────────── SVG → single-entry C-array */
  159.     function svgToEntry(txt,name){
  160.       const key=name.replace(/\.[^/.]+$/,'');
  161.       const doc=new DOMParser().parseFromString(txt,'image/svg+xml');
  162.       const parts=[];
  163.       doc.querySelectorAll('path').forEach(p=>{
  164.         const raw=findFill(p) || 'black';
  165.         const col=cssToRGB(raw);
  166.         parts.push(`${p.getAttribute('d')?esc(p.getAttribute('d')):''}[{${col}}]`);
  167.       });
  168.       return `${INDENT}{"${key}", "${parts.join('')}"}`;
  169.     }
  170.  
  171.     /* ────────────────────────────── TTF → full C-array */
  172.     function ttfToArray(buf){
  173.       const font=opentype.parse(buf);
  174.       const upm=font.unitsPerEm||1000;
  175.       const scale=1024/upm;
  176.       const seen=new Set(),lines=[];
  177.       for(const codeStr of Object.keys(font.tables.cmap.glyphIndexMap)){
  178.         const code=parseInt(codeStr);
  179.         if(seen.has(code)) continue;
  180.         seen.add(code);
  181.         const ch=String.fromCodePoint(code);
  182.         const glyph=font.charToGlyph(ch);
  183.         const path=glyph.getPath(0,0,upm*scale);
  184.         lines.push(`${INDENT}{"${esc(ch)}", "${esc(path.toPathData())}[{rgb(unknown)}]"}`);
  185.       }
  186.       return `{\n${lines.join(',\n')}\n}`;
  187.     }
  188.  
  189.     /* ────────────────────────────── UI: build output box */
  190.     function addBox(text){
  191.       const box=document.createElement('div');
  192.       box.className='output-box'; box.setAttribute('draggable',true);
  193.  
  194.       const pre=document.createElement('pre'); pre.textContent=text; box.appendChild(pre);
  195.  
  196.       const btn=document.createElement('button');
  197.       btn.className='copy-btn'; btn.textContent='Copy';
  198.       btn.onclick=(()=>{ let t; return ()=>{
  199.         navigator.clipboard.writeText(pre.textContent);
  200.         clearTimeout(t); btn.textContent='Copied!';
  201.         t=setTimeout(()=>btn.textContent='Copy',600);
  202.       };})();
  203.       box.appendChild(btn);
  204.       outputs.appendChild(box);
  205.  
  206.       /* drag-merge */
  207.       box.addEventListener('dragstart',e=>{
  208.         e.dataTransfer.setData('text/plain',pre.textContent);
  209.         box.classList.add('dragging');
  210.       });
  211.       box.addEventListener('dragend',()=>box.classList.remove('dragging'));
  212.       box.addEventListener('dragover',e=>{e.preventDefault();box.classList.add('dragover');});
  213.       box.addEventListener('dragleave',()=>box.classList.remove('dragover'));
  214.       box.addEventListener('drop',e=>{
  215.         e.preventDefault(); box.classList.remove('dragover');
  216.         const other=e.dataTransfer.getData('text/plain');
  217.         if(other && other!==pre.textContent){
  218.          pre.textContent=mergeArrays(pre.textContent,other);
  219.           document.querySelectorAll('.output-box.dragging').forEach(b=>b.remove());
  220.         }
  221.       });
  222.     }
  223.  
  224.     /* ────────────────────────────── merge helpers */
  225.     const stripLines=s=>s.replace(/^\s*\{|\}\s*$/g,'')
  226.                          .split(/\n/)
  227.                          .map(l=>l.trim().replace(/,$/,''))
  228.                          .filter(Boolean);
  229.     function mergeArrays(a,b){
  230.       const seen=new Set(),out=[];
  231.       [...stripLines(a),...stripLines(b)].forEach(l=>{
  232.         if(!seen.has(l)){seen.add(l);out.push(l);}
  233.       });
  234.       return '{\n'+out.map(l=>INDENT+l).join(',\n')+'\n}';
  235.     }
  236.     function mergeAll(){
  237.       const pres=[...document.querySelectorAll('.output-box pre')];
  238.       if(pres.length<2) return;
  239.      let merged=pres[0].textContent;
  240.      for(let i=1;i<pres.length;i++) merged=mergeArrays(merged,pres[i].textContent);
  241.      outputs.innerHTML=''; addBox(merged);
  242.    }
  243.  
  244.    /* ────────────────────────────── file handlers */
  245.    function esc(s){ return s.replace(/\\/g,'\\\\').replace(/"/g,'\\"'); }
  246.    function handleFile(f){
  247.      const r=new FileReader();
  248.      r.onload=()=>{
  249.         if(f.name.endsWith('.svg')) addBox('{\n'+svgToEntry(r.result,f.name)+'\n}');
  250.         else if(f.name.endsWith('.ttf')) addBox(ttfToArray(r.result));
  251.         else addBox('Unsupported file: '+f.name);
  252.       };
  253.       f.name.endsWith('.svg') ? r.readAsText(f) : r.readAsArrayBuffer(f);
  254.     }
  255.  
  256.     /* ────────────────────────────── UI wiring */
  257.     dropzone.addEventListener('click',()=>fileInput.click());
  258.     dropzone.addEventListener('dragover',e=>{e.preventDefault();dropzone.classList.add('hover');});
  259.     ['dragleave','drop'].forEach(ev=>dropzone.addEventListener(ev,()=>dropzone.classList.remove('hover')));
  260.     dropzone.addEventListener('drop',e=>{
  261.       e.preventDefault();
  262.       [...e.dataTransfer.files].forEach(handleFile);
  263.     });
  264.     fileInput.addEventListener('change',e=>[...e.target.files].forEach(handleFile));
  265.  
  266.     document.getElementById('mergeAllBtn').addEventListener('click',mergeAll);
  267.     document.getElementById('clearBtn').addEventListener('click',()=>outputs.innerHTML='');
  268.   </script>
  269. </body>
  270. </html>
Add Comment
Please, Sign In to add comment