Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
82 / 82
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
ImageStyle
100.00% covered (success)
100.00%
82 / 82
100.00% covered (success)
100.00%
4 / 4
29
100.00% covered (success)
100.00%
1 / 1
 readVml
100.00% covered (success)
100.00%
34 / 34
100.00% covered (success)
100.00%
1 / 1
9
 readDrawing
100.00% covered (success)
100.00%
34 / 34
100.00% covered (success)
100.00%
1 / 1
11
 position
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
 cssToPoint
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3/**
4 * This file is part of PHPWord - A pure PHP library for reading and writing
5 * word processing documents.
6 *
7 * PHPWord is free software distributed under the terms of the GNU Lesser
8 * General Public License version 3 as published by the Free Software Foundation.
9 *
10 * For the full copyright and license information, please read the LICENSE
11 * file that was distributed with this source code. For the full list of
12 * contributors, visit https://github.com/PHPOffice/PHPWord/contributors.
13 *
14 * @see         https://github.com/PHPOffice/PHPWord
15 *
16 * @license     http://www.gnu.org/licenses/lgpl.txt LGPL version 3
17 */
18
19namespace PhpOffice\PhpWord\Reader\Word2007;
20
21use DOMElement;
22use PhpOffice\PhpWord\Shared\Converter;
23use PhpOffice\PhpWord\Shared\XMLReader;
24use PhpOffice\PhpWord\Style\Frame;
25
26/**
27 * Size, position and wrapping of an image, read from VML or DrawingML, as an array for Style\Image.
28 */
29class ImageStyle
30{
31    private const EMU_PER_POINT = 12700;
32
33    private const H_POS = [Frame::POS_ABSOLUTE, Frame::POS_LEFT, Frame::POS_CENTER, Frame::POS_RIGHT, Frame::POS_INSIDE, Frame::POS_OUTSIDE];
34    private const V_POS = [Frame::POS_ABSOLUTE, Frame::POS_TOP, Frame::POS_CENTER, Frame::POS_BOTTOM, Frame::POS_INSIDE, Frame::POS_OUTSIDE];
35
36    /** mso-position-horizontal-relative, and relativeFrom of wp:positionH */
37    private const H_POS_REL_TO = [
38        'margin' => Frame::POS_RELTO_MARGIN,
39        'page' => Frame::POS_RELTO_PAGE,
40        'text' => Frame::POS_RELTO_COLUMN,
41        'column' => Frame::POS_RELTO_COLUMN,
42        'char' => Frame::POS_RELTO_CHAR,
43        'character' => Frame::POS_RELTO_CHAR,
44        'left-margin-area' => Frame::POS_RELTO_LMARGIN,
45        'leftMargin' => Frame::POS_RELTO_LMARGIN,
46        'right-margin-area' => Frame::POS_RELTO_RMARGIN,
47        'rightMargin' => Frame::POS_RELTO_RMARGIN,
48        'inner-margin-area' => Frame::POS_RELTO_IMARGIN,
49        'insideMargin' => Frame::POS_RELTO_IMARGIN,
50        'outer-margin-area' => Frame::POS_RELTO_OMARGIN,
51        'outsideMargin' => Frame::POS_RELTO_OMARGIN,
52    ];
53
54    /** mso-position-vertical-relative, and relativeFrom of wp:positionV */
55    private const V_POS_REL_TO = [
56        'margin' => Frame::POS_RELTO_MARGIN,
57        'page' => Frame::POS_RELTO_PAGE,
58        'text' => Frame::POS_RELTO_TEXT,
59        'paragraph' => Frame::POS_RELTO_TEXT,
60        'line' => Frame::POS_RELTO_LINE,
61        'top-margin-area' => Frame::POS_RELTO_TMARGIN,
62        'topMargin' => Frame::POS_RELTO_TMARGIN,
63        'bottom-margin-area' => Frame::POS_RELTO_BMARGIN,
64        'bottomMargin' => Frame::POS_RELTO_BMARGIN,
65        'inner-margin-area' => Frame::POS_RELTO_IMARGIN,
66        'insideMargin' => Frame::POS_RELTO_IMARGIN,
67        'outer-margin-area' => Frame::POS_RELTO_OMARGIN,
68        'outsideMargin' => Frame::POS_RELTO_OMARGIN,
69    ];
70
71    private const WRAP = [
72        'square' => Frame::WRAP_SQUARE,
73        'tight' => Frame::WRAP_TIGHT,
74        'through' => Frame::WRAP_THROUGH,
75        'topAndBottom' => Frame::WRAP_TOPBOTTOM,
76    ];
77
78    /**
79     * Read the style of a VML v:shape, as Word and the Word2007 writer write it.
80     *
81     * @return array<string, float|string>
82     */
83    public static function readVml(XMLReader $xmlReader, DOMElement $shape): array
84    {
85        $css = [];
86        foreach (explode(';', $shape->getAttribute('style')) as $declaration) {
87            $pair = explode(':', $declaration, 2);
88            if (count($pair) === 2) {
89                $css[strtolower(trim($pair[0]))] = trim($pair[1]);
90            }
91        }
92
93        $style = [];
94        $lengths = [
95            'width' => 'width',
96            'height' => 'height',
97            'margin-left' => 'left',
98            'margin-top' => 'top',
99            'mso-wrap-distance-top' => 'wrapDistanceTop',
100            'mso-wrap-distance-bottom' => 'wrapDistanceBottom',
101            'mso-wrap-distance-left' => 'wrapDistanceLeft',
102            'mso-wrap-distance-right' => 'wrapDistanceRight',
103        ];
104        foreach ($lengths as $property => $key) {
105            $points = self::cssToPoint($css[$property] ?? '');
106            if ($points !== null) {
107                $style[$key] = $points;
108            }
109        }
110        $style = array_merge($style, self::position(
111            $css['mso-position-horizontal'] ?? '',
112            $css['mso-position-horizontal-relative'] ?? '',
113            $css['mso-position-vertical'] ?? '',
114            $css['mso-position-vertical-relative'] ?? ''
115        ));
116
117        $position = $css['position'] ?? '';
118        if ($position === Frame::POS_ABSOLUTE || $position === Frame::POS_RELATIVE) {
119            $style['pos'] = $position;
120            // A floating shape wraps as w10:wrap says, or is behind or in front of the text without it
121            $wrap = self::WRAP[(string) $xmlReader->getAttribute('type', $shape, 'w10:wrap')] ?? null;
122            if ($wrap === null) {
123                $wrap = (int) ($css['z-index'] ?? 0) < 0 ? Frame::WRAP_BEHIND : Frame::WRAP_INFRONT;
124            }
125            $style['wrap'] = $wrap;
126        }
127
128        return $style;
129    }
130
131    /**
132     * Read the style of a DrawingML wp:inline or wp:anchor.
133     *
134     * @return array<string, float|string>
135     */
136    public static function readDrawing(XMLReader $xmlReader, DOMElement $frame): array
137    {
138        $style = [];
139        $lengths = [
140            'wp:extent/@cx' => 'width',
141            'wp:extent/@cy' => 'height',
142            '@distT' => 'wrapDistanceTop',
143            '@distB' => 'wrapDistanceBottom',
144            '@distL' => 'wrapDistanceLeft',
145            '@distR' => 'wrapDistanceRight',
146        ];
147        foreach ($lengths as $path => $key) {
148            $emu = $xmlReader->getValue($path, $frame);
149            if (is_numeric($emu)) {
150                $style[$key] = $emu / self::EMU_PER_POINT;
151            }
152        }
153        if ($frame->localName !== 'anchor') {
154            return $style;
155        }
156
157        $style['pos'] = Frame::POS_ABSOLUTE;
158        $style = array_merge($style, self::position(
159            (string) $xmlReader->getValue('wp:positionH/wp:align', $frame),
160            (string) $xmlReader->getAttribute('relativeFrom', $frame, 'wp:positionH'),
161            (string) $xmlReader->getValue('wp:positionV/wp:align', $frame),
162            (string) $xmlReader->getAttribute('relativeFrom', $frame, 'wp:positionV')
163        ));
164        foreach (['H' => 'left', 'V' => 'top'] as $axis => $key) {
165            $emu = $xmlReader->getValue("wp:position{$axis}/wp:posOffset", $frame);
166            if (is_numeric($emu)) {
167                $style[$axis === 'H' ? 'hPos' : 'vPos'] = Frame::POS_ABSOLUTE;
168                $style[$key] = $emu / self::EMU_PER_POINT;
169            }
170        }
171
172        foreach (self::WRAP as $name => $wrap) {
173            if ($xmlReader->elementExists('wp:wrap' . ucfirst($name), $frame)) {
174                $style['wrap'] = $wrap;
175            }
176        }
177        if (!isset($style['wrap'])) {
178            $behind = in_array($frame->getAttribute('behindDoc'), ['1', 'true', 'on'], true);
179            $style['wrap'] = $behind ? Frame::WRAP_BEHIND : Frame::WRAP_INFRONT;
180        }
181
182        return $style;
183    }
184
185    /**
186     * Alignment and reference of the position, left out when Style\Frame does not know the value.
187     *
188     * @return array<string, string>
189     */
190    private static function position(string $hPos, string $hPosRelTo, string $vPos, string $vPosRelTo): array
191    {
192        $style = [];
193        if (in_array($hPos, self::H_POS, true)) {
194            $style['hPos'] = $hPos;
195        }
196        if (isset(self::H_POS_REL_TO[$hPosRelTo])) {
197            $style['hPosRelTo'] = self::H_POS_REL_TO[$hPosRelTo];
198        }
199        if (in_array($vPos, self::V_POS, true)) {
200            $style['vPos'] = $vPos;
201        }
202        if (isset(self::V_POS_REL_TO[$vPosRelTo])) {
203            $style['vPosRelTo'] = self::V_POS_REL_TO[$vPosRelTo];
204        }
205
206        return $style;
207    }
208
209    /**
210     * A VML length in points; a number without a unit is in pixels.
211     */
212    private static function cssToPoint(string $value): ?float
213    {
214        if (!preg_match('/^([+-]?)(\d*\.?\d+)(pt|px|in|cm|mm|pc)?$/', $value, $matches)) {
215            return null;
216        }
217        $points = (float) Converter::cssToPoint($matches[2] . (empty($matches[3]) ? 'px' : $matches[3]));
218
219        return $matches[1] === '-' ? -$points : $points;
220    }
221}