Apply text layer and/or image layer changes to baseline file

    This is where the magic happens. infiles will be the main PDF to modify,
    and optional .text.pdf and .image-layer.pdf files, organized however ruffus
    organizes them.

    From .text.pdf, we copy the content stream (which contains the Tesseract
    OCR results), and rotate it into place. The first time we do this, we also
    copy the GlyphlessFont, and then reference that font again.

    For .image-layer.pdf, we check if this is a "pointer" to the original file,
    or a new file. If a new file, we replace the page and remember that we
    replaced this page.

    Every 100 open files, we save intermediate results, to avoid any resource
    limits, since pikepdf/qpdf need to keep a lot of open file handles in the
    background. When objects are copied from one file to another qpdf, qpdf
    doesn't actually copy the data until asked to write, so all the resources
    it may need to remain available.

    For completeness, we set up a /ProcSet on every page, although it's
    unlikely any PDF viewer cares about this anymore.

def weave_layers(infiles, output_file, log, context):
    """Apply text layer and/or image layer changes to baseline file

    This is where the magic happens. infiles will be the main PDF to modify,
    and optional .text.pdf and .image-layer.pdf files, organized however ruffus
    organizes them.

    From .text.pdf, we copy the content stream (which contains the Tesseract
    OCR results), and rotate it into place. The first time we do this, we also
    copy the GlyphlessFont, and then reference that font again.

    For .image-layer.pdf, we check if this is a "pointer" to the original file,
    or a new file. If a new file, we replace the page and remember that we
    replaced this page.

    Every 100 open files, we save intermediate results, to avoid any resource
    limits, since pikepdf/qpdf need to keep a lot of open file handles in the
    background. When objects are copied from one file to another qpdf, qpdf
    doesn't actually copy the data until asked to write, so all the resources
    it may need to remain available.

    For completeness, we set up a /ProcSet on every page, although it's
    unlikely any PDF viewer cares about this anymore.

    """

    def input_sorter(key):
        try:
            return page_number(key)
        except ValueError:
            return -1

    flat_inputs = sorted(flatten_groups(infiles), key=input_sorter)
    groups = groupby(flat_inputs, key=input_sorter)

    # Extract first item
    _, basegroup = next(groups)
    base = list(basegroup)[0]
    path_base = Path(base).resolve()
    pdf_base = pikepdf.open(path_base)
    font, font_key, procset = None, None, None
    pdfinfo = context.get_pdfinfo()
    pagerefs = {}

    procset = pdf_base.make_indirect(
        pikepdf.Object.parse(b'[ /PDF /Text /ImageB /ImageC /ImageI ]')
    )

    replacements = 0

    # Iterate rest
    for page_num, layers in groups:
        layers = list(layers)
        log.debug(page_num)
        log.debug(layers)

        text = next((ii for ii in layers if ii.endswith('.text.pdf')), None)
        image = next((ii for ii in layers if ii.endswith('.image-layer.pdf')), None)

        if text and not font:
            font, font_key = _find_font(text, pdf_base)

        replacing = False
        content_rotation = pdfinfo[page_num - 1].rotation

        path_image = Path(image).resolve() if image else None
        if path_image is not None and path_image != path_base:
            # We are replacing the old page with a rasterized PDF of the new
            # page
            log.debug("Replace")
            old_objgen = pdf_base.pages[page_num - 1].objgen

            with pikepdf.open(image) as pdf_image:
                replacements += 1
                image_page = pdf_image.pages[0]
                pdf_base.pages[page_num - 1] = image_page

            # We're adding a new page, which will get a new objgen number pair,
            # so we need to update any references to it.  qpdf did not like
            # my attempt to update the old object in place, but that is an
            # option to consider
            pagerefs[old_objgen] = pdf_base.pages[page_num - 1].objgen
            replacing = True

        autorotate_correction = context.get_rotation(page_num - 1)
        if replacing:
            content_rotation = autorotate_correction
        text_rotation = autorotate_correction
        text_misaligned = (text_rotation - content_rotation) % 360
        log.debug(
            '%r',
            [text_rotation, autorotate_correction, text_misaligned, content_rotation],
        )

        if text and font:
            # Graft the text layer onto this page, whether new or old
            strip_old = context.get_options().redo_ocr
            _weave_layers_graft(
                pdf_base=pdf_base,
                page_num=page_num,
                text=text,
                font=font,
                font_key=font_key,
                rotation=text_misaligned,
                procset=procset,
                strip_old_text=strip_old,
                log=log,
            )

        # Correct the rotation if applicable
        pdf_base.pages[page_num - 1].Rotate = (
            content_rotation - autorotate_correction
        ) % 360

        if replacements % MAX_REPLACE_PAGES == 0:
            # Periodically save and reload the Pdf object. This will keep a
            # lid on our memory usage for very large files. Attach the font to
            # page 1 even if page 1 doesn't use it, so we have a way to get it
            # back.
            # TODO refactor this to outside the loop
            page0 = pdf_base.pages[0]
            _update_page_resources(
                page=page0, font=font, font_key=font_key, procset=procset
            )
            interim = output_file + f'_working{page_num}.pdf'
            pdf_base.save(interim)
            pdf_base.close()

            pdf_base = pikepdf.open(interim)
            procset = pdf_base.pages[0].Resources.ProcSet
            font, font_key = None, None  # Reacquire this information

    _fix_toc(pdf_base, pagerefs, log)
    pdf_base.save(output_file)
    pdf_base.close()