quantized-lab 0.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quantized/__init__.py +11 -0
- quantized/__main__.py +14 -0
- quantized/api.py +220 -0
- quantized/app.py +206 -0
- quantized/calc/__init__.py +8 -0
- quantized/calc/_clipfit.py +76 -0
- quantized/calc/_natural_neighbor.py +219 -0
- quantized/calc/aggregate.py +159 -0
- quantized/calc/backgrounds.py +353 -0
- quantized/calc/baseline.py +349 -0
- quantized/calc/batch_fit.py +148 -0
- quantized/calc/constants.py +27 -0
- quantized/calc/corrections.py +192 -0
- quantized/calc/crystallography.py +400 -0
- quantized/calc/diffusion.py +120 -0
- quantized/calc/electrical.py +248 -0
- quantized/calc/electrochemistry.py +176 -0
- quantized/calc/element_data.json +1 -0
- quantized/calc/element_data.py +59 -0
- quantized/calc/errors.py +246 -0
- quantized/calc/figure.py +417 -0
- quantized/calc/figure_break.py +152 -0
- quantized/calc/figure_categorical.py +156 -0
- quantized/calc/figure_corner.py +229 -0
- quantized/calc/figure_facets.py +127 -0
- quantized/calc/figure_field.py +137 -0
- quantized/calc/figure_hitmap.py +116 -0
- quantized/calc/figure_labels.py +62 -0
- quantized/calc/figure_map.py +287 -0
- quantized/calc/figure_overrides.py +159 -0
- quantized/calc/figure_page.py +266 -0
- quantized/calc/figure_scale.py +125 -0
- quantized/calc/figure_statplots.py +167 -0
- quantized/calc/figure_styles.py +131 -0
- quantized/calc/figure_ternary.py +239 -0
- quantized/calc/figure_ticks.py +217 -0
- quantized/calc/fit_autoguess.py +156 -0
- quantized/calc/fit_bootstrap.py +163 -0
- quantized/calc/fit_bumps.py +258 -0
- quantized/calc/fit_constraints.py +114 -0
- quantized/calc/fit_equation.py +264 -0
- quantized/calc/fit_findxy.py +80 -0
- quantized/calc/fit_models.py +195 -0
- quantized/calc/fit_models_special.py +189 -0
- quantized/calc/fit_odr.py +99 -0
- quantized/calc/fit_scan.py +243 -0
- quantized/calc/fit_stats.py +215 -0
- quantized/calc/fitting.py +199 -0
- quantized/calc/formula.py +90 -0
- quantized/calc/global_curve_fit.py +305 -0
- quantized/calc/global_fit.py +181 -0
- quantized/calc/interp2d.py +260 -0
- quantized/calc/linecut.py +269 -0
- quantized/calc/magnetic.py +414 -0
- quantized/calc/magnetometry.py +464 -0
- quantized/calc/map.py +228 -0
- quantized/calc/mcmc.py +177 -0
- quantized/calc/optics.py +228 -0
- quantized/calc/pawley.py +251 -0
- quantized/calc/peak_batch.py +128 -0
- quantized/calc/peak_fit.py +259 -0
- quantized/calc/peak_integrate.py +104 -0
- quantized/calc/peak_multifit.py +260 -0
- quantized/calc/peak_track.py +134 -0
- quantized/calc/peaks.py +298 -0
- quantized/calc/peakshapes.py +85 -0
- quantized/calc/plotting.py +147 -0
- quantized/calc/processing.py +232 -0
- quantized/calc/qspace.py +48 -0
- quantized/calc/reductions.py +155 -0
- quantized/calc/reductions_fft.py +383 -0
- quantized/calc/refl_sld_presets.json +1 -0
- quantized/calc/reflectivity.py +80 -0
- quantized/calc/registry.py +303 -0
- quantized/calc/relaxation.py +119 -0
- quantized/calc/report.py +253 -0
- quantized/calc/report_emit.py +227 -0
- quantized/calc/resample.py +142 -0
- quantized/calc/rsm.py +91 -0
- quantized/calc/rsm_analyze.py +245 -0
- quantized/calc/semiconductor.py +488 -0
- quantized/calc/sld.py +131 -0
- quantized/calc/sld_formula.py +138 -0
- quantized/calc/spectral.py +357 -0
- quantized/calc/statplots.py +214 -0
- quantized/calc/stats.py +399 -0
- quantized/calc/stats_anova2.py +202 -0
- quantized/calc/stats_anova_ext.py +338 -0
- quantized/calc/stats_dist.py +196 -0
- quantized/calc/stats_glm.py +245 -0
- quantized/calc/stats_multivar.py +289 -0
- quantized/calc/stats_roc.py +157 -0
- quantized/calc/stats_survival.py +261 -0
- quantized/calc/stats_tests.py +380 -0
- quantized/calc/substrates.py +181 -0
- quantized/calc/superconductor.py +359 -0
- quantized/calc/surface_fit.py +290 -0
- quantized/calc/surface_models.py +156 -0
- quantized/calc/thermal.py +119 -0
- quantized/calc/thin_film.py +425 -0
- quantized/calc/unit_convert.py +259 -0
- quantized/calc/units.py +80 -0
- quantized/calc/vacuum.py +290 -0
- quantized/calc/xray.py +169 -0
- quantized/cli.py +214 -0
- quantized/datastruct.py +153 -0
- quantized/io/__init__.py +11 -0
- quantized/io/_hdf5_layout.py +308 -0
- quantized/io/_jcamp_asdf.py +135 -0
- quantized/io/_xrdml_scan.py +291 -0
- quantized/io/base.py +82 -0
- quantized/io/bruker_brml.py +177 -0
- quantized/io/bruker_raw.py +158 -0
- quantized/io/cif.py +266 -0
- quantized/io/consolidated.py +122 -0
- quantized/io/delimited.py +222 -0
- quantized/io/excel.py +135 -0
- quantized/io/hdf5.py +192 -0
- quantized/io/import_filters.py +178 -0
- quantized/io/import_preview.py +262 -0
- quantized/io/jcamp.py +179 -0
- quantized/io/lakeshore.py +163 -0
- quantized/io/ncnr.py +278 -0
- quantized/io/netcdf.py +195 -0
- quantized/io/opus.py +231 -0
- quantized/io/origin.py +346 -0
- quantized/io/origin_com.py +194 -0
- quantized/io/origin_project/__init__.py +221 -0
- quantized/io/origin_project/annotation_marks.py +288 -0
- quantized/io/origin_project/container.py +262 -0
- quantized/io/origin_project/curve_style_color.py +359 -0
- quantized/io/origin_project/figure_geometry.py +108 -0
- quantized/io/origin_project/figure_layers.py +333 -0
- quantized/io/origin_project/figure_text.py +258 -0
- quantized/io/origin_project/figures.py +210 -0
- quantized/io/origin_project/figures_opju.py +440 -0
- quantized/io/origin_project/notes.py +302 -0
- quantized/io/origin_project/opj.py +459 -0
- quantized/io/origin_project/opj_curves.py +297 -0
- quantized/io/origin_project/opj_shapes.py +148 -0
- quantized/io/origin_project/opju.py +146 -0
- quantized/io/origin_project/opju_axis_real_form.py +418 -0
- quantized/io/origin_project/opju_axis_specimen_form.py +167 -0
- quantized/io/origin_project/opju_codec.py +370 -0
- quantized/io/origin_project/opju_curves.py +497 -0
- quantized/io/origin_project/opju_curves_allcols.py +258 -0
- quantized/io/origin_project/opju_figure_curves.py +302 -0
- quantized/io/origin_project/opju_figure_text.py +245 -0
- quantized/io/origin_project/opju_reports.py +129 -0
- quantized/io/origin_project/origin_richtext.py +145 -0
- quantized/io/origin_project/preview.py +132 -0
- quantized/io/origin_project/templates.py +314 -0
- quantized/io/origin_project/tree.py +379 -0
- quantized/io/origin_project/tree_opju.py +228 -0
- quantized/io/origin_project/windows.py +238 -0
- quantized/io/origin_project/windows_opju.py +393 -0
- quantized/io/origin_project/writer.py +156 -0
- quantized/io/origin_project/writer_blocks.py +282 -0
- quantized/io/qd.py +380 -0
- quantized/io/refl1d.py +132 -0
- quantized/io/registry.py +210 -0
- quantized/io/report_export.py +347 -0
- quantized/io/rigaku.py +100 -0
- quantized/io/sims.py +398 -0
- quantized/io/spc.py +311 -0
- quantized/io/xrd_csv.py +308 -0
- quantized/io/xrdml.py +394 -0
- quantized/jobs.py +173 -0
- quantized/plugins/__init__.py +50 -0
- quantized/plugins/contract.py +111 -0
- quantized/plugins/loader.py +394 -0
- quantized/plugins/steps.py +90 -0
- quantized/routes/__init__.py +7 -0
- quantized/routes/_bookcache.py +62 -0
- quantized/routes/_export_common.py +27 -0
- quantized/routes/_payload.py +58 -0
- quantized/routes/_uploadcache.py +59 -0
- quantized/routes/aggregate.py +46 -0
- quantized/routes/baseline.py +210 -0
- quantized/routes/books.py +117 -0
- quantized/routes/calc.py +56 -0
- quantized/routes/corrections.py +78 -0
- quantized/routes/crystallography.py +80 -0
- quantized/routes/diffusion.py +58 -0
- quantized/routes/electrical.py +101 -0
- quantized/routes/electrochemistry.py +83 -0
- quantized/routes/export.py +280 -0
- quantized/routes/export_facets.py +83 -0
- quantized/routes/export_figures.py +471 -0
- quantized/routes/export_page.py +125 -0
- quantized/routes/fitting.py +379 -0
- quantized/routes/fitting_bumps.py +97 -0
- quantized/routes/import_template.py +97 -0
- quantized/routes/import_wizard.py +150 -0
- quantized/routes/jobs_api.py +59 -0
- quantized/routes/magnetic.py +135 -0
- quantized/routes/magnetometry.py +133 -0
- quantized/routes/optics.py +98 -0
- quantized/routes/parsers.py +281 -0
- quantized/routes/peaks.py +184 -0
- quantized/routes/plot.py +103 -0
- quantized/routes/reductions.py +121 -0
- quantized/routes/reference.py +58 -0
- quantized/routes/reflectivity.py +91 -0
- quantized/routes/report_export.py +119 -0
- quantized/routes/rsm.py +136 -0
- quantized/routes/samples.py +32 -0
- quantized/routes/semiconductor.py +207 -0
- quantized/routes/sld.py +42 -0
- quantized/routes/spectral.py +54 -0
- quantized/routes/statplots.py +99 -0
- quantized/routes/stats.py +418 -0
- quantized/routes/stats_design.py +321 -0
- quantized/routes/substrates.py +46 -0
- quantized/routes/superconductor.py +139 -0
- quantized/routes/thermal.py +57 -0
- quantized/routes/thin_film.py +153 -0
- quantized/routes/vacuum.py +113 -0
- quantized/routes/xray.py +32 -0
- quantized/samples/demo_vsm.csv +42 -0
- quantized/server_launch.py +251 -0
- quantized/web/assets/JetBrainsMono-Bold-CUogYd9I.woff2 +0 -0
- quantized/web/assets/JetBrainsMono-Regular-CA-Os4ii.woff2 +0 -0
- quantized/web/assets/index-BHmmCL-x.js +27 -0
- quantized/web/assets/index-BiZzN7J6.css +1 -0
- quantized/web/index.html +13 -0
- quantized/web/loading.html +69 -0
- quantized_lab-0.8.0.dist-info/METADATA +122 -0
- quantized_lab-0.8.0.dist-info/RECORD +233 -0
- quantized_lab-0.8.0.dist-info/WHEEL +4 -0
- quantized_lab-0.8.0.dist-info/entry_points.txt +4 -0
- quantized_lab-0.8.0.dist-info/licenses/LICENSE +201 -0
- quantized_lab-0.8.0.dist-info/licenses/NOTICE +11 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
"""Per-curve visual style (color / symbol kind / line-vs-scatter) for Origin
|
|
2
|
+
graph curves -- shared by BOTH containers (solved 2026-07-06 against the
|
|
3
|
+
4-stem ``curve_style.json`` oracle: ``hc2convert`` (.opj), ``Hc2 data``,
|
|
4
|
+
``RockingCurve``, ``UnpolPlots`` (.opju)).
|
|
5
|
+
|
|
6
|
+
**The key structural fact.** The ``.opj`` (CPYA) curve-anchor record (the
|
|
7
|
+
519/515-byte block ``opj_curves.py`` reads the column id from) and the
|
|
8
|
+
``.opju`` (CPYUA) curve token are the SAME fixed-layout record: CPYUA stores
|
|
9
|
+
it as a sparse serialization that elides zero runs. The grammar (derived by
|
|
10
|
+
aligning ``hc2convert.opj`` against ``Hc2 data.opju`` -- the same project in
|
|
11
|
+
both containers -- and validated byte-exact through record offset 310+):
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
<tag 0x80..0xbf> <len:u8> <payload:len> skip (tag-0x80)+3 zero bytes, then
|
|
15
|
+
write len literal bytes
|
|
16
|
+
<0xc0..0xc3> <rep:1> write (tag-0xc0)+3 copies of rep
|
|
17
|
+
(the same count law the axis-record
|
|
18
|
+
RLE uses: c2=5, c3=6)
|
|
19
|
+
<len 0x01..0x7f> <payload:len> bare literal continuation
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Reconstructing the record from the token's own ``80 01/03 <id>`` chunk
|
|
23
|
+
(position 1 before it; the id lands at offset 4, exactly the ``.opj``
|
|
24
|
+
anchor's layout) reproduces the ``.opj`` record byte-for-byte, and the
|
|
25
|
+
stream always completes the record to exactly 519 bytes (chained zero-len
|
|
26
|
+
skip chunks, e.g. ``9a 00``, pad the tail) before the DataPlot body object
|
|
27
|
+
(``58 80 09 98 03 40 b3``) begins.
|
|
28
|
+
|
|
29
|
+
**Style fields inside the record** (same offsets in both containers;
|
|
30
|
+
landmark constants at 370/378/386/394/404/420/440/448/456/472/480/488
|
|
31
|
+
verified at identical offsets for both the 519- and 515-byte ``.opj``
|
|
32
|
+
variants -- the 4-byte size difference sits past offset 492):
|
|
33
|
+
|
|
34
|
+
* offset 23 -- **symbol kind** (Origin's symbol-gallery index; 0 = none).
|
|
35
|
+
Oracle-verified 49/49 (.opj) + 43/43 (.opju).
|
|
36
|
+
* offset 76 -- the **line-vs-scatter byte** (``0xc8`` line / ``0xc9``
|
|
37
|
+
scatter -- the very byte ``opju_codec.curve_plot_style``'s
|
|
38
|
+
``8f 01 <style> 83`` tag writes: that tag is this record's sparse chunk
|
|
39
|
+
for offset 76). ``0xca`` (Moke) and ``0xe7``/``0xe9`` (PNR corpus) also
|
|
40
|
+
occur; unmapped -- omitted, never guessed.
|
|
41
|
+
* offsets 302-305 -- a constant ``0xFFFFFFF7`` "auto" sentinel u32.
|
|
42
|
+
* offsets 306-309 -- the **symbol color** (ocolor u32 LE), terminator
|
|
43
|
+
``0xff`` at 310 (the validity gate: hc2convert's 24 non-oracle anchors
|
|
44
|
+
carrying ``0x1e`` there get no color, fail-closed).
|
|
45
|
+
* offsets 362-365 -- the **line color** (ocolor u32 LE, no terminator).
|
|
46
|
+
|
|
47
|
+
The plot's *effective* color (what the oracle's COM capture reports) is the
|
|
48
|
+
symbol color when symbol kind > 0, else the line color -- verified 48/49 on
|
|
49
|
+
``hc2convert`` reachable plots (the 49th is the palette case below) and
|
|
50
|
+
39/43 on the ``.opju`` stems (the other 4 are honestly auto-on-disk: error-
|
|
51
|
+
bar curves whose oracle reports the inherited effective black).
|
|
52
|
+
|
|
53
|
+
**On-disk ocolor encoding** (the low-level u32; three cases):
|
|
54
|
+
|
|
55
|
+
* high byte ``0x01`` -- direct RGB, COLORREF byte order: ``0x01BBGGRR``.
|
|
56
|
+
Verified: every one of the oracle's 96 type-1 plots decodes to its
|
|
57
|
+
``color_rgb`` under this model.
|
|
58
|
+
* high byte ``0x00`` -- a **0-based** index into Origin's classic 24-color
|
|
59
|
+
palette. The oracle/LabTalk convention is 1-based (``1`` = black); disk
|
|
60
|
+
stores ``index-1`` (verified: hc2convert Graph2's black plot stores 0
|
|
61
|
+
where the oracle says 1; UnpolPlots' orange line stores 0x0e where the
|
|
62
|
+
oracle says 0x0f). :func:`raw_color` returns the ORACLE (1-based) form so
|
|
63
|
+
callers and tests compare against the oracle int directly.
|
|
64
|
+
* ``0xFFFFFFF7`` -- "auto/increment" (Origin assigns by plot order). The
|
|
65
|
+
oracle reports the *effective* resolved color (e.g. ``-4`` or an
|
|
66
|
+
inherited black); that resolution is Origin-side state we cannot decode,
|
|
67
|
+
so auto yields ``None`` -- the frontend palette default stands.
|
|
68
|
+
|
|
69
|
+
**Line width + symbol size (SOLVED 2026-07-06, 92/92 oracle-exact both
|
|
70
|
+
containers):**
|
|
71
|
+
|
|
72
|
+
* offset 21 -- **line width** u16 LE, in units of 1/500 pt (``1500`` = 3.0pt,
|
|
73
|
+
``250`` = 0.5pt).
|
|
74
|
+
* offset 25 -- **symbol size** u16 LE, same 1/500-pt units (``4500`` = 9pt).
|
|
75
|
+
|
|
76
|
+
Both store the value LabTalk itself reports: when the user resizes a graph
|
|
77
|
+
window Origin *bakes* the rescale into these stored values (``795`` = 1.59pt
|
|
78
|
+
shown as "1.6"), so there is NO separate print-factor field to decode -- the
|
|
79
|
+
2026-07-05 "layer print factor" model (§13.2 #1's old blocker) was an
|
|
80
|
+
artifact of reading two CONSTANT fields (the DataPlot-body 213-236 triple
|
|
81
|
+
and record offset 282, both fixed boilerplate corpus-wide) and mistaking
|
|
82
|
+
oracle/constant ratios for a scale. Exhaustive per-offset search across all
|
|
83
|
+
31 width-varying oracle plots isolated offsets 21/25 as the only fields
|
|
84
|
+
that group plots exactly by width/size.
|
|
85
|
+
|
|
86
|
+
See ``tests/test_io_origin_curve_style.py`` for the synthetic + oracle
|
|
87
|
+
verification suite.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
from __future__ import annotations
|
|
91
|
+
|
|
92
|
+
import struct
|
|
93
|
+
|
|
94
|
+
__all__ = [
|
|
95
|
+
"ORIGIN_PALETTE",
|
|
96
|
+
"SYSTEM_COLOR_LIST",
|
|
97
|
+
"apply_increment_colors",
|
|
98
|
+
"ocolor_to_rgb",
|
|
99
|
+
"opju_style_record",
|
|
100
|
+
"raw_color",
|
|
101
|
+
"style_fields",
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
_RECORD_LEN = 519 # the .opj curve-anchor record length (515 variant differs past 492)
|
|
105
|
+
_AUTO = 0xFFFFFFF7 # the on-disk "auto/increment" color sentinel (-9 as i32)
|
|
106
|
+
_SYMBOL_COLOR_OFF = 306
|
|
107
|
+
_COLOR_TERM_OFF = 310
|
|
108
|
+
_LINE_COLOR_OFF = 362
|
|
109
|
+
_SYMBOL_KIND_OFF = 23
|
|
110
|
+
_STYLE_BYTE_OFF = 76
|
|
111
|
+
_LINE_WIDTH_OFF = 21 # u16 LE, 1/500 pt (see module docstring)
|
|
112
|
+
_SYMBOL_SIZE_OFF = 25 # u16 LE, 1/500 pt
|
|
113
|
+
_PT500_MAX = 50_000 # plausibility ceiling: 100 pt — reject junk, never guess
|
|
114
|
+
|
|
115
|
+
# Origin's classic 24-color list (LabTalk ``color()`` indices 1-24, here
|
|
116
|
+
# 0-indexed 0-23): black, red, green, blue, cyan, magenta, yellow, dark
|
|
117
|
+
# yellow, navy, purple, wine, olive, dark cyan, royal, orange, violet, pink,
|
|
118
|
+
# white, light gray, gray, light yellow, light cyan, light magenta, dark
|
|
119
|
+
# gray. Ported verbatim from Origin's documented default color list -- the
|
|
120
|
+
# calibrated values are intentional, do not "fix".
|
|
121
|
+
ORIGIN_PALETTE: tuple[str, ...] = (
|
|
122
|
+
"#000000", # 1 black
|
|
123
|
+
"#FF0000", # 2 red
|
|
124
|
+
"#00FF00", # 3 green
|
|
125
|
+
"#0000FF", # 4 blue
|
|
126
|
+
"#00FFFF", # 5 cyan
|
|
127
|
+
"#FF00FF", # 6 magenta
|
|
128
|
+
"#FFFF00", # 7 yellow
|
|
129
|
+
"#808000", # 8 dark yellow
|
|
130
|
+
"#000080", # 9 navy
|
|
131
|
+
"#800080", # 10 purple
|
|
132
|
+
"#800000", # 11 wine
|
|
133
|
+
"#008000", # 12 olive
|
|
134
|
+
"#008080", # 13 dark cyan
|
|
135
|
+
"#0000A0", # 14 royal
|
|
136
|
+
"#FF8000", # 15 orange
|
|
137
|
+
"#8000FF", # 16 violet
|
|
138
|
+
"#FF0080", # 17 pink
|
|
139
|
+
"#FFFFFF", # 18 white
|
|
140
|
+
"#C0C0C0", # 19 light gray
|
|
141
|
+
"#808080", # 20 gray
|
|
142
|
+
"#FFFF80", # 21 light yellow
|
|
143
|
+
"#80FFFF", # 22 light cyan
|
|
144
|
+
"#FF80FF", # 23 light magenta
|
|
145
|
+
"#404040", # 24 dark gray
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Origin's symbol-gallery indices (LabTalk ``set -k``): 1 square, 2 circle,
|
|
149
|
+
# 3 up-triangle, 4 down-triangle, 5 diamond, 6 cross(+), 7 cross(x),
|
|
150
|
+
# 8 star. Indices 1-3 are oracle-verified; 4-8 port the documented gallery
|
|
151
|
+
# order. Names match the frontend MarkerShape union so they pass through.
|
|
152
|
+
_SYMBOL_SHAPES = {
|
|
153
|
+
1: "square",
|
|
154
|
+
2: "circle",
|
|
155
|
+
3: "triangle",
|
|
156
|
+
4: "downtriangle",
|
|
157
|
+
5: "diamond",
|
|
158
|
+
6: "plus",
|
|
159
|
+
7: "cross",
|
|
160
|
+
8: "star",
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
# Same byte table opju_codec._STYLE_BYTES validated (fig_pairs oracle);
|
|
164
|
+
# 0xca/0xe7/0xe9 also occur in the corpus but are unmapped -- never guessed.
|
|
165
|
+
_CONNECT_STYLE = {0xC8: "line", 0xC9: "scatter"}
|
|
166
|
+
|
|
167
|
+
# ── auto/increment colours (2026-07-06, §13.2 #2) ────────────────────────────
|
|
168
|
+
#
|
|
169
|
+
# A curve whose colour field holds the EXACT u32 ``0x81010151`` is an
|
|
170
|
+
# "increment placeholder": Origin resolves it at render time by walking its
|
|
171
|
+
# active colour list. Pinned by-construction (style_group/style_ungrouped/
|
|
172
|
+
# style_group12 specimens, generate_specimens_style.py) with a RENDER-PIXEL
|
|
173
|
+
# oracle (expGraph PNG, sampled line colours — the COM ``layer.plotN.color``
|
|
174
|
+
# property reports only the group-level colour, so pixels are the only
|
|
175
|
+
# per-member ground truth):
|
|
176
|
+
#
|
|
177
|
+
# * record byte 6 carries the plot's group role: ``0x09`` standalone,
|
|
178
|
+
# ``0x29`` group head, ``0x19`` group member (byte-diff of the grouped vs
|
|
179
|
+
# ungrouped specimens, otherwise identical records).
|
|
180
|
+
# * a GROUPED placeholder takes SYSTEM_COLOR_LIST[k], k = index within its
|
|
181
|
+
# group (verified for k=0..11, twelve distinct colours, no wrap);
|
|
182
|
+
# * an UNGROUPED placeholder always renders the list's FIRST colour.
|
|
183
|
+
#
|
|
184
|
+
# SYSTEM_COLOR_LIST is Origin's default "System Color List" (2018+); the 12
|
|
185
|
+
# entries below are the pixel-verified ones. A project using a CUSTOM colour
|
|
186
|
+
# list is Origin-side state we cannot see — any 0x81-typed value OTHER than
|
|
187
|
+
# the observed 0x81010151 payload is left unresolved (None) rather than
|
|
188
|
+
# guessed, and members past index 11 are likewise left unresolved.
|
|
189
|
+
# Values are calibrated/verified — do not "fix".
|
|
190
|
+
SYSTEM_COLOR_LIST: tuple[str, ...] = (
|
|
191
|
+
"#515151", "#F14040", "#1A6FDF", "#37AD6B", "#B177DE", "#CC9900",
|
|
192
|
+
"#00CBCC", "#7D4E4E", "#8E8E00", "#FB6501", "#6699CC", "#6FB802",
|
|
193
|
+
) # fmt: skip
|
|
194
|
+
|
|
195
|
+
_INCREMENT_PLACEHOLDER = 0x81010151
|
|
196
|
+
_GROUP_ROLE_OFF = 6
|
|
197
|
+
_GROUP_ROLES = {0x09: "standalone", 0x29: "head", 0x19: "member"}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def ocolor_to_rgb(raw: int) -> str | None:
|
|
201
|
+
"""An Origin ocolor int (ORACLE/LabTalk form) -> ``"#RRGGBB"``, or
|
|
202
|
+
``None`` for auto/unrecognized (never guessed).
|
|
203
|
+
|
|
204
|
+
Type 1 (high byte ``0x01``) is a direct COLORREF: ``0x01BBGGRR``.
|
|
205
|
+
Type 0 (high byte ``0x00``) is a 1-based classic-palette index (1-24) --
|
|
206
|
+
the LabTalk convention the ground-truth oracle uses; the on-disk field
|
|
207
|
+
is 0-based and :func:`raw_color` converts before returning.
|
|
208
|
+
"""
|
|
209
|
+
raw &= 0xFFFFFFFF
|
|
210
|
+
kind = raw >> 24
|
|
211
|
+
if kind == 1: # direct RGB (COLORREF low 24 bits, BGR order)
|
|
212
|
+
r, g, b = raw & 0xFF, (raw >> 8) & 0xFF, (raw >> 16) & 0xFF
|
|
213
|
+
return f"#{r:02X}{g:02X}{b:02X}"
|
|
214
|
+
if kind == 0 and 1 <= raw <= len(ORIGIN_PALETTE): # classic palette, 1-based
|
|
215
|
+
return ORIGIN_PALETTE[raw - 1]
|
|
216
|
+
return None # auto/increment or an unrecognized type: no color, never guess
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def raw_color(record: bytes) -> int | None:
|
|
220
|
+
"""The plot's effective ocolor from a curve-anchor record, in ORACLE
|
|
221
|
+
(1-based-palette) form, or ``None`` (auto on disk / gates failed).
|
|
222
|
+
|
|
223
|
+
Reads the symbol color (offset 306) for symbol plots (kind > 0) and the
|
|
224
|
+
line color (offset 362) otherwise -- the rule the oracle verified 87/92.
|
|
225
|
+
Gated on the color-group terminator byte (``0xff`` at 310); a record
|
|
226
|
+
that fails it (or is too short) yields ``None``, never a guess.
|
|
227
|
+
"""
|
|
228
|
+
if len(record) < _LINE_COLOR_OFF + 4 or record[_COLOR_TERM_OFF] != 0xFF:
|
|
229
|
+
return None
|
|
230
|
+
off = _SYMBOL_COLOR_OFF if record[_SYMBOL_KIND_OFF] > 0 else _LINE_COLOR_OFF
|
|
231
|
+
field = struct.unpack_from("<I", record, off)[0]
|
|
232
|
+
if field == _AUTO:
|
|
233
|
+
return None # auto/increment: resolved Origin-side, not decodable here
|
|
234
|
+
kind = field >> 24
|
|
235
|
+
if kind == 0:
|
|
236
|
+
return field + 1 if field < len(ORIGIN_PALETTE) else None # disk is 0-based
|
|
237
|
+
return field if kind == 1 else None
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def style_fields(record: bytes) -> dict[str, str | float]:
|
|
241
|
+
"""Decoded per-curve style keys from one curve-anchor record (raw ``.opj``
|
|
242
|
+
payload or :func:`opju_style_record` reconstruction): any of ``color``
|
|
243
|
+
(``"#RRGGBB"``), ``symbol`` (marker shape name), ``style``
|
|
244
|
+
(``"line"``/``"scatter"``), ``lineWidth`` / ``symbolSize`` (points, the
|
|
245
|
+
1/500-pt u16 fields at offsets 21/25 — 92/92 oracle-exact). Undecodable
|
|
246
|
+
or implausible fields are simply absent, never defaulted."""
|
|
247
|
+
out: dict[str, str | float] = {}
|
|
248
|
+
if len(record) < _LINE_COLOR_OFF + 4:
|
|
249
|
+
return out
|
|
250
|
+
style = _CONNECT_STYLE.get(record[_STYLE_BYTE_OFF])
|
|
251
|
+
if style:
|
|
252
|
+
out["style"] = style
|
|
253
|
+
shape = _SYMBOL_SHAPES.get(record[_SYMBOL_KIND_OFF])
|
|
254
|
+
if shape:
|
|
255
|
+
out["symbol"] = shape
|
|
256
|
+
width500 = struct.unpack_from("<H", record, _LINE_WIDTH_OFF)[0]
|
|
257
|
+
if 0 < width500 <= _PT500_MAX:
|
|
258
|
+
out["lineWidth"] = width500 / 500.0
|
|
259
|
+
size500 = struct.unpack_from("<H", record, _SYMBOL_SIZE_OFF)[0]
|
|
260
|
+
if 0 < size500 <= _PT500_MAX:
|
|
261
|
+
out["symbolSize"] = size500 / 500.0
|
|
262
|
+
raw = raw_color(record)
|
|
263
|
+
if raw is not None:
|
|
264
|
+
rgb = ocolor_to_rgb(raw)
|
|
265
|
+
if rgb:
|
|
266
|
+
out["color"] = rgb
|
|
267
|
+
return out
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _effective_color_field(record: bytes) -> int | None:
|
|
271
|
+
"""The raw u32 of the plot's effective colour field (symbol colour for
|
|
272
|
+
symbol plots, line colour otherwise), or ``None`` on a short record."""
|
|
273
|
+
if len(record) < _LINE_COLOR_OFF + 4:
|
|
274
|
+
return None
|
|
275
|
+
off = _SYMBOL_COLOR_OFF if record[_SYMBOL_KIND_OFF] > 0 else _LINE_COLOR_OFF
|
|
276
|
+
return int(struct.unpack_from("<I", record, off)[0])
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def apply_increment_colors(
|
|
280
|
+
curves: list[dict[str, str | float]], records: list[bytes | None]
|
|
281
|
+
) -> None:
|
|
282
|
+
"""Resolve auto/increment placeholder colours in-place for one layer's
|
|
283
|
+
curves (plot order). See the SYSTEM_COLOR_LIST block comment for the
|
|
284
|
+
verified rule. ``records[i]`` is curve ``i``'s style record (``None``
|
|
285
|
+
when unavailable). Only a curve with NO decoded ``color``, whose
|
|
286
|
+
effective colour field is the exact ``0x81010151`` placeholder, and
|
|
287
|
+
whose group role byte is recognized, is filled — everything else is
|
|
288
|
+
left untouched (never guessed).
|
|
289
|
+
"""
|
|
290
|
+
group_index: int | None = None # None = not inside a group
|
|
291
|
+
for curve, record in zip(curves, records, strict=True):
|
|
292
|
+
if record is None or len(record) <= _GROUP_ROLE_OFF:
|
|
293
|
+
group_index = None
|
|
294
|
+
continue
|
|
295
|
+
role = _GROUP_ROLES.get(record[_GROUP_ROLE_OFF])
|
|
296
|
+
if role == "head":
|
|
297
|
+
group_index = 0
|
|
298
|
+
elif role == "member":
|
|
299
|
+
group_index = group_index + 1 if group_index is not None else None
|
|
300
|
+
else: # standalone or unrecognized: any open group ends here
|
|
301
|
+
group_index = None
|
|
302
|
+
if "color" in curve or _effective_color_field(record) != _INCREMENT_PLACEHOLDER:
|
|
303
|
+
continue
|
|
304
|
+
if role == "standalone":
|
|
305
|
+
curve["color"] = SYSTEM_COLOR_LIST[0]
|
|
306
|
+
elif role in ("head", "member") and group_index is not None:
|
|
307
|
+
if group_index < len(SYSTEM_COLOR_LIST):
|
|
308
|
+
curve["color"] = SYSTEM_COLOR_LIST[group_index]
|
|
309
|
+
# past the verified list: leave unresolved, never wrap-guess
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def opju_style_record(b: bytes, tag_pos: int) -> bytes | None:
|
|
313
|
+
"""Reconstruct the 519-byte curve-anchor record from the CPYUA sparse
|
|
314
|
+
stream whose id chunk (``80 01/03 <id>``) starts at ``tag_pos``.
|
|
315
|
+
|
|
316
|
+
Follows the chunk grammar in the module docstring. Returns ``None``
|
|
317
|
+
unless the stream demonstrably completes the record (reaches offset
|
|
318
|
+
519 exactly, as every validated real stream does) -- a partial
|
|
319
|
+
reconstruction could misread an unreached zero region as palette black,
|
|
320
|
+
which is exactly the wrong-color failure this gate forbids."""
|
|
321
|
+
buf = bytearray(_RECORD_LEN)
|
|
322
|
+
pos = 1 # the id chunk's skip starts after the record's offset-0 byte
|
|
323
|
+
p = tag_pos
|
|
324
|
+
n = len(b)
|
|
325
|
+
while p < n and pos < _RECORD_LEN:
|
|
326
|
+
t = b[p]
|
|
327
|
+
if 0x80 <= t <= 0xBF: # tagged chunk: skip zeros, then literal bytes
|
|
328
|
+
if p + 2 > n:
|
|
329
|
+
return None
|
|
330
|
+
ln = b[p + 1]
|
|
331
|
+
if p + 2 + ln > n:
|
|
332
|
+
return None
|
|
333
|
+
pos += (t - 0x80) + 3
|
|
334
|
+
payload = b[p + 2 : p + 2 + ln]
|
|
335
|
+
end = min(pos + ln, _RECORD_LEN)
|
|
336
|
+
if pos < _RECORD_LEN:
|
|
337
|
+
buf[pos:end] = payload[: end - pos]
|
|
338
|
+
pos += ln
|
|
339
|
+
p += 2 + ln
|
|
340
|
+
elif 0xC0 <= t <= 0xC3: # RLE run: (t-0xc0)+3 copies of the next byte
|
|
341
|
+
if p + 2 > n:
|
|
342
|
+
return None
|
|
343
|
+
run = (t - 0xC0) + 3
|
|
344
|
+
end = min(pos + run, _RECORD_LEN)
|
|
345
|
+
if pos < _RECORD_LEN:
|
|
346
|
+
buf[pos:end] = bytes([b[p + 1]]) * (end - pos)
|
|
347
|
+
pos += run
|
|
348
|
+
p += 2
|
|
349
|
+
elif 0 < t < 0x80: # bare literal continuation
|
|
350
|
+
if p + 1 + t > n:
|
|
351
|
+
return None
|
|
352
|
+
end = min(pos + t, _RECORD_LEN)
|
|
353
|
+
if pos < _RECORD_LEN:
|
|
354
|
+
buf[pos:end] = b[p + 1 : p + 1 + (end - pos)]
|
|
355
|
+
pos += t
|
|
356
|
+
p += 1 + t
|
|
357
|
+
else: # 0x00 / unknown escape: the stream ended before the record did
|
|
358
|
+
return None
|
|
359
|
+
return bytes(buf) if pos >= _RECORD_LEN else None
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Layer-frame and page geometry for Origin graph windows (§13.2 #7).
|
|
2
|
+
|
|
3
|
+
A graph *page* has a size in "page units"; each *layer* (panel) occupies a
|
|
4
|
+
frame rect on it. Both containers store the same quantities, in the same
|
|
5
|
+
units the annotation/legend object boxes use (``annotation_marks``), so
|
|
6
|
+
frame + page size place every panel of a multi-panel window and every
|
|
7
|
+
positioned object — the full faithful-layout geometry chain.
|
|
8
|
+
|
|
9
|
+
**`.opj` (CPYA)** — solved 2026-07-06 against the live-COM
|
|
10
|
+
``layer_geometry.json`` oracle (``export_layer_geometry.py``):
|
|
11
|
+
|
|
12
|
+
* layer frame: u16 LE quad at layer-continuation payload offsets
|
|
13
|
+
**113/115/117/119** = (left, top, right, bottom) page units — 41/44
|
|
14
|
+
oracle layers exact (the 3 misses are Moke's LINKED composite layers
|
|
15
|
+
where COM itself reports out-of-page link-mode values);
|
|
16
|
+
* page size: u16 LE (width, height) pair at window-header payload offset
|
|
17
|
+
**35** — 39/39 oracle windows exact.
|
|
18
|
+
|
|
19
|
+
**`.opju` (CPYUA)**:
|
|
20
|
+
|
|
21
|
+
* layer frame: the marker ``12 00 20 22`` within ~300 bytes after the
|
|
22
|
+
layer's axis-record anchor, immediately followed by the same u16 quad —
|
|
23
|
+
30/30 oracle layers exact where the marker exists; absent on some
|
|
24
|
+
composite/embedded layers (fail-closed ``None``);
|
|
25
|
+
* page size: near the page-header start, a u16 (width, height) pair with
|
|
26
|
+
variable framing — recovered by scanning the first bytes of the page
|
|
27
|
+
span for a plausible pair that CONTAINS every decoded layer frame
|
|
28
|
+
(a structural validation, not a fixed offset); ``None`` when no
|
|
29
|
+
candidate or several disagree.
|
|
30
|
+
|
|
31
|
+
Pure leaf: bytes in → ints out.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import struct
|
|
37
|
+
|
|
38
|
+
__all__ = ["opj_layer_frame", "opj_page_size", "opju_layer_frame", "opju_page_size"]
|
|
39
|
+
|
|
40
|
+
_OPJ_FRAME_OFFSET = 113
|
|
41
|
+
_OPJ_PAGE_SIZE_OFFSET = 35
|
|
42
|
+
_OPJU_FRAME_MARK = b"\x12\x00\x20\x22"
|
|
43
|
+
_OPJU_FRAME_SCAN = 300
|
|
44
|
+
_OPJU_PAGE_SCAN = 80
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _plausible_quad(q: tuple[int, int, int, int]) -> bool:
|
|
48
|
+
left, top, right, bottom = q
|
|
49
|
+
return left < right and top < bottom
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def opj_layer_frame(layer_payload: bytes) -> tuple[int, int, int, int] | None:
|
|
53
|
+
"""The ``.opj`` layer frame quad ``(left, top, right, bottom)`` in page
|
|
54
|
+
units, or ``None`` when missing/degenerate (older layer-block variants) —
|
|
55
|
+
callers fall back to the fraction-pair position model."""
|
|
56
|
+
if len(layer_payload) < _OPJ_FRAME_OFFSET + 8:
|
|
57
|
+
return None
|
|
58
|
+
quad = struct.unpack_from("<4H", layer_payload, _OPJ_FRAME_OFFSET)
|
|
59
|
+
if not _plausible_quad(quad):
|
|
60
|
+
return None
|
|
61
|
+
return int(quad[0]), int(quad[1]), int(quad[2]), int(quad[3])
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def opj_page_size(header_payload: bytes) -> dict[str, int] | None:
|
|
65
|
+
"""The graph page's (width, height) in page units from its window-header
|
|
66
|
+
payload, or ``None`` when implausible — never guessed."""
|
|
67
|
+
if len(header_payload) < _OPJ_PAGE_SIZE_OFFSET + 4:
|
|
68
|
+
return None
|
|
69
|
+
w, h = struct.unpack_from("<2H", header_payload, _OPJ_PAGE_SIZE_OFFSET)
|
|
70
|
+
if not (200 <= w <= 60000 and 200 <= h <= 60000):
|
|
71
|
+
return None
|
|
72
|
+
return {"width": int(w), "height": int(h)}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def opju_layer_frame(b: bytes, anchor: int, end: int) -> tuple[int, int, int, int] | None:
|
|
76
|
+
"""The ``.opju`` layer frame quad for the axis record at ``anchor``, or
|
|
77
|
+
``None`` when the marker isn't in range (composite/embedded layers)."""
|
|
78
|
+
j = b.find(_OPJU_FRAME_MARK, anchor, min(end, anchor + _OPJU_FRAME_SCAN))
|
|
79
|
+
if j < 0 or j + 4 + 8 > len(b):
|
|
80
|
+
return None
|
|
81
|
+
quad = struct.unpack_from("<4H", b, j + 4)
|
|
82
|
+
if not _plausible_quad(quad):
|
|
83
|
+
return None
|
|
84
|
+
return int(quad[0]), int(quad[1]), int(quad[2]), int(quad[3])
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def opju_page_size(
|
|
88
|
+
b: bytes, page_start: int, frames: list[tuple[int, int, int, int]]
|
|
89
|
+
) -> dict[str, int] | None:
|
|
90
|
+
"""The ``.opju`` graph page's (width, height): the unique plausible u16
|
|
91
|
+
pair near the page-header start that CONTAINS every decoded layer frame
|
|
92
|
+
(right <= width, bottom <= height, within 8x of the frames' extent so an
|
|
93
|
+
absurdly large accidental pair can't win). ``None`` without frames to
|
|
94
|
+
validate against, or when no/multiple distinct candidates pass."""
|
|
95
|
+
if not frames:
|
|
96
|
+
return None
|
|
97
|
+
max_r = max(f[2] for f in frames)
|
|
98
|
+
max_b = max(f[3] for f in frames)
|
|
99
|
+
found: set[tuple[int, int]] = set()
|
|
100
|
+
hi = min(len(b) - 4, page_start + _OPJU_PAGE_SCAN)
|
|
101
|
+
for off in range(page_start, hi):
|
|
102
|
+
w, h = struct.unpack_from("<2H", b, off)
|
|
103
|
+
if max_r <= w <= 8 * max_r and max_b <= h <= 8 * max_b:
|
|
104
|
+
found.add((int(w), int(h)))
|
|
105
|
+
if len(found) != 1:
|
|
106
|
+
return None # ambiguous or absent: no page size, never guessed
|
|
107
|
+
w, h = next(iter(found))
|
|
108
|
+
return {"width": w, "height": h}
|