javascriptdatatablespdfmake

How do I correctly render glyphs in dataTables PDF export?


I am using dataTables to render a table of best times for a swim team. There are two levels of qualifying times in the applicable universe, and I am annotating times in the table with unicode star glyphs. Outline for level B, and solid for level A. U+2606 and U+2605, respectively. The browser view is as expected:

dataTables view of best times table

However, when I export to PDF, the glyphs don't render.

PDF export of dataTables view of best times

What I've read says this is an issue with the fact that Roboto, the default font in pdfMake doesn't support the glyph, and that to include other fonts I would have to edit the vfs_fonts.js file and include the base-64 encoded definitions of the font I want to include. That presents its own challenge in that you need the redistribution license for a given font. I have found it rather difficult to confirm whether a given font has the glyph I need, and my attempt to use a free font I found produced strange errors.

$.ajax({
    url: '/ttf/FreeSans-LrmZ.ttf' ,
    success: function(result) {
        window.pdfMake.vfs["FreeSans.ttf"] = btoa(unescape(encodeURIComponent(result))) ;
    }
});

....

customize: function (doc) {
    pdfMake.fonts = {
        Roboto: {
            normal: 'Roboto-Regular.ttf',
            bold: 'Roboto-Medium.ttf',
            italics: 'Roboto-Italic.ttf',
            bolditalics: 'Roboto-MediumItalic.ttf'
        },
        freeSans: {
            normal: 'FreeSans.ttf',
        }
    };
    doc.defaultStyle.font = "freeSans";

....

embedded.js:15 Uncaught (in promise) TypeError: Cannot read properties of null (reading 'advanceWidth')

I suppose that I really have two questions: 1) How do I find an appropriate font to correctly render the two unicode characters that I need? 2) How do I properly incorporate it so that I can resolve my js error and successfully export the correctly rendered PDF?

I have referenced the following questions and was unable to solve my problem:

Cannot use special characters/glyphs with PDFMAKE, even with imported Fonts which include those

pdfmake - using own fonts not working

DataTables & PDFmake


Solution

  • I recommend you only ask one specific question at a time. I think your two questions are quite different.

    Having said that, here are some partial answers:


    1) How do I find an appropriate font to correctly render the two unicode characters that I need?

    This is off-topic for Stack Overflow - it's not a programming question.

    Also, I don't have a good (off-topic!) answer for you, anyway. All of the "free commercial use" downloadable fonts I found in a quick search did not include the and symbols.

    That includes the font it looks like you are trying to use in your question, as well. Even though it is "free" and "arial", it still has a limited set of Unicode characters, symbols and glyphs. The Unicode Miscellaneous Symbols block is not included.

    You can use this free for personal use font - but that is obviously not suitable for commercial use.

    It does include the stars, though:

    enter image description here


    2) How do I properly incorporate it so that I can resolve my js error and successfully export the correctly rendered PDF?

    The JavaScript code in your question does not handle the downloaded binary font file data correctly, resulting in an invalid base-64 string.

    jQuery does not have a straightforward built-in way to handle binary files, although you can search Stack Overflow for some approaches.

    In this case, I find it simpler to use pure JavaScript, not jQuery's ajax.

    Specifically, the following uses FleReader.readAsDataUrl because when using this approach:

    the result attribute contains... the file's data as a base64 encoded string.

    That is what we want!

    var req = new XMLHttpRequest();
    req.open("GET", fontUrl, true);
    req.responseType = "blob";
    req.onload = function (event) {
      const blob = req.response;
             
      var reader = new FileReader();
      reader.readAsDataURL(blob); 
      reader.onloadend = function() {
        const base64data = reader.result.replace(/^data:.+;base64,/, '');            
      }
    }
    

    The above code is based on this answer to a similar question.


    However, this still does not solve the PDF font problem because (as noted above) the specific font you appear to be using does not contain the glyphs you need.

    But despite that, here is the full DataTables code you can copy and run for yourself.

    (This is using the latest version of DataTables (2.0.0), with the newly introduced layout option. You may be using an older version, without that option - but that does not matter for the specific base 64 problem you are facing. You can use the old DataTables dom option, if you prefer.)

    <!doctype html>
    <html>
    <head>
      <meta charset="UTF-8">
      <title>Demo</title>
    
      <link href="https://cdn.datatables.net/2.0.5/css/dataTables.dataTables.css" rel="stylesheet">
      <link href="https://cdn.datatables.net/buttons/3.0.2/css/buttons.dataTables.css" rel="stylesheet">
     
      <script src="https://code.jquery.com/jquery-3.7.0.js"></script>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/pdfmake.js"></script>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/vfs_fonts.js"></script>
      <script src="https://cdn.datatables.net/2.0.5/js/dataTables.js"></script>
      <script src="https://cdn.datatables.net/buttons/3.0.2/js/dataTables.buttons.js"></script>
      <script src="https://cdn.datatables.net/buttons/3.0.2/js/buttons.html5.js"></script>
    
      <link rel="stylesheet" type="text/css" href="https://datatables.net/media/css/site-examples.css">  
      
    </head>
    
    <body>
    
    <div style="margin: 20px;">
    
        <table id="example" class="display dataTable cell-border" style="width:100%">
            <thead>
                <tr>
                    <th>Name</th>
                    <th>Position</th>
                    <th>Office in Country</th>
                    <th>Age</th>
                    <th>Start date</th>
                    <th>Salary</th>
                </tr>
            </thead>
            <tbody>
                <tr>
                    <td>Tiger Nixon</td>
                    <td>System Architect</td>
                    <td>Edinburgh</td>
                    <td>61 ☆</td>
                    <td>2011/04/25</td>
                    <td>$320,800</td>
                </tr>
                <tr>
                    <td>Garrett Winters</td>
                    <td>Accountant</td>
                    <td>Tokyo</td>
                    <td>63 ★</td>
                    <td>2011/07/25</td>
                    <td>$170,750</td>
                </tr>
                <tr>
                    <td>Ashton Cox</td>
                    <td>Junior "Technical" Author</td>
                    <td>San Francisco</td>
                    <td>66</td>
                    <td>2009/01/12</td>
                    <td>$86,000</td>
                </tr>
            </tbody>
        </table>
    
    </div>
    
    <script>
    
    $(document).ready(function() {
    
      $('#example').DataTable({
        layout: {
          topStart: {
            buttons: [{
              extend: 'pdfHtml5',
              customize: function ( doc ) {
                processDoc( doc );
              }
            }]
          }
        }
      });
    
    } );
    
      function processDoc( doc ) {
      
        const fontUrl = 'https://static.miraheze.org/argobdpwiki/5/5d/FreeSans-LrmZ.ttf';
        
        var req = new XMLHttpRequest();
        req.open("GET", fontUrl, true);
        req.responseType = "blob";
        req.onload = function (event) {
          const blob = req.response;
             
          var reader = new FileReader();
          reader.readAsDataURL(blob); 
          reader.onloadend = function() {
            const base64data = reader.result.replace(/^data:.+;base64,/, '');            
            pdfMake.vfs["FreeSans.ttf"] = base64data;
            pdfMake.fonts = {
              Roboto: {
                normal: 'Roboto-Regular.ttf',
                bold: 'Roboto-Medium.ttf',
                italics: 'Roboto-Italic.ttf',
                bolditalics: 'Roboto-MediumItalic.ttf'
              },
              FreeSans: {
                normal: 'FreeSans.ttf',
                bold: 'FreeSans.ttf',
                italics: 'FreeSans.ttf',
                bolditalics: 'FreeSans.ttf'
              }
            };
            // modify the PDF to use a different default font:
            doc.defaultStyle.font = "FreeSans";
          }
        };
    
        req.send();
        
      }
    
    </script>
    
    </body>
    </html>
    

    You can see the URL I am using in the above code:

    const fontUrl = 'https://static.miraheze.org/argobdpwiki/5/5d/FreeSans-LrmZ.ttf';
    

    If you find a font file with the glyphs you need and with a suitable license, you can change my demo URL to the one you want to use instead.

    (You can obviously host the relevant font file yourself - it doesn't have to be an external URL, like my example. It can be a URL specific to your application.)


    Follow-Up Notes

    Based on the comment, I took a look at that font, and I think there may be some issues with it - and with my code, also... so my apologies for that.

    The font lets you scroll through a list of all the symbols it contains (the "view all glyphs" buttons) - and I do not see the stars in that list.

    So, I generated a base-64 string for the main font file (arial.ttf). Then I created a simplified version of the DataTables code, where I do not use the provided vfs_fonts.js file. Instead I create the objects provided by that file in the code directly, and I use the hard-coded base-64 string - just to make sure we really are loading and using that font for the PDF:

    <!doctype html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Export to PDF</title>
      
      <link href="https://cdn.datatables.net/2.0.5/css/dataTables.dataTables.css" rel="stylesheet">
      <link href="https://cdn.datatables.net/buttons/3.0.2/css/buttons.dataTables.css" rel="stylesheet">
     
      <script src="https://code.jquery.com/jquery-3.7.0.js"></script>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/pdfmake.js"></script>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/vfs_fonts.js"></script>
      <script src="https://cdn.datatables.net/2.0.5/js/dataTables.js"></script>
      <script src="https://cdn.datatables.net/buttons/3.0.2/js/dataTables.buttons.js"></script>
      <script src="https://cdn.datatables.net/buttons/3.0.2/js/buttons.html5.js"></script>
    
      <link rel="stylesheet" type="text/css" href="https://datatables.net/media/css/site-examples.css">
      
      <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
      <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0" />  
    
    
    </head>
    
    <body>
    
    <div style="margin: 20px;">
    
    <table id="example" class="display nowrap dataTable cell-border" style="width:100%">
            <thead>
                <tr>
                    <th>Data</th>
                </tr>
            </thead>
            <tbody>
                <tr>
                    <td>ABC ʓ ★ ☆ ☻ ☼ ֍ ػ ۞ ‰</td>
                </tr>
            </tbody>
        </table>
    
    </div>
    
    <script type="text/javascript">
    
      this.pdfMake = this.pdfMake || {}; 
      this.pdfMake.vfs = {
        "arial.ttf": "AAEAA...hZQA=" // very long string here!
      };
    
      $(document).ready(function() {
        $('#example').DataTable({
        layout: {
          topStart: {
            buttons: [{
              extend: 'pdfHtml5',
              customize: function ( doc ) {
                processDoc( doc );
              }
            }]
          }
        }
      });
      });
    
      function processDoc(doc) {
    
        pdfMake.fonts = {
          arial: {
            normal: 'arial.ttf',
            bold: 'arial.ttf',
            italics: 'arial.ttf',
            bolditalics: 'arial.ttf'
          }
        };
        // modify the PDF to use a different default font:
        doc.defaultStyle.font = "arial";
        var i = 1;
      }
    
    </script>
    
    </body>
    

    In the DataTable, I see all the symbols in my test data:

    enter image description here

    But in the PDF, I see everything except the two symbols you want to see:

    enter image description here

    So, unfortunately, it really does look as if the stars are not part of that font set, after all - even though they were shown in the preview.

    The PDF is definitely using the font, as shown when using Acrobat and looking at the fonts used:

    enter image description here

    I don't know how to investigate this further, but maybe these notes can help you to ask a more targeted question - or at least improve the JavaScript, if you don't want to pre-generate (and hard-code) the base-64 string, using my simplified approach.