9

My application needs to print out an arbitrarily large canvas that can span multiple page width and height widths.

There was a similar question some time back where it was claimed the browser won't print to multiple page widths.

Since this was a while back I am wondering if it is still true. Also, what strategies are available to print out a large canvas without splitting it up?

var canvas = document.getElementById("canvas1");

function draw_a() {
  var context = canvas.getContext("2d");
  //   //  LEVER

  //plane
  context.fillStyle = '#aaa';
  context.fillRect(25, 90, 2500, 400);


}

$(document).ready(function() {
  draw_a();

});
canvas {
  border: 1px dotted;
}

.printOnly {
  display: none;
}

@media print {
  html,
  body {
    height: 100%;
    background-color: yellow;
  }
  .myDivToPrint {
    background-color: yellow;
    /*
        height: 100%;
    
        width: 100%;
        position: fixed;*/
    top: 0;
    left: 0;
    margin: 0;
  }
  .no-print,
  .no-print * {
    display: none !important;
  }
  .printOnly {
    display: block;
  }
}

@media print and (-ms-high-contrast: active),
(-ms-high-contrast: none) {
  html,
  body {
    height: 100%;
    background-color: yellow;
  }
  .myDivToPrint {
    background-color: yellow;
    /*
        height: 100%;
    
        width: 100%;
        position: fixed;*/
    top: 0;
    left: 0;
    margin: 0;
    padding: 15px;
    font-size: 14px;
    line-height: 18px;
    position: absolute;
    display: flex;
    align-items: center;
    justify-content: center;
    -webkit-transform: rotate(90deg);
    -moz-transform: rotate(90deg);
    -o-transform: rotate(90deg);
    -ms-transform: rotate(90deg);
    transform: rotate(90deg);
  }
  .no-print,
  .no-print * {
    display: none !important;
  }
  .printOnly {
    display: block;
  }
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<button onclick="window.print();" class="no-print">Print Canvas</button>
<div class="myDivToPrint">
  <div class="Aligner-item">
    <canvas height="2500px" width="4000px" id="canvas1"></canvas>
    <div class="printOnly Aligner-item--bottom"> Print Only</div>
  </div>

</div>
Paolo Forgia
  • 6,572
  • 8
  • 46
  • 58
  • chrome does support [`@page{size:4050px 2550px;}`](https://developer.mozilla.org/en-US/docs/Web/CSS/@page/size) which will make your page big enough for your canvas to fit. FF doesn't... https://jsfiddle.net/3ngqg3x2/ – Kaiido Sep 05 '17 at 04:22

5 Answers5

4

It does seem that browsers will split up a large canvas into multiple pages. I tested on MacOS Sierra using latest chrome and safari browsers.

A possible approach for printing a canvas is to first transform it to a data URI containing a representation of the image using canvas.toDataURL(). You can then manipulate the image dimensions prior to printing.

"<img src='" + canvas.toDataURL() + "' height='500px' width='500px' />'"

In the following example, the large 4500px by 4500px canvas is translated into an img and placed inside an iframe, used for printing. You can probably append the image to the original document and than print that specific element, but the iframe may be more flexible to handle print output. You can manipulate the img dimensions according to your requirements and print a scaled representation of the canvas. Note that I hardcoded the width and height of the image but this can be calculated and changed as needed for printing.

Due to iframe cross-origin restrictions, the code snippet below will not work here, but it does work on this jsfiddle.

The scaled 500px by 500px image representing the canvas fits on one page when printed.

var canvas = document.getElementById("canvas1");

function draw_a() {
  var context = canvas.getContext("2d");
  //   //  LEVER

  //plane
  context.fillStyle = '#aaa';
  context.fillRect(25, 90, 4500, 4500);
}

print = function() {
 window.frames["myFrame"].focus();
 window.frames["myFrame"].print();
}

function setupPrintFrame() {
 $('<iframe id="myFrame" name="myFrame">').appendTo("body").ready(function(){
    setTimeout(function(){
        $('#myFrame').contents().find('body').append("<img src='" + canvas.toDataURL() + "' height='500px' width='500px' />'");
    },50);
 });
}

$(document).ready(function() {
  draw_a();
 setupPrintFrame();
});
canvas {
  border: 1px dotted;
}

.printOnly, #myFrame {
  display: none;
}

@media print {
  html,
  body {
    height: 100%;
    background-color: yellow;
  }
  .myDivToPrint {
    background-color: yellow;
    /*
        height: 100%;
    
        width: 100%;
        position: fixed;*/
    top: 0;
    left: 0;
    margin: 0;
  }
  .no-print,
  .no-print * {
    display: none !important;
  }
  .printOnly {
    display: block;
  }
}

@media print and (-ms-high-contrast: active),
(-ms-high-contrast: none) {
  html,
  body {
    height: 100%;
    background-color: yellow;
  }
  .myDivToPrint {
    background-color: yellow;
    /*
        height: 100%;
    
        width: 100%;
        position: fixed;*/
    top: 0;
    left: 0;
    margin: 0;
    padding: 15px;
    font-size: 14px;
    line-height: 18px;
    position: absolute;
    display: flex;
    align-items: center;
    justify-content: center;
    -webkit-transform: rotate(90deg);
    -moz-transform: rotate(90deg);
    -o-transform: rotate(90deg);
    -ms-transform: rotate(90deg);
    transform: rotate(90deg);
  }
  .no-print,
  .no-print * {
    display: none !important;
  }
  .printOnly {
    display: block;
  }
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<button onclick="print()" class="no-print">Print Canvas</button>
<div class="myDivToPrint">
  <div class="Aligner-item">
    <canvas height="4500px" width="4500px" id="canvas1"></canvas>
    <div class="printOnly Aligner-item--bottom"> Print Only</div>
  </div>

</div>
Julio Feferman
  • 2,658
  • 3
  • 15
  • 26
  • Use the jsfiddle link provided. [jsfiddle link]:(https://jsfiddle.net/jfeferman/52cmo255/). – Julio Feferman Sep 06 '17 at 14:51
  • I want this to work, but it doesn't work at all in IE and appears truncated in chrome. I recreated your work in a pen: https://codepen.io/mjankowski/pen/bryWGM – Matthew David Jankowski Sep 06 '17 at 16:21
  • Thank you, this will definitely work. I tested your codepen successfully with Windows 10 (Chrome), MacOS Sierra (Chrome and Safari). On IE11, the problem seems to be in your `@media -ms-high-contrast` rule. I removed this rule in the following pen [https://codepen.io/jfeferman/pen/eEaWxJ](https://codepen.io/jfeferman/pen/eEaWxJ) and IE11 worked but with a large canvas size. In IE11, use `msToBlob()` instead of `toDataURL()` according to this post [Download canvas to image in IE](https://stackoverflow.com/questions/21860633/download-canvas-to-image-in-ie-using-javascript). – Julio Feferman Sep 06 '17 at 17:18
  • More info: tested successfully on Windows 10, Edge browser. `toDataURL() ` works as expected and renders the small 500x500 image on print. IE11, however, seems problematic and would require more investigation. – Julio Feferman Sep 06 '17 at 17:33
  • Thank you for these edits. I'm taking a look into them. – Matthew David Jankowski Sep 06 '17 at 21:15
  • Well, I'll keep looking at this approach. It doesn't spread the image out over multiple pages but shrinks the image down to where it will assuredly fit on one page. I need multiple page widths. – Matthew David Jankowski Sep 06 '17 at 21:17
  • Yes, that is understood. You can manipulate the image dimensions to fit on one page or spread it out over several pages. The trick here is that this approach transforms an unmanageable canvas to an image that can be manipulated prior to printing. – Julio Feferman Sep 06 '17 at 21:38
  • Awarded to this answer because it comes closest to providing a solution. After more experimentation I think the original goal of printing out a canvas image over multiple pages is not possible right now. The only workable solution seems to be to come up with a strategy to shrink the image to a printable page width. – Matthew David Jankowski Sep 12 '17 at 15:28
3
@media print {  
  @page {
    size: 297mm 210mm; /* landscape */
    /* you can also specify margins here: */
    margin: 25mm;
    margin-right: 45mm; /* for compatibility with both A4 and Letter */
  }
}

var canvas = document.getElementById("canvas1");
function draw_a() {
  var context = canvas.getContext("2d");
  //   //  LEVER
  //plane
  context.fillStyle = '#aaa';
  context.fillRect(25, 90, 2500, 400);
}
$(document).ready(function() {
  draw_a();
});
canvas {
  border: 1px dotted;
}
.printOnly {
  display: none;
}
@media print {
 @page {
    size: 297mm 210mm; /* landscape */
    /* you can also specify margins here: */
    margin: 25mm;
    margin-right: 45mm; /* for compatibility with both A4 and Letter */
  }
  html,
  body {
    height: 100%;
    background-color: yellow;
  }
  .myDivToPrint {
    background-color: yellow;
    /*
        height: 100%;
    
        width: 100%;
        position: fixed;*/
    top: 0;
    left: 0;
    margin: 0;
  }
  .no-print,
  .no-print * {
    display: none !important;
  }
  .printOnly {
    display: block;
  }
}

@media print and (-ms-high-contrast: active),
(-ms-high-contrast: none) {
  html,
  body {
    height: 100%;
    background-color: yellow;
  }
  .myDivToPrint {
    background-color: yellow;
    /*
        height: 100%;
    
        width: 100%;
        position: fixed;*/
    top: 0;
    left: 0;
    margin: 0;
    padding: 15px;
    font-size: 14px;
    line-height: 18px;
    position: absolute;
    display: flex;
    align-items: center;
    justify-content: center;
    -webkit-transform: rotate(90deg);
    -moz-transform: rotate(90deg);
    -o-transform: rotate(90deg);
    -ms-transform: rotate(90deg);
    transform: rotate(90deg);
  }
  .no-print,
  .no-print * {
    display: none !important;
  }
  .printOnly {
    display: block;
  }
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<button onclick="window.print();" class="no-print">Print Canvas</button>
<div class="myDivToPrint">
  <div class="Aligner-item">
    <canvas height="2500px" width="4000px" id="canvas1"></canvas>
  <div class="printOnly Aligner-item--bottom"> Print Only</div>
  </div>
</div>
Farhad Bagherlo
  • 6,725
  • 3
  • 25
  • 47
  • Thank you for your reply. It does show a the printout across multiple vertical pages. However, I need to figure out how to print across multiple page widths. How does that work? – Matthew David Jankowski Sep 06 '17 at 14:34
1

I just tested this fiddle in both browsers firefox and chrome using a localhost environment and it worked within both. Here is the original js fiddle

And here is the html I tested

var canvas = document.getElementById("canvas1");

function draw_a() {
  var context = canvas.getContext("2d");
  //   //  LEVER

  //plane
  context.fillStyle = '#aaa';
  context.fillRect(25, 90, 2500, 400);


}

$(document).ready(function() {
  draw_a();

});
div.sizePage {
  color: #333;
}

canvas {
  border: 1px dotted;
}

.printOnly {
  display: none;
}

@media print {
  html,
  body {
    height: 100%;
    background-color: yellow;
  }
  .myDivToPrint {
    background-color: yellow;
    /*
            height: 100%;
    
            width: 100%;
            position: fixed;*/
    top: 0;
    left: 0;
    margin: 0;
  }
  .no-print,
  .no-print * {
    display: none !important;
  }
  .printOnly {
    display: block;
  }
}

@media print and (-ms-high-contrast: active),
(-ms-high-contrast: none) {
  html,
  body {
    height: 100%;
    background-color: yellow;
  }
  .myDivToPrint {
    background-color: yellow;
    /*
            height: 100%;
    
            width: 100%;
            position: fixed;*/
    top: 0;
    left: 0;
    margin: 0;
    padding: 15px;
    font-size: 14px;
    line-height: 18px;
    position: absolute;
    display: flex;
    align-items: center;
    justify-content: center;
    -webkit-transform: rotate(90deg);
    -moz-transform: rotate(90deg);
    -o-transform: rotate(90deg);
    -ms-transform: rotate(90deg);
    transform: rotate(90deg);
  }
  .no-print,
  .no-print * {
    display: none !important;
  }
  .printOnly {
    display: block;
  }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<button onclick="window.print();" class="no-print">Print Canvas</button>
<div class="myDivToPrint">
  <div class="Aligner-item">
    <canvas height="2500px" width="4000px" id="canvas1"></canvas>
    <div class="printOnly Aligner-item--bottom"> Print Only</div>
  </div>
</div>

So I thinks it's safe to say that it is supported in both browsers now.

I am using the most recent update on both browsers.

MrGreyKnight
  • 37
  • 11
1

Try this!

var canvas = document.getElementById("canvas1");
    function draw_a() {
      var context = canvas.getContext("2d");
     context.fillStyle   = '#aaa';
     context.fillRect  (25, 90, 2500, 400);
    } 
    
    $(document).ready(function(){
      draw_a();
    });
@page Section1 {
    size:8.27in 11.69in; 
    margin:0; 
    mso-header-margin:0; 
    mso-footer-margin:0; 
    mso-paper-source:0;
}
  <button onclick="window.print();" class="no-print">Print Canvas</button>
  <div class="myDivToPrint">
    <div class="Aligner-item">
      <canvas height="2500px" width="4000px" id="canvas1" style="border: solid 10px #000;"></canvas>
    </div>
  </div>
grinmax
  • 1,835
  • 1
  • 10
  • 13
1

It is impossible to handle this problem using plain CSS styles. I recommend to clone element to print multiple times (in this case), put copies after each other and make them "print only" using CSS. Additionally, canvas can't be just cloned - it needs to be redrawn for each copy.

Number of copies depends on the element and page widths. Default page width is 210mm, it can be converted to px (Pixel to Centimeter?) and compared to the element's width.

When we have page width in pixels, we can set negative left margin for each copy respectively. Having that, entire canvas will be "divided" into columns and printed from top to bottom.

In order to have each copy printed starting from the new page, simple use this CSS rule:

page-break-before: always;

There is a lot of hardcoded things, however I think that you could use it to build a generic solution for your problem.

var divide = function(selector, pageWidth) {
  var elementToDivide = document.querySelector(selector);
  var widthPx = elementToDivide.offsetWidth;
  var pageWidthPx = pageWidth * 3.7795;

  for (var i = 1; i <= parseInt(widthPx/pageWidthPx); i++) {
    var clone = elementToDivide.cloneNode(true);
    elementToDivide.parentNode.appendChild(clone);

    draw_a(document.getElementsByTagName("canvas"))
    clone.style.marginLeft = "-" + (pageWidthPx * i) + "px";
    clone.className += " printOnly";
  }
}

var standardPrint = window.print;

window.print = function() {
  if (!window.pagesDivided) {
    divide(".myDivToPrint", 210);
    window.pagesDivided = true;
  }
  standardPrint();
};

function draw(canvas) {
  var context = canvas.getContext("2d");
  var grd = context.createLinearGradient(0, 0, 4000, 2500);
  grd.addColorStop(0, "yellow");
  grd.addColorStop(1, "red");

  context.fillStyle = grd;
  context.fillRect(25, 25, 4000, 2500);
}

function draw_a(elem) {
  if (elem.length != null && elem.length > 1) {
    for (var i = 0; i < elem.length; i++) {
      draw(elem[i]);
    }
  } else {
    draw(elem);
  }
}
$(document).ready(function() {
  draw_a(document.getElementById("canvas1"));
});
canvas {
  border: 5px dashed;
}
.printOnly {
  display: none;
}
.myDivToPrint { 
  float: left;
}
@media print {
 @page {
    size: 297mm 210mm;
    margin: 0mm;
    margin-right: 0mm;
  }
  html,
  body {
    height: 100%;
    background-color: yellow;
  }
  .myDivToPrint {
    page-break-before: always;
    background-color: yellow;
    margin: 0;
  }
  .no-print,
  .no-print * {
    display: none !important;
  }
  .printOnly {
    display: block;
    page-break-before: always;
  }
}

@media print and (-ms-high-contrast: active),
(-ms-high-contrast: none) {
  html,
  body {
    height: 100%;
    background-color: yellow;
  }
  .myDivToPrint {
    background-color: yellow;
    top: 0;
    left: 0;
    margin: 0;
    padding: 15px;
    font-size: 14px;
    line-height: 18px;
    align-items: center;
    justify-content: center;
  }
  .no-print,
  .no-print * {
    display: none !important;
  }
  .printOnly {
    display: block;
  }
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<button onclick="window.print();" class="no-print">Print Canvas</button>
<div class="myDivToPrint">
  <div class="Aligner-item">
    <canvas height="2500px" width="4000px" id="canvas1"></canvas>
  <div class="printOnly Aligner-item--bottom"> Print Only</div>
  </div>
</div>
luke
  • 3,531
  • 1
  • 26
  • 46
  • 1
    I think this was the technically most 'correct' answer. This approach shows the most promise to print a canvas element over multiple pages. However, I didn't award it the bounty because the accepted answer provided a workable alternative. It was really close. If I could have split the bounty I would have. – Matthew David Jankowski Sep 12 '17 at 15:30