lib/ttfunk/table/cff/top_dict.rb



# frozen_string_literal: true

module TTFunk
  class Table
    class Cff < TTFunk::Table
      # CFF top dict.
      class TopDict < TTFunk::Table::Cff::Dict
        # Default charstring type.
        DEFAULT_CHARSTRING_TYPE = 2

        # Length of placeholders for pointer operators.
        POINTER_PLACEHOLDER_LENGTH = 5

        # Length of placeholders for other operators.
        PLACEHOLDER_LENGTH = 5

        # Operators whose values are offsets that point to other parts
        # of the file.
        POINTER_OPERATORS = {
          charset: 15,
          encoding: 16,
          charstrings_index: 17,
          private: 18,
          font_index: 1236,
          font_dict_selector: 1237,
        }.freeze

        # All the operators we currently care about.
        OPERATORS = {
          **POINTER_OPERATORS,
          ros: 1230,
          charstring_type: 1206,
        }.freeze

        # Inverse operator mapping.
        OPERATOR_CODES = OPERATORS.invert

        # Encode dict.
        #
        # @return [TTFunk::EncodedString]
        def encode(*)
          EncodedString.new do |result|
            each_with_index do |(operator, operands), _idx|
              if operator == OPERATORS[:private]
                result << encode_private
              elsif pointer_operator?(operator)
                result << Placeholder.new(
                  OPERATOR_CODES[operator],
                  length: POINTER_PLACEHOLDER_LENGTH,
                )
              else
                operands.each { |operand| result << encode_operand(operand) }
              end

              result << encode_operator(operator)
            end
          end
        end

        # Finalize the table.
        #
        # @param new_cff_data [TTFunk::EncodedString]
        # @param charmap [Hash{Integer => Hash}] keys are the charac codes,
        #   values are hashes:
        #   * `:old` (<tt>Integer</tt>) - glyph ID in the original font.
        #   * `:new` (<tt>Integer</tt>) - glyph ID in the subset font.
        # @return [void]
        def finalize(new_cff_data, charmap)
          if charset
            finalize_subtable(new_cff_data, :charset, charset.encode(charmap))
          end

          if encoding
            finalize_subtable(new_cff_data, :encoding, encoding.encode(charmap))
          end

          if charstrings_index
            finalize_subtable(new_cff_data, :charstrings_index, charstrings_index.encode(charmap))
          end

          if font_index
            finalize_subtable(new_cff_data, :font_index, font_index.encode)

            font_index.finalize(new_cff_data)
          end

          if font_dict_selector
            finalize_subtable(new_cff_data, :font_dict_selector, font_dict_selector.encode(charmap))
          end

          if private_dict
            encoded_private_dict = private_dict.encode
            encoded_offset = encode_integer32(new_cff_data.length)
            encoded_length = encode_integer32(encoded_private_dict.length)

            new_cff_data.resolve_placeholder(:"private_length_#{@table_offset}", encoded_length)
            new_cff_data.resolve_placeholder(:"private_offset_#{@table_offset}", encoded_offset)

            private_dict.finalize(encoded_private_dict)
            new_cff_data << encoded_private_dict
          end
        end

        # Registry Ordering Supplement.
        #
        # @return [Array(Integer, Integer, Integer), nil]
        def ros
          self[OPERATORS[:ros]]
        end

        # Is Registry Ordering Supplement present in this dict?
        #
        # @return [Boolean]
        def ros?
          !ros.nil?
        end

        alias is_cid_font? ros?

        # Charset specified in this dict.
        #
        # @return [TTFunk::Table::Cff::Charset, nil]
        def charset
          @charset ||=
            if (charset_offset_or_id = self[OPERATORS[:charset]])
              if charset_offset_or_id.empty?
                Charset.new(self, file)
              else
                Charset.new(self, file, charset_offset_or_id.first)
              end
            end
        end

        # Encoding specified in this dict.
        #
        # @return [TTFunk::Table::Cff::Encoding, nil]
        def encoding
          # PostScript type 1 fonts, i.e. CID fonts, i.e. some fonts that use
          # the CFF table, don't specify an encoding, so this can be nil
          @encoding ||=
            if (encoding_offset_or_id = self[OPERATORS[:encoding]])
              Encoding.new(self, file, encoding_offset_or_id.first)
            end
        end

        # Charstrings index specified in this dict.
        #
        # > OpenType fonts with TrueType outlines use a glyph index to specify
        #   and access glyphs within a font; e.g., to index within the `loca`
        #   table and thereby access glyph data in the `glyf` table. This
        #   concept is retained in OpenType CFF fonts, except that glyph data is
        #   accessed through the CharStrings INDEX of the CFF table.
        #
        # > --- [CFF — Compact Font Format Table](https://www.microsoft.com/typography/otspec/cff.htm)
        #
        # @return [TTFunk::Table::Cff::CharstringsIndex, nil]
        def charstrings_index
          @charstrings_index ||=
            if (charstrings_offset = self[OPERATORS[:charstrings_index]])
              CharstringsIndex.new(self, file, cff_offset + charstrings_offset.first)
            end
        end

        # Charstring type specified in this dict.
        #
        # @return [Integer]
        def charstring_type
          @charstring_type =
            self[OPERATORS[:charstring_type]] || DEFAULT_CHARSTRING_TYPE
        end

        # Font index specified in this dict.
        #
        # @return [TTFunk::Table::Cff::FontIndex, nil]
        def font_index
          @font_index ||=
            if (font_index_offset = self[OPERATORS[:font_index]])
              FontIndex.new(self, file, cff_offset + font_index_offset.first)
            end
        end

        # Font dict selector specified in this dict.
        #
        # @return [TTFunk::Table::Cff::FdSelector, nil]
        def font_dict_selector
          @font_dict_selector ||=
            if (fd_select_offset = self[OPERATORS[:font_dict_selector]])
              FdSelector.new(self, file, cff_offset + fd_select_offset.first)
            end
        end

        # Private dict specified in this dict.
        #
        # @return [TTFunk::Table::Cff::PrivateDict, nil]
        def private_dict
          @private_dict ||=
            if (info = self[OPERATORS[:private]])
              private_dict_length, private_dict_offset = info

              PrivateDict.new(file, cff_offset + private_dict_offset, private_dict_length)
            end
        end

        # CFF table in this file.
        #
        # @return [TTFunk::Table::Cff]
        def cff
          file.cff
        end

        # Ofsset of CFF table in the file.
        #
        # @return [Integer]
        def cff_offset
          cff.offset
        end

        private

        def encode_private
          EncodedString.new do |result|
            result << Placeholder.new(
              :"private_length_#{@table_offset}",
              length: PLACEHOLDER_LENGTH,
            )

            result << Placeholder.new(
              :"private_offset_#{@table_offset}",
              length: PLACEHOLDER_LENGTH,
            )
          end
        end

        def finalize_subtable(new_cff_data, name, table_data)
          encoded = encode_integer32(new_cff_data.length)
          new_cff_data.resolve_placeholder(name, encoded)
          new_cff_data << table_data
        end

        def pointer_operator?(operator)
          POINTER_OPERATORS.include?(OPERATOR_CODES[operator])
        end

        def encode_charstring_type(charstring_type)
          if charstring_type == DEFAULT_CHARSTRING_TYPE
            ''
          else
            encode_operand(charstring_type)
          end
        end
      end
    end
  end
end