DataTables image (or at least image title) export to PDF
Asked Answered
C

5

5

Using DataTables and Buttons (NOT TableTools, which is retired) extension. Some cells have progressbars and small icons. Is there a way to export these images (or at least their titles) to PDF? Found some possible hacks on this page, but all of them were for retired TableTools.

Checked https://datatables.net/reference/button/pdf and https://datatables.net/reference/api/buttons.exportData%28%29 but couldn't find any method to achieve this goal. Tested by adding this code:

stripHtml: false

but whole HTML code (like img src=...) was included in PDF file instead of images.

If exporting images isn't possible, is there a way to export at least alt or title attribute of each image? That would be enough.

Cruet answered 29/1, 2016 at 17:42 Comment(0)
L
13

I assume you are using pdfHtml5. dataTables is using pdfmake in order to export pdf files. When pdfmake is used from within a browser it needs images to be defined as base64 encoded dataurls.

Example : You have rendered a <img src="myglyph.png"> in the first column of some of the rows - those glyphs should be included in the PDF. First create an Image reference to the glyph :

var myGlyph = new Image();
myGlyph.src = 'myglyph.png';

In your customize function you must now

1) build a dictionary with all images that should be included in the PDF
2) replace text nodes with image nodes to reference images

buttons : [
    { 
    extend : 'pdfHtml5',
    customize: function(doc) {

        //ensure doc.images exists
        doc.images = doc.images || {};

        //build dictionary
        doc.images['myGlyph'] = getBase64Image(myGlyph);
        //..add more images[xyz]=anotherDataUrl here

        //when the content is <img src="myglyph.png">
        //remove the text node and insert an image node
        for (var i=1;i<doc.content[1].table.body.length;i++) {
            if (doc.content[1].table.body[i][0].text == '<img src="myglyph.png">') {
                delete doc.content[1].table.body[i][0].text;
                doc.content[1].table.body[i][0].image = 'myGlyph';
            }
        }
    },
    exportOptions : {
        stripHtml: false
    }
}

Here is a an example of a getBase64Image function

function getBase64Image(img) {
    var canvas = document.createElement("canvas");
    canvas.width = img.width;
    canvas.height = img.height;
    var ctx = canvas.getContext("2d");
    ctx.drawImage(img, 0, 0);
    return canvas.toDataURL("image/png");
}

If you just want to show the title of images in the PDF - or in any other way want to manipulate the text content of the PDF - then it is a little bit easier. The content of each column in each row can be formatted through the exportOptions.format.body callback :

buttons : [
    { 
    extend : 'pdfHtml5',
    exportOptions : {
        stripHtml: false
        format: {
            body: function(data, col, row) {
                var isImg = ~data.toLowerCase().indexOf('img') ? $(data).is('img') : false;
                if (isImg) {
                    return $(data).attr('title');
                }
                return data;
            }
        }
    }
]

The reason format.body cannot be used along with images is that is only let us pass data back to the text node part of the PDF document.

See also

Lacroix answered 31/1, 2016 at 10:52 Comment(3)
Thanks for a detailed answer. Should I add the same code (if (doc.content[1].table.body[i][0].text == '<img src="myglyph.png">... for each image? The issue is that I don't know in advance what images will be included in the table. The application where code is used is some type of calculator; therefore, the number of images and image names depend on the final results of calculation.Cruet
@MindaugasLi, Yes! Do that test for each <img> you want to replace, remember table.body[i][0] is col0, table.body[i][1] col 2 and so on. You do not have to create a dictionary, it is mostly so the same glyph / image not is repeated 1000 times - you can use the dataUrl directly -> doc.content[1].table.body[i][0].image = getBase64Image(myGlyph) but then the size of the PDF can be increased dramatically.Lacroix
@Lacroix can you help me in this issue ? #44951544Stoplight
C
6

Since no suggestions received, I had to make a hack in order to get PDF file formatted the way I want.

In case someone has the same issue, you can use hidden span to display image alt/title near image itself. I'm sure it's not the best practice, but it will do the trick. So the code will look like:

<img src='image.png' alt='some_title'/><span class='hidden'>some_title</span>

This way datatables will show only the image, while PDF file will contain text you need.

Cruet answered 31/1, 2016 at 10:38 Comment(0)
B
1

This is my CODE!

HTML

<div class="dt-btn"></div>
<table>
  <thead><tr><th>No</th><th>IMAGE</th><th>NOTE</th></tr></thead>
  <tbody>
    <tr>
      <td>{{$NO}}</td>
      <td>{{$imgSRC}}</td>
      <td>{{$NAME}}<br />
          {{$GRADE}}<br />
          {{$PROFILE}}<br />
          {{$CODE}}<br />
      </td>
    </tr>
  </tbody>
</table>

JAVASCRIPT

$.extend( true, $.fn.dataTable.defaults, {
buttons: [{
  text: '<i class="bx bx-download font-medium-1"></i><span class="align-middle ml-25">Download PDF</span>',
  className: 'btn btn-light-secondary mb-1 mx-1 dnPDF',
  extend: 'pdfHtml5',
  pageSize: 'A4',
  styles: {
   fullWidth: { fontSize: 11, bold: true, alignment: 'left', margin: [0,0,0,0] }
  },
  action: function ( e, dt, node, config ) {
    var that = this;
    setTimeout( function () {
      $.fn.dataTable.ext.buttons.pdfHtml5.action.call(that, e, dt, node, config);
      $( ".donePDF" ).remove();
      $( ".dnPDF" ).prop("disabled", false);
    }, 50);
  },
  customize: function(doc) {
    doc.defaultStyle.fontSize = 11;
    doc.defaultStyle.alignment = 'left';
    doc.content[1].table.dontBreakRows = true;
    if (doc) {
      for (var i = 1; i < doc.content[1].table.body.length; i++) {
        // 1st Column - display IMAGE
        var imgtext = doc.content[1].table.body[i][0].text;
        delete doc.content[1].table.body[i][0].text;
        jQuery.ajax({
          type: "GET",
          dataType: "json",
          url: "{{route('base64')}}",
          data: { src: imgtext },
          async: false,
          success: function(resp) {
            //console.log(resp.data);
            doc.content[1].table.body[i][0] = {
                margin: [0, 0, 0, 3],
                alignment: 'center',
                image: resp.data,
                width: 80,
                height: 136
            };
          }
        });
        // 2nd Column - display NOTE(4 line)
        var bodyhtml = doc.content[1].table.body[i][1].text;
        var bodytext = bodyhtml.split("\n");
        var bodystyle = []
        for (var j = 0; j < bodytext.length; j++) {
          switch(j) {
            case 0:
              // NAME
              var _text = { margin:[0, 0, 0, 3], color:"#000000", fillColor:'#ffffff', bold:true, fontSize:13, alignment:'center', text:bodytext[j] };
              break;
            case 1:
              // GRADE
              var _text = { margin:[0, 0, 0, 3], color:"blue", fillColor:'#ffffff', bold:false, fontSize:11, alignment:'left', text:bodytext[j] };
              break;
            case 3:
              // CODE
              var _text = { margin:[0, 0, 0, 3], color:"#000000", fillColor:'#ffffff', bold:true, fontSize:11, alignment:'left', text:bodytext[j] };
              break;
            default:
              // OTHERS
              var _text = { margin:[0, 0, 0, 3], color:"#000000", fillColor:'#ffffff', bold:false, fontSize:11, alignment:'left', text:bodytext[j] };
              break;
          }
          bodystyle[j] = _text;
        };
        doc.content[1].table.body[i][1] = bodystyle;
      }
    }
  },
  exportOptions: {
    columns: [ 1, 2 ],
    stripNewlines: false,
    stripHtml: true,
    modifier: {
      page: 'all' // 'all', 'current'
    },
  }
}],
columns: [
  { className: 'iNo', orderable: true, visible: true},
  { className: 'iIMG', orderable: false, visible: false },
  { className: 'iPDF', orderable: false, visible: false, responsivePriority: 10001 } ]
});
var table = $('#table').DataTable();
table.buttons().container().appendTo('.dt-btn');
$('.dnPDF').on('click', function(){
  $(this).append('<span class="spinner-border spinner-border-sm donePDF" role="status" aria-hidden="true"></span>').closest('button').attr('disabled','disabled');
});
$.fn.dataTable.Buttons.defaults.dom.container.className = '';
$.fn.dataTable.Buttons.defaults.dom.button.className = 'btn';

PHP

public function base64(Request $request)
{
  $request->validate([
    'src' => 'required|string'
  ]);
  $fTYPE = pathinfo($request->src, PATHINFO_EXTENSION);
  $fDATA = @file_get_contents($request->src);
  $imgDATA = base64_encode($fDATA);
  $imgSRC = 'data:image/' . $fTYPE . ';base64,'.$imgDATA;
  $error = ($imgDATA!='') ? 0 : 1;
  $msg = ($error) ? 'Error' : 'Success';
  return response()->json([ 'msg' => $msg, 'error'=> $error, 'data' => $imgSRC]);
}

[Sample][1]: https://i.sstatic.net/35Wlm.jpg

Buprestid answered 29/6, 2020 at 14:9 Comment(0)
B
0

In addition to davidkonrad's answer. I created dynamically all base64 images and used them in Datatables pdfmake like this:

for (var i = 1; i < doc.content[2].table.body.length; i++) {
    if (doc.content[2].table.body[i][1].text.indexOf('<img src=') !== -1) {
        html = doc.content[2].table.body[i][1].text;

        var regex = /<img.*?src=['"](.*?)['"]/;
        var src = regex.exec(html)[1];


        var tempImage = new Image();
        tempImage.src = src;

        doc.images[src] = getBase64Image(tempImage)

        delete doc.content[2].table.body[i][1].text;
        doc.content[2].table.body[i][1].image = src;
        doc.content[2].table.body[i][1].fit = [50, 50];
    }

    //here i am removing the html links so that i can use stripHtml: true,
    if (doc.content[2].table.body[i][2].text.indexOf('<a href="details.php?') !== -1) {
        html = $.parseHTML(doc.content[2].table.body[i][2].text);
        delete doc.content[2].table.body[i][1].text;
        doc.content[2].table.body[i][2].text = html[0].innerHTML;
    }

}
Beiderbecke answered 12/7, 2017 at 23:4 Comment(1)
Where exactly in davidkonrad's answer did you put this? Thanks!Chymotrypsin
L
0

Basic thought process(can be applied to any stack, I am using .net)

This approach use orthogonal key of exportOptions of datatable (refer datatable docs)

  1. In table image <td>, declare "data-filter" as shown
  • Simply set "data-filter" to get image title in exported pdf/excel, no other change is required except in exportOptions (skip customize key changes of datatable)

  • Set "data-filter" to base64 string with additional data, as shown below in <td> to get image by using markup language (pure javascript also can be used)

  1. In customize: function (window) {} key, of datatable pdf button, do following
  • get data-filter value (base64 string/ image title) from image key of window as shown (hardcoded numbers represent columns of datatable where data-filter is stored)

  • delete text key of window

  • add image key and assign base64 string/title extracted in point a above, to image key.

Relevant actual code lines to solve this problem (please change according to your stack)

// jQuery

1. @Scripts.Render("~/Assets/Plugins/datatables-exs/js/pdfmake.min.js?v=" + DateTime.Now.Ticks.ToString())//----------make sure to include this min.js file

2. Please apply datatable according to your approach 

var table = $('#datatable').DataTable({
    lengthChange: true,
    dom: 'Blfrtip',
    lengthMenu: [
        [10, 25, 50, 100, 500, -1],
        [10, 25, 50, 100, 500, 'All'],
    ],
    order: [[4, "desc"]],
    buttons: [
        { extend: 'copy', text: 'Copy to clipboard', className: 'btn btn-primary' },
        {
            extend: 'excel', text: 'Download Excel', className: 'btn btn-danger',
            exportOptions: {
                columns: ':not(.tbl-op)'
            }
        },
        {
            extend: 'pdfHtml5', text: 'Download Pdf', className: 'btn btn-warning',
            customize: function (window) {//---------------------------------------window contains table data and styles this callback is called on download pdf button click
                if (window) {
                    window.pageSize = "A2";//-------------------------------setting various style and pdf values(reference http://pdfmake.org/#/gettingstarted)
                    window.pageOrientation= 'portrait';
                    window.defaultStyle.fontSize = '10';
                    window.margin = [0, 0, 0, 0];
                    window.alignment = 'center';

                    for (var i = 1; i < window.content[1].table.body.length; i++) {//-----traversing entire table data

                        var img1 = window.content[1].table.body[i][6].text;//-------getting base64 string stored in <td> in data-filter used as orthogonal
                        var img2 = window.content[1].table.body[i][7].text;//-------6 and 7 are column of tables where image is stored 

                        delete window.content[1].table.body[i][6].text;//-----------deleting text key and adding image key (reference http://pdfmake.org/#/gettingstarted)
                        delete window.content[1].table.body[i][7].text;

                        window.content[1].table.body[i][6].image = img1;
                        window.content[1].table.body[i][6].width = 100;//-----------use debugger to know more about window
                        window.content[1].table.body[i][6].height = 100;

                        window.content[1].table.body[i][7].image = img2;
                        window.content[1].table.body[i][7].width = 100;
                        window.content[1].table.body[i][7].height = 100;

                    }
                }
            },
            exportOptions: {
                columns: ':not(.tbl-op)',
                orthogonal: 'filter' /*Do not change filter as this will break excel export for column*///-----------------important
            }
        }
    ],
    "fnRowCallback": function (nRow, aData, iDisplayIndex) {
        $("td:first", nRow).html(iDisplayIndex + 1);
        return nRow;
    }
});

table.buttons().container().appendTo('#datatable_wrapper .col-md-6:eq(0)');
table.on('order.dt search.dt', function () {
    let i = 1;
    table.cells(null, 0, { search: 'applied', order: 'applied' }).every(function (cell) {
        this.data(i++);
    });
}).draw();

// cshtml page

3. 

@if (!string.IsNullOrWhiteSpace(Obs.Media1))
{
    // -------- below line append base64 string to data:image/jpeg;base64,(remember comma)
    <td data-filter="@("data:image/jpeg;base64," + ToBase64String(----inser image path here----))"><img src="@(----inser image path here----)" class="img-thumbnail" width="150" /></td>
}
else
{
    <td>No Image</td>
}


4. @functions{//----------------------------------------------marked up c# function to get base64 image string
public string ToBase64String(string path)
{
    byte[] imageArray = System.IO.File.ReadAllBytes(Server.MapPath(path));//----server.mappath is imporetant here to get absolute path ...relative path no workinh
    string base64Str = Convert.ToBase64String(imageArray);

    return base64Str;
}
}
Lancers answered 7/3, 2023 at 19:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.