oapi-profile-builder 3.2.0__tar.gz → 3.3.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.
- {oapi_profile_builder-3.2.0/src/oapi_profile_builder.egg-info → oapi_profile_builder-3.3.0}/PKG-INFO +46 -6
- {oapi_profile_builder-3.2.0 → oapi_profile_builder-3.3.0}/README.md +45 -5
- {oapi_profile_builder-3.2.0 → oapi_profile_builder-3.3.0}/pyproject.toml +1 -1
- {oapi_profile_builder-3.2.0 → oapi_profile_builder-3.3.0}/src/oapi_profile_builder/cli.py +1 -1
- oapi_profile_builder-3.3.0/src/oapi_profile_builder/compile.py +126 -0
- {oapi_profile_builder-3.2.0 → oapi_profile_builder-3.3.0}/src/oapi_profile_builder/cover.py +106 -19
- {oapi_profile_builder-3.2.0 → oapi_profile_builder-3.3.0}/src/oapi_profile_builder/generate.py +293 -14
- {oapi_profile_builder-3.2.0 → oapi_profile_builder-3.3.0}/src/oapi_profile_builder/models.py +123 -0
- {oapi_profile_builder-3.2.0 → oapi_profile_builder-3.3.0}/src/oapi_profile_builder/server_validation.py +91 -0
- {oapi_profile_builder-3.2.0 → oapi_profile_builder-3.3.0/src/oapi_profile_builder.egg-info}/PKG-INFO +46 -6
- oapi_profile_builder-3.2.0/src/oapi_profile_builder/compile.py +0 -75
- {oapi_profile_builder-3.2.0 → oapi_profile_builder-3.3.0}/LICENSE +0 -0
- {oapi_profile_builder-3.2.0 → oapi_profile_builder-3.3.0}/setup.cfg +0 -0
- {oapi_profile_builder-3.2.0 → oapi_profile_builder-3.3.0}/src/oapi_profile_builder/__init__.py +0 -0
- {oapi_profile_builder-3.2.0 → oapi_profile_builder-3.3.0}/src/oapi_profile_builder/cite.py +0 -0
- {oapi_profile_builder-3.2.0 → oapi_profile_builder-3.3.0}/src/oapi_profile_builder/cite_features.py +0 -0
- {oapi_profile_builder-3.2.0 → oapi_profile_builder-3.3.0}/src/oapi_profile_builder.egg-info/SOURCES.txt +0 -0
- {oapi_profile_builder-3.2.0 → oapi_profile_builder-3.3.0}/src/oapi_profile_builder.egg-info/dependency_links.txt +0 -0
- {oapi_profile_builder-3.2.0 → oapi_profile_builder-3.3.0}/src/oapi_profile_builder.egg-info/entry_points.txt +0 -0
- {oapi_profile_builder-3.2.0 → oapi_profile_builder-3.3.0}/src/oapi_profile_builder.egg-info/requires.txt +0 -0
- {oapi_profile_builder-3.2.0 → oapi_profile_builder-3.3.0}/src/oapi_profile_builder.egg-info/top_level.txt +0 -0
{oapi_profile_builder-3.2.0/src/oapi_profile_builder.egg-info → oapi_profile_builder-3.3.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: oapi-profile-builder
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.3.0
|
|
4
4
|
Summary: Authoritative tooling for creating OGC API Service Profiles (EDR, Features)
|
|
5
5
|
Author-email: Shane Mill <shane.mill@noaa.gov>
|
|
6
6
|
License: Apache License
|
|
@@ -234,18 +234,20 @@ Everything about the document — cover, title-page metadata, legal page, footer
|
|
|
234
234
|
|
|
235
235
|
| What you want to change | Field | Mechanism |
|
|
236
236
|
|---|---|---|
|
|
237
|
-
| Cover logo
|
|
237
|
+
| Cover logo, tagline, fonts, watermark (replaces the OGC cover) | `document_metadata.cover` | Generated cover image via `:coverpage-image:` full replacement |
|
|
238
238
|
| Title, doc number, edition, dates, sub-title/status | `title`, `version`, `document_metadata.{doc_number, doc_type, doc_subtype, status, *_date}` | Metanorma document attributes |
|
|
239
239
|
| PDF colours (titles, tables, cover, backgrounds) | `document_metadata.colors` | `:presentation-metadata-color-*:` |
|
|
240
240
|
| Page-ii legal text (copyright / license / notice) | `document_metadata.boilerplate` | `:boilerplate-authority:` |
|
|
241
241
|
| Footer organisation name | `document_metadata.copyright_holder` | `:copyright-holder:` |
|
|
242
242
|
| Remove the OGC logo from the legal page | `document_metadata.suppress_flavor_logo` | targeted `:pdf-stylesheet-override:` |
|
|
243
|
+
| Remove OGC design (crossing lines, circled numbers, rules, navy divider pages) | `document_metadata.suppress_design_elements` (or the individual `suppress_crossing_lines` / `plain_section_numbers` / `suppress_title_underlines`) | `:pdf-stylesheet-override:` + colour attributes |
|
|
244
|
+
| Every-page DRAFT-style watermark (over content) | `document_metadata.page_watermark` | `:pdf-stylesheet-override:` (footer overlay) |
|
|
243
245
|
| Requirement / conformance identifier namespace | `spec_uri_base` | derives `req_uri` / `conf_uri` |
|
|
244
|
-
| Submitter table, notice paragraph, normative refs | `document_metadata.{submitters, notice, normative_references}` | Metanorma attributes / sections |
|
|
246
|
+
| Submitter table, notice paragraph, normative refs, extra terms | `document_metadata.{submitters, notice, normative_references, terms}` | Metanorma attributes / sections |
|
|
245
247
|
|
|
246
|
-
Custom branding requires Pillow for the cover image: `pip install 'oapi-profile-builder[pdf]'`. Field-by-field detail is in the [`document_metadata`](#document_metadata) reference below; [`examples/nwp_radar.yaml`](examples/nwp_radar.yaml) is a complete DGIWG-branded example.
|
|
248
|
+
Custom branding requires Pillow for the cover image: `pip install 'oapi-profile-builder[pdf]'`. Field-by-field detail is in the [`document_metadata`](#document_metadata) reference below; [`examples/nwp_radar.yaml`](examples/nwp_radar.yaml) is a complete DGIWG-branded example. Omit all of these fields and the output keeps the standard OGC house style.
|
|
247
249
|
|
|
248
|
-
> The `suppress_flavor_logo`
|
|
250
|
+
> The `suppress_flavor_logo`, `suppress_design_elements`/`suppress_*`, and `page_watermark` overrides reach into the OGC Metanorma flavor's XSL templates (e.g. `insertLogoPreface`, `insertCrossingLines`, `insertSectionNumInCircle`, `insertFooter`) and are coupled to that flavor. Everything else uses documented Metanorma attributes. mn2pdf merges our same-named templates over the flavor's at equal precedence (via `merge_override.xsl`), which is what makes these work; a future metanorma-ogc release could require refreshing the template names. For long-term SDO adoption, a dedicated Metanorma "taste"/flavor is the most durable path.
|
|
249
251
|
|
|
250
252
|
---
|
|
251
253
|
|
|
@@ -701,6 +703,10 @@ document_metadata:
|
|
|
701
703
|
tagline: Delivering Military Advantage through multi-national geospatial interoperability
|
|
702
704
|
background: "#FFFFFF" # optional cover background (default white)
|
|
703
705
|
text_color: "#1F3864" # optional cover text colour (default black)
|
|
706
|
+
font_regular: "Source Sans Pro" # optional: .ttf/.otf path OR installed family name (fontconfig)
|
|
707
|
+
font_bold: "Source Sans Pro" # optional bold face; derived from font_regular when unset
|
|
708
|
+
font_italic: "Source Sans Pro" # optional italic face for the tagline
|
|
709
|
+
watermark: "DRAFT" # optional diagonal watermark stamped on the cover
|
|
704
710
|
colors:
|
|
705
711
|
text: "#000000" # body text
|
|
706
712
|
cover_text: "#1F3864" # cover text / section numbers / ToC
|
|
@@ -733,7 +739,41 @@ document_metadata:
|
|
|
733
739
|
feedback: "Comments on this document should be directed to DGIWG."
|
|
734
740
|
```
|
|
735
741
|
|
|
736
|
-
|
|
742
|
+
#### Removing the OGC page design (crossing lines, circled numbers, divider pages)
|
|
743
|
+
|
|
744
|
+
The OGC flavor also draws decorative design elements: the blue "crossing lines with a dot" motif, circled section-divider numbers, a short rule under section titles, and full-navy section-divider pages. One switch removes them all:
|
|
745
|
+
|
|
746
|
+
- **`suppress_design_elements: true`** — a consolidated switch that:
|
|
747
|
+
- removes the blue crossing-lines + dot motif (on divider/cover pages via a template override; on the Contents/preface pages, which draw the lines inline in the flavor's own colour, by whiting that colour out);
|
|
748
|
+
- renders section numbers as plain text (e.g. `1 Scope`, `i Abstract`) instead of coloured circles;
|
|
749
|
+
- removes the short rule beneath section titles;
|
|
750
|
+
- rebrands the full-navy section-divider pages to a clean white page with a DGIWG-navy (from `colors.title`) plain number and title.
|
|
751
|
+
|
|
752
|
+
The individual toggles `suppress_crossing_lines`, `plain_section_numbers`, and `suppress_title_underlines` are still available if you want finer control; `suppress_design_elements` simply turns all three on plus the divider rebrand.
|
|
753
|
+
|
|
754
|
+
- **`page_watermark`** — stamps a light diagonal watermark (e.g. `DRAFT`) on **every page**, rendered *over* the content including tables. This is distinct from `cover.watermark`, which marks only the generated cover image. The cover page is left unmarked.
|
|
755
|
+
|
|
756
|
+
```yaml
|
|
757
|
+
document_metadata:
|
|
758
|
+
suppress_design_elements: true # plain numbers, no crossing lines/rules, white divider pages
|
|
759
|
+
page_watermark: DRAFT # every-page diagonal watermark (over content)
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
With `cover` + `colors` + `copyright_holder` + `boilerplate` + `suppress_flavor_logo` + `suppress_design_elements` (and `spec_uri_base` for the requirement/conformance identifiers), the generated PDF carries no OGC branding — only legitimate citations to the OGC standards the profile conforms to. Omit these fields and you get the standard OGC house style. See [`examples/nwp_radar.yaml`](examples/nwp_radar.yaml).
|
|
763
|
+
|
|
764
|
+
> These design-element overrides (and the `page_watermark` footer overlay) reach into the OGC Metanorma flavor's XSL template names and are coupled to that flavor; a future metanorma-ogc release could require refreshing them.
|
|
765
|
+
|
|
766
|
+
#### Extending Terms and Definitions
|
|
767
|
+
|
|
768
|
+
Add profile-specific terms (e.g. EDR Part 3 or Pub/Sub terms) appended after the base terms in the Terms and Definitions section:
|
|
769
|
+
|
|
770
|
+
```yaml
|
|
771
|
+
document_metadata:
|
|
772
|
+
terms:
|
|
773
|
+
- term: service profile
|
|
774
|
+
definition: A named, referenceable set of constraints and additional requirements applied to an OGC API - EDR implementation.
|
|
775
|
+
source: "SOURCE: OGC API - EDR Part 3: Service Profiles (draft)"
|
|
776
|
+
```
|
|
737
777
|
|
|
738
778
|
---
|
|
739
779
|
|
|
@@ -187,18 +187,20 @@ Everything about the document — cover, title-page metadata, legal page, footer
|
|
|
187
187
|
|
|
188
188
|
| What you want to change | Field | Mechanism |
|
|
189
189
|
|---|---|---|
|
|
190
|
-
| Cover logo
|
|
190
|
+
| Cover logo, tagline, fonts, watermark (replaces the OGC cover) | `document_metadata.cover` | Generated cover image via `:coverpage-image:` full replacement |
|
|
191
191
|
| Title, doc number, edition, dates, sub-title/status | `title`, `version`, `document_metadata.{doc_number, doc_type, doc_subtype, status, *_date}` | Metanorma document attributes |
|
|
192
192
|
| PDF colours (titles, tables, cover, backgrounds) | `document_metadata.colors` | `:presentation-metadata-color-*:` |
|
|
193
193
|
| Page-ii legal text (copyright / license / notice) | `document_metadata.boilerplate` | `:boilerplate-authority:` |
|
|
194
194
|
| Footer organisation name | `document_metadata.copyright_holder` | `:copyright-holder:` |
|
|
195
195
|
| Remove the OGC logo from the legal page | `document_metadata.suppress_flavor_logo` | targeted `:pdf-stylesheet-override:` |
|
|
196
|
+
| Remove OGC design (crossing lines, circled numbers, rules, navy divider pages) | `document_metadata.suppress_design_elements` (or the individual `suppress_crossing_lines` / `plain_section_numbers` / `suppress_title_underlines`) | `:pdf-stylesheet-override:` + colour attributes |
|
|
197
|
+
| Every-page DRAFT-style watermark (over content) | `document_metadata.page_watermark` | `:pdf-stylesheet-override:` (footer overlay) |
|
|
196
198
|
| Requirement / conformance identifier namespace | `spec_uri_base` | derives `req_uri` / `conf_uri` |
|
|
197
|
-
| Submitter table, notice paragraph, normative refs | `document_metadata.{submitters, notice, normative_references}` | Metanorma attributes / sections |
|
|
199
|
+
| Submitter table, notice paragraph, normative refs, extra terms | `document_metadata.{submitters, notice, normative_references, terms}` | Metanorma attributes / sections |
|
|
198
200
|
|
|
199
|
-
Custom branding requires Pillow for the cover image: `pip install 'oapi-profile-builder[pdf]'`. Field-by-field detail is in the [`document_metadata`](#document_metadata) reference below; [`examples/nwp_radar.yaml`](examples/nwp_radar.yaml) is a complete DGIWG-branded example.
|
|
201
|
+
Custom branding requires Pillow for the cover image: `pip install 'oapi-profile-builder[pdf]'`. Field-by-field detail is in the [`document_metadata`](#document_metadata) reference below; [`examples/nwp_radar.yaml`](examples/nwp_radar.yaml) is a complete DGIWG-branded example. Omit all of these fields and the output keeps the standard OGC house style.
|
|
200
202
|
|
|
201
|
-
> The `suppress_flavor_logo`
|
|
203
|
+
> The `suppress_flavor_logo`, `suppress_design_elements`/`suppress_*`, and `page_watermark` overrides reach into the OGC Metanorma flavor's XSL templates (e.g. `insertLogoPreface`, `insertCrossingLines`, `insertSectionNumInCircle`, `insertFooter`) and are coupled to that flavor. Everything else uses documented Metanorma attributes. mn2pdf merges our same-named templates over the flavor's at equal precedence (via `merge_override.xsl`), which is what makes these work; a future metanorma-ogc release could require refreshing the template names. For long-term SDO adoption, a dedicated Metanorma "taste"/flavor is the most durable path.
|
|
202
204
|
|
|
203
205
|
---
|
|
204
206
|
|
|
@@ -654,6 +656,10 @@ document_metadata:
|
|
|
654
656
|
tagline: Delivering Military Advantage through multi-national geospatial interoperability
|
|
655
657
|
background: "#FFFFFF" # optional cover background (default white)
|
|
656
658
|
text_color: "#1F3864" # optional cover text colour (default black)
|
|
659
|
+
font_regular: "Source Sans Pro" # optional: .ttf/.otf path OR installed family name (fontconfig)
|
|
660
|
+
font_bold: "Source Sans Pro" # optional bold face; derived from font_regular when unset
|
|
661
|
+
font_italic: "Source Sans Pro" # optional italic face for the tagline
|
|
662
|
+
watermark: "DRAFT" # optional diagonal watermark stamped on the cover
|
|
657
663
|
colors:
|
|
658
664
|
text: "#000000" # body text
|
|
659
665
|
cover_text: "#1F3864" # cover text / section numbers / ToC
|
|
@@ -686,7 +692,41 @@ document_metadata:
|
|
|
686
692
|
feedback: "Comments on this document should be directed to DGIWG."
|
|
687
693
|
```
|
|
688
694
|
|
|
689
|
-
|
|
695
|
+
#### Removing the OGC page design (crossing lines, circled numbers, divider pages)
|
|
696
|
+
|
|
697
|
+
The OGC flavor also draws decorative design elements: the blue "crossing lines with a dot" motif, circled section-divider numbers, a short rule under section titles, and full-navy section-divider pages. One switch removes them all:
|
|
698
|
+
|
|
699
|
+
- **`suppress_design_elements: true`** — a consolidated switch that:
|
|
700
|
+
- removes the blue crossing-lines + dot motif (on divider/cover pages via a template override; on the Contents/preface pages, which draw the lines inline in the flavor's own colour, by whiting that colour out);
|
|
701
|
+
- renders section numbers as plain text (e.g. `1 Scope`, `i Abstract`) instead of coloured circles;
|
|
702
|
+
- removes the short rule beneath section titles;
|
|
703
|
+
- rebrands the full-navy section-divider pages to a clean white page with a DGIWG-navy (from `colors.title`) plain number and title.
|
|
704
|
+
|
|
705
|
+
The individual toggles `suppress_crossing_lines`, `plain_section_numbers`, and `suppress_title_underlines` are still available if you want finer control; `suppress_design_elements` simply turns all three on plus the divider rebrand.
|
|
706
|
+
|
|
707
|
+
- **`page_watermark`** — stamps a light diagonal watermark (e.g. `DRAFT`) on **every page**, rendered *over* the content including tables. This is distinct from `cover.watermark`, which marks only the generated cover image. The cover page is left unmarked.
|
|
708
|
+
|
|
709
|
+
```yaml
|
|
710
|
+
document_metadata:
|
|
711
|
+
suppress_design_elements: true # plain numbers, no crossing lines/rules, white divider pages
|
|
712
|
+
page_watermark: DRAFT # every-page diagonal watermark (over content)
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
With `cover` + `colors` + `copyright_holder` + `boilerplate` + `suppress_flavor_logo` + `suppress_design_elements` (and `spec_uri_base` for the requirement/conformance identifiers), the generated PDF carries no OGC branding — only legitimate citations to the OGC standards the profile conforms to. Omit these fields and you get the standard OGC house style. See [`examples/nwp_radar.yaml`](examples/nwp_radar.yaml).
|
|
716
|
+
|
|
717
|
+
> These design-element overrides (and the `page_watermark` footer overlay) reach into the OGC Metanorma flavor's XSL template names and are coupled to that flavor; a future metanorma-ogc release could require refreshing them.
|
|
718
|
+
|
|
719
|
+
#### Extending Terms and Definitions
|
|
720
|
+
|
|
721
|
+
Add profile-specific terms (e.g. EDR Part 3 or Pub/Sub terms) appended after the base terms in the Terms and Definitions section:
|
|
722
|
+
|
|
723
|
+
```yaml
|
|
724
|
+
document_metadata:
|
|
725
|
+
terms:
|
|
726
|
+
- term: service profile
|
|
727
|
+
definition: A named, referenceable set of constraints and additional requirements applied to an OGC API - EDR implementation.
|
|
728
|
+
source: "SOURCE: OGC API - EDR Part 3: Service Profiles (draft)"
|
|
729
|
+
```
|
|
690
730
|
|
|
691
731
|
---
|
|
692
732
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "oapi-profile-builder"
|
|
7
|
-
version = "3.
|
|
7
|
+
version = "3.3.0"
|
|
8
8
|
description = "Authoritative tooling for creating OGC API Service Profiles (EDR, Features)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { file = "LICENSE" }
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# =================================================================
|
|
2
|
+
#
|
|
3
|
+
# Authors: Shane Mill <shane.mill@noaa.gov>
|
|
4
|
+
#
|
|
5
|
+
# Copyright (c) 2026 Shane Mill
|
|
6
|
+
#
|
|
7
|
+
# Permission is hereby granted, free of charge, to any person
|
|
8
|
+
# obtaining a copy of this software and associated documentation
|
|
9
|
+
# files (the "Software"), to deal in the Software without
|
|
10
|
+
# restriction, including without limitation the rights to use,
|
|
11
|
+
# copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
12
|
+
# copies of the Software, and to permit persons to whom the
|
|
13
|
+
# Software is furnished to do so, subject to the following
|
|
14
|
+
# conditions:
|
|
15
|
+
#
|
|
16
|
+
# The above copyright notice and this permission notice shall be
|
|
17
|
+
# included in all copies or substantial portions of the Software.
|
|
18
|
+
#
|
|
19
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
20
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
21
|
+
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
22
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
23
|
+
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
24
|
+
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
25
|
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
26
|
+
# OTHER DEALINGS IN THE SOFTWARE.
|
|
27
|
+
#
|
|
28
|
+
# =================================================================
|
|
29
|
+
"""
|
|
30
|
+
PDF compilation: shell out to the Metanorma Docker image.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import shutil
|
|
36
|
+
import subprocess
|
|
37
|
+
import sys
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def compile_pdf(output_dir: Path, profile=None) -> bool:
|
|
42
|
+
"""
|
|
43
|
+
Run `docker run metanorma/metanorma compile` against document.adoc
|
|
44
|
+
in output_dir. Returns True on success.
|
|
45
|
+
|
|
46
|
+
If *profile* is provided, custom fonts declared in document_metadata will be
|
|
47
|
+
pre-installed into the fontist cache so Metanorma can embed them.
|
|
48
|
+
"""
|
|
49
|
+
if shutil.which("docker") is None:
|
|
50
|
+
print("docker not found. Install Docker to use --pdf.", file=sys.stderr)
|
|
51
|
+
sys.exit(1)
|
|
52
|
+
|
|
53
|
+
doc = output_dir / "document.adoc"
|
|
54
|
+
if not doc.exists():
|
|
55
|
+
print(f"document.adoc not found in {output_dir}", file=sys.stderr)
|
|
56
|
+
sys.exit(1)
|
|
57
|
+
|
|
58
|
+
fonts_dir = Path.home() / ".fontist" / "fonts"
|
|
59
|
+
fonts_dir.mkdir(parents=True, exist_ok=True)
|
|
60
|
+
|
|
61
|
+
# Pre-install any custom fonts requested by the profile so fontist inside
|
|
62
|
+
# the container recognizes them during PDF generation.
|
|
63
|
+
extra_fonts: list[str] = []
|
|
64
|
+
if profile is not None:
|
|
65
|
+
m = getattr(profile, "document_metadata", None)
|
|
66
|
+
if m:
|
|
67
|
+
for attr in ("body_font", "header_font", "monospace_font"):
|
|
68
|
+
val = getattr(m, attr, None)
|
|
69
|
+
if val:
|
|
70
|
+
extra_fonts.append(val)
|
|
71
|
+
|
|
72
|
+
# De-duplicate while preserving order.
|
|
73
|
+
if extra_fonts:
|
|
74
|
+
seen: set[str] = set()
|
|
75
|
+
extra_fonts = [f for f in extra_fonts if not (f in seen or seen.add(f))] # type: ignore[func-returns-value]
|
|
76
|
+
|
|
77
|
+
# Build the Docker command. When custom fonts are needed, we run fontist
|
|
78
|
+
# install + metanorma compile in the *same* container so the fontist
|
|
79
|
+
# database is available at compile time. We also monkey-patch the OGC
|
|
80
|
+
# processor's fonts_manifest so mn2pdf receives the correct font paths.
|
|
81
|
+
if extra_fonts:
|
|
82
|
+
install_cmds = " && ".join(f"fontist install '{f}'" for f in extra_fonts)
|
|
83
|
+
# Ruby snippet to inject extra fonts into the OGC processor's manifest
|
|
84
|
+
# so mn2pdf's font-manifest YAML includes them.
|
|
85
|
+
font_hash_entries = ", ".join(f'\\"{f}\\" => nil' for f in extra_fonts)
|
|
86
|
+
# Write the Ruby patch via heredoc to avoid shell quoting issues.
|
|
87
|
+
patch_rb_escaped = (
|
|
88
|
+
'require \\"metanorma-ogc\\"; '
|
|
89
|
+
"module Metanorma; module Ogc; class Processor; "
|
|
90
|
+
"alias_method :_orig_fonts_manifest, :fonts_manifest; "
|
|
91
|
+
f"def fonts_manifest; _orig_fonts_manifest.merge({{{font_hash_entries}}}); end; "
|
|
92
|
+
"end; end; end"
|
|
93
|
+
)
|
|
94
|
+
shell_script = (
|
|
95
|
+
f"{install_cmds} && "
|
|
96
|
+
f'printf "%s" "{patch_rb_escaped}" > /tmp/_font_patch.rb && '
|
|
97
|
+
"RUBYOPT='-r/tmp/_font_patch.rb' "
|
|
98
|
+
"metanorma compile --agree-to-terms -t ogc -x pdf document.adoc"
|
|
99
|
+
)
|
|
100
|
+
cmd = [
|
|
101
|
+
"docker", "run", "--rm",
|
|
102
|
+
"-v", f"{output_dir.resolve()}:/metanorma",
|
|
103
|
+
"-v", f"{fonts_dir}:/config/fonts",
|
|
104
|
+
"-w", "/metanorma",
|
|
105
|
+
"metanorma/metanorma",
|
|
106
|
+
"bash", "-c", shell_script,
|
|
107
|
+
]
|
|
108
|
+
print(f"Installing custom fonts ({', '.join(extra_fonts)}) and compiling PDF...")
|
|
109
|
+
else:
|
|
110
|
+
cmd = [
|
|
111
|
+
"docker", "run", "--rm",
|
|
112
|
+
"-v", f"{output_dir.resolve()}:/metanorma",
|
|
113
|
+
"-v", f"{fonts_dir}:/config/fonts",
|
|
114
|
+
"metanorma/metanorma",
|
|
115
|
+
"metanorma", "compile",
|
|
116
|
+
"--agree-to-terms",
|
|
117
|
+
"-t", "ogc",
|
|
118
|
+
"-x", "pdf",
|
|
119
|
+
"document.adoc",
|
|
120
|
+
]
|
|
121
|
+
print("Compiling PDF via Metanorma Docker...")
|
|
122
|
+
result = subprocess.run(cmd, check=False)
|
|
123
|
+
if result.returncode == 0:
|
|
124
|
+
pdf = output_dir / "document.pdf"
|
|
125
|
+
print(f"PDF written to {pdf}")
|
|
126
|
+
return result.returncode == 0
|
|
@@ -31,6 +31,8 @@ so no system font paths are required.
|
|
|
31
31
|
|
|
32
32
|
from __future__ import annotations
|
|
33
33
|
|
|
34
|
+
import shutil
|
|
35
|
+
import subprocess
|
|
34
36
|
from datetime import datetime
|
|
35
37
|
from pathlib import Path
|
|
36
38
|
|
|
@@ -40,25 +42,83 @@ _PAGE_H = 1754
|
|
|
40
42
|
|
|
41
43
|
_COVER_FILENAME = "cover.png"
|
|
42
44
|
|
|
45
|
+
# Default font family (bundled with Pillow, resolved by name).
|
|
46
|
+
_DEFAULT_REGULAR = "DejaVuSans.ttf"
|
|
47
|
+
_DEFAULT_BOLD = "DejaVuSans-Bold.ttf"
|
|
48
|
+
_DEFAULT_ITALIC = "DejaVuSans-Oblique.ttf"
|
|
43
49
|
|
|
44
|
-
def _load_font(bold: bool, size: int):
|
|
45
|
-
from PIL import ImageFont
|
|
46
50
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return ImageFont.truetype(name, size)
|
|
50
|
-
except Exception:
|
|
51
|
-
# Last-resort fallback; not scalable but keeps rendering from failing.
|
|
52
|
-
return ImageFont.load_default()
|
|
51
|
+
def _resolve_font_file(spec: str | None, *, bold: bool = False, italic: bool = False) -> str | None:
|
|
52
|
+
"""Resolve a user font spec to a usable font file path.
|
|
53
53
|
|
|
54
|
+
``spec`` may be either a path to a ``.ttf``/``.otf`` file or a font family
|
|
55
|
+
name (e.g. ``"Source Sans Pro"``). Family names are resolved via
|
|
56
|
+
``fc-match`` (fontconfig) when available. Returns ``None`` when the spec is
|
|
57
|
+
empty or cannot be resolved, so the caller can fall back to a default.
|
|
58
|
+
"""
|
|
59
|
+
if not spec:
|
|
60
|
+
return None
|
|
61
|
+
p = Path(spec).expanduser()
|
|
62
|
+
if p.is_file():
|
|
63
|
+
return str(p)
|
|
64
|
+
if shutil.which("fc-match"):
|
|
65
|
+
styles = []
|
|
66
|
+
if bold:
|
|
67
|
+
styles.append("Bold")
|
|
68
|
+
if italic:
|
|
69
|
+
styles.append("Italic")
|
|
70
|
+
query = spec if not styles else f"{spec}:style={' '.join(styles)}"
|
|
71
|
+
try:
|
|
72
|
+
out = subprocess.run(
|
|
73
|
+
["fc-match", "-f", "%{file}", query],
|
|
74
|
+
capture_output=True, text=True, timeout=5, check=False,
|
|
75
|
+
)
|
|
76
|
+
path = out.stdout.strip()
|
|
77
|
+
if path and Path(path).is_file():
|
|
78
|
+
return path
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class _FontSet:
|
|
85
|
+
"""Resolves cover fonts from optional profile config, with sane defaults.
|
|
86
|
+
|
|
87
|
+
Each of ``regular``/``bold``/``italic`` may be a font-file path or a family
|
|
88
|
+
name; unset styles are derived from ``regular`` (with the matching style) and
|
|
89
|
+
ultimately fall back to the bundled DejaVu family.
|
|
90
|
+
"""
|
|
54
91
|
|
|
55
|
-
def
|
|
56
|
-
|
|
92
|
+
def __init__(self, cover=None):
|
|
93
|
+
self.regular = getattr(cover, "font_regular", None) if cover else None
|
|
94
|
+
self.bold = getattr(cover, "font_bold", None) if cover else None
|
|
95
|
+
self.italic = getattr(cover, "font_italic", None) if cover else None
|
|
96
|
+
|
|
97
|
+
def _font(self, size: int, *, bold: bool = False, italic: bool = False):
|
|
98
|
+
from PIL import ImageFont
|
|
99
|
+
|
|
100
|
+
# Prefer an explicit per-style spec, else derive from the regular family.
|
|
101
|
+
explicit = self.bold if bold else (self.italic if italic else self.regular)
|
|
102
|
+
candidates = [
|
|
103
|
+
_resolve_font_file(explicit, bold=bold, italic=italic),
|
|
104
|
+
_resolve_font_file(self.regular, bold=bold, italic=italic),
|
|
105
|
+
]
|
|
106
|
+
default = _DEFAULT_BOLD if bold else (_DEFAULT_ITALIC if italic else _DEFAULT_REGULAR)
|
|
107
|
+
candidates.append(default)
|
|
108
|
+
for cand in candidates:
|
|
109
|
+
if not cand:
|
|
110
|
+
continue
|
|
111
|
+
try:
|
|
112
|
+
return ImageFont.truetype(cand, size)
|
|
113
|
+
except Exception:
|
|
114
|
+
continue
|
|
115
|
+
return ImageFont.load_default()
|
|
57
116
|
|
|
58
|
-
|
|
59
|
-
return
|
|
60
|
-
|
|
61
|
-
|
|
117
|
+
def regular_font(self, size: int, bold: bool = False):
|
|
118
|
+
return self._font(size, bold=bold)
|
|
119
|
+
|
|
120
|
+
def italic_font(self, size: int):
|
|
121
|
+
return self._font(size, italic=True)
|
|
62
122
|
|
|
63
123
|
|
|
64
124
|
def _format_date(iso_date: str | None) -> str | None:
|
|
@@ -100,6 +160,7 @@ def build_cover_image(profile, output_dir: Path) -> str | None:
|
|
|
100
160
|
|
|
101
161
|
bg = cover.background or "#FFFFFF"
|
|
102
162
|
fg = cover.text_color or "#000000"
|
|
163
|
+
fonts = _FontSet(cover)
|
|
103
164
|
|
|
104
165
|
page = Image.new("RGB", (_PAGE_W, _PAGE_H), bg)
|
|
105
166
|
draw = ImageDraw.Draw(page)
|
|
@@ -129,28 +190,32 @@ def build_cover_image(profile, output_dir: Path) -> str | None:
|
|
|
129
190
|
|
|
130
191
|
# --- Tagline (italic) ---
|
|
131
192
|
if cover.tagline:
|
|
132
|
-
y = centered(cover.tagline,
|
|
193
|
+
y = centered(cover.tagline, fonts.italic_font(30), y, fg) + 60
|
|
133
194
|
|
|
134
195
|
# --- Document number ---
|
|
135
196
|
if m and m.doc_number:
|
|
136
|
-
y = centered(m.doc_number,
|
|
197
|
+
y = centered(m.doc_number, fonts.regular_font(46, bold=True), y, fg) + 40
|
|
137
198
|
|
|
138
199
|
# --- Edition ---
|
|
139
200
|
edition = getattr(profile, "version", None)
|
|
140
201
|
if edition:
|
|
141
|
-
y = centered(f"Edition {edition}",
|
|
202
|
+
y = centered(f"Edition {edition}", fonts.regular_font(28), y, fg) + 24
|
|
142
203
|
|
|
143
204
|
# --- Date ---
|
|
144
205
|
date_str = _format_date(m.publication_date if m else None)
|
|
145
206
|
if date_str:
|
|
146
|
-
y = centered(f"Dated {date_str}",
|
|
207
|
+
y = centered(f"Dated {date_str}", fonts.regular_font(28), y, fg) + 80
|
|
147
208
|
|
|
148
209
|
# --- Title (large, bold, wrapped) ---
|
|
149
210
|
title = getattr(profile, "title", "") or ""
|
|
150
|
-
title_font =
|
|
211
|
+
title_font = fonts.regular_font(54, bold=True)
|
|
151
212
|
for line in _wrap(draw, title, title_font, _PAGE_W - 240):
|
|
152
213
|
y = centered(line, title_font, y, fg) + 16
|
|
153
214
|
|
|
215
|
+
# --- Draft watermark (diagonal, semi-transparent) ---
|
|
216
|
+
if getattr(cover, "watermark", None):
|
|
217
|
+
_stamp_watermark(page, cover.watermark, fonts)
|
|
218
|
+
|
|
154
219
|
target = (output_dir / _COVER_FILENAME).resolve()
|
|
155
220
|
if not str(target).startswith(str(output_dir.resolve())):
|
|
156
221
|
raise RuntimeError(f"Refusing to write cover outside output directory: {target}")
|
|
@@ -158,6 +223,28 @@ def build_cover_image(profile, output_dir: Path) -> str | None:
|
|
|
158
223
|
return _COVER_FILENAME
|
|
159
224
|
|
|
160
225
|
|
|
226
|
+
def _stamp_watermark(page, text: str, fonts: "_FontSet") -> None:
|
|
227
|
+
"""Overlay a large, diagonal, semi-transparent watermark on the cover.
|
|
228
|
+
|
|
229
|
+
Rendered on its own RGBA layer and rotated 45°, then alpha-composited onto
|
|
230
|
+
the (RGB) cover page. Gives a clear 'DRAFT' style indication on the cover;
|
|
231
|
+
an every-page watermark additionally requires the Metanorma layer.
|
|
232
|
+
"""
|
|
233
|
+
from PIL import Image, ImageDraw
|
|
234
|
+
|
|
235
|
+
layer = Image.new("RGBA", page.size, (0, 0, 0, 0))
|
|
236
|
+
ldraw = ImageDraw.Draw(layer)
|
|
237
|
+
font = fonts.regular_font(150, bold=True)
|
|
238
|
+
bbox = ldraw.textbbox((0, 0), text, font=font)
|
|
239
|
+
tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1]
|
|
240
|
+
ldraw.text(
|
|
241
|
+
((page.width - tw) / 2, (page.height - th) / 2),
|
|
242
|
+
text, font=font, fill=(200, 0, 0, 70),
|
|
243
|
+
)
|
|
244
|
+
layer = layer.rotate(45, resample=Image.BICUBIC, center=(page.width / 2, page.height / 2))
|
|
245
|
+
page.paste(Image.alpha_composite(page.convert("RGBA"), layer).convert("RGB"), (0, 0))
|
|
246
|
+
|
|
247
|
+
|
|
161
248
|
def _wrap(draw, text: str, font, max_width: int) -> list[str]:
|
|
162
249
|
"""Greedy word-wrap to fit max_width pixels."""
|
|
163
250
|
words = text.split()
|