Search code examples
javascriptreactjscanvasprintingfabricjs

fabric.js - React.js - canvas - Printing multiple fabric canvases that contain images (w/ react-to-print)


So I'm trying to document print multiple fabric images. Here is the preparation process. I see a blank window. I can provide additional information on request. Some sort of async logic I'm missing out maybe ?

1- I initialize a canvas from each canvas object (looping over an array of canvases - each canvas object has an image and possibly multiple textbox objects)

2- I then use toDataURL() and create image element for each, attach the src to img

3- I loop over the images and append them to printWindow.

   const handleCustomPrint = () => {

    const div = document.querySelector('.print-content');

    for (let i = 0; i < canvasArrayToBePrinted.length; i++) {
      const canvas = new fabric.Canvas(`${i}`)
      canvas.loadFromJSON(canvasArrayToBePrinted[i], () => {
        const img = canvas.toDataURL({
          format: 'jpeg',
          quality: 0.75
        });
        const singleImg = `<img src=${img} class='image-content' />`
        div.innerHTML += singleImg;
      });
    }

    console.log(div);

    var windowUrl = 'about:blank';
    var uniqueName = new Date();
    var windowName = 'Print' + uniqueName.getTime();
    var printWindow = window.open(windowUrl, windowName, 'left=50000,top=50000,width=1000000,height=10000');
    printWindow.document.write(div.innerHTML);

    printWindow.document.close();

    printWindow.onload = function() {
      printWindow.focus();
      printWindow.print();
      printWindow.close();
    }
    return true;

  };

EDIT: OK. I think I'm getting close. But still can't see nothing.

This is the output of console.log(div)

<div class="print-content">
<img src="data:image/jpeg;base64,/9j/4AAQSkZJ..." class="img-content"/>
<img src="data:image/jpeg;base64,/9j/4AAQSkZJ..." class="img-content"/>
</div>

This is the CSS for targeting print styles

@media all {
  .img-content {
    display: none !important;
  }
  .print-content {
    display: none !important;
  }
}
@media print {

  .print-content {
    display: block !important;
  }

  .img-print {
    display: block !important;
  }
}

Solution

  • Preparing canvases (loadFromJSON callback is the indicator that indicates whether the canvas is initialized with all its objects (images/textboxes) attached to it... )

    const PrintContent = React.forwardRef((props, ref) => {
    
      const { canvasData, setIsPrintReady, shouldExcludeImage } = props;
    
      const [preparedCanvases, setPreparedCanvases] = React.useState({ prepared: 0, total: canvasData.length });
    
      const incrementPrepared = () => setPreparedCanvases(prev => ({ ...prev, prepared: prev.prepared + 1 }));
    
      const imageToBePrinted = canvasData && canvasData.length && canvasData[0].objects.find(o => o.type === 'image');
    
      const { width, height } = imageToBePrinted;
    
      React.useEffect(() => {
        if (preparedCanvases.prepared === preparedCanvases.total) {
          setIsPrintReady(true);
        }
      }, [preparedCanvases, setIsPrintReady])
    
      return (
        <div className="content-to-be-printed" ref={ref}>
          {
            props.canvasData && props.canvasData.length && props.canvasData.map((singleCanvas, idx) => {
              return <SingleCanvasData
                key={`${idx}--canvas`}
                singleCanvas={singleCanvas}
                index={idx}
                incrementPrepared={incrementPrepared}
                imgWidth={width}
                imgHeight={height}
                shouldExcludeImage={shouldExcludeImage}
                />
            })
          }
        </div>
      )
    });
    
    const SingleCanvasData = ({ singleCanvas, index, incrementPrepared, imgWidth, imgHeight, shouldExcludeImage }) => {
    
      const [canvas, setCanvas] = React.useState("");
      const [once, setOnce] = React.useState(true);
    
      React.useEffect(() => {
        const initCanvas = () =>
          new fabric.Canvas(`${index}`, {
            height: imgHeight,
            width: imgWidth,
            preserveObjectStacking: true
          });
        setCanvas(initCanvas());
      }, [index, imgHeight, imgWidth]);
    
      React.useEffect(() => {
        if (once && canvas) {
    
          let canvasWithoutImage;
          if(!shouldExcludeImage) {
            canvasWithoutImage = {...singleCanvas, objects: singleCanvas.objects.filter(o => o.type !== 'image')};
          }
    
          canvas.loadFromJSON(canvasWithoutImage || singleCanvas, () => {
            incrementPrepared();
          });
          setOnce(false);
        }
    
      }, [canvas, incrementPrepared, once, singleCanvas, shouldExcludeImage]);
    
      return (
        <canvas
          style={{ width: 'auto', height: 'auto' }}
          className="single-canvas"
          id={index}
          key={`canvas-${index}`}>
    
        </canvas>
      )
    }
    
    export default PrintContent
    

    Parent

          {
            canPrint && (
              <PrintContent
                canvasData={canvasArrayToBePrinted}
                setIsPrintReady={setIsPrintReady}
                ref={componentRef}
                shouldExcludeImage={shouldExcludeImage} />
            )
          }
    

    useReactToPrint hook (attach ref to the component you want to print (in my case its forwardRef because I'm passing ref down to a component) - you can also declare print styles via props - super nifty) that allows you to print a react component (react-to-print)

      const handlePrint = useReactToPrint({
        pageStyle: `
        @page { size: ${pageSize.widthPX}px ${pageSize.heightPX}px; margin: 0mm; }
        `,
        content: () => componentRef.current,
        onAfterPrint: () => handleResetPrintModal()
      });
    

    Enable print button when print is ready

                <button disabled={!isPrintReady || !pageSize.widthPX}
                  onClick={handlePrint}
                  className='btn-primary btn-success'>
                  Print
                </button>