ctrl-kd 4.7.0__tar.gz → 4.8.0__tar.gz
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.
- {ctrl_kd-4.7.0/src/ctrl_kd.egg-info → ctrl_kd-4.8.0}/PKG-INFO +1 -1
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0/src/ctrl_kd.egg-info}/PKG-INFO +1 -1
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/src/ctrl_kd.egg-info/SOURCES.txt +4 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/src/ctrlkd/__init__.py +1 -1
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/src/ctrlkd/cli.py +10 -7
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/src/ctrlkd/core.py +81 -2
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/src/ctrlkd/emit.py +143 -54
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/src/ctrlkd/layout.py +97 -8
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/src/ctrlkd/pdf.py +1011 -107
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_ctrlkd.py +256 -22
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_graphic_cells.py +2 -2
- ctrl_kd-4.8.0/tests/test_head_foot_placement_m16.py +146 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_head_foot_timing.py +91 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_justification.py +1 -1
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_merge_page_number_variable.py +8 -1
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_modern_centring_clip.py +11 -0
- ctrl_kd-4.8.0/tests/test_modern_columns.py +379 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_modern_line_spacing.py +25 -2
- ctrl_kd-4.8.0/tests/test_modern_page_number.py +344 -0
- ctrl_kd-4.8.0/tests/test_modern_sheet_height.py +137 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_modern_verse_defrow.py +32 -4
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_pcl_tolerance.py +97 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_pcl_v4_column_geometry.py +1 -1
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_pn_page_timing.py +158 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_printed_fidelity.py +143 -8
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_rtf_section_spine.py +25 -15
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_structural_checks.py +2 -4
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/LICENSE +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/README.md +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/pyproject.toml +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/setup.cfg +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/src/ctrl_kd.egg-info/dependency_links.txt +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/src/ctrl_kd.egg-info/entry_points.txt +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/src/ctrl_kd.egg-info/top_level.txt +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/src/ctrlkd/afm.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/src/ctrlkd/convert.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/src/ctrlkd/fontmap.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/src/ctrlkd/info.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/src/ctrlkd/pictures.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/src/ctrlkd/pix.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/src/ctrlkd/piximg.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/src/ctrlkd/samples/LYING.WS +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/src/ctrlkd/samples/OCAPTAIN.WS +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/src/ctrlkd/samples/TWAINLET.WS +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/src/ctrlkd/samples/WARPRAYR.WS +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/src/ctrlkd/symbolmap.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/src/ctrlkd/typestyles.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/src/ctrlkd/writer.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/src/ctrlkd/wschange.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_bare_tab_modulus8.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_dot_comment_prints_nothing.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_driver_substitutions_exports.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_endnote_leading_gap.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_fidelity_gate.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_flags_toc_inline.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_form_feed_line_marks.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_glyph_aspect.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_graphic_cell_ops.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_html_print_stylesheet.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_layout_marks.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_lj6dtp_bullet_glyph.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_lj6dtp_char_substitution.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_lj6dtp_colour_restore.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_lj6dtp_heading_face.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_lj6dtp_hp_patterns.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_lj6dtp_legend_line_spacing.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_lj6dtp_pcl_rectangles.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_lj6dtp_shading_table_rules.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_lj6dtp_table_rule_weight.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_load_plugins.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_mailmerge_data_files.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_modern_blank_after_tightened_line.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_modern_box_regions.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_modern_lint.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_modern_page_baseline.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_modern_rtf_structure_rows.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_note_rulings_20260824.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_page_parity.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_paper_verdicts.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_pcl_fidelity.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_pcl_page_membership.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_pcl_page_size_eject.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_pcl_transparent_print.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_pcl_v4_untriaged_fixes.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_pcl_vertical_wraparound.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_peseta_euro_driver.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_pf_print_reformat.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_pictures.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_pix.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_polarity_gate.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_print_control_labels_invisible.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_rtf_facing_pages.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_rtf_head_style_attrs.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_rtf_keep_with_next.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_rtf_page_numbers.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_samples.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_sawyer_corpus.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_screenplay_detection.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_screenplay_pdf.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_screenplay_rendering.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_sentence_spacing_n9.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_style_leading.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_symbol_span_graphic_chars.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_trailing_pa_break.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_verse_quote_couplet.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_verse_spacing.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_width_and_cp1252_memo.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_writer.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_wschange.py +0 -0
- {ctrl_kd-4.7.0 → ctrl_kd-4.8.0}/tests/test_zero_width_tab.py +0 -0
|
@@ -38,6 +38,7 @@ tests/test_form_feed_line_marks.py
|
|
|
38
38
|
tests/test_glyph_aspect.py
|
|
39
39
|
tests/test_graphic_cell_ops.py
|
|
40
40
|
tests/test_graphic_cells.py
|
|
41
|
+
tests/test_head_foot_placement_m16.py
|
|
41
42
|
tests/test_head_foot_timing.py
|
|
42
43
|
tests/test_html_print_stylesheet.py
|
|
43
44
|
tests/test_justification.py
|
|
@@ -57,10 +58,13 @@ tests/test_merge_page_number_variable.py
|
|
|
57
58
|
tests/test_modern_blank_after_tightened_line.py
|
|
58
59
|
tests/test_modern_box_regions.py
|
|
59
60
|
tests/test_modern_centring_clip.py
|
|
61
|
+
tests/test_modern_columns.py
|
|
60
62
|
tests/test_modern_line_spacing.py
|
|
61
63
|
tests/test_modern_lint.py
|
|
62
64
|
tests/test_modern_page_baseline.py
|
|
65
|
+
tests/test_modern_page_number.py
|
|
63
66
|
tests/test_modern_rtf_structure_rows.py
|
|
67
|
+
tests/test_modern_sheet_height.py
|
|
64
68
|
tests/test_modern_verse_defrow.py
|
|
65
69
|
tests/test_note_rulings_20260824.py
|
|
66
70
|
tests/test_page_parity.py
|
|
@@ -130,15 +130,17 @@ def main(argv=None):
|
|
|
130
130
|
help='omit paragraph-style pass-through (HTML classes + '
|
|
131
131
|
'generated CSS, RTF stylesheet) from the output')
|
|
132
132
|
ap.add_argument('--headers', choices=('on', 'off'), default='on',
|
|
133
|
-
help='
|
|
134
|
-
'surfaces
|
|
133
|
+
help="the document's own running heads and feet (.he/.fo) "
|
|
134
|
+
'in the paged surfaces -- Printed PDF, Modern PDF and '
|
|
135
|
+
"both RTF modes. WordStar's own automatic page number "
|
|
136
|
+
'is --page-numbers\' business alone. Default: on')
|
|
135
137
|
ap.add_argument('--page-numbers', choices=('auto', 'on', 'off'),
|
|
136
138
|
default='auto',
|
|
137
139
|
help="WordStar's own AUTOMATIC page number -- the one "
|
|
138
140
|
'.pc positions, a separate mechanism from a # the '
|
|
139
|
-
'author placed inside a real .he/.fo (that
|
|
140
|
-
'
|
|
141
|
-
'
|
|
141
|
+
'author placed inside a real .he/.fo (that goes '
|
|
142
|
+
'with its head, under --headers, never this flag). '
|
|
143
|
+
'Every paged surface. auto (DEFAULT): the '
|
|
142
144
|
'document\'s own dot '
|
|
143
145
|
'commands decide -- .pn/.pg turn it on, .op turns '
|
|
144
146
|
'it off, exactly like real WordStar; a document '
|
|
@@ -149,8 +151,9 @@ def main(argv=None):
|
|
|
149
151
|
'silent document. off: suppress it unconditionally. '
|
|
150
152
|
'A declared footer always pre-empts it, in every '
|
|
151
153
|
'mode (WSFORMAT.WS: "active only when the footers '
|
|
152
|
-
'are not in use")
|
|
153
|
-
'it,
|
|
154
|
+
'are not in use") -- a property of the document, so '
|
|
155
|
+
'--headers off does not lift it, and does not '
|
|
156
|
+
'suppress the number either.')
|
|
154
157
|
ap.add_argument('--line-numbers', choices=('on', 'off'), default='on',
|
|
155
158
|
help="the document's own .l# line-number gutter in the "
|
|
156
159
|
'paged surfaces (Printed/Native PDF and RTF); no '
|
|
@@ -1427,6 +1427,46 @@ class Document:
|
|
|
1427
1427
|
# can `|` it into a run's own styles unconditionally.
|
|
1428
1428
|
header_style_attrs: dict = field(default_factory=dict)
|
|
1429
1429
|
footer_style_attrs: dict = field(default_factory=dict)
|
|
1430
|
+
# M16 (2026-09-15, WS7 probes B0-B5 against sawyer/REF/BOOKLET.WS): the
|
|
1431
|
+
# RIGHT MARGIN the head/foot line's own style declares, in print
|
|
1432
|
+
# columns at 10 CPI (`right_margin_hmi / 180`), or absent when the
|
|
1433
|
+
# style declares none. `{line: cols}`.
|
|
1434
|
+
#
|
|
1435
|
+
# A right- or centre-aligned running head aligns against THIS, measured
|
|
1436
|
+
# from `.po` -- not against the document's `.rm`, not against the
|
|
1437
|
+
# column grid, and not against the sheet. Measured, five probes printed
|
|
1438
|
+
# by real WS7 under DOSBox-X: turning `.co 2` off (B2) and changing it
|
|
1439
|
+
# to `.co 3` with a `.rm` of 2.50" instead of 4.50" (B3) both leave
|
|
1440
|
+
# BOOKLET.WS's "Header Odd 1" at exactly the x the unmodified document
|
|
1441
|
+
# prints it at, while moving `.po` from 0.2in to 1.2in (B4) moves it by
|
|
1442
|
+
# exactly 1.00in. Its style's own `right_margin_hmi` is 18000 = 100
|
|
1443
|
+
# columns = 10.00in, and `.po` 0.2in + 10.00in = 10.20in is where the
|
|
1444
|
+
# last glyph of "Header Odd 1" ends, to 0.5pt.
|
|
1445
|
+
#
|
|
1446
|
+
# Every other document in the corpus with a right-aligned head declares
|
|
1447
|
+
# a style right margin EQUAL to its own `.rm` (a "Header at Top of
|
|
1448
|
+
# Page" style at 11700 = 6.50in beside a `.rm 65`), which is why the
|
|
1449
|
+
# `.rm` reading was right everywhere else and wrong here.
|
|
1450
|
+
header_style_rm: dict = field(default_factory=dict)
|
|
1451
|
+
footer_style_rm: dict = field(default_factory=dict)
|
|
1452
|
+
# M16: the LINE HEIGHT in force where this head/foot line was DEFINED,
|
|
1453
|
+
# in 48ths of an inch -- the step between successive running-head
|
|
1454
|
+
# lines, and 0 when the document asked for zero. `{line: lh_48}`.
|
|
1455
|
+
#
|
|
1456
|
+
# Measured (probe B1): removing BOOKLET.WS's own `.lh 0` -- which sits
|
|
1457
|
+
# immediately before its `.f1`/`.h1`/`.h2` and is cancelled by a
|
|
1458
|
+
# `.lh12` immediately after them -- makes real WS7 print its two header
|
|
1459
|
+
# lines on two rows 1/6in apart instead of both on one row, with the
|
|
1460
|
+
# columns left in place. Turning the columns off instead (B2) leaves
|
|
1461
|
+
# them on one row. So the collapse is the line height's doing, not the
|
|
1462
|
+
# column grid's, and it is the value in force at the COMMAND (the
|
|
1463
|
+
# `.lh12` that follows governs the body and not these lines).
|
|
1464
|
+
#
|
|
1465
|
+
# The block is anchored on its LAST line either way (probe B5: a
|
|
1466
|
+
# one-line head prints on the same row the second of two occupies), so
|
|
1467
|
+
# a zero step stacks every line onto that row.
|
|
1468
|
+
header_leads: dict = field(default_factory=dict)
|
|
1469
|
+
footer_leads: dict = field(default_factory=dict)
|
|
1430
1470
|
# Planning #250: `.H1E`/`.H1O`/`.F1E`/`.F1O` (WSFORMAT.TXT: ".HE can
|
|
1431
1471
|
# optionally specify even or odd numbered page headers" / the matching
|
|
1432
1472
|
# ".FO" sentence -- confirmed the ONLY two lines that do; `.H2`-`.H5`/
|
|
@@ -3273,7 +3313,18 @@ def _parse_format_dot(cmd: bytes, state: dict) -> None:
|
|
|
3273
3313
|
resolved = _resolve_lh_arg(value, m.group(2))
|
|
3274
3314
|
if resolved is not None: # junk/non-positive: state stands
|
|
3275
3315
|
state['lead_48'] = resolved
|
|
3316
|
+
state['hf_lead_48'] = resolved
|
|
3276
3317
|
state['lh_auto'] = False
|
|
3318
|
+
elif value == 0:
|
|
3319
|
+
# M16 (2026-09-15): `.lh 0` leaves `lead_48` alone --
|
|
3320
|
+
# a zero body leading is not something any renderer or
|
|
3321
|
+
# the pagination arithmetic can use, and no evidence
|
|
3322
|
+
# says WordStar applies it to the body -- but it IS
|
|
3323
|
+
# what a running head's own line step becomes, proved
|
|
3324
|
+
# by probes B0/B1/B2 against real WS7 (see
|
|
3325
|
+
# `Document.header_leads`). Recorded on its own key so
|
|
3326
|
+
# the head can read it and nothing else changes.
|
|
3327
|
+
state['hf_lead_48'] = 0.0
|
|
3277
3328
|
elif name in (b'LM', b'RM', b'PM'): # left / right / paragraph margin
|
|
3278
3329
|
# Print columns at 10 CPI, matching `.po`; a unit suffix converts. The
|
|
3279
3330
|
# archive writes both (`.rm 65` and `.rm 6.5"`).
|
|
@@ -3637,7 +3688,8 @@ def _hf_within(doc, cur, cur_line):
|
|
|
3637
3688
|
|
|
3638
3689
|
def _parse_head_foot(cmd: bytes, doc, encoding: str, anchor=None, font_idx=None,
|
|
3639
3690
|
tab_mark=None, align=None, style_attrs=None,
|
|
3640
|
-
pctl_marks=None, within=None
|
|
3691
|
+
pctl_marks=None, within=None, style_rm_cols=None,
|
|
3692
|
+
lead_48=None):
|
|
3641
3693
|
"""Record `.he`/`.h1`-`.h5` and `.fo`/`.f1`-`.f5` text on the Document.
|
|
3642
3694
|
|
|
3643
3695
|
`.HE` and `.FO` are line 1; the numbered forms select their own line, so a
|
|
@@ -3766,6 +3818,18 @@ def _parse_head_foot(cmd: bytes, doc, encoding: str, anchor=None, font_idx=None,
|
|
|
3766
3818
|
which_style_attrs = (doc.header_style_attrs if kind == 'H'
|
|
3767
3819
|
else doc.footer_style_attrs)
|
|
3768
3820
|
which_style_attrs[line] = style_attrs or frozenset()
|
|
3821
|
+
# M16: see `Document.header_style_rm`/`header_leads` for the probes.
|
|
3822
|
+
which_style_rm = (doc.header_style_rm if kind == 'H'
|
|
3823
|
+
else doc.footer_style_rm)
|
|
3824
|
+
if style_rm_cols is not None:
|
|
3825
|
+
which_style_rm[line] = style_rm_cols
|
|
3826
|
+
else:
|
|
3827
|
+
which_style_rm.pop(line, None)
|
|
3828
|
+
which_leads = doc.header_leads if kind == 'H' else doc.footer_leads
|
|
3829
|
+
if lead_48 is not None:
|
|
3830
|
+
which_leads[line] = lead_48
|
|
3831
|
+
else:
|
|
3832
|
+
which_leads.pop(line, None)
|
|
3769
3833
|
if parity is not None:
|
|
3770
3834
|
which_p = doc.headers_parity if kind == 'H' else doc.footers_parity
|
|
3771
3835
|
which_fonts_p = (doc.header_fonts_parity if kind == 'H'
|
|
@@ -6146,6 +6210,7 @@ def parse_ws(data: bytes, encoding: str = 'cp437') -> Document:
|
|
|
6146
6210
|
hf_align = None
|
|
6147
6211
|
hf_style_font_idx = None
|
|
6148
6212
|
hf_style_attrs = None
|
|
6213
|
+
hf_style_rm = None
|
|
6149
6214
|
if hf_style_w0 is not None and (hf_style_w0 >> 8) == 0x02:
|
|
6150
6215
|
_hf_entry = style_slots.get(hf_style_w0 & 0xFF)
|
|
6151
6216
|
if _hf_entry is not None:
|
|
@@ -6153,6 +6218,11 @@ def parse_ws(data: bytes, encoding: str = 'cp437') -> Document:
|
|
|
6153
6218
|
if _hf_entry.get('font') and any(_hf_entry['font']):
|
|
6154
6219
|
hf_style_font_idx = _style_font(_hf_entry['font'])
|
|
6155
6220
|
hf_style_attrs = _hf_entry.get('attrs')
|
|
6221
|
+
# M16: the same 180-HMI-per-column conversion
|
|
6222
|
+
# `emit.py` already applies to a BODY block's own
|
|
6223
|
+
# style right margin, read here for the head.
|
|
6224
|
+
if _hf_entry.get('right_margin_hmi'):
|
|
6225
|
+
hf_style_rm = _hf_entry['right_margin_hmi'] / 180.0
|
|
6156
6226
|
_parse_head_foot(cmd if strip_hibit else raw.rstrip(), doc,
|
|
6157
6227
|
encoding,
|
|
6158
6228
|
anchor=len(doc.blocks) + (1 if cur.lines or
|
|
@@ -6175,7 +6245,16 @@ def parse_ws(data: bytes, encoding: str = 'cp437') -> Document:
|
|
|
6175
6245
|
else hf_style_font_idx),
|
|
6176
6246
|
tab_mark=hf_tab_mark,
|
|
6177
6247
|
align=hf_align, style_attrs=hf_style_attrs,
|
|
6178
|
-
pctl_marks=hf_pctl_marks
|
|
6248
|
+
pctl_marks=hf_pctl_marks,
|
|
6249
|
+
style_rm_cols=hf_style_rm,
|
|
6250
|
+
# M16: the `.lh` in force RIGHT HERE, which is
|
|
6251
|
+
# what the step between running-head lines
|
|
6252
|
+
# turns out to be -- `hf_lead_48` and not
|
|
6253
|
+
# `lead_48`, because a `.lh 0` leaves the
|
|
6254
|
+
# latter standing (it is not a body leading
|
|
6255
|
+
# any renderer could use) while still being
|
|
6256
|
+
# the head's own answer.
|
|
6257
|
+
lead_48=fmt.get('hf_lead_48'))
|
|
6179
6258
|
# The index of the block this entry POINTS AT -- the one that follows it,
|
|
6180
6259
|
# which is the block still open (if it has content) or the next to open.
|
|
6181
6260
|
# "This heading is in the table of contents" refers forward, not back.
|
|
@@ -2883,6 +2883,30 @@ def hf_runs(txt):
|
|
|
2883
2883
|
return runs
|
|
2884
2884
|
|
|
2885
2885
|
|
|
2886
|
+
def _rtf_footer_in_use(doc, slots=None):
|
|
2887
|
+
"""Is a FOOTER declared at all -- with text, bare, or carrying only
|
|
2888
|
+
invisible characters?
|
|
2889
|
+
|
|
2890
|
+
The question WordStar's automatic page number turns on ("active only
|
|
2891
|
+
when the footers are not in use", WSFORMAT.WS), and deliberately NOT
|
|
2892
|
+
the same question as "is there footer text to draw". `_hf_slots` is
|
|
2893
|
+
the drawing answer and drops an event with no text; this is the
|
|
2894
|
+
declaration answer and counts it, matching `pdf._close_page`'s own
|
|
2895
|
+
`footer_in_use = bool(page_ftrs)` taken before the empty slots are
|
|
2896
|
+
dropped.
|
|
2897
|
+
|
|
2898
|
+
`slots` is `_rtf_running_heads`' own argument: None for the document
|
|
2899
|
+
(section 1, which is the whole document for every file that opens no
|
|
2900
|
+
later section), or a resolved `(hdr_slots, ftr_slots)` pair for a
|
|
2901
|
+
later section -- in which case that section's own already-resolved
|
|
2902
|
+
footer slots are the answer, because `_rtf_hf_slots_at` is what
|
|
2903
|
+
decided them."""
|
|
2904
|
+
if slots is not None:
|
|
2905
|
+
return bool(slots[1])
|
|
2906
|
+
return any(kind == 'F' for kind, _l, _t, _a
|
|
2907
|
+
in getattr(doc, 'hf_events', ()))
|
|
2908
|
+
|
|
2909
|
+
|
|
2886
2910
|
def _rtf_auto_page_number(doc, page_numbers='auto'):
|
|
2887
2911
|
"""Whether WordStar's own AUTOMATIC page number -- the one `.pc`
|
|
2888
2912
|
positions, never a `#` the author typed into a real `.he`/`.fo` -- is
|
|
@@ -3073,7 +3097,7 @@ def _rtf_keep_plan(doc):
|
|
|
3073
3097
|
return plan
|
|
3074
3098
|
|
|
3075
3099
|
|
|
3076
|
-
def _rtf_section_breaks(doc
|
|
3100
|
+
def _rtf_section_breaks(doc):
|
|
3077
3101
|
r"""`{block_index: (cols, gutter, hdr_slots, ftr_slots)}` -- THE SECTION
|
|
3078
3102
|
SPINE (planning #264 R1, ruled 2026-09-14; packet section 3's
|
|
3079
3103
|
recommendation and rows A7/A9/A13).
|
|
@@ -3087,13 +3111,16 @@ def _rtf_section_breaks(doc, printed):
|
|
|
3087
3111
|
|
|
3088
3112
|
TWO things open one:
|
|
3089
3113
|
|
|
3090
|
-
A7 a change of newspaper-column regime (`.co n, gutter`).
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
the
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3114
|
+
A7 a change of newspaper-column regime (`.co n, gutter`). BOTH
|
|
3115
|
+
MODES since M17b (2026-09-15). R1 made this Printed-only on the
|
|
3116
|
+
stated grounds that "Modern PDF has no column model at all, and
|
|
3117
|
+
the 2026-08-05 ruling is that Modern PDF needs to be the printed
|
|
3118
|
+
version of the Modern RTF -- so a columnar Modern RTF would be a
|
|
3119
|
+
Modern RTF its own PDF could not render". Modern PDF has a
|
|
3120
|
+
column model now (M17, `pdf._modern_column_width`), so the same
|
|
3121
|
+
ruling read the same way now says the opposite: a ONE-column
|
|
3122
|
+
Modern RTF is the one its own PDF cannot render. (Modern HTML's
|
|
3123
|
+
`column-count` is a separate, older surface and is untouched.)
|
|
3097
3124
|
A13 a running head or foot REDEFINED mid-document -- a second
|
|
3098
3125
|
definition of the same slot with different text. The new section
|
|
3099
3126
|
carries its own `\header`/`\footer` groups.
|
|
@@ -3105,18 +3132,17 @@ def _rtf_section_breaks(doc, printed):
|
|
|
3105
3132
|
re-anchor (`\pgnrestart\pgnstarts`), and a mid-document `.po`/margin
|
|
3106
3133
|
change. R1 names A7, A9 and A13; those are the three built."""
|
|
3107
3134
|
anchors = set(_rtf_hf_redefinitions(doc))
|
|
3108
|
-
state = _rtf_columns_state(doc)
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
anchors.add(bi)
|
|
3135
|
+
state = _rtf_columns_state(doc)
|
|
3136
|
+
for bi, b in enumerate(doc.blocks):
|
|
3137
|
+
# Only a REAL block opens a column regime: a sentinel inherits
|
|
3138
|
+
# the state around it and must not read as a change.
|
|
3139
|
+
if b.columns is None or bi == 0:
|
|
3140
|
+
continue
|
|
3141
|
+
if state[bi] != state[bi - 1]:
|
|
3142
|
+
anchors.add(bi)
|
|
3117
3143
|
out = {}
|
|
3118
3144
|
for bi in sorted(a for a in anchors if 0 < a < len(doc.blocks)):
|
|
3119
|
-
cols, gutter = state[bi]
|
|
3145
|
+
cols, gutter = state[bi]
|
|
3120
3146
|
out[bi] = (cols, gutter,
|
|
3121
3147
|
_rtf_hf_slots_at(doc, 'H', bi),
|
|
3122
3148
|
_rtf_hf_slots_at(doc, 'F', bi))
|
|
@@ -3233,11 +3259,17 @@ def _rtf_running_heads(doc, headers=True, auto_page_number=False,
|
|
|
3233
3259
|
A4 the head's own alignment (`header_align`/`footer_align`, the
|
|
3234
3260
|
`.h#`/`.f#` argument's embedded style-sheet reference, planning
|
|
3235
3261
|
#255) becomes `\\qr`/`\\qc` on the group's paragraph. RTF's
|
|
3236
|
-
header is ONE paragraph carrying every declared line,
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3262
|
+
header is ONE paragraph carrying every declared line, joined by
|
|
3263
|
+
`\\line`, for as long as those lines AGREE on their alignment --
|
|
3264
|
+
which is every head and foot in the corpus but one. This row
|
|
3265
|
+
originally applied the FIRST line's alignment to the whole
|
|
3266
|
+
group, on the stated grounds that no corpus document declares
|
|
3267
|
+
two head lines with different alignments; `sawyer/REF/
|
|
3268
|
+
BOOKLET.WS` does, and its left-aligned second head line was
|
|
3269
|
+
right-aligned with its first. A group whose lines disagree is
|
|
3270
|
+
written as a paragraph PER LINE instead (2026-09-15), since
|
|
3271
|
+
`\\line` is a break inside a paragraph and cannot carry a second
|
|
3272
|
+
alignment. Every other document's bytes are unchanged.
|
|
3241
3273
|
A5 `\\headery`/`\\footery` -- see `_rtf_head_foot_distance`.
|
|
3242
3274
|
|
|
3243
3275
|
Planning #264 item 5: the head's OWN print attributes
|
|
@@ -3273,13 +3305,28 @@ def _rtf_running_heads(doc, headers=True, auto_page_number=False,
|
|
|
3273
3305
|
# and WordStar's automatic number stays off -- which is exactly what
|
|
3274
3306
|
# `pdf._close_page` records (`footer_in_use = bool(page_ftrs)`, before
|
|
3275
3307
|
# the empty slots are dropped).
|
|
3308
|
+
#
|
|
3309
|
+
# M15 FOLLOW-UP (2026-09-15): that is what the paragraph above says,
|
|
3310
|
+
# and `ftr_slots` was not it. `_hf_slots` drops an event with NO text
|
|
3311
|
+
# at all (`if ... not txt: continue`), so a document whose only footer
|
|
3312
|
+
# command is a BARE `.fo` read as "no footer in use" and RTF printed
|
|
3313
|
+
# the automatic number on it -- while both PDFs, reading
|
|
3314
|
+
# `footer_in_use` off the un-dropped slots, printed none. Real WS7
|
|
3315
|
+
# prints none: "That holds whether the footer has text, is bare, or
|
|
3316
|
+
# contains only invisible characters" (research/2026-09-15_ws7-
|
|
3317
|
+
# missing-auto-page-number.md, rule 2; the same rule the PDF's own
|
|
3318
|
+
# triage cause 1 fixed on 2026-09-12). Five documents in the public
|
|
3319
|
+
# archive are that shape -- MACROS/HOLYMAC/4MAC2, 4MAC3, 7MAC2, 7MAC3
|
|
3320
|
+
# and LSRBOX/LSRBOX.WS, four of them named in that research as WS7
|
|
3321
|
+
# printing no number on any of their 41/53/35/35 pages -- and their
|
|
3322
|
+
# `rtf.printed` and `rtf.modern` both move here.
|
|
3276
3323
|
# planning #264 R7 (ruled 2026-09-14): the two flags are SEPARATE and
|
|
3277
3324
|
# RTF follows the PDF. `headers` governs running heads and feet -- and
|
|
3278
3325
|
# so a `#` the author typed INSIDE one, which is that head's own text
|
|
3279
3326
|
# -- while `--page-numbers` governs WordStar's AUTOMATIC number alone.
|
|
3280
3327
|
# The first RTF batch made `--headers off` swallow the automatic number
|
|
3281
3328
|
# too; that is reverted here, on both surfaces.
|
|
3282
|
-
show_auto_num = auto_page_number and not
|
|
3329
|
+
show_auto_num = auto_page_number and not _rtf_footer_in_use(doc, slots)
|
|
3283
3330
|
if not headers:
|
|
3284
3331
|
hdr_slots, ftr_slots = {}, {}
|
|
3285
3332
|
headery, footery = _rtf_head_foot_distance(doc.meta.get('page') or {},
|
|
@@ -3299,7 +3346,7 @@ def _rtf_running_heads(doc, headers=True, auto_page_number=False,
|
|
|
3299
3346
|
def group(name, lines, faces, aligns, attrs):
|
|
3300
3347
|
if not lines:
|
|
3301
3348
|
return ''
|
|
3302
|
-
rendered,
|
|
3349
|
+
rendered, line_aligns = [], []
|
|
3303
3350
|
for n in sorted(lines):
|
|
3304
3351
|
txt = lines[n]
|
|
3305
3352
|
if subst is not None:
|
|
@@ -3309,8 +3356,7 @@ def _rtf_running_heads(doc, headers=True, auto_page_number=False,
|
|
|
3309
3356
|
runs = hf_runs(txt)
|
|
3310
3357
|
if not runs:
|
|
3311
3358
|
continue # control-bytes-only head
|
|
3312
|
-
|
|
3313
|
-
align_ctl = _RTF_HF_ALIGN.get(aligns.get(n), '')
|
|
3359
|
+
line_aligns.append(_RTF_HF_ALIGN.get(aligns.get(n), ''))
|
|
3314
3360
|
# planning #264 item 5 (planning #255's `header_style_attrs`):
|
|
3315
3361
|
# the print attributes the head's OWN style turns on. A
|
|
3316
3362
|
# `.h#`/`.f#` argument can name a style-sheet entry, and that
|
|
@@ -3330,8 +3376,26 @@ def _rtf_running_heads(doc, headers=True, auto_page_number=False,
|
|
|
3330
3376
|
for text, styles in runs))
|
|
3331
3377
|
if not rendered:
|
|
3332
3378
|
return ''
|
|
3333
|
-
|
|
3334
|
-
|
|
3379
|
+
# ONE PARAGRAPH while every rendered line agrees on its alignment
|
|
3380
|
+
# -- which is every head and foot in the corpus but one, so this
|
|
3381
|
+
# is byte-identical to the single-paragraph group RTF has always
|
|
3382
|
+
# written. Planning #264 item 4 (row A4) applied the FIRST line's
|
|
3383
|
+
# alignment to the whole group on the stated grounds that "no
|
|
3384
|
+
# corpus document declares two head lines with different
|
|
3385
|
+
# alignments"; `sawyer/REF/BOOKLET.WS` does (a right-aligned
|
|
3386
|
+
# "Header Odd" over a left-aligned "Header Even"), and the group
|
|
3387
|
+
# right-aligned both. `\line` is a break INSIDE a paragraph and
|
|
3388
|
+
# cannot carry a second alignment, so the lines that disagree get
|
|
3389
|
+
# a paragraph each -- the only form RTF has for this -- and the
|
|
3390
|
+
# reader lays them out exactly as Modern PDF now draws them
|
|
3391
|
+
# (2026-08-05: Modern PDF is the printed form of the Modern RTF).
|
|
3392
|
+
if len(set(line_aligns)) <= 1:
|
|
3393
|
+
return (r'{\%s \pard\plain %s\f0\fs22 %s\par}'
|
|
3394
|
+
% (name, line_aligns[0], r'\line '.join(rendered)))
|
|
3395
|
+
return (r'{\%s %s}'
|
|
3396
|
+
% (name, ''.join(r'\pard\plain %s\f0\fs22 %s\par'
|
|
3397
|
+
% (a, r) for a, r in zip(line_aligns,
|
|
3398
|
+
rendered))))
|
|
3335
3399
|
|
|
3336
3400
|
def sided(which, name, slots):
|
|
3337
3401
|
"""One head/foot family, as `\\headerl`/`\\headerr` when the
|
|
@@ -3950,8 +4014,8 @@ def emit_rtf(doc, mode='printed', notes=DEFAULT_NOTE_KINDS, styles=True,
|
|
|
3950
4014
|
# `_rtf_section_breaks` for what opens a section and what deliberately
|
|
3951
4015
|
# does not. A document whose geometry never changes gets an empty dict
|
|
3952
4016
|
# here and emits exactly the bytes it always did.
|
|
3953
|
-
section_breaks = _rtf_section_breaks(doc
|
|
3954
|
-
columns_state = _rtf_columns_state(doc)
|
|
4017
|
+
section_breaks = _rtf_section_breaks(doc)
|
|
4018
|
+
columns_state = _rtf_columns_state(doc)
|
|
3955
4019
|
# planning #264 R2 (packet row A8): `.cp n`/`.cc n` -> `\keep`/`\keepn`
|
|
3956
4020
|
# on the paragraphs they asked to hold together. See `_rtf_keep_plan`.
|
|
3957
4021
|
keep_plan = _rtf_keep_plan(doc)
|
|
@@ -3983,14 +4047,12 @@ def emit_rtf(doc, mode='printed', notes=DEFAULT_NOTE_KINDS, styles=True,
|
|
|
3983
4047
|
quote_fi_cols = None
|
|
3984
4048
|
if b.kind == 'colbreak':
|
|
3985
4049
|
# planning #264 R1 (packet row A9): `.cb` breaks to the next
|
|
3986
|
-
# column -- the reader's own `\column`.
|
|
3987
|
-
#
|
|
3988
|
-
#
|
|
3989
|
-
#
|
|
3990
|
-
#
|
|
3991
|
-
#
|
|
3992
|
-
# would mean the same thing to a reader, which is not what
|
|
3993
|
-
# WordStar did with it either -- nothing is written.
|
|
4050
|
+
# column -- the reader's own `\column`. Inside a columnar
|
|
4051
|
+
# region only; outside one the control would mean a break a
|
|
4052
|
+
# reader could still take, which is not what WordStar did with
|
|
4053
|
+
# it either, so nothing is written. BOTH MODES since M17b:
|
|
4054
|
+
# Modern PDF's own column cursor takes `.cb` to the next
|
|
4055
|
+
# column (`pdf._modern_streams`), so its RTF says the same.
|
|
3994
4056
|
quote_open = False
|
|
3995
4057
|
quote_fi_cols = None
|
|
3996
4058
|
if columns_state is not None and columns_state[bi][0] > 1:
|
|
@@ -4007,9 +4069,8 @@ def emit_rtf(doc, mode='printed', notes=DEFAULT_NOTE_KINDS, styles=True,
|
|
|
4007
4069
|
# WINGDING.CHT's own WS7 capture (its author's `.pa` markers are
|
|
4008
4070
|
# a manual column-simulation convention that predates the real
|
|
4009
4071
|
# `.co5` governing the same content; honouring them fragments
|
|
4010
|
-
# one real column into two short pages).
|
|
4011
|
-
#
|
|
4012
|
-
# break; now it has, so it reads the same fact the same way.
|
|
4072
|
+
# one real column into two short pages). Modern PDF absorbs it
|
|
4073
|
+
# on the same evidence since M17, so Modern RTF does too.
|
|
4013
4074
|
if columns_state is not None and columns_state[bi][0] > 1:
|
|
4014
4075
|
continue
|
|
4015
4076
|
parts.append(r'\page ')
|
|
@@ -4283,10 +4344,15 @@ def emit_rtf(doc, mode='printed', notes=DEFAULT_NOTE_KINDS, styles=True,
|
|
|
4283
4344
|
# derived margins are left exactly as declared (still top/bottom/left
|
|
4284
4345
|
# relative to the text, same as WordStar's own driver-level rotation
|
|
4285
4346
|
# never re-interpreted them either) -- only the CANVAS they sit against
|
|
4286
|
-
# changes shape.
|
|
4287
|
-
#
|
|
4288
|
-
#
|
|
4289
|
-
|
|
4347
|
+
# changes shape. BOTH MODES since M17b (2026-09-15): this used to be
|
|
4348
|
+
# Printed-only, on the reading that "Modern's page is its own fixed
|
|
4349
|
+
# Letter". M17 retired that reading for Modern PDF -- a landscape
|
|
4350
|
+
# document's Modern MediaBox is 792x612 now -- and Modern PDF is the
|
|
4351
|
+
# printed form of THIS file (ruled 2026-08-05), so a square 8.5x8.5
|
|
4352
|
+
# Modern RTF would be an RTF its own PDF does not print. It is also
|
|
4353
|
+
# the paged-surface doctrine's point 2 read literally: "honor `.pr
|
|
4354
|
+
# or=l` in ALL paged surfaces", and Modern RTF is one.
|
|
4355
|
+
landscape = doc.meta.get('formatting', {}).get('orientation') == 'landscape'
|
|
4290
4356
|
if landscape:
|
|
4291
4357
|
# planning #256 (sawyer/REF/-HOW-TO.RJS + the HP-ENV.LST/HP-ENVMM.LST
|
|
4292
4358
|
# mailing-label pair): a plain swap of `page['height_in']`/`pw_in`
|
|
@@ -4312,11 +4378,30 @@ def emit_rtf(doc, mode='printed', notes=DEFAULT_NOTE_KINDS, styles=True,
|
|
|
4312
4378
|
def _twips_lines(key, default_lines):
|
|
4313
4379
|
v = page.get(key, default_lines)
|
|
4314
4380
|
return int(round(float(v) * 240)) # 1 line at 6 LPI = 240 twips
|
|
4381
|
+
|
|
4382
|
+
def _paper_twips(height_in):
|
|
4383
|
+
"""A declared sheet height in inches -> `\\paperh` twips, with `.pl 0`
|
|
4384
|
+
falling back to Letter.
|
|
4385
|
+
|
|
4386
|
+
`.pl 0` IS NOT A SHEET. It is WordStar's "page breaks off" (bug
|
|
4387
|
+
12284, `core._text_lines_per_page`), and this emitter's text model
|
|
4388
|
+
already never breaks, so the PAGE BOX falls back to Letter -- the
|
|
4389
|
+
rule Printed PDF has carried since `_resolved_page_height` was
|
|
4390
|
+
written and Modern PDF adopted in M18 (`pdf._modern_sheet_h`),
|
|
4391
|
+
quoted here rather than re-decided.
|
|
4392
|
+
|
|
4393
|
+
RTF was the surface still writing the arithmetic straight through:
|
|
4394
|
+
`\\paperh0`, which LibreOffice refuses to open at all. Five
|
|
4395
|
+
documents: `LSRBOX/LSRBOXES.MRG`, `LSRBOX/LSRLINES.MRG`,
|
|
4396
|
+
`RTF-RJS/1-5LINES.WS`, `RTF-RJS/1-SINGLE.WS`, `RTF-RJS/2-DOUBLE.WS`.
|
|
4397
|
+
A document that declares any real height is untouched."""
|
|
4398
|
+
twips = int(round(float(height_in) * 1440))
|
|
4399
|
+
return twips if twips > 0 else 15840
|
|
4315
4400
|
if printed:
|
|
4316
4401
|
margt = _twips_lines('mt_lines', 3.0)
|
|
4317
4402
|
margb = _twips_lines('mb_lines', 8.0)
|
|
4318
4403
|
margl = int(round(float(page.get('po_cols', 8.0)) * 144))
|
|
4319
|
-
paperh =
|
|
4404
|
+
paperh = _paper_twips(page.get('height_in', 11.0))
|
|
4320
4405
|
else:
|
|
4321
4406
|
margt = (_twips_lines('mt_lines', 6.0)
|
|
4322
4407
|
if page.get('mt_source', 'default') != 'default' else 1440)
|
|
@@ -4324,8 +4409,14 @@ def emit_rtf(doc, mode='printed', notes=DEFAULT_NOTE_KINDS, styles=True,
|
|
|
4324
4409
|
if page.get('mb_source', 'default') != 'default' else 1440)
|
|
4325
4410
|
margl = (int(round(float(page.get('po_cols', 10.0)) * 144))
|
|
4326
4411
|
if page.get('po_source', 'default') != 'default' else 1440)
|
|
4327
|
-
|
|
4328
|
-
|
|
4412
|
+
# A LANDSCAPE sheet is a declared sheet whatever `.pl` says, so it
|
|
4413
|
+
# is never the 11in default: `page` above is already the swapped,
|
|
4414
|
+
# orientation-aware pair (a `.pl 8.5"` resolves to 8.5 tall x 11
|
|
4415
|
+
# wide, not to the un-landscape-aware 8.5x8.5 SQUARE the portrait
|
|
4416
|
+
# resolution gives). Modern PDF composes on exactly that height
|
|
4417
|
+
# (`pdf._modern_sheet_h`), which is what makes the two agree.
|
|
4418
|
+
paperh = (_paper_twips(page.get('height_in', 11.0))
|
|
4419
|
+
if landscape or page.get('size_source', 'default') != 'default'
|
|
4329
4420
|
else 15840)
|
|
4330
4421
|
# width joined the page model 2026-08-06: A4-tall documents get the
|
|
4331
4422
|
# 210mm sheet; everything else (and every default) stays 12240 twips
|
|
@@ -4390,11 +4481,9 @@ def emit_rtf(doc, mode='printed', notes=DEFAULT_NOTE_KINDS, styles=True,
|
|
|
4390
4481
|
# regime. `\cols` is a section property and the page setup is section
|
|
4391
4482
|
# 1's; a document that opens outside a columnar region (all but a
|
|
4392
4483
|
# handful) resolves to one column and writes nothing, so its bytes do
|
|
4393
|
-
# not move.
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
pagesetup += _rtf_cols_control(*(_rtf_columns_state(doc)[0]
|
|
4397
|
-
if doc.blocks else (1, None)))
|
|
4484
|
+
# not move. BOTH MODES since M17b -- see `_rtf_section_breaks`.
|
|
4485
|
+
pagesetup += _rtf_cols_control(*(_rtf_columns_state(doc)[0]
|
|
4486
|
+
if doc.blocks else (1, None)))
|
|
4398
4487
|
# round 18 (RULINGS-LEDGER row 4): TOC/Index at the document's own end,
|
|
4399
4488
|
# gated by `--toc` (default off, the ruled default).
|
|
4400
4489
|
toc_index = _rtf_toc_index(doc, printed) if toc else ''
|