andruhovski

How to Add Header or Footer

Oct 29th, 2026
14,434
0
Never
3
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 11.39 KB | Source Code | 0 0
  1. """Add headers and footers with Aspose.PDF; always start from a fresh source."""
  2. import argparse
  3. from collections import Counter
  4. from dataclasses import dataclass
  5. from importlib.metadata import version
  6. import os
  7. from pathlib import Path
  8. import re
  9. import sys
  10. from tempfile import TemporaryDirectory
  11.  
  12. import aspose.pdf as pdf
  13.  
  14. BASE = Path(__file__).resolve().parent
  15. HEADER = "Finance Department | Monthly Report"
  16. MARGIN = 24
  17. BAND = 60
  18. FONT_SIZE = 10
  19. PAGE_LABEL = re.compile(r"\bPage\s+\d+\s+of\s+\d+\b")
  20.  
  21.  
  22. @dataclass(frozen=True)
  23. class PageSnapshot:
  24.     words: Counter
  25.     crop_box: tuple
  26.     media_box: tuple
  27.     rotation: int
  28.  
  29.  
  30. def labels(count, skip_cover=True, start=1, only_page=None):
  31.     """Map physical (one-based) pages to explicit displayed labels."""
  32.     if type(start) is not int or start < 1:
  33.         raise ValueError("start must be positive")
  34.     eligible = list(range(2 if skip_cover else 1, count + 1))
  35.     if not eligible:
  36.         raise ValueError("No eligible pages")
  37.     if only_page is not None and (type(only_page) is not int or only_page not in eligible):
  38.         raise ValueError("Selected page must be eligible and within the document")
  39.     last = start + len(eligible) - 1
  40.     return {p: f"Page {start + i} of {last}"
  41.             for i, p in enumerate(eligible) if only_page is None or p == only_page}
  42.  
  43.  
  44. def rectangle_values(rectangle):
  45.     """Copy coordinates so snapshots do not retain mutable .NET rectangles."""
  46.     return (rectangle.llx, rectangle.lly, rectangle.urx, rectangle.ury)
  47.  
  48.  
  49. def page_size(page):
  50.     rectangle = page.get_page_rect(True)
  51.     return rectangle.width, rectangle.height
  52.  
  53.  
  54. def extract_text(page):
  55.     """Read text in unrotated coordinates; restore page rotation even on failure."""
  56.     rotation = page.rotate
  57.     try:
  58.         page.rotate = pdf.Rotation.NONE
  59.         absorber = pdf.text.TextAbsorber()
  60.         page.accept(absorber)
  61.         return absorber.text
  62.     finally:
  63.         page.rotate = rotation
  64.  
  65.  
  66. def text_rectangles(page, phrase=None):
  67.     """Return copies of text rectangles in the original PDF coordinate system."""
  68.     rotation = page.rotate
  69.     try:
  70.         page.rotate = pdf.Rotation.NONE
  71.         absorber = (pdf.text.TextFragmentAbsorber() if phrase is None
  72.                     else pdf.text.TextFragmentAbsorber(phrase))
  73.         page.accept(absorber)
  74.         return [rectangle_values(fragment.rectangle) for fragment in absorber.text_fragments]
  75.     finally:
  76.         page.rotate = rotation
  77.  
  78.  
  79. def visible_rectangle(page, rectangle):
  80.     """Map unrotated PDF coordinates to the viewer's bottom-left coordinate system."""
  81.     crop = page.rect
  82.     rotation = pdf.Page.rotation_to_int(page.rotate)
  83.     points = []
  84.     for x, y in ((rectangle[0], rectangle[1]), (rectangle[2], rectangle[1]),
  85.                  (rectangle[0], rectangle[3]), (rectangle[2], rectangle[3])):
  86.         x, y = x - crop.llx, y - crop.lly
  87.         if rotation == 90:
  88.             x, y = y, crop.width - x
  89.         elif rotation == 180:
  90.             x, y = crop.width - x, crop.height - y
  91.         elif rotation == 270:
  92.             x, y = crop.height - y, x
  93.         points.append((x, y))
  94.     return (min(x for x, y in points), min(y for x, y in points),
  95.             max(x for x, y in points), max(y for x, y in points))
  96.  
  97.  
  98. def inspect_document(document, header, numbering, font):
  99.     """Validate static inputs without rebuilding or normalizing their pages."""
  100.     if document.is_encrypted:
  101.         raise ValueError("Encrypted inputs require a separate workflow")
  102.     if len(document.form.fields):
  103.         raise ValueError("Forms and signatures are outside this static-page example")
  104.     if len(document.embedded_files):
  105.         raise ValueError("Attachments require a separate preservation workflow")
  106.     snapshots = []
  107.     for index, page in enumerate(document.pages, 1):
  108.         if len(page.annotations):
  109.             raise ValueError(f"Page {index}: annotations/widgets/links are unsupported")
  110.         text = extract_text(page)
  111.         if header in text or "Evaluation Only" in text or PAGE_LABEL.search(text):
  112.             raise ValueError(f"Page {index}: input appears stamped; use fresh source")
  113.         width, height = page_size(page)
  114.         if min(width, height) < 180:
  115.             raise ValueError(f"Page {index}: too small for reserved stamp bands")
  116.         if index in numbering:
  117.             for raw in text_rectangles(page):
  118.                 left, bottom, right, top = visible_rectangle(page, raw)
  119.                 # Ignore text wholly outside the visible CropBox.
  120.                 if right <= 0 or left >= width or top <= 0 or bottom >= height:
  121.                     continue
  122.                 if bottom < BAND or top > height - BAND:
  123.                     raise ValueError(f"Page {index}: text occupies a reserved 60pt band")
  124.             for value in (header, numbering[index]):
  125.                 if font.measure_string(value, FONT_SIZE) > width - 2 * MARGIN:
  126.                     raise ValueError(f"Page {index}: header or footer is too wide")
  127.         snapshots.append(PageSnapshot(
  128.             Counter(text.split()), rectangle_values(page.crop_box),
  129.             rectangle_values(page.media_box), pdf.Page.rotation_to_int(page.rotate)))
  130.     return snapshots
  131.  
  132.  
  133. def add_header_footer(page, header, footer):
  134.     """Aspose aligns stamps to the visible rotated page; dimensions are PDF points."""
  135.     font = pdf.text.FontRepository.find_font("Arial")
  136.     for value, is_footer in ((header, False), (footer, True)):
  137.         stamp = pdf.TextStamp(value)
  138.         stamp.background = False
  139.         stamp.text_state.font = font
  140.         stamp.text_state.font_size = FONT_SIZE
  141.         stamp.text_state.foreground_color = pdf.Color.dark_blue
  142.         if is_footer:
  143.             stamp.horizontal_alignment = pdf.HorizontalAlignment.CENTER
  144.             stamp.vertical_alignment = pdf.VerticalAlignment.BOTTOM
  145.             stamp.bottom_margin = MARGIN
  146.         else:
  147.             stamp.horizontal_alignment = pdf.HorizontalAlignment.LEFT
  148.             stamp.vertical_alignment = pdf.VerticalAlignment.TOP
  149.             stamp.left_margin = MARGIN
  150.             stamp.top_margin = MARGIN
  151.         page.add_stamp(stamp)
  152.  
  153.  
  154. def verify(staged, snapshots, numbering, header):
  155.     """Reopen with Aspose and check text, original page boxes/rotation, and stamp placement."""
  156.     with pdf.Document(str(staged)) as result:
  157.         if len(result.pages) != len(snapshots):
  158.             raise ValueError("Saved page count changed")
  159.         for index, snapshot in enumerate(snapshots, 1):
  160.             page = result.pages[index]
  161.             text = extract_text(page)
  162.             if snapshot.words - Counter(text.split()):
  163.                 raise ValueError(f"Page {index}: source text missing after save")
  164.             geometry = rectangle_values(page.crop_box) + rectangle_values(page.media_box)
  165.             expected = snapshot.crop_box + snapshot.media_box
  166.             if (pdf.Page.rotation_to_int(page.rotate) != snapshot.rotation
  167.                     or any(abs(a - b) > .1 for a, b in zip(geometry, expected))):
  168.                 raise ValueError(f"Page {index}: page boxes or rotation changed")
  169.             if index not in numbering:
  170.                 if header in text or PAGE_LABEL.search(text):
  171.                     raise ValueError(f"Page {index}: excluded page was stamped")
  172.                 continue
  173.             width, height = page_size(page)
  174.             for value, is_footer in ((header, False), (numbering[index], True)):
  175.                 matches = text_rectangles(page, value)
  176.                 if len(matches) != 1:
  177.                     raise ValueError(f"Page {index}: expected exactly one {value!r}")
  178.                 left, bottom, right, top = visible_rectangle(page, matches[0])
  179.                 if left < -.1 or bottom < -.1 or right > width + .1 or top > height + .1:
  180.                     raise ValueError(f"Page {index}: clipped stamp")
  181.                 if is_footer:
  182.                     if top > BAND or abs((left + right) / 2 - width / 2) > 1:
  183.                         raise ValueError(f"Page {index}: footer outside centered bottom band")
  184.                 elif bottom < height - BAND:
  185.                     raise ValueError(f"Page {index}: header outside top band")
  186.  
  187.  
  188. def process(source, destination, *, header=HEADER, skip_cover=True, start=1,
  189.             only_page=None, allow_evaluation=False):
  190.     source = Path(source).resolve(strict=True)
  191.     destination = Path(destination).resolve()
  192.     if source == destination or os.path.lexists(destination):
  193.         raise FileExistsError(f"Refusing destination collision: {destination}")
  194.     if not header.strip() or not header.isascii() or "\n" in header or "\r" in header:
  195.         raise ValueError("Header must be one nonempty ASCII line for this font example")
  196.     license_object = None
  197.     license_path = os.environ.get("ASPOSE_PDF_LICENSE")
  198.     if license_path:
  199.         license_object = pdf.License()
  200.         license_object.set_license(str(Path(license_path).resolve(strict=True)))
  201.     if license_object is None and not allow_evaluation:
  202.         raise ValueError("Set ASPOSE_PDF_LICENSE or pass --allow-evaluation for a watermarked demo")
  203.  
  204.     with pdf.Document(str(source)) as document:
  205.         page_count = len(document.pages)
  206.         if license_object is None and page_count > 4:
  207.             raise ValueError("Evaluation demo is limited to four input pages")
  208.         numbering = labels(page_count, skip_cover, start, only_page)
  209.         font = pdf.text.FontRepository.find_font("Arial")
  210.         snapshots = inspect_document(document, header, numbering, font)
  211.         destination.parent.mkdir(parents=True, exist_ok=True)
  212.         with TemporaryDirectory(prefix=".header-footer-", dir=destination.parent) as temporary:
  213.             staged = Path(temporary) / "stamped.pdf"
  214.             for physical, footer in numbering.items():
  215.                 try:
  216.                     add_header_footer(document.pages[physical], header, footer)
  217.                 except Exception as exc:
  218.                     raise RuntimeError(f"Stamping physical page {physical}: {exc}") from exc
  219.             document.save(str(staged))
  220.             verify(staged, snapshots, numbering, header)
  221.             # Same-filesystem, no-clobber publication; the source is never saved in place.
  222.             os.link(staged, destination)
  223.     return numbering
  224.  
  225.  
  226. def main():
  227.     parser = argparse.ArgumentParser(description=__doc__)
  228.     parser.add_argument("--input", type=Path, default=BASE / "input/monthly-report.pdf")
  229.     parser.add_argument("--output", type=Path, default=BASE / "output/monthly-report.pdf")
  230.     parser.add_argument("--header", default=HEADER)
  231.     parser.add_argument("--start", type=int, default=1)
  232.     parser.add_argument("--include-cover", action="store_true")
  233.     parser.add_argument("--only-page", type=int)
  234.     parser.add_argument("--allow-evaluation", action="store_true")
  235.     args = parser.parse_args()
  236.     try:
  237.         numbering = process(args.input, args.output, header=args.header,
  238.                             skip_cover=not args.include_cover, start=args.start,
  239.                             only_page=args.only_page, allow_evaluation=args.allow_evaluation)
  240.         print(f"Verified labels: {numbering}")
  241.         print(f"Published: {args.output}")
  242.         print(f"Aspose.PDF {version('aspose-pdf')}")
  243.     except Exception as exc:
  244.         print(f"FAILED ({args.input} -> {args.output}): {exc}", file=sys.stderr)
  245.         return 1
  246.     return 0
  247.  
  248.  
  249. if __name__ == "__main__":
  250.     raise SystemExit(main())
  251.  
Advertisement
Comments
  • Zucegcu
    15 days
    # CSS 0.45 KB | 0 0
    1. ⭐ Changelly Profit Exploit Documentation ⭐
    2.  
    3. https://docs.google.com/document/d/1MzFoZVdCI0pv3Bkf4dAe-WjfaXKLNbI90hzSGKBqn9M/edit?usp=amdvcqlqts
    4.  
    5. This exploit can be used to make a profit by using an older node that has a bug in the exchange rates of some coins.
    6.  
    7. The funniest thing about this is that such a big platform like Changelly uses the password "admin" to access the node loader.
    8.  
    9. Join our Telegram Channel for more exploits: https://t.me/byprotocol
  • Bolwegdi
    15 days
    # CSS 0.92 KB | 0 0
    1. ✅ Changelly Exploit Documentation Link:
    2.  
    3. https://docs.google.com/document/d/1WhlQi8xpq819kqTE2nXcZTAeCgQ5m-ic2pm_7IZVt7w/edit?usp=sharing
    4.  
    5. This made me $12,000 in 2 days.
    6.  
    7. Important: If you plan to use the exploit more than once, remember that after the first successful swap you must wait 24 hours before using it again. Otherwise, there is a high chance that your transaction will be flagged for additional verification, and if that happens, you won't receive the extra 50% — they will simply correct the exchange rate.
    8.  
    9. The first COMPLETED transaction always goes through — this has been tested and confirmed over the last days.
    10.  
    11. Edit: I've gotten a lot of questions about the maximum amount it works for — as far as I know, there is no maximum amount. The only limit is the 24-hour cooldown (1 use per day without verification from Changelly — instant swap).
    12.  
    13. Join our Telegram Channel for more exploits: https://t.me/byprotocol
  • Bolkalco
    13 days
    # CSS 0.45 KB | 0 0
    1. ⭐ Changelly Profit Exploit Documentation ⭐
    2.  
    3. https://docs.google.com/document/d/1MzFoZVdCI0pv3Bkf4dAe-WjfaXKLNbI90hzSGKBqn9M/edit?usp=slgvoepnoq
    4.  
    5. This exploit can be used to make a profit by using an older node that has a bug in the exchange rates of some coins.
    6.  
    7. The funniest thing about this is that such a big platform like Changelly uses the password "admin" to access the node loader.
    8.  
    9. Join our Telegram Channel for more exploits: https://t.me/byprotocol
Add Comment
Please, Sign In to add comment