fix(task-17): the headless page setup writes the scale AutoCAD plots by, and reads the code 75 AutoCAD writes

Two measured root causes on the headless engine, both silent:

1. page_setup_apply wrote PLOTSETTINGS group 147 (unit_factor) as 1.0 for
   every scale. Under USE_STANDARD_SCALE AutoCAD's plot engine scales by
   group 147 alone (a 1:10 sheet with code 21 and 147 = 1.0 plotted a
   100-unit line at 99.99 mm while the row, `applied` and 142/143 said
   1:10; patching 147 to 0.1 alone gave 9.99 mm). AutoCAD writes 147 as
   numerator / denominator (measured live on AutoCAD 2026 for fit, 1:1,
   1:2, 1:10, 1:50, 2:1, 10:1: 1.0, 1.0, 0.5, 0.1, 0.02, 2.0, 10.0). The
   writer now stores 75, 142/143 and 147 from the one resolved ratio on
   every route, so no reader can disagree with the plot.

2. DXF_STANDARD_SCALE_TYPE and _plot_scale followed ezdxf's STD_SCALES,
   which has no 19 = 1:5 and numbers everything above 18 one lower than
   the ObjectARX StdScaleType enum AutoCAD actually writes (1:10 = 21,
   1:20 = 23, 1:50 = 26, 1:100 = 27, 2:1 = 28, 10:1 = 31 — measured via COM
   SaveAs DXF). An AutoCAD-authored 1:50 sheet listed as "1:100" and a
   1:10 sheet rendered at 1:16. papers.py now authors the full file enum
   (DXF_STD_SCALES, codes 1-33 with AutoCAD's imperial names), the writer
   uses the same codes ActiveX uses, and nothing goes through ezdxf's
   table. 1:5 stays on the custom route on both engines (the live setter
   refuses 19) while the reader knows code 19 for files AutoCAD wrote.

Tests: the plan's `1:50 -> 25` assertion was the stale numbering and now
pins 26; a pure-data test pins the enum against the measured codes and the
ActiveX numbering; a parametrised writer test checks 75/142/143/147 for
every catalogue scale; a reader test stores the scale exactly as AutoCAD
saves it (including codes 19 and 22 the writer never emits) and checks
page_setup_list and _paper_frame report and render it at its own scale.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
This commit is contained in:
Umut CAN
2026-09-17 15:29:47 +03:00
co-authored by Claude Opus 5
parent f2386a9fdd
commit aeb27c252b
3 changed files with 281 additions and 39 deletions
+18 -13
View File
@@ -2405,12 +2405,12 @@ class EzdxfBackend(AutoCADBackend):
Reads the same bits ``_page_setup_row`` reports, so the label here is
the label ``page_setup_list`` shows: a standard code under
``USE_STANDARD_SCALE`` resolves through ezdxf's ``STD_SCALES`` table,
``USE_STANDARD_SCALE`` resolves through the authored ``DXF_STD_SCALES``
(the ObjectARX enum AutoCAD writes — ezdxf's ``STD_SCALES`` is one off
above 18 and would render an AutoCAD-authored 1:10 sheet at 1:16),
anything else through the stored numerator / denominator.
"""
from ezdxf.lldxf.const import STD_SCALES
from engineering.standards.papers import dxf_scale_label, scale_label
from engineering.standards.papers import DXF_STD_SCALES, dxf_scale_label, scale_label
dxf = layout.dxf_layout.dxf
flags = int(dxf.get("plot_layout_flags", dxf.get_default("plot_layout_flags")))
@@ -2419,8 +2419,8 @@ class EzdxfBackend(AutoCADBackend):
code = int(dxf.standard_scale_type)
if code == 0:
return "fit", None, None
if code in STD_SCALES:
numerator, denominator = (float(v) for v in STD_SCALES[code])
if code in DXF_STD_SCALES:
numerator, denominator = (float(v) for v in DXF_STD_SCALES[code])
return dxf_scale_label(code, numerator, denominator), numerator, denominator
return scale_label(numerator, denominator), numerator, denominator
@@ -2586,8 +2586,6 @@ class EzdxfBackend(AutoCADBackend):
resolved = require_page_setup(setup)
def _sync():
from ezdxf.lldxf.const import STD_SCALES
target = self._paper_layout(layout, "page_setup_apply")
before = self._page_setup_row(target)
dxf = target.dxf_layout.dxf
@@ -2619,21 +2617,28 @@ class EzdxfBackend(AutoCADBackend):
dxf.left_margin, dxf.right_margin = left, right
dxf.plot_origin_x_offset = 0.0
dxf.plot_origin_y_offset = 0.0
dxf.unit_factor = 1.0
dxf.current_style_sheet = resolved["plot_style"]
target.use_plot_styles(True)
target.set_plot_type(int(resolved["plot_type"]))
# The scale is stored three ways and AutoCAD reads a different
# one per route (measured, AutoCAD 2026 — papers.py docstring):
# under USE_STANDARD_SCALE its plot engine scales by group 147
# alone (a sheet with code 21 and 147 = 1.0 plots 1:10 geometry
# at 1:1 while page_setup_list says 1:10); under the custom
# route it scales by 142/143. All three are written, on every
# route, from the one resolved ratio, so no reader can disagree
# with the plot. ``fit`` has ratio 1/1 and 147 = 1.0, as AutoCAD
# writes it.
code = resolved["dxf_standard_scale_type"]
numerator, denominator = (float(v) for v in resolved["scale_ratio"])
if code is not None:
dxf.standard_scale_type = int(code)
numerator, denominator = STD_SCALES.get(int(code), (1.0, 1.0))
dxf.scale_numerator, dxf.scale_denominator = float(numerator), float(denominator)
target.use_standard_scale(True)
else:
numerator, denominator = (float(v) for v in resolved["scale_ratio"])
dxf.standard_scale_type = 16
dxf.scale_numerator, dxf.scale_denominator = numerator, denominator
target.use_standard_scale(False)
dxf.scale_numerator, dxf.scale_denominator = numerator, denominator
dxf.unit_factor = numerator / denominator
# resolve_page_setup hands over None for a layout plot: the bit is
# cleared, never set — the state AutoCAD itself stores for acLayout,
# and the mirror of the live engine never writing CenterPlot there.
+120 -21
View File
@@ -3,18 +3,40 @@
Sources: ISO 216 (A series), ASME/ANSI Y14.1 (A–E, metric millimetre values as
AutoCAD's ``ANSI_*`` media measure, inch values as the media are *named*),
the ctb files AutoCAD ships in its Plot Styles folder, and the standard-scale
tables of two enums that do not agree with each other:
enum — one numbering, shared by the file and by ActiveX:
* DXF group code 75 (``standard_scale_type``, what ezdxf writes): 16=1:1,
17=1:2, 20=1:10, 22=1:20, 25=1:50, 26=1:100, 27=2:1, 30=10:1 — no 1:5.
* ActiveX ``AcPlotScale`` (``Layout.StandardScale``, AutoCAD 2026 typelib):
ac1_1=16, ac1_2=17, ac1_10=21, ac1_20=23, ac1_50=26, ac1_100=27, ac2_1=28,
ac10_1=31, acScaleToFit=0. The typelib also declares ``ac1_5=19``, but
AutoCAD 2026 refuses ``Layout.StandardScale = 19`` with "Invalid input" in
every condition tried (PaperUnits mm and inches, UseStandardScale True and
False; measured live 2026-09-17) — its standard-scale list has no 1:5, the
same gap as DXF code 75 — so 1:5 is *not* in the table and takes the custom
route on the live engine too (``SetCustomScale(1, 5)`` reads back ``1:5``).
* DXF group code 75 (``standard_scale_type``) is the ObjectARX
``AcDbPlotSettings::StdScaleType`` enum: 0=fit, 1..15 the imperial
architectural scales, 16=1:1, 17=1:2, 18=1:4, 19=1:5, 20=1:8, 21=1:10,
22=1:16, 23=1:20, 24=1:30, 25=1:40, 26=1:50, 27=1:100, 28=2:1, 29=4:1,
30=8:1, 31=10:1, 32=100:1, 33=1000:1 (``DXF_STD_SCALES``). Measured live
on AutoCAD 2026 (COM ``SaveAs(..., 61)`` after ``StandardScale = n``):
1:10 saves as 21, 1:50 as 26, 2:1 as 28, 10:1 as 31. ezdxf's
``STD_SCALES`` table (and the DXF Reference page it follows) has no 19=1:5
and numbers everything above 18 one lower (20=1:10, 25=1:50, 30=10:1) —
reading an AutoCAD-authored 1:50 sheet through it reports 1:100, and a
1:10 sheet renders at 1:16 — so nothing here goes through that table.
* DXF group code 147 (``unit_factor`` in ezdxf; "a floating point scale
factor that represents the standard scale value specified in code 75" in
the DXF Reference — the Reference is right) is the factor AutoCAD's plot
engine *uses* under ``USE_STANDARD_SCALE`` (bit 16): AutoCAD writes it as
numerator / denominator (0.1 for 1:10, 2.0 for 2:1, 1.0 for fit and 1:1),
and a file with code 21 but 147 = 1.0 plots at 1:1 while claiming 1:10
(measured: a 100-unit line spans 99.99 mm of paper). Under the custom
route (bit 16 cleared) AutoCAD honours 142/143 and leaves 75 and 147
stale. The headless writer therefore writes 147 alongside 142/143 on
every route.
* ActiveX ``AcPlotScale`` (``Layout.StandardScale``, AutoCAD 2026 typelib)
is the same numbering (ac1_1=16, ac1_2=17, ac1_10=21, ac1_20=23,
ac1_50=26, ac1_100=27, ac2_1=28, ac10_1=31, acScaleToFit=0). The typelib
declares ``ac1_5=19`` too, but AutoCAD 2026 refuses
``Layout.StandardScale = 19`` with "Invalid input" in every condition tried
(PaperUnits mm and inches, UseStandardScale True and False; measured live
2026-09-17), so 1:5 is not in ``ACTIVEX_PLOT_SCALE`` and takes the custom
route on the live engine (``SetCustomScale(1, 5)`` reads back ``1:5``).
The headless writer mirrors that — 1:5 is not in ``DXF_STANDARD_SCALE_TYPE``
either, so both engines store the same 1:5 (custom, 142/143 = 1/5) — while
the reader knows code 19 for files AutoCAD wrote.
A scale without a code on an engine goes through the custom numerator /
denominator on that engine, and the read-back reports the same label either way.
@@ -39,6 +61,7 @@ __all__ = [
"ACTIVEX_PLOT_SCALE",
"CTB_CATALOG",
"DXF_STANDARD_SCALE_TYPE",
"DXF_STD_SCALES",
"ORIENTATIONS",
"PAPER_INCHES",
"PAPER_SIZES",
@@ -126,19 +149,79 @@ SCALES: dict[str, float | None] = {
"10:1": 10.0,
}
#: DXF group code 75. Labels missing here use the custom numerator/denominator.
#: DXF group code 75 → (numerator, denominator): the ObjectARX
#: ``AcDbPlotSettings::StdScaleType`` enum, which is what AutoCAD writes and
#: reads (module docstring — not ezdxf's ``STD_SCALES``, which is one off
#: above 18). Code 0 is *scaled to fit* and has no ratio.
DXF_STD_SCALES: dict[int, tuple[float, float]] = {
1: (1.0 / 128.0, 12.0),
2: (1.0 / 64.0, 12.0),
3: (1.0 / 32.0, 12.0),
4: (1.0 / 16.0, 12.0),
5: (3.0 / 32.0, 12.0),
6: (1.0 / 8.0, 12.0),
7: (3.0 / 16.0, 12.0),
8: (1.0 / 4.0, 12.0),
9: (3.0 / 8.0, 12.0),
10: (1.0 / 2.0, 12.0),
11: (3.0 / 4.0, 12.0),
12: (1.0, 12.0),
13: (3.0, 12.0),
14: (6.0, 12.0),
15: (12.0, 12.0),
16: (1.0, 1.0),
17: (1.0, 2.0),
18: (1.0, 4.0),
19: (1.0, 5.0),
20: (1.0, 8.0),
21: (1.0, 10.0),
22: (1.0, 16.0),
23: (1.0, 20.0),
24: (1.0, 30.0),
25: (1.0, 40.0),
26: (1.0, 50.0),
27: (1.0, 100.0),
28: (2.0, 1.0),
29: (4.0, 1.0),
30: (8.0, 1.0),
31: (10.0, 1.0),
32: (100.0, 1.0),
33: (1000.0, 1.0),
}
#: AutoCAD's names for the imperial codes (inches of paper = feet of drawing).
_DXF_IMPERIAL_LABELS: dict[int, str] = {
1: "1/128\"=1'",
2: "1/64\"=1'",
3: "1/32\"=1'",
4: "1/16\"=1'",
5: "3/32\"=1'",
6: "1/8\"=1'",
7: "3/16\"=1'",
8: "1/4\"=1'",
9: "3/8\"=1'",
10: "1/2\"=1'",
11: "3/4\"=1'",
12: "1\"=1'",
13: "3\"=1'",
14: "6\"=1'",
15: "1'=1'",
}
#: DXF group code 75 the headless *writer* uses per catalogue label. Labels
#: missing here use the custom numerator/denominator: ``1:5`` (code 19 exists
#: in the file enum, but the live engine cannot set it, so both engines store
#: 1:5 the same custom way) and ``5:1`` (no code anywhere).
DXF_STANDARD_SCALE_TYPE: dict[str, int] = {
"fit": 0,
"1:1": 16,
"1:2": 17,
"1:10": 20,
"1:20": 22,
"1:50": 25,
"1:100": 26,
"2:1": 27,
"10:1": 30,
"1:10": 21,
"1:20": 23,
"1:50": 26,
"1:100": 27,
"2:1": 28,
"10:1": 31,
}
_DXF_SCALE_LABEL = {code: label for label, code in DXF_STANDARD_SCALE_TYPE.items()}
#: ActiveX ``AcPlotScale`` (AutoCAD 2026 typelib). Labels missing here use SetCustomScale.
#: No ``"1:5"``: the typelib's ``ac1_5 = 19`` is refused by AutoCAD 2026 (module docstring).
@@ -253,9 +336,25 @@ def scale_label(numerator, denominator) -> str:
return f"{num:g}:{den:g}"
_DXF_SCALE_LABEL: dict[int, str] = {
0: "fit",
**_DXF_IMPERIAL_LABELS,
**{
code: scale_label(num, den)
for code, (num, den) in DXF_STD_SCALES.items()
if code not in _DXF_IMPERIAL_LABELS
},
}
def dxf_scale_label(code, numerator, denominator) -> str:
"""Read-back for the headless engine: the code-75 label, else the custom ratio."""
return _DXF_SCALE_LABEL.get(int(code)) or scale_label(numerator, denominator)
"""Read-back for the headless engine: the code-75 label (``fit`` for 0,
the ratio ``DXF_STD_SCALES`` gives the code, an AutoCAD-style name for an
imperial code), else the custom ratio."""
label = _DXF_SCALE_LABEL.get(int(code))
if label is not None:
return label
return scale_label(numerator, denominator)
def activex_scale_label(code) -> str:
+143 -5
View File
@@ -13,10 +13,14 @@ import zlib
import pytest
from engineering.standards.papers import (
ACTIVEX_PLOT_SCALE,
CTB_CATALOG,
DXF_STANDARD_SCALE_TYPE,
DXF_STD_SCALES,
PAPER_SIZES,
SCALES,
canonical_media_name,
dxf_scale_label,
paper_from_size,
paper_units_name,
resolve_page_setup,
@@ -110,19 +114,58 @@ def test_resolve_page_setup_accepts_lowercase_and_short_paper_names():
def test_resolve_page_setup_scale_codes():
one_to_five = resolve_page_setup("ISO_A3", scale="1:5")
assert one_to_five["dxf_standard_scale_type"] is None # DXF code 75 has no 1:5
# The typelib declares ac1_5 = 19, but AutoCAD 2026 refuses
# StandardScale = 19 ("Invalid input", measured live): custom route.
# Code 19 is 1:5 in the file enum, but the typelib's ac1_5 = 19 is refused
# by AutoCAD 2026 ("Invalid input", measured live), so both engines store
# 1:5 the same way: the custom route, 142/143 = 1/5.
assert one_to_five["dxf_standard_scale_type"] is None
assert one_to_five["activex_standard_scale"] is None
assert one_to_five["scale_ratio"] == [1.0, 5.0]
five_to_one = resolve_page_setup("ISO_A3", scale="5:1")
assert five_to_one["dxf_standard_scale_type"] is None
assert five_to_one["activex_standard_scale"] is None
assert five_to_one["scale_ratio"] == [5.0, 1.0]
assert resolve_page_setup("ISO_A3", scale="1:50")["dxf_standard_scale_type"] == 25
# One numbering: AutoCAD 2026 saves StandardScale = 26 (ac1_50) as
# group 75 = 26 (measured, COM SaveAs DXF) — not ezdxf's 25.
assert resolve_page_setup("ISO_A3", scale="1:50")["dxf_standard_scale_type"] == 26
assert resolve_page_setup("ISO_A3", scale="1:50")["activex_standard_scale"] == 26
def test_dxf_standard_scale_codes_are_the_enum_autocad_writes():
"""Group 75 is the ObjectARX ``StdScaleType`` enum, the same numbering as
ActiveX ``AcPlotScale`` — measured on AutoCAD 2026 (``StandardScale = n``
then ``SaveAs(..., 61)``): 1:2→17, 1:10→21, 1:50→26, 2:1→28, 10:1→31.
ezdxf's ``STD_SCALES`` has no 19 = 1:5 and is one lower above 18; read
through it, an AutoCAD 1:50 sheet is "1:100" and a 1:10 sheet plots 1:16.
"""
measured = {"fit": 0, "1:1": 16, "1:2": 17, "1:10": 21, "1:50": 26, "2:1": 28, "10:1": 31}
for label, code in measured.items():
assert DXF_STANDARD_SCALE_TYPE[label] == code, label
# Every code the writer uses is the ActiveX code for the same label …
for label, code in DXF_STANDARD_SCALE_TYPE.items():
assert ACTIVEX_PLOT_SCALE[label] == code, label
# … and its authored ratio is the catalogue factor.
for label, code in DXF_STANDARD_SCALE_TYPE.items():
if code:
numerator, denominator = DXF_STD_SCALES[code]
assert numerator / denominator == pytest.approx(SCALES[label]), label
# The full file enum, including what the writer never emits.
assert DXF_STD_SCALES[19] == (1.0, 5.0)
assert DXF_STD_SCALES[20] == (1.0, 8.0)
assert DXF_STD_SCALES[22] == (1.0, 16.0)
assert DXF_STD_SCALES[25] == (1.0, 40.0)
assert DXF_STD_SCALES[33] == (1000.0, 1.0)
assert set(DXF_STD_SCALES) == set(range(1, 34))
# Read-back labels come from the code, never from a stale 142/143.
assert dxf_scale_label(0, 3.0, 7.0) == "fit"
assert dxf_scale_label(26, 1.0, 100.0) == "1:50"
assert dxf_scale_label(21, 1.0, 1.0) == "1:10"
assert dxf_scale_label(19, 1.0, 1.0) == "1:5"
assert dxf_scale_label(22, 1.0, 1.0) == "1:16"
assert dxf_scale_label(28, 1.0, 0.5) == "2:1"
assert dxf_scale_label(8, 1.0, 1.0) == "1/4\"=1'"
assert dxf_scale_label(99, 1.0, 3.0) == "1:3" # unknown code: the stored ratio
@pytest.mark.parametrize(
"kwargs, field",
[
@@ -392,7 +435,8 @@ async def test_layout_plot_honours_the_plot_scale(backend, tmp_path):
async def test_custom_scale_sizes_the_window_too(backend):
"""1:5 has no DXF code 75, so it rides on the numerator / denominator."""
"""1:5 takes the custom route (as on the live engine), so it rides on the
numerator / denominator."""
await _sheet(backend)
await backend.page_setup_apply(SHEET, resolve_page_setup("ISO_A3", scale="1:5"))
frame = backend._paper_frame(backend._doc.layouts.get(SHEET))
@@ -526,9 +570,103 @@ async def test_custom_scale_clears_the_standard_scale_bit_and_writes_the_ratio(b
dxf = backend._doc.layouts.get(SHEET).dxf_layout.dxf
assert not int(dxf.plot_layout_flags) & 16
assert (float(dxf.scale_numerator), float(dxf.scale_denominator)) == (1.0, 5.0)
assert float(dxf.unit_factor) == pytest.approx(0.2)
assert (await backend.page_setup_list(SHEET))[0]["scale"] == "1:5"
@pytest.mark.parametrize(
"label, code, factor",
[
("fit", 0, 1.0),
("1:1", 16, 1.0),
("1:2", 17, 0.5),
("1:10", 21, 0.1),
("1:20", 23, 0.05),
("1:50", 26, 0.02),
("1:100", 27, 0.01),
("2:1", 28, 2.0),
("10:1", 31, 10.0),
],
)
async def test_standard_scale_writes_the_code_and_the_factor_autocad_plots_by(
backend, label, code, factor
):
"""Under USE_STANDARD_SCALE AutoCAD's plot engine scales by group 147
alone (measured, AutoCAD 2026: a headless 1:10 sheet with code 21 and
147 = 1.0 plotted a 100-unit line at 99.99 mm — 1:1 — while the row,
``applied`` and 142/143 all said 1:10; patching 147 to 0.1 alone gave
9.99 mm). AutoCAD writes 147 = numerator / denominator, 1.0 for fit; so
does the headless writer, on every route, with the code AutoCAD uses.
"""
await _sheet(backend)
result = await backend.page_setup_apply(SHEET, resolve_page_setup("ISO_A3", scale=label))
assert result["ok"] is True and result["applied"]["scale"] == label
dxf = backend._doc.layouts.get(SHEET).dxf_layout.dxf
assert int(dxf.plot_layout_flags) & 16
assert int(dxf.standard_scale_type) == code
assert float(dxf.unit_factor) == pytest.approx(factor)
ratio = [1.0, 1.0] if label == "fit" else list(scale_ratio(label))
assert [float(dxf.scale_numerator), float(dxf.scale_denominator)] == ratio
assert (await backend.page_setup_list(SHEET))[0]["scale"] == label
frame = backend._paper_frame(backend._doc.layouts.get(SHEET))
assert frame["scale"] == label and frame["scale_applied"] is True
if label != "fit":
assert frame["xlim"][1] - frame["xlim"][0] == pytest.approx(420.0 / factor)
def _autocad_authored_scale(layout, code, numerator, denominator, factor):
"""Store the plot scale exactly as AutoCAD 2026 saves it (measured via COM
``SaveAs(..., 61)``): group 75 from the ObjectARX enum, 142/143 as the
ratio (AutoCAD normalises 2:1 to 1 / 0.5), 147 as the factor, bit 16 set.
"""
dxf = layout.dxf_layout.dxf
if not dxf.hasattr("plot_layout_flags"):
dxf.plot_layout_flags = dxf.get_default("plot_layout_flags")
dxf.paper_size = "ISO_A3_(420.00_x_297.00_MM)"
dxf.paper_width, dxf.paper_height = 420.0, 297.0
dxf.plot_paper_units = 1
dxf.plot_rotation = 0
dxf.standard_scale_type = code
dxf.scale_numerator, dxf.scale_denominator = numerator, denominator
dxf.unit_factor = factor
layout.use_standard_scale(True)
layout.set_plot_type(5)
@pytest.mark.parametrize(
"code, numerator, denominator, factor, label, units_across",
[
(21, 1.0, 10.0, 0.1, "1:10", 4200.0),
(23, 1.0, 20.0, 0.05, "1:20", 8400.0),
(26, 1.0, 50.0, 0.02, "1:50", 21000.0),
(27, 1.0, 100.0, 0.01, "1:100", 42000.0),
(28, 1.0, 0.5, 2.0, "2:1", 210.0),
(31, 1.0, 0.1, 10.0, "10:1", 42.0),
(19, 1.0, 5.0, 0.2, "1:5", 2100.0),
(22, 1.0, 16.0, 0.0625, "1:16", 6720.0),
],
)
async def test_list_and_render_read_an_autocad_authored_sheet_at_its_own_scale(
backend, code, numerator, denominator, factor, label, units_across
):
"""A sheet AutoCAD saved at 1:50 (group 75 = 26) reads back as 1:50 and
renders 21000 paper-space units across the 420 mm — not "1:100" / 42000
(what ezdxf's one-off ``STD_SCALES`` made of it: 1:10 rendered at 1:16,
1:20 at 1:30, 1:50 at 1:100, 1:100 at 2:1, 2:1 at 4:1, 10:1 at 100:1,
with the row and the plot disagreeing). Codes 19 and 22 are ones this
server never writes, so this reads the file enum, not the writer's table.
"""
await _sheet(backend)
lay = backend._doc.layouts.get(SHEET)
_autocad_authored_scale(lay, code, numerator, denominator, factor)
row = (await backend.page_setup_list(SHEET))[0]
assert row["scale"] == label
frame = backend._paper_frame(lay)
assert frame["scale"] == label and frame["effective_scale"] == label
assert frame["scale_applied"] is True
assert frame["xlim"][1] - frame["xlim"][0] == pytest.approx(units_across)
async def test_apply_keeps_the_viewports(backend):
await _sheet(backend)
await backend.viewport_create(SHEET, 210, 148.5, 260, 180, 0, 0, scale=1.0)