@cteno/sdk-linux-x64 0.0.0-stage → 2.10.14

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.
Files changed (116) hide show
  1. package/LICENSE.daemon +31 -0
  2. package/README.md +4 -2
  3. package/agents/flow-analyzer/AGENT.md +71 -0
  4. package/bin/cteno-host-cli +4 -0
  5. package/bin/cteno-wiki +0 -0
  6. package/bin/rg +0 -0
  7. package/bin/secret-exec +0 -0
  8. package/manifest.json +12 -0
  9. package/package.json +33 -4
  10. package/plugins/agent-secrets/.cteno-plugin/plugin.json +133 -0
  11. package/plugins/agent-secrets/bin/linux-x64/fnox +4 -0
  12. package/plugins/agent-secrets/bin/linux-x64/secret-exec +0 -0
  13. package/plugins/agent-secrets/skills/secret/SKILL.md +201 -0
  14. package/plugins/cteno.browser/.cteno-plugin/plugin.json +65 -0
  15. package/plugins/cteno.browser/skills/browser/SKILL.md +28 -0
  16. package/plugins/cteno.inbox/.cteno-plugin/plugin.json +132 -0
  17. package/plugins/cteno.inbox/bin/all/cteno-connector-credential +31 -0
  18. package/plugins/cteno.inbox/bin/all/cteno-telegram-connector +33 -0
  19. package/plugins/cteno.inbox/bin/linux-x64/cteno-inbox +0 -0
  20. package/plugins/cteno.inbox/skills/cteno-inbox/SKILL.md +374 -0
  21. package/plugins/cteno.inbox/tools/inbox_focus/TOOL.md +44 -0
  22. package/plugins/cteno.memory/.cteno-plugin/plugin.json +176 -0
  23. package/plugins/cteno.memory/agents/llm-wiki/AGENT.md +96 -0
  24. package/plugins/cteno.memory/bin/linux-x64/cteno-wiki +0 -0
  25. package/plugins/cteno.memory/skills/codebase-wiki/SKILL.md +52 -0
  26. package/plugins/cteno.memory/skills/cteno.memory.native/SKILL.md +110 -0
  27. package/plugins/cteno.memory/tools/memory/TOOL.md +492 -0
  28. package/plugins/cteno.memory/ui/index.html +822 -0
  29. package/plugins/cteno.models/.cteno-plugin/plugin.json +191 -0
  30. package/plugins/cteno.websearch/.cteno-plugin/plugin.json +45 -0
  31. package/plugins/jev/.cteno-plugin/plugin.json +309 -0
  32. package/plugins/jev/skills/jev-browser/SKILL.md +66 -0
  33. package/plugins/jev/skills/jev-browser/jev_browser.py +335 -0
  34. package/skills/agent-create/SKILL.md +86 -0
  35. package/skills/cteno-host-cli/SKILL.md +256 -0
  36. package/skills/docx/LICENSE +23 -0
  37. package/skills/docx/NOTICE.md +8 -0
  38. package/skills/docx/SKILL.md +69 -0
  39. package/skills/docx/references/cjk_typography.md +357 -0
  40. package/skills/docx/references/design_principles.md +819 -0
  41. package/skills/docx/references/revisions-and-comments.md +88 -0
  42. package/skills/docx/references/track_changes_guide.md +200 -0
  43. package/skills/docx/references/typography_guide.md +294 -0
  44. package/skills/docx/scripts/cjk_fonts.py +244 -0
  45. package/skills/docx/scripts/docx_comments.py +289 -0
  46. package/skills/docx/scripts/docx_common.py +92 -0
  47. package/skills/docx/scripts/docx_create.py +259 -0
  48. package/skills/docx/scripts/docx_edit.py +250 -0
  49. package/skills/docx/scripts/docx_read.py +149 -0
  50. package/skills/docx/scripts/docx_revisions.py +146 -0
  51. package/skills/docx/scripts/docx_template.py +70 -0
  52. package/skills/docx/scripts/docx_validate.py +156 -0
  53. package/skills/docx/scripts/render.py +250 -0
  54. package/skills/docx/tests/test_docx_skill.py +525 -0
  55. package/skills/home-devices/SKILL.md +121 -0
  56. package/skills/mcp/SKILL.md +72 -0
  57. package/skills/mcp/scripts/mcp_setup.py +439 -0
  58. package/skills/pdf/LICENSE +23 -0
  59. package/skills/pdf/NOTICE.md +8 -0
  60. package/skills/pdf/SKILL.md +63 -0
  61. package/skills/pdf/references/design-tokens.md +381 -0
  62. package/skills/pdf/references/forms.md +99 -0
  63. package/skills/pdf/references/nano-pdf-editing.md +40 -0
  64. package/skills/pdf/references/ocr-extraction.md +165 -0
  65. package/skills/pdf/scripts/_raster.py +76 -0
  66. package/skills/pdf/scripts/extract_marker.py +88 -0
  67. package/skills/pdf/scripts/extract_pymupdf.py +101 -0
  68. package/skills/pdf/scripts/pdf_create.py +191 -0
  69. package/skills/pdf/scripts/pdf_fill_form.py +97 -0
  70. package/skills/pdf/scripts/pdf_form_layout.py +168 -0
  71. package/skills/pdf/scripts/pdf_make_form.py +145 -0
  72. package/skills/pdf/scripts/pdf_merge.py +50 -0
  73. package/skills/pdf/scripts/pdf_meta.py +115 -0
  74. package/skills/pdf/scripts/pdf_page_image.py +99 -0
  75. package/skills/pdf/scripts/pdf_read.py +153 -0
  76. package/skills/pdf/scripts/pdf_secure.py +71 -0
  77. package/skills/pdf/scripts/pdf_split.py +84 -0
  78. package/skills/pdf/scripts/pdf_stamp.py +143 -0
  79. package/skills/pdf/scripts/pdf_watermark.py +51 -0
  80. package/skills/pdf/scripts/render.py +250 -0
  81. package/skills/pdf/tests/test_pdf_skill.py +415 -0
  82. package/skills/pptx/LICENSE +23 -0
  83. package/skills/pptx/NOTICE.md +8 -0
  84. package/skills/pptx/SKILL.md +62 -0
  85. package/skills/pptx/references/design-system.md +392 -0
  86. package/skills/pptx/references/pptxgenjs.md +420 -0
  87. package/skills/pptx/references/slide-types.md +413 -0
  88. package/skills/pptx/scripts/cjk_fonts.py +244 -0
  89. package/skills/pptx/scripts/pptx_create.py +240 -0
  90. package/skills/pptx/scripts/pptx_edit.py +436 -0
  91. package/skills/pptx/scripts/pptx_from_template.py +88 -0
  92. package/skills/pptx/scripts/pptx_read.py +131 -0
  93. package/skills/pptx/scripts/pptx_render.py +96 -0
  94. package/skills/pptx/scripts/render.py +250 -0
  95. package/skills/pptx/tests/test_powerpoint_skill.py +477 -0
  96. package/skills/skill-create/SKILL.md +114 -0
  97. package/skills/video-generation/SKILL.md +27 -0
  98. package/skills/xlsx/LICENSE +23 -0
  99. package/skills/xlsx/NOTICE.md +8 -0
  100. package/skills/xlsx/SKILL.md +63 -0
  101. package/skills/xlsx/references/format.md +71 -0
  102. package/skills/xlsx/references/restructuring.md +71 -0
  103. package/skills/xlsx/scripts/csv_to_xlsx.py +110 -0
  104. package/skills/xlsx/scripts/formula_check.py +428 -0
  105. package/skills/xlsx/scripts/render.py +250 -0
  106. package/skills/xlsx/scripts/xlsx_create.py +265 -0
  107. package/skills/xlsx/scripts/xlsx_edit.py +269 -0
  108. package/skills/xlsx/scripts/xlsx_read.py +166 -0
  109. package/skills/xlsx/scripts/xlsx_recalc.py +119 -0
  110. package/skills/xlsx/scripts/xlsx_restructure.py +343 -0
  111. package/skills/xlsx/scripts/xlsx_to_csv.py +75 -0
  112. package/skills/xlsx/tests/test_xlsx_skill.py +544 -0
  113. package/tools/create_goal/TOOL.md +36 -0
  114. package/tools/image_generation/TOOL.md +81 -0
  115. package/tools/skill/TOOL.md +117 -0
  116. package/tools/update_profile_notes/TOOL.md +39 -0
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env python3
2
+ """Read a PDF: per-page text, tables, metadata, or form fields. JSON to stdout."""
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import csv
7
+ import json
8
+ import os
9
+ import sys
10
+
11
+
12
+ def _reconfigure_stdio() -> None:
13
+ for stream in (sys.stdout, sys.stderr):
14
+ try:
15
+ stream.reconfigure(encoding="utf-8")
16
+ except Exception:
17
+ pass
18
+
19
+
20
+ def _need(module: str, package: str):
21
+ try:
22
+ return __import__(module)
23
+ except ImportError:
24
+ print(f"Missing dependency: install with 'python3 -m pip install {package}'", file=sys.stderr)
25
+ raise SystemExit(2)
26
+
27
+
28
+ def read_text(path: str, password: str | None) -> dict:
29
+ pdfplumber = _need("pdfplumber", "pdfplumber")
30
+ pages = []
31
+ with pdfplumber.open(path, password=password) as pdf:
32
+ for page in pdf.pages:
33
+ pages.append(page.extract_text() or "")
34
+ return {"page_count": len(pages), "pages": pages}
35
+
36
+
37
+ def read_tables(path: str, password: str | None, csv_dir: str | None) -> dict:
38
+ pdfplumber = _need("pdfplumber", "pdfplumber")
39
+ result = []
40
+ written = []
41
+ with pdfplumber.open(path, password=password) as pdf:
42
+ for pageno, page in enumerate(pdf.pages, start=1):
43
+ for tidx, table in enumerate(page.extract_tables()):
44
+ result.append({"page": pageno, "index": tidx, "rows": table})
45
+ if csv_dir:
46
+ os.makedirs(csv_dir, exist_ok=True)
47
+ csv_path = os.path.join(csv_dir, f"page{pageno}_table{tidx}.csv")
48
+ with open(csv_path, "w", encoding="utf-8", newline="") as fh:
49
+ csv.writer(fh).writerows([[c if c is not None else "" for c in row] for row in table])
50
+ written.append(csv_path)
51
+ out = {"table_count": len(result), "tables": result}
52
+ if csv_dir:
53
+ out["csv_files"] = written
54
+ return out
55
+
56
+
57
+ def read_meta(path: str, password: str | None) -> dict:
58
+ pypdf = _need("pypdf", "pypdf")
59
+ reader = pypdf.PdfReader(path)
60
+ encrypted = reader.is_encrypted
61
+ if encrypted:
62
+ if password is None or not reader.decrypt(password):
63
+ return {"encrypted": True, "note": "Provide --password to read metadata of an encrypted file."}
64
+ meta = {}
65
+ if reader.metadata:
66
+ for key, value in reader.metadata.items():
67
+ meta[str(key).lstrip("/")] = str(value)
68
+ pages = []
69
+ for idx, page in enumerate(reader.pages, start=1):
70
+ box = page.mediabox
71
+ pages.append({
72
+ "page": idx,
73
+ "width": float(box.width),
74
+ "height": float(box.height),
75
+ "rotation": int(page.get("/Rotate", 0)),
76
+ })
77
+ # scanned-page heuristic: no extractable text but page has images
78
+ likely_scanned = []
79
+ try:
80
+ pdfplumber = _need("pdfplumber", "pdfplumber")
81
+ with pdfplumber.open(path, password=password) as pdf:
82
+ for pageno, page in enumerate(pdf.pages, start=1):
83
+ text = (page.extract_text() or "").strip()
84
+ if not text and page.images:
85
+ likely_scanned.append(pageno)
86
+ except SystemExit:
87
+ raise
88
+ except Exception as exc: # pragma: no cover - heuristic only
89
+ print(f"Warning: scanned-page check failed: {exc}", file=sys.stderr)
90
+ out = {
91
+ "encrypted": encrypted,
92
+ "page_count": len(reader.pages),
93
+ "metadata": meta,
94
+ "pages": pages,
95
+ "likely_scanned_pages": likely_scanned,
96
+ }
97
+ if likely_scanned:
98
+ out["note"] = ("Image-only pages detected: no text layer to extract. "
99
+ "Use the references/ocr-extraction.md in this skill for OCR.")
100
+ return out
101
+
102
+
103
+ FIELD_TYPES = {"/Tx": "text", "/Btn": "button", "/Ch": "choice", "/Sig": "signature"}
104
+
105
+
106
+ def read_fields(path: str, password: str | None) -> dict:
107
+ pypdf = _need("pypdf", "pypdf")
108
+ reader = pypdf.PdfReader(path)
109
+ if reader.is_encrypted:
110
+ if password is None or not reader.decrypt(password):
111
+ print("File is encrypted; pass --password.", file=sys.stderr)
112
+ raise SystemExit(3)
113
+ fields = reader.get_fields() or {}
114
+ out = {}
115
+ for name, field in fields.items():
116
+ ftype = FIELD_TYPES.get(str(field.get("/FT")), str(field.get("/FT")))
117
+ value = field.get("/V")
118
+ states = field.get("/_States_")
119
+ entry = {"type": ftype, "value": None if value is None else str(value)}
120
+ if states:
121
+ entry["options"] = [str(s) for s in states]
122
+ out[name] = entry
123
+ return {"field_count": len(out), "fields": out}
124
+
125
+
126
+ def main() -> int:
127
+ _reconfigure_stdio()
128
+ parser = argparse.ArgumentParser(description="Extract text, tables, metadata, or form fields from a PDF.")
129
+ parser.add_argument("pdf", help="Input PDF path")
130
+ mode = parser.add_mutually_exclusive_group(required=True)
131
+ mode.add_argument("--text", action="store_true", help="Per-page text as JSON")
132
+ mode.add_argument("--tables", action="store_true", help="Tables as JSON (optionally CSV via --csv-dir)")
133
+ mode.add_argument("--meta", action="store_true", help="Metadata, page sizes, encrypted/scanned flags")
134
+ mode.add_argument("--fields", action="store_true", help="AcroForm fields with types and values")
135
+ parser.add_argument("--csv-dir", help="Also write each table as a CSV file into this directory")
136
+ parser.add_argument("--password", help="Password for encrypted PDFs")
137
+ args = parser.parse_args()
138
+
139
+ if args.text:
140
+ result = read_text(args.pdf, args.password)
141
+ elif args.tables:
142
+ result = read_tables(args.pdf, args.password, args.csv_dir)
143
+ elif args.meta:
144
+ result = read_meta(args.pdf, args.password)
145
+ else:
146
+ result = read_fields(args.pdf, args.password)
147
+ json.dump(result, sys.stdout, ensure_ascii=False, indent=2)
148
+ print()
149
+ return 0
150
+
151
+
152
+ if __name__ == "__main__":
153
+ sys.exit(main())
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env python3
2
+ """Encrypt or decrypt a PDF with passwords (AES-256 via pypdf).
3
+
4
+ Note: permission flags set at encryption time are advisory — viewers may honor
5
+ them, but any PDF library can strip them. Only the user password gates content.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import json
11
+ import sys
12
+
13
+
14
+ def main() -> int:
15
+ for stream in (sys.stdout, sys.stderr):
16
+ try:
17
+ stream.reconfigure(encoding="utf-8")
18
+ except Exception:
19
+ pass
20
+ parser = argparse.ArgumentParser(description="Encrypt/decrypt PDFs (pypdf, AES-256).")
21
+ parser.add_argument("pdf", help="Input PDF path")
22
+ parser.add_argument("-o", "--output", required=True, help="Output PDF path")
23
+ mode = parser.add_mutually_exclusive_group(required=True)
24
+ mode.add_argument("--encrypt", action="store_true", help="Encrypt the PDF")
25
+ mode.add_argument("--decrypt", action="store_true", help="Remove encryption (password required)")
26
+ parser.add_argument("--user-password", help="User (open) password for --encrypt")
27
+ parser.add_argument("--owner-password", help="Owner password for --encrypt (defaults to user password)")
28
+ parser.add_argument("--password", help="Known password for --decrypt")
29
+ args = parser.parse_args()
30
+
31
+ try:
32
+ from pypdf import PdfReader, PdfWriter
33
+ except ImportError:
34
+ print("Missing dependency: install with 'python3 -m pip install pypdf'", file=sys.stderr)
35
+ return 2
36
+
37
+ reader = PdfReader(args.pdf)
38
+ if args.encrypt:
39
+ if not args.user_password:
40
+ print("Error: --encrypt requires --user-password", file=sys.stderr)
41
+ return 2
42
+ if reader.is_encrypted:
43
+ print("Error: input already encrypted; decrypt first", file=sys.stderr)
44
+ return 3
45
+ writer = PdfWriter()
46
+ writer.append(reader)
47
+ writer.encrypt(
48
+ user_password=args.user_password,
49
+ owner_password=args.owner_password or args.user_password,
50
+ algorithm="AES-256",
51
+ )
52
+ action = "encrypted"
53
+ else:
54
+ if not reader.is_encrypted:
55
+ print("Error: input is not encrypted", file=sys.stderr)
56
+ return 3
57
+ if args.password is None or not reader.decrypt(args.password):
58
+ print("Error: wrong or missing --password", file=sys.stderr)
59
+ return 4
60
+ writer = PdfWriter()
61
+ writer.append(reader)
62
+ action = "decrypted"
63
+
64
+ with open(args.output, "wb") as fh:
65
+ writer.write(fh)
66
+ print(json.dumps({"output": args.output, "action": action, "page_count": len(reader.pages)}))
67
+ return 0
68
+
69
+
70
+ if __name__ == "__main__":
71
+ sys.exit(main())
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env python3
2
+ """Extract page ranges from a PDF, optionally rotating and/or compressing pages."""
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+
9
+
10
+ def parse_pages(spec: str, page_count: int) -> list[int]:
11
+ """Parse a 1-based page spec like '1-3,5,9-' into 0-based indices."""
12
+ indices: list[int] = []
13
+ for part in spec.split(","):
14
+ part = part.strip()
15
+ if not part:
16
+ continue
17
+ if "-" in part:
18
+ start_s, _, end_s = part.partition("-")
19
+ start = int(start_s) if start_s else 1
20
+ end = int(end_s) if end_s else page_count
21
+ else:
22
+ start = end = int(part)
23
+ if start < 1 or end > page_count or start > end:
24
+ raise ValueError(f"Page range {part!r} out of bounds (1-{page_count})")
25
+ indices.extend(range(start - 1, end))
26
+ return indices
27
+
28
+
29
+ def main() -> int:
30
+ for stream in (sys.stdout, sys.stderr):
31
+ try:
32
+ stream.reconfigure(encoding="utf-8")
33
+ except Exception:
34
+ pass
35
+ parser = argparse.ArgumentParser(
36
+ description="Split/extract pages from a PDF (pypdf). Pages are 1-based: '1-3,5,9-'.")
37
+ parser.add_argument("pdf", help="Input PDF path")
38
+ parser.add_argument("--pages", required=True, help="1-based page spec, e.g. '1-3,5,9-'")
39
+ parser.add_argument("-o", "--output", required=True, help="Output PDF path")
40
+ parser.add_argument("--rotate", type=int, default=0,
41
+ help="Rotate extracted pages clockwise (multiple of 90)")
42
+ parser.add_argument("--compress", action="store_true",
43
+ help="Deflate content streams (modest savings; does not recompress images)")
44
+ parser.add_argument("--password", help="Password if the input is encrypted")
45
+ args = parser.parse_args()
46
+
47
+ if args.rotate % 90 != 0:
48
+ print("Error: --rotate must be a multiple of 90", file=sys.stderr)
49
+ return 2
50
+
51
+ try:
52
+ from pypdf import PdfReader, PdfWriter
53
+ except ImportError:
54
+ print("Missing dependency: install with 'python3 -m pip install pypdf'", file=sys.stderr)
55
+ return 2
56
+
57
+ reader = PdfReader(args.pdf)
58
+ if reader.is_encrypted:
59
+ if args.password is None or not reader.decrypt(args.password):
60
+ print("Error: input is encrypted; pass --password", file=sys.stderr)
61
+ return 3
62
+ try:
63
+ indices = parse_pages(args.pages, len(reader.pages))
64
+ except ValueError as exc:
65
+ print(f"Error: {exc}", file=sys.stderr)
66
+ return 2
67
+
68
+ writer = PdfWriter()
69
+ for idx in indices:
70
+ page = reader.pages[idx]
71
+ if args.rotate:
72
+ page.rotate(args.rotate)
73
+ writer.add_page(page)
74
+ if args.compress:
75
+ for page in writer.pages:
76
+ page.compress_content_streams()
77
+ with open(args.output, "wb") as fh:
78
+ writer.write(fh)
79
+ print(json.dumps({"output": args.output, "page_count": len(indices), "rotated": args.rotate}))
80
+ return 0
81
+
82
+
83
+ if __name__ == "__main__":
84
+ sys.exit(main())
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env python3
2
+ """Stamp text or an image at coordinates onto selected PDF pages.
3
+
4
+ Builds an in-memory single-page overlay with reportlab, then merges it
5
+ onto each selected page with pypdf. Coordinates are PDF points, origin
6
+ bottom-left. Covers 'sign here' arrows, diagonal DRAFT banners, and
7
+ page-corner labels.
8
+
9
+ Examples:
10
+ pdf_stamp.py in.pdf -o out.pdf --text "DRAFT" --x 200 --y 400 \
11
+ --font-size 60 --rotation 45 --opacity 0.3 --color "#cc0000"
12
+ pdf_stamp.py in.pdf -o out.pdf --image sig.png --x 400 --y 60 \
13
+ --width 120 --pages 3
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import io
19
+ import json
20
+ import sys
21
+
22
+
23
+ def parse_pages(spec: str, page_count: int) -> list[int]:
24
+ pages: set[int] = set()
25
+ for part in spec.split(","):
26
+ part = part.strip()
27
+ if not part:
28
+ continue
29
+ if "-" in part:
30
+ start_s, _, end_s = part.partition("-")
31
+ start = int(start_s) if start_s else 1
32
+ end = int(end_s) if end_s else page_count
33
+ pages.update(range(start, end + 1))
34
+ else:
35
+ pages.add(int(part))
36
+ bad = [p for p in pages if not 1 <= p <= page_count]
37
+ if bad:
38
+ raise ValueError(f"pages out of range 1-{page_count}: {sorted(bad)}")
39
+ return sorted(pages)
40
+
41
+
42
+ def build_overlay(args, page_width: float, page_height: float) -> bytes:
43
+ from reportlab.lib.colors import HexColor
44
+ from reportlab.pdfgen import canvas
45
+ buf = io.BytesIO()
46
+ c = canvas.Canvas(buf, pagesize=(page_width, page_height))
47
+ c.saveState()
48
+ try:
49
+ c.setFillAlpha(float(args.opacity))
50
+ c.setStrokeAlpha(float(args.opacity))
51
+ except Exception:
52
+ pass # very old reportlab: no alpha support
53
+ c.translate(float(args.x), float(args.y))
54
+ if args.rotation:
55
+ c.rotate(float(args.rotation))
56
+ if args.text:
57
+ c.setFont(args.font, float(args.font_size))
58
+ c.setFillColor(HexColor(args.color))
59
+ c.drawString(0, 0, args.text)
60
+ else:
61
+ kwargs = {}
62
+ if args.width:
63
+ kwargs["width"] = float(args.width)
64
+ if args.height:
65
+ kwargs["height"] = float(args.height)
66
+ if "width" in kwargs and "height" not in kwargs:
67
+ from PIL import Image as PILImage
68
+ with PILImage.open(args.image) as im:
69
+ kwargs["height"] = kwargs["width"] * im.height / im.width
70
+ c.drawImage(args.image, 0, 0, mask="auto", **kwargs)
71
+ c.restoreState()
72
+ c.save()
73
+ return buf.getvalue()
74
+
75
+
76
+ def main() -> int:
77
+ for stream in (sys.stdout, sys.stderr):
78
+ try:
79
+ stream.reconfigure(encoding="utf-8")
80
+ except Exception:
81
+ pass
82
+ parser = argparse.ArgumentParser(description="Stamp text or an image onto PDF pages.")
83
+ parser.add_argument("pdf", help="Input PDF path")
84
+ parser.add_argument("-o", "--output", required=True, help="Output PDF path")
85
+ what = parser.add_mutually_exclusive_group(required=True)
86
+ what.add_argument("--text", help="Text to stamp")
87
+ what.add_argument("--image", help="Image file to stamp (PNG/JPEG)")
88
+ parser.add_argument("--x", type=float, required=True, help="X in points (origin bottom-left)")
89
+ parser.add_argument("--y", type=float, required=True, help="Y in points")
90
+ parser.add_argument("--pages", default="1-", help="1-based ranges, e.g. '1-3,5' (default: all)")
91
+ parser.add_argument("--font", default="Helvetica", help="Font name (default Helvetica)")
92
+ parser.add_argument("--font-size", type=float, default=24, help="Font size in points")
93
+ parser.add_argument("--color", default="#000000", help="Text color as #RRGGBB")
94
+ parser.add_argument("--rotation", type=float, default=0, help="Degrees counterclockwise")
95
+ parser.add_argument("--opacity", type=float, default=1.0, help="0.0-1.0 (default 1.0)")
96
+ parser.add_argument("--width", type=float, help="Image width in points")
97
+ parser.add_argument("--height", type=float, help="Image height in points")
98
+ parser.add_argument("--under", action="store_true",
99
+ help="Place the stamp under existing content instead of over it")
100
+ parser.add_argument("--password", help="Password if the input is encrypted")
101
+ args = parser.parse_args()
102
+
103
+ try:
104
+ from pypdf import PdfReader, PdfWriter
105
+ except ImportError:
106
+ print("Missing dependency: install with 'python3 -m pip install pypdf reportlab'",
107
+ file=sys.stderr)
108
+ return 2
109
+
110
+ reader = PdfReader(args.pdf)
111
+ if reader.is_encrypted:
112
+ if args.password is None or not reader.decrypt(args.password):
113
+ print("Error: input is encrypted; pass --password", file=sys.stderr)
114
+ return 3
115
+ try:
116
+ pages = set(parse_pages(args.pages, len(reader.pages)))
117
+ except ValueError as exc:
118
+ print(f"Error: {exc}", file=sys.stderr)
119
+ return 4
120
+
121
+ writer = PdfWriter()
122
+ overlay_cache: dict[tuple[float, float], object] = {}
123
+ for idx, page in enumerate(reader.pages, start=1):
124
+ if idx in pages:
125
+ size = (float(page.mediabox.width), float(page.mediabox.height))
126
+ if size not in overlay_cache:
127
+ overlay_pdf = PdfReader(io.BytesIO(build_overlay(args, *size)))
128
+ overlay_cache[size] = overlay_pdf.pages[0]
129
+ stamp = overlay_cache[size]
130
+ if args.under:
131
+ page.merge_page(stamp, over=False)
132
+ else:
133
+ page.merge_page(stamp)
134
+ writer.add_page(page)
135
+ with open(args.output, "wb") as fh:
136
+ writer.write(fh)
137
+ print(json.dumps({"output": args.output, "stamped_pages": sorted(pages),
138
+ "kind": "text" if args.text else "image"}, ensure_ascii=False))
139
+ return 0
140
+
141
+
142
+ if __name__ == "__main__":
143
+ sys.exit(main())
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env python3
2
+ """Stamp/watermark every page of a PDF with page 1 of another PDF."""
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+
9
+
10
+ def main() -> int:
11
+ for stream in (sys.stdout, sys.stderr):
12
+ try:
13
+ stream.reconfigure(encoding="utf-8")
14
+ except Exception:
15
+ pass
16
+ parser = argparse.ArgumentParser(
17
+ description="Overlay (stamp) or underlay (watermark) a one-page PDF onto every page.")
18
+ parser.add_argument("pdf", help="Input PDF path")
19
+ parser.add_argument("--stamp", required=True, help="One-page PDF to apply (page 1 is used)")
20
+ parser.add_argument("-o", "--output", required=True, help="Output PDF path")
21
+ parser.add_argument("--under", action="store_true",
22
+ help="Place stamp under the page content (background watermark)")
23
+ parser.add_argument("--password", help="Password if the input is encrypted")
24
+ args = parser.parse_args()
25
+
26
+ try:
27
+ from pypdf import PdfReader, PdfWriter
28
+ except ImportError:
29
+ print("Missing dependency: install with 'python3 -m pip install pypdf'", file=sys.stderr)
30
+ return 2
31
+
32
+ reader = PdfReader(args.pdf)
33
+ if reader.is_encrypted:
34
+ if args.password is None or not reader.decrypt(args.password):
35
+ print("Error: input is encrypted; pass --password", file=sys.stderr)
36
+ return 3
37
+ stamp_page = PdfReader(args.stamp).pages[0]
38
+
39
+ writer = PdfWriter()
40
+ writer.append(reader)
41
+ for page in writer.pages:
42
+ page.merge_page(stamp_page, over=not args.under)
43
+ with open(args.output, "wb") as fh:
44
+ writer.write(fh)
45
+ print(json.dumps({"output": args.output, "page_count": len(writer.pages),
46
+ "mode": "under" if args.under else "over"}))
47
+ return 0
48
+
49
+
50
+ if __name__ == "__main__":
51
+ sys.exit(main())