Skip to content

Commit 5edb2ad

Browse files
committed
Fix bdata byte order for big-endian platforms
to_typed_array_spec base64-encodes arrays in the host's native byte order with no normalization. plotly.js decodes bdata with typed arrays (e.g. Float64Array), which always read in the platform's native order, little-endian on every real browser. On a big-endian host this produces bdata that decodes to garbage in any browser. Normalize to little-endian before encoding, verified on real big-endian hardware (z/OS/s390x) that this resolves it.
1 parent 0de8a38 commit 5edb2ad

2 files changed

Lines changed: 66 additions & 1 deletion

File tree

‎_plotly_utils/utils.py‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,15 @@ def to_typed_array_spec(v):
7979
dtype = str(v.dtype)
8080

8181
if dtype in plotlyjsShortTypes:
82+
# plotly.js decodes bdata with typed arrays (e.g. Float64Array),
83+
# which always read in the platform's native order, little-endian
84+
# on every real browser. Normalize to little-endian before encoding
85+
# so bdata is correct regardless of the host's own byte order.
8286
arrObj = {
8387
"dtype": plotlyjsShortTypes[dtype],
84-
"bdata": base64.b64encode(v).decode("ascii"),
88+
"bdata": base64.b64encode(v.astype(v.dtype.newbyteorder("<"))).decode(
89+
"ascii"
90+
),
8591
}
8692

8793
if v.ndim > 1:
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"""
2+
Tests for _plotly_utils.utils.to_typed_array_spec's byte order.
3+
4+
plotly.js decodes "bdata" in the browser with typed arrays (e.g.
5+
new Float64Array(buffer)), which always read in the platform's native
6+
order, little-endian on every real browser. to_typed_array_spec must
7+
therefore always produce little-endian bytes, regardless of the host's
8+
own native byte order.
9+
10+
Note: these arrays are built in plain/native order (dtype="float64" etc,
11+
not ">f8"/"<f8"), exactly as every real caller does. An array built with
12+
an explicit non-native byte order (e.g. ">f8") reports str(dtype) as
13+
">f8" rather than "float64", so it never reaches the plotlyjsShortTypes
14+
dispatch in to_typed_array_spec at all, it can't be used to simulate
15+
"a big-endian host" from a little-endian machine. These tests therefore
16+
only catch a real regression when actually run on big-endian hardware
17+
(e.g. z/OS/s390x), not on a little-endian CI runner.
18+
"""
19+
20+
import base64
21+
22+
import numpy as np
23+
import pytest
24+
25+
from _plotly_utils.utils import to_typed_array_spec
26+
27+
28+
@pytest.mark.parametrize(
29+
"dtype,np_dtype",
30+
[
31+
("float64", "f8"),
32+
("float32", "f4"),
33+
("int32", "i4"),
34+
("int16", "i2"),
35+
("uint32", "u4"),
36+
],
37+
)
38+
def test_bdata_is_little_endian(dtype, np_dtype):
39+
v = np.array([1, 2, 3, 4, 5], dtype=dtype)
40+
assert str(v.dtype) == dtype # sanity: native order, routes through the real dispatch
41+
42+
spec = to_typed_array_spec(v)
43+
assert isinstance(spec, dict), "did not reach the typed-array-spec encoding path"
44+
45+
raw = base64.b64decode(spec["bdata"])
46+
decoded_as_browser_would_read_it = np.frombuffer(raw, dtype=f"<{np_dtype}")
47+
48+
np.testing.assert_array_equal(decoded_as_browser_would_read_it, v)
49+
50+
51+
def test_bdata_is_little_endian_for_floats_with_fraction():
52+
v = np.array([1.1, 2.3, 3.5])
53+
54+
spec = to_typed_array_spec(v)
55+
56+
raw = base64.b64decode(spec["bdata"])
57+
decoded_as_browser_would_read_it = np.frombuffer(raw, dtype="<f8")
58+
59+
np.testing.assert_array_equal(decoded_as_browser_would_read_it, v)

0 commit comments

Comments
 (0)