qq15725 / modern-screenshot

📸 Quickly generate image from DOM node using HTML5 canvas and SVG
https://www.toolpkg.com/image/html-to-image
MIT License
457 stars 32 forks source link

Not able to render element which has display:contents; property #83

Open nextend opened 4 months ago

nextend commented 4 months ago

When the node which you want to render has display: contents; property applied, the library unable to generate screenshot as the canvas size will be 0px/0px. It is caused by that the library depends on the .getBoundingClientRect() of the given node, but display:contents element do not have size.

The problem lies in create-context.ts

function resolveBoundingBox(node: Node, context: Context) {
  let { width, height } = context

  if (isElementNode(node) && (!width || !height)) {
    const box = node.getBoundingClientRect()

    width = width
      || box.width
      || Number(node.getAttribute('width'))
      || 0

    height = height
      || box.height
      || Number(node.getAttribute('height'))
      || 0
  }

  return { width, height }
}

Here is an alternative implementation of getBoundingClientRect() which tries to fix the problems with display:contents elements.

/**
 *
 * @param element
 * @return {DOMRect}
 */
export function recursiveGetBoundingClientRect(element) {

    const cs = element.ownerDocument.defaultView.getComputedStyle(element);

    if (cs.display === 'contents') {
        /**
         *
         * @type {Node[]}
         */
        const childNodes = Array.from(element.childNodes);
        /**
         * @type DOMRect[]
         */
        const rects = childNodes.reduce((accumulator, childNode) => {
            if (childNode.nodeType === Node.ELEMENT_NODE) {

                accumulator.push(recursiveGetBoundingClientRect(childNode));
                return accumulator;
            } else if (childNode.nodeType === Node.TEXT_NODE) {
                const range = element.ownerDocument.createRange();
                range.selectNode(childNode);
                return range.getBoundingClientRect();
            }
        }, []);

        if (rects.length) {
            return mergeDOMRects(rects);
        }

        return findParentDomRect(element);
    }

    return element.getBoundingClientRect();
}

function mergeDOMRects(rects) {
    if (!rects || rects.length === 0) {
        return null;
    }

    let minX = rects[0].left;
    let minY = rects[0].top;
    let maxX = rects[0].right;
    let maxY = rects[0].bottom;

    rects.forEach(rect => {
        if (rect.left < minX) minX = rect.left;
        if (rect.top < minY) minY = rect.top;
        if (rect.right > maxX) maxX = rect.right;
        if (rect.bottom > maxY) maxY = rect.bottom;
    });

    return new DOMRect(minX, minY, maxX - minX, maxY - minY);
}

/**
 *
 * @param {Node} node
 * @return {DOMRect}
 */
function findParentDomRect(node) {

    const ownerDocument = node.ownerDocument;
    let currentElement = node.parentElement;

    while (currentElement && currentElement !== ownerDocument.documentElement) {
        if (ownerDocument.defaultView.getComputedStyle(currentElement).display !== 'contents') {
            return currentElement.getBoundingClientRect();
        }
        currentElement = currentElement.parentElement;
    }

    const window = ownerDocument.defaultView;

    return new DOMRect(0, 0, window.innerWidth, window.innerHeight);
}