Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
76.92% covered (warning)
76.92%
10 / 13
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
MathmlIntrinsicSizer
76.92% covered (warning)
76.92%
10 / 13
66.67% covered (warning)
66.67%
2 / 3
7.60
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 measure
62.50% covered (warning)
62.50%
5 / 8
0.00% covered (danger)
0.00%
0 / 1
4.84
 renderer
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace Phpdftk\HtmlToPdf\Mathml;
6
7use Phpdftk\Html\Dom\Element as HtmlElement;
8use Phpdftk\MathmlToPdf\MathmlRenderer;
9use Phpdftk\Pdf\Writer\PdfWriter;
10
11/**
12 * Measure an inline `<math>` subtree during LAYOUT.
13 *
14 * {@see MathmlRenderer::intrinsicSize()} could always do this, but its
15 * only caller was the painter โ€” which runs after layout has already
16 * placed every box. So layout sized the `<math>` atomic inline at 0x0
17 * and the painter then drew a correctly-sized equation into a box that
18 * reserved no room for it: content after an equation overprinted it,
19 * and a line holding nothing but math collapsed to the height of an
20 * empty text line.
21 *
22 * `BlockLayout` consults this from its inline-atomic pre-pass, which is
23 * the same point where an `inline-block`'s used content size is
24 * resolved, so the result flows into the existing `laidOut*` protocol
25 * without `InlineLayout` needing to know MathML exists.
26 *
27 * ### The scratch writer
28 *
29 * `MathmlRenderer` takes a page and a writer because it normally
30 * paints. Measuring touches neither โ€” it only needs the two standard
31 * faces to exist so the paint context can be constructed. Layout has no
32 * page yet (pagination creates them later), so this holds a scratch
33 * `PdfWriter` whose output is discarded. It is created lazily and
34 * reused, so documents without MathML pay nothing at all and a document
35 * with many equations pays for one throwaway writer.
36 *
37 * The parsed-document cache is shared with {@see InlineMathmlAdapter} by
38 * passing the painter's adapter in, so a `<math>` measured at layout and
39 * painted afterwards is parsed once rather than twice.
40 */
41final class MathmlIntrinsicSizer
42{
43    private ?MathmlRenderer $renderer = null;
44
45    public function __construct(
46        private readonly InlineMathmlAdapter $adapter = new InlineMathmlAdapter(),
47    ) {}
48
49    /**
50     * Intrinsic size of `$element`'s MathML content at `$fontSize`.
51     *
52     * Returns `[width, height, baseline]` in PDF user-space units,
53     * where `baseline` is the distance DOWN from the content-box top
54     * edge to the math baseline โ€” the offset CSS 2.1 ยง10.8.1 aligns an
55     * atomic inline on. Returns null when the element is not MathML,
56     * fails to parse, or measures to nothing, so callers keep whatever
57     * sizing they would otherwise have applied.
58     *
59     * A parse failure is swallowed for the same reason the painter
60     * swallows it: one malformed expression must not poison the page.
61     *
62     * @return array{0: float, 1: float, 2: float}|null
63     */
64    public function measure(HtmlElement $element, float $fontSize): ?array
65    {
66        try {
67            $document = $this->adapter->adapt($element);
68        } catch (\Throwable) {
69            return null;
70        }
71        $renderer = $this->renderer();
72        [$width, $height] = $renderer->intrinsicSize($document, $fontSize);
73        if ($width <= 0.0 && $height <= 0.0) {
74            return null;
75        }
76
77        return [$width, $height, $renderer->intrinsicAscent($document, $fontSize)];
78    }
79
80    private function renderer(): MathmlRenderer
81    {
82        if ($this->renderer === null) {
83            $writer = new PdfWriter();
84            $this->renderer = new MathmlRenderer($writer->addPage(), $writer);
85        }
86
87        return $this->renderer;
88    }
89}