Beispiel #1
0
def compare_output(writer: BasePdfFileWriter, expected_output_path):
    with tempfile.TemporaryDirectory() as working_dir:
        output_path = os.path.join(working_dir, 'output.pdf')
        with open(output_path, 'wb') as outf:
            writer.write(outf)
        expected_png = _render_pdf(
            expected_output_path, os.path.join(working_dir, 'expected')
        )
        actual_png = _render_pdf(
            output_path, os.path.join(working_dir, 'actual')
        )
        result = subprocess.run(
            # use the Absolute Error metric, since it's a single number
            # and hence very easy to process
            [
                compare_path, '-metric', 'ae',
                expected_png, actual_png, os.path.join(working_dir, 'diff.png')
            ],
            capture_output=True
        )
        # TODO maintain a directory of failed test outputs?
        if result.stderr != b'0':
            raise RuntimeError(
                f"Output compare test failed --- absolute error: "
                f"{result.stderr.decode('utf8')}"
            )
Beispiel #2
0
 def register_widget_annotation(self, writer: BasePdfFileWriter,
                                sig_field_ref):
     annot_dict = self.annot_dict
     if annot_dict is not self:
         annot_ref = writer.add_object(annot_dict)
         self['/Kids'] = generic.ArrayObject([annot_ref])
     else:
         annot_ref = sig_field_ref
     writer.register_annotation(self.page_ref, annot_ref)
Beispiel #3
0
 def embed(self, writer: BasePdfFileWriter, obj_stream=None):
     fd = self._font_descriptor
     self[pdf_name('/FontDescriptor')] = fd_ref = writer.add_object(
         fd, obj_stream=obj_stream
     )
     font_stream_ref = self.set_font_file(writer)
     return fd_ref, font_stream_ref
Beispiel #4
0
def _insert_or_get_field_at(writer: BasePdfFileWriter,
                            fields,
                            path,
                            parent_ref=None,
                            modified=False,
                            field_obj=None):

    current_partial, tail = path[0], path[1:]

    for field_ref in fields:
        assert isinstance(field_ref, generic.IndirectObject)
        field = field_ref.get_object()
        if field.get('/T', None) == current_partial:
            break
    else:
        # have to insert a new element into the fields array
        if field_obj is not None and not tail:
            field = field_obj
        else:
            # create a generic field
            field = generic.DictionaryObject()
        field['/T'] = pdf_string(current_partial)
        if parent_ref is not None:
            field['/Parent'] = parent_ref
        field_ref = writer.add_object(field)
        fields.append(field_ref)
        writer.update_container(fields)
        modified = True

    if not tail:
        return modified, field_ref
    # check for /Kids, and create it if necessary
    try:
        kids = field['/Kids']
    except KeyError:
        kids = field['/Kids'] = generic.ArrayObject()
        writer.update_container(field)
        modified = True

    # recurse in to /Kids array
    return _insert_or_get_field_at(writer,
                                   kids,
                                   tail,
                                   parent_ref=field_ref,
                                   modified=modified,
                                   field_obj=field_obj)
Beispiel #5
0
 def __init__(self, writer: BasePdfFileWriter, base_postscript_name: str,
              embedded_subset: bool, obj_stream=None):
     fsc = writer.get_subset_collection(base_postscript_name)
     if embedded_subset:
         self.subset_prefix = prefix = fsc.add_subset()
     else:
         self.subset_prefix = prefix = None
     fsc.subsets[prefix] = self
     self.writer = writer
     self.obj_stream = obj_stream
Beispiel #6
0
    def set_font_file(self, writer: BasePdfFileWriter):
        stream_buf = BytesIO()
        self.tt_font.save(stream_buf)
        stream_buf.seek(0)

        font_stream = generic.StreamObject(stream_data=stream_buf.read())
        font_stream.compress()
        font_stream_ref = writer.add_object(font_stream)
        self._font_descriptor[pdf_name('/FontFile2')] = font_stream_ref
        return font_stream_ref
Beispiel #7
0
    def __init__(self, writer: BasePdfFileWriter, font_handle, font_size,
                 features=None, ot_language_tag=None, ot_script_tag=None,
                 writing_direction=None, bcp47_lang_code=None, obj_stream=None):

        # harfbuzz expects bytes
        font_handle.seek(0)
        font_bytes = font_handle.read()
        font_handle.seek(0)
        face = hb.Face(font_bytes)
        self.font_size = font_size
        self.hb_font = hb.Font(face)
        self.tt = tt = ttLib.TTFont(font_handle)
        base_ps_name = _read_ps_name(tt)
        super().__init__(
            writer, base_ps_name, embedded_subset=True, obj_stream=obj_stream
        )
        self._font_ref = writer.allocate_placeholder()
        try:
            cff = self.tt['CFF ']
            self.cff_charset = cff.cff[0].charset

            # CFF font programs are embedded differently
            #  (in a more Adobe-native way)
            self.cidfont_obj = cidfont_obj = CIDFontType0(
                tt, base_ps_name, self.subset_prefix
            )
            self.use_raw_gids = cidfont_obj.use_raw_gids
        except KeyError:
            self.cff_charset = None
            self.use_raw_gids = True
            self.cidfont_obj = CIDFontType2(
                tt, base_ps_name, self.subset_prefix
            )

        self.features = features

        # the 'head' table is mandatory
        self.units_per_em = tt['head'].unitsPerEm

        self._glyphs = {}
        self._glyph_set = tt.getGlyphSet(preferCFF=True)

        self._cid_to_unicode = {}
        self.__reverse_cmap = None
        self.ot_language_tag = _check_ot_tag(ot_language_tag)
        self.ot_script_tag = _check_ot_tag(ot_script_tag)
        if writing_direction is not None and \
                writing_direction not in ('ltr', 'rtl', 'ttb', 'btt'):
            raise ValueError(
                "Writing direction must be one of 'ltr', 'rtl', 'ttb' or 'btt'."
            )
        self.writing_direction = writing_direction
        self.bcp47_lang_code = bcp47_lang_code
        self._subset_created = self._write_prepared = False
Beispiel #8
0
 def set_font_file(self, writer: BasePdfFileWriter):
     stream_buf = BytesIO()
     # write the CFF table to the stream
     self.cff.compile(stream_buf, self.tt_font)
     stream_buf.seek(0)
     font_stream = generic.StreamObject({
         # this is a Type0 CFF font program (see Table 126 in ISO 32000)
         pdf_name('/Subtype'): pdf_name('/CIDFontType0C'),
     }, stream_data=stream_buf.read())
     font_stream.compress()
     font_stream_ref = writer.add_object(font_stream)
     self._font_descriptor[pdf_name('/FontFile3')] = font_stream_ref
     return font_stream_ref
Beispiel #9
0
    def set_font_file(self, writer: BasePdfFileWriter):
        stream_buf = BytesIO()
        self.tt_font.save(stream_buf)
        stream_buf.seek(0)

        font_stream = generic.StreamObject({
            # this is a Type2 TTF font program
            pdf_name('/Subtype'): pdf_name('/CIDFontType2'),
        }, stream_data=stream_buf.read())
        font_stream.compress()
        font_stream_ref = writer.add_object(font_stream)
        self._font_descriptor[pdf_name('/FontFile2')] = font_stream_ref
        return font_stream_ref
Beispiel #10
0
def _get_or_create_sigfield(field_name,
                            pdf_out: BasePdfFileWriter,
                            existing_fields_only,
                            new_field_spec: Optional[SigFieldSpec] = None):
    root = pdf_out.root
    if field_name is None:
        if not existing_fields_only:
            raise SigningError('Not specifying a field name is only allowed '
                               'when existing_fields_only=True')

        # most of the logic in prepare_sig_field has to do with preparing
        # for the potential addition of a new field. That is completely
        # irrelevant in this special case, so we might as well short circuit
        # things.
        field_created = False
        empty_fields = enumerate_sig_fields(pdf_out, filled_status=False)
        try:
            found_field_name, _, sig_field_ref = next(empty_fields)
        except StopIteration:
            raise SigningError('There are no empty signature fields.')

        others = ', '.join(fn for fn, _, _ in empty_fields if fn is not None)
        if others:
            raise SigningError(
                'There are several empty signature fields. Please specify '
                'a field name. The options are %s, %s.' %
                (found_field_name, others))
    else:
        # grab or create a sig field
        if new_field_spec is not None:
            sig_field_kwargs = {
                'box':
                new_field_spec.box,
                'include_on_page':
                pdf_out.find_page_for_modification(new_field_spec.on_page)[0],
                'combine_annotation':
                new_field_spec.combine_annotation
            }
        else:
            sig_field_kwargs = {}

        field_created, sig_field_ref = prepare_sig_field(
            field_name,
            root,
            update_writer=pdf_out,
            existing_fields_only=existing_fields_only,
            **sig_field_kwargs)

    ensure_sig_flags(writer=pdf_out, lock_sig_flags=True)

    return field_created, sig_field_ref
Beispiel #11
0
def append_signature_field(pdf_out: BasePdfFileWriter,
                           sig_field_spec: SigFieldSpec):
    """
    Append signature fields to a PDF file.

    :param pdf_out:
        Incremental writer to house the objects.
    :param sig_field_spec:
        A :class:`.SigFieldSpec` object describing the signature field
        to add.
    """
    root = pdf_out.root

    page_ref = pdf_out.find_page_for_modification(sig_field_spec.on_page)[0]
    # use default appearance
    field_created, sig_field_ref = _prepare_sig_field(
        sig_field_spec.sig_field_name, root, update_writer=pdf_out,
        existing_fields_only=False, lock_sig_flags=False,
        box=sig_field_spec.box, include_on_page=page_ref,
        combine_annotation=sig_field_spec.combine_annotation
    )
    if not field_created:
        raise PdfWriteError(
            'Signature field with name %s already exists.'
            % sig_field_spec.sig_field_name
        )

    sig_field = sig_field_ref.get_object()
    if sig_field_spec.seed_value_dict is not None:
        # /SV must be an indirect reference as per the spec
        sv_ref = pdf_out.add_object(
            sig_field_spec.seed_value_dict.as_pdf_object()
        )
        sig_field[pdf_name('/SV')] = sv_ref

    lock = sig_field_spec.format_lock_dictionary()
    if lock is not None:
        sig_field[pdf_name('/Lock')] = pdf_out.add_object(lock)
Beispiel #12
0
def _prepare_sig_field(sig_field_name,
                       root,
                       update_writer: BasePdfFileWriter,
                       existing_fields_only=False,
                       lock_sig_flags=True,
                       **kwargs):
    """
    Returns a tuple of a boolean and a reference to a signature field.
    The boolean is ``True`` if the field was created, and ``False`` otherwise.
    """
    if sig_field_name is None:  # pragma: nocover
        raise ValueError

    try:
        form = root['/AcroForm']

        try:
            fields = form['/Fields']
        except KeyError:
            raise ValueError('/AcroForm has no /Fields')

        candidates = enumerate_sig_fields_in(fields, with_name=sig_field_name)
        sig_field_ref = None
        try:
            field_name, value, sig_field_ref = next(candidates)
            if value is not None:
                raise SigningError(
                    'Signature field with name %s appears to be filled already.'
                    % sig_field_name)
        except StopIteration:
            if existing_fields_only:
                raise SigningError(
                    'No empty signature field with name %s found.' %
                    sig_field_name)
        form_created = False
    except KeyError:
        # we have to create the form
        if existing_fields_only:
            raise SigningError('This file does not contain a form.')
        # no AcroForm present, so create one
        form = generic.DictionaryObject()
        root[pdf_name('/AcroForm')] = update_writer.add_object(form)
        fields = generic.ArrayObject()
        form[pdf_name('/Fields')] = fields
        # now we need to mark the root as updated
        update_writer.update_root()
        form_created = True
        sig_field_ref = None

    if sig_field_ref is not None:
        return False, sig_field_ref

    # no signature field exists, so create one
    # default: grab a reference to the first page
    page_ref = update_writer.find_page_for_modification(0)[0]
    sig_form_kwargs = {'include_on_page': page_ref}
    sig_form_kwargs.update(**kwargs)
    sig_field = SignatureFormField(sig_field_name, **sig_form_kwargs)
    created, sig_field_ref = _insert_or_get_field_at(
        update_writer,
        fields,
        path=sig_field_name.split('.'),
        field_obj=sig_field)
    update_writer.register_annotation(page_ref, sig_field_ref)

    # make sure /SigFlags is present. If not, create it
    sig_flags = 3 if lock_sig_flags else 1
    form.setdefault(pdf_name('/SigFlags'), generic.NumberObject(sig_flags))
    # if a field was added to an existing form, register an extra update
    if not form_created:
        update_writer.update_container(fields)
    return True, sig_field_ref
Beispiel #13
0
    def write_cms(self,
                  field_name: str,
                  writer: BasePdfFileWriter,
                  existing_fields_only=False):
        """
        .. versionadded:: 0.3.0

        .. versionchanged:: 0.7.0
            Digest wrapped in
            :class:`~pyhanko.sign.signers.pdf_byterange.PreparedByteRangeDigest`
            in step 3; ``output`` returned in step 3 instead of step 4.

        This method returns a generator coroutine that controls the process
        of embedding CMS data into a PDF signature field.
        Can be used for both timestamps and regular signatures.

        .. danger::
            This is a very low-level interface that performs virtually no
            error checking, and is intended to be used in situations
            where the construction of the CMS object to be embedded
            is not under the caller's control (e.g. a remote signer
            that produces full-fledged CMS objects).

            In almost every other case, you're better of using
            :class:`.PdfSigner` instead, with a custom :class:`.Signer`
            implementation to handle the cryptographic operations if necessary.

        The coroutine follows the following specific protocol.

        1. First, it retrieves or creates the signature field to embed the
           CMS object in, and yields a reference to said field.
        2. The caller should then send in a :class:`.SigObjSetup` object, which
           is subsequently processed by the coroutine. For convenience, the
           coroutine will then yield a reference to the signature dictionary
           (as embedded in the PDF writer).
        3. Next, the caller should send a :class:`.SigIOSetup` object,
           describing how the resulting document should be hashed and written
           to the output. The coroutine will write the entire document with a
           placeholder region reserved for the signature and compute the
           document's hash and yield it to the caller.
           It will then yield a ``prepared_digest, output`` tuple, where
           ``prepared_digest`` is a :class:`.PreparedByteRangeDigest` object
           containing the document digest and the relevant offsets, and
           ``output`` is the output stream to which the document to be
           signed was written.

           From this point onwards, **no objects may be changed or added** to
           the :class:`.IncrementalPdfFileWriter` currently in use.
        4. Finally, the caller should pass in a CMS object to place inside
           the signature dictionary. The CMS object can be supplied as a raw
           :class:`bytes` object, or an :mod:`asn1crypto`-style object.
           The coroutine's final yield is the value of the signature
           dictionary's ``/Contents`` entry, given as a hexadecimal string.

        .. caution::
            It is the caller's own responsibility to ensure that enough room
            is available in the placeholder signature object to contain
            the final CMS object.

        :param field_name:
            The name of the field to fill in. This should be a field of type
            ``/Sig``.
        :param writer:
            An :class:`.IncrementalPdfFileWriter` containing the
            document to sign.
        :param existing_fields_only:
            If ``True``, never create a new empty signature field to contain
            the signature.
            If ``False``, a new field may be created if no field matching
            ``field_name`` exists.
        :return:
            A generator coroutine implementing the protocol described above.
        """

        new_field_spec = self.new_field_spec \
            if not existing_fields_only else None
        # start by creating or fetching the appropriate signature field
        field_created, sig_field_ref = _get_or_create_sigfield(
            field_name,
            writer,
            existing_fields_only,
            new_field_spec=new_field_spec)

        # yield control to caller to further process the field dictionary
        # if necessary, request setup specs for sig object
        sig_obj_setup = yield sig_field_ref
        assert isinstance(sig_obj_setup, SigObjSetup)

        sig_field = sig_field_ref.get_object()

        # take care of the field's visual appearance (if applicable)
        appearance_setup = sig_obj_setup.appearance_setup
        if appearance_setup is not None:
            try:
                sig_annot, = sig_field['/Kids']
                sig_annot = sig_annot.get_object()
            except (ValueError, TypeError):
                raise SigningError(
                    "Failed to access signature field's annotation. "
                    "Signature field must have exactly one child annotation, "
                    "or it must be combined with its annotation.")
            except KeyError:
                sig_annot = sig_field

            appearance_setup.apply(sig_annot, writer)

        sig_obj = sig_obj_setup.sig_placeholder
        sig_obj_ref = writer.add_object(sig_obj)

        # fill in a reference to the (empty) signature object
        sig_field[pdf_name('/V')] = sig_obj_ref

        if not field_created:
            # still need to mark it for updating
            writer.mark_update(sig_field_ref)

        mdp_setup = sig_obj_setup.mdp_setup
        if mdp_setup is not None:
            mdp_setup.apply(sig_obj_ref, writer)

        # again, pass control to the caller
        # and request I/O parameters for putting the cryptographic signature
        # into the output.
        # We pass a reference to the embedded signature object as a convenience.

        sig_io = yield sig_obj_ref
        assert isinstance(sig_io, SigIOSetup)

        # pass control to the sig object's write_signature coroutine
        yield from sig_obj.fill(writer,
                                sig_io.md_algorithm,
                                in_place=sig_io.in_place,
                                output=sig_io.output,
                                chunk_size=sig_io.chunk_size)
Beispiel #14
0
    def supply_dss_in_writer(cls, pdf_out: BasePdfFileWriter,
                             sig_contents, *, certs=None,
                             ocsps=None, crls=None, paths=None,
                             validation_context=None,
                             embed_roots: bool = True) \
            -> 'DocumentSecurityStore':
        """
        Add or update a DSS, and optionally associate the new information with a
        VRI entry tied to a signature object.

        You can either specify the CMS objects to include directly, or
        pass them in as output from `pyhanko_certvalidator`.

        :param pdf_out:
            PDF writer to write to.
        :param sig_contents:
            Contents of the new signature (used to compute the VRI hash), as
            as a hexadecimal string, including any padding.
            If ``None``, the information will not be added to any VRI
            dictionary.
        :param certs:
            Certificates to include in the VRI entry.
        :param ocsps:
            OCSP responses to include in the VRI entry.
        :param crls:
            CRLs to include in the VRI entry.
        :param paths:
            Validation paths that have been established, and need to be added
            to the DSS.
        :param validation_context:
            Validation context from which to draw OCSP responses and CRLs.
        :param embed_roots:
            .. versionadded:: 0.9.0

            Option that controls whether the root certificate of each validation
            path should be embedded into the DSS. The default is ``True``.

            .. note::
                Trust roots are configured by the validator, so embedding them
                typically does nothing in a typical validation process.
                Therefore they can be safely omitted in most cases.
                Nonetheless, embedding the roots can be useful for documentation
                purposes.

            .. warning::
                This only applies to paths, not the ``certs`` parameter.

        :return:
            a :class:`DocumentSecurityStore` object containing both the new
            and existing contents of the DSS (if any).
        """
        try:
            dss = cls.read_dss(pdf_out)
            created = False
        except ValidationInfoReadingError:
            created = True
            dss = cls(writer=pdf_out)

        if sig_contents is not None:
            identifier = \
                DocumentSecurityStore.sig_content_identifier(sig_contents)
        else:
            identifier = None

        def _certs():
            yield from certs or ()
            path: ValidationPath
            for path in (paths or ()):
                path_parts = iter(path)
                if not embed_roots:
                    # skip the first cert (i.e. the root)
                    next(path_parts)
                yield from path_parts

        def _ocsps():
            yield from ocsps or ()
            if validation_context is not None:
                yield from validation_context.ocsps

        def _crls():
            yield from crls or ()
            if validation_context is not None:
                yield from validation_context.crls

        dss.register_vri(
            identifier, certs=_certs(), ocsps=_ocsps(), crls=_crls()
        )
        dss_dict = dss.as_pdf_object()
        # if we're updating the DSS, this is all we need to do.
        # if we're adding a fresh DSS, we need to register it.

        if created:
            dss_ref = pdf_out.add_object(dss_dict)
            pdf_out.root[pdf_name('/DSS')] = dss_ref
            pdf_out.update_root()
        return dss
Beispiel #15
0
    def fill(self,
             writer: BasePdfFileWriter,
             md_algorithm,
             in_place=False,
             output=None,
             chunk_size=misc.DEFAULT_CHUNK_SIZE):
        """
        Generator coroutine that handles the document hash computation and
        the actual filling of the placeholder data.

        .. danger::
            This is internal API; you should use use :class:`.PdfSigner`
            wherever possible. If you *really* need fine-grained control,
            use :class:`~pyhanko.sign.signers.cms_embedder.PdfCMSEmbedder`
            instead.
        """

        if in_place:
            if not isinstance(writer, IncrementalPdfFileWriter):
                raise TypeError(
                    "in_place is only meaningful for incremental writers."
                )  # pragma: nocover
            output = writer.prev.stream
            writer.write_in_place()
        else:
            output = misc.prepare_rw_output_stream(output)

            writer.write(output)

        # retcon time: write the proper values of the /ByteRange entry
        #  in the signature object
        eof = output.tell()
        sig_start, sig_end = self.contents.offsets
        self.byte_range.fill_offsets(output, sig_start, sig_end, eof)

        # compute the digests
        md_spec = get_pyca_cryptography_hash(md_algorithm)
        md = hashes.Hash(md_spec)

        # attempt to get a memoryview for automatic buffering
        output_buffer = None
        if isinstance(output, BytesIO):
            output_buffer = output.getbuffer()
        else:
            try:
                output_buffer = memoryview(output)
            except (TypeError, IOError):
                pass

        if output_buffer is not None:
            # these are memoryviews, so slices should not copy stuff around
            #   (also, the interface files for pyca/cryptography don't specify
            #    that memoryviews are allowed, but they are)
            # noinspection PyTypeChecker
            md.update(output_buffer[:sig_start])
            # noinspection PyTypeChecker
            md.update(output_buffer[sig_end:eof])
            output_buffer.release()
        else:
            temp_buffer = bytearray(chunk_size)
            output.seek(0)
            misc.chunked_digest(temp_buffer, output, md, max_read=sig_start)
            output.seek(sig_end)
            misc.chunked_digest(temp_buffer,
                                output,
                                md,
                                max_read=eof - sig_end)

        digest_value = md.finalize()
        prepared_br_digest = PreparedByteRangeDigest(
            document_digest=digest_value,
            md_algorithm=md_algorithm,
            reserved_region_start=sig_start,
            reserved_region_end=sig_end)
        cms_data = yield prepared_br_digest, output
        yield prepared_br_digest.fill_with_cms(output, cms_data)
Beispiel #16
0
    def embed_subset(self, writer: BasePdfFileWriter, obj_stream=None):
        """
        Embed a subset of this glyph accumulator's font into the provided PDF
        writer. Said subset will include all glyphs necessary to render the
        strings provided to the accumulator via :meth:`feed_string`.

        .. danger::
            Due to the way ``fontTools`` handles subsetting, this is a
            destructive operation. The in-memory representation of the original
            font will be overwritten by the generated subset.

        :param writer:
            A PDF writer.
        :param obj_stream:
            If provided, write all relevant objects to the provided
            `obj_stream`. If ``None`` (the default), they will simply be written
            to the file as top-level objects.
        :return:
            A reference to the embedded ``/Font`` object.
        """
        if self._font_ref is not None:
            return self._font_ref
        self._extract_subset()
        cidfont_obj = CIDFontType0(self.tt)
        # TODO keep track of used subset prefixes in the writer!
        cff_topdict = self.tt['CFF '].cff[0]
        name = cidfont_obj.name
        cff_topdict.rawDict['FullName'] = '%s+%s' % (generate_subset_prefix(),
                                                     name)
        cidfont_obj.embed(writer, obj_stream=obj_stream)
        cidfont_ref = writer.add_object(cidfont_obj)
        to_unicode = self._format_tounicode_cmap(*cidfont_obj.ros)
        type0 = generic.DictionaryObject({
            pdf_name('/Type'):
            pdf_name('/Font'),
            pdf_name('/Subtype'):
            pdf_name('/Type0'),
            pdf_name('/DescendantFonts'):
            generic.ArrayObject([cidfont_ref]),
            # take the Identity-H encoding to inherit from the /Encoding
            # entry specified in our CIDSystemInfo dict
            pdf_name('/Encoding'):
            pdf_name('/Identity-H'),
            pdf_name('/BaseFont'):
            pdf_name('/%s-Identity-H' % cidfont_obj.name),
            pdf_name('/ToUnicode'):
            writer.add_object(to_unicode)
        })
        to_unicode.compress()
        # compute widths entry
        # (easiest to do here, since it seems we need the original CIDs)
        by_cid = iter(sorted(self._glyphs.values(), key=lambda t: t[0]))

        def _widths():
            current_chunk = []
            prev_cid = None
            (first_cid, _, _), itr = peek(by_cid)
            for cid, _, g in itr:
                if current_chunk and cid != prev_cid + 1:
                    yield generic.NumberObject(first_cid)
                    yield generic.ArrayObject(current_chunk)
                    current_chunk = []
                    first_cid = cid

                current_chunk.append(generic.NumberObject(g.width))
                prev_cid = cid
            if current_chunk:
                yield generic.NumberObject(first_cid)
                yield generic.ArrayObject(current_chunk)

        cidfont_obj[pdf_name('/W')] = generic.ArrayObject(list(_widths()))
        self._font_ref = ref = writer.add_object(type0, obj_stream=obj_stream)
        return ref