// Copyright 2016 The PDFium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// Original code copyright 2014 Foxit Software Inc. http://www.foxitsoftware.com

#include "core/fpdfapi/edit/cpdf_pagecontentgenerator.h"

#include <map>
#include <memory>
#include <optional>
#include <set>
#include <sstream>
#include <tuple>
#include <utility>

#include "constants/page_object.h"
#include "core/fpdfapi/edit/cpdf_contentstream_write_utils.h"
#include "core/fpdfapi/edit/cpdf_pagecontentmanager.h"
#include "core/fpdfapi/edit/cpdf_stringarchivestream.h"
#include "core/fpdfapi/font/cpdf_truetypefont.h"
#include "core/fpdfapi/font/cpdf_type1font.h"
#include "core/fpdfapi/page/cpdf_contentmarks.h"
#include "core/fpdfapi/page/cpdf_docpagedata.h"
#include "core/fpdfapi/page/cpdf_form.h"
#include "core/fpdfapi/page/cpdf_formobject.h"
#include "core/fpdfapi/page/cpdf_image.h"
#include "core/fpdfapi/page/cpdf_imageobject.h"
#include "core/fpdfapi/page/cpdf_page.h"
#include "core/fpdfapi/page/cpdf_path.h"
#include "core/fpdfapi/page/cpdf_pathobject.h"
#include "core/fpdfapi/page/cpdf_textobject.h"
#include "core/fpdfapi/parser/cpdf_array.h"
#include "core/fpdfapi/parser/cpdf_dictionary.h"
#include "core/fpdfapi/parser/cpdf_document.h"
#include "core/fpdfapi/parser/cpdf_name.h"
#include "core/fpdfapi/parser/cpdf_number.h"
#include "core/fpdfapi/parser/cpdf_reference.h"
#include "core/fpdfapi/parser/cpdf_stream.h"
#include "core/fpdfapi/parser/fpdf_parser_decode.h"
#include "core/fpdfapi/parser/fpdf_parser_utility.h"
#include "core/fpdfapi/parser/object_tree_traversal_util.h"
#include "core/fxcrt/check.h"
#include "core/fxcrt/check_op.h"
#include "core/fxcrt/containers/contains.h"
#include "core/fxcrt/notreached.h"
#include "core/fxcrt/numerics/safe_conversions.h"
#include "core/fxcrt/span.h"
#include "core/fxcrt/zip.h"

namespace {

// TODO(thestig): Remove/restore other unused resource types:
// - ColorSpace
// - Pattern
// - Shading
constexpr const char* kResourceKeys[] = {"ExtGState", "Font", "XObject"};

// Key: The resource type.
// Value: The resource names of a given type.
using ResourcesMap = std::map<ByteString, std::set<ByteString>>;

// Returns whether it wrote to `buf` or not.
bool WriteColorToStream(fxcrt::ostringstream& buf, const CPDF_Color* color) {
  if (!color || (!color->IsColorSpaceRGB() && !color->IsColorSpaceGray())) {
    return false;
  }

  std::optional<FX_RGB_STRUCT<float>> colors = color->GetRGB();
  if (!colors.has_value()) {
    return false;
  }

  WriteFloat(buf, colors.value().red) << " ";
  WriteFloat(buf, colors.value().green) << " ";
  WriteFloat(buf, colors.value().blue);
  return true;
}

// Balances the "q" operator ProcessGraphics() emitted.
void EndProcessGraphics(fxcrt::ostringstream& buf) {
  buf << " Q\n";
}

void RecordPageObjectResourceUsage(const CPDF_PageObject* page_object,
                                   ResourcesMap& seen_resources) {
  const ByteString& resource_name = page_object->GetResourceName();
  if (!resource_name.IsEmpty()) {
    switch (page_object->GetType()) {
      case CPDF_PageObject::Type::kText:
        seen_resources["Font"].insert(resource_name);
        break;
      case CPDF_PageObject::Type::kImage:
      case CPDF_PageObject::Type::kForm:
        seen_resources["XObject"].insert(resource_name);
        break;
      case CPDF_PageObject::Type::kPath:
        break;
      case CPDF_PageObject::Type::kShading:
        break;
    }
  }
  for (const auto& name : page_object->GetGraphicsResourceNames()) {
    CHECK(!name.IsEmpty());
    seen_resources["ExtGState"].insert(name);
  }
}

CPDF_PageObjectHolder::RemovedResourceMap RemoveUnusedResources(
    CPDF_Dictionary* resource_dict,
    const std::vector<ByteString>& keys,
    const std::set<ByteString>* resource_in_use_of_current_type) {
  CPDF_PageObjectHolder::RemovedResourceMap removed_resource_map;
  for (const ByteString& key : keys) {
    if (resource_in_use_of_current_type &&
        pdfium::Contains(*resource_in_use_of_current_type, key)) {
      continue;
    }

    removed_resource_map[key] = resource_dict->RemoveFor(key.AsStringView());
  }
  return removed_resource_map;
}

void SaveUnusedResources(
    CPDF_PageObjectHolder::RemovedResourceMap& removed_resource_map,
    CPDF_PageObjectHolder::RemovedResourceMap& saved_resource_map) {
  for (auto& entry : removed_resource_map) {
    saved_resource_map[entry.first] = std::move(entry.second);
  }
}

std::vector<ByteString> FindKeysToRestore(
    const std::vector<ByteString>& keys,
    const std::set<ByteString>* resource_in_use_of_current_type,
    const CPDF_PageObjectHolder::RemovedResourceMap& saved_resource_map) {
  if (!resource_in_use_of_current_type) {
    return {};
  }

  std::vector<ByteString> missing_keys;
  for (const ByteString& key : *resource_in_use_of_current_type) {
    if (!pdfium::Contains(keys, key)) {
      missing_keys.push_back(key);
    }
  }

  std::vector<ByteString> keys_to_restore;
  for (const ByteString& key : missing_keys) {
    if (pdfium::Contains(saved_resource_map, key)) {
      keys_to_restore.push_back(key);
    }
  }
  return keys_to_restore;
}

// `keys_to_restore` entries must exist in `saved_resource_map`.
void RestoreUsedResources(
    CPDF_Dictionary* resource_dict,
    const std::vector<ByteString>& keys_to_restore,
    CPDF_PageObjectHolder::RemovedResourceMap& saved_resource_map) {
  for (const ByteString& key : keys_to_restore) {
    auto node = saved_resource_map.extract(key);
    CHECK(!node.empty());
    resource_dict->SetFor(node.key(), std::move(node.mapped()));
  }
}

void RemoveOrRestoreUnusedResources(
    RetainPtr<CPDF_Dictionary> resources_dict,
    const ResourcesMap& resources_in_use,
    CPDF_PageObjectHolder::AllRemovedResourcesMap& all_removed_resources_map) {
  for (const char* resource_key : kResourceKeys) {
    auto resources_in_use_it = resources_in_use.find(resource_key);
    const std::set<ByteString>* resource_in_use_of_current_type =
        resources_in_use_it != resources_in_use.end()
            ? &resources_in_use_it->second
            : nullptr;

    RetainPtr<CPDF_Dictionary> current_resource_dict =
        resources_dict->GetMutableDictFor(resource_key);
    if (!current_resource_dict && !resource_in_use_of_current_type) {
      continue;
    }

    const std::vector<ByteString> keys = current_resource_dict->GetKeys();
    CPDF_PageObjectHolder::RemovedResourceMap removed_resource_map =
        RemoveUnusedResources(current_resource_dict, keys,
                              resource_in_use_of_current_type);
    if (!removed_resource_map.empty()) {
      // Note that this may create a new entry in `all_removed_resources_map`.
      // Only do this if `removed_resource_map` is not empty.
      SaveUnusedResources(removed_resource_map,
                          all_removed_resources_map[resource_key]);
    }

    // Restore in-use objects if possible. To do so, first see if there is a
    // saved resource map to restore from.
    auto saved_resource_map_it = all_removed_resources_map.find(resource_key);
    if (saved_resource_map_it == all_removed_resources_map.end()) {
      continue;
    }

    auto& saved_resource_map = saved_resource_map_it->second;
    std::vector<ByteString> keys_to_restore = FindKeysToRestore(
        keys, resource_in_use_of_current_type, saved_resource_map);
    if (keys_to_restore.empty()) {
      continue;
    }

    // Create the dictionary if needed, so there is a place to restore to.
    if (!current_resource_dict) {
      current_resource_dict =
          resources_dict->SetNewFor<CPDF_Dictionary>(resource_key);
    }
    RestoreUsedResources(current_resource_dict, keys_to_restore,
                         saved_resource_map);
  }
}

bool IsPageResourceShared(CPDF_Document* doc,
                          RetainPtr<const CPDF_Dictionary> page_dict,
                          RetainPtr<const CPDF_Dictionary> resources_dict) {
  CHECK(doc);
  CHECK(page_dict);
  CHECK(resources_dict);

  const uint32_t resources_object_number = resources_dict->GetObjNum();
  if (resources_object_number) {
    // If `resources_dict` is not an inline object, then check to see if is a
    // shared object.
    if (pdfium::Contains(GetObjectsWithMultipleReferences(doc),
                         resources_object_number)) {
      return true;
    }
  }

  // The check above may not catch all cases. e.g. inline objects. Check all
  // pages in the document to see if another page is using the same resources.
  int page_dict_seen = 0;
  int resources_dict_seen = 0;
  for (int i = 0; i < doc->GetPageCount(); ++i) {
    RetainPtr<CPDF_Dictionary> current_page_dict =
        doc->GetMutablePageDictionary(i);
    if (!current_page_dict) {
      continue;
    }

    // Check to see if the current page's page dictionary is seen twice: Once
    // for this page, and once for another. If the same page dictionary is in
    // use twice, then the resource dictionary within must also be shared.
    if (current_page_dict == page_dict) {
      ++page_dict_seen;
      if (page_dict_seen == 2) {
        return true;
      }
    }

    // Similar check as above, for the current page's resource dictionary.
    auto page =
        pdfium::MakeRetain<CPDF_Page>(doc, std::move(current_page_dict));
    if (resources_dict == page->GetMutableResources()) {
      ++resources_dict_seen;
      if (resources_dict_seen == 2) {
        return true;
      }
    }
  }

  return false;
}

void CloneResourcesDictEntries(CPDF_Document* doc,
                               RetainPtr<CPDF_Dictionary> resources_dict) {
  struct KeyAndObject {
    ByteString key;
    RetainPtr<const CPDF_Object> object;
  };
  std::vector<KeyAndObject> entries_to_maybe_clone;
  {
    CPDF_DictionaryLocker locker(resources_dict);
    for (const auto& it : locker) {
      const ByteString& key = it.first;
      const RetainPtr<CPDF_Object>& object = it.second;
      // Clone resource dictionaries that may get modified. While out for
      // self-referencing loops.
      if (object != resources_dict &&
          object->GetType() == CPDF_Object::kReference &&
          pdfium::Contains(kResourceKeys, key)) {
        RetainPtr<const CPDF_Object> direct_object = object->GetDirect();
        if (direct_object) {
          entries_to_maybe_clone.emplace_back(key, std::move(direct_object));
        }
      }
    }
  }

  if (entries_to_maybe_clone.empty()) {
    return;
  }

  std::set<uint32_t> shared_objects = GetObjectsWithMultipleReferences(doc);
  if (shared_objects.empty()) {
    return;
  }

  // Must modify `resources_dict` after `locker` goes out of scope.
  for (const auto& entry : entries_to_maybe_clone) {
    if (pdfium::Contains(shared_objects, entry.object->GetObjNum())) {
      const uint32_t clone_object_number =
          doc->AddIndirectObject(entry.object->Clone());
      resources_dict->SetNewFor<CPDF_Reference>(entry.key, doc,
                                                clone_object_number);
    }
  }
}

}  // namespace

CPDF_PageContentGenerator::CPDF_PageContentGenerator(
    CPDF_PageObjectHolder* pObjHolder)
    : obj_holder_(pObjHolder), document_(pObjHolder->GetDocument()) {
  // Copy all page objects, even if they are inactive. They are needed in
  // GenerateModifiedStreams() below.
  for (const auto& pObj : *pObjHolder) {
    page_objects_.emplace_back(pObj.get());
  }
}

CPDF_PageContentGenerator::~CPDF_PageContentGenerator() = default;

void CPDF_PageContentGenerator::GenerateContent() {
  std::map<int32_t, fxcrt::ostringstream> new_stream_data =
      GenerateModifiedStreams();
  // If no streams were regenerated or removed, nothing to do here.
  if (new_stream_data.empty()) {
    return;
  }

  UpdateContentStreams(std::move(new_stream_data));
  UpdateResourcesDict();
}

std::map<int32_t, fxcrt::ostringstream>
CPDF_PageContentGenerator::GenerateModifiedStreams() {
  // Figure out which streams are dirty.
  std::set<int32_t> all_dirty_streams;
  for (auto& pPageObj : page_objects_) {
    // Must include dirty page objects even if they are marked as inactive.
    // Otherwise an inactive object will not be detected that its stream needs
    // to be removed as part of regeneration.
    if (pPageObj->IsDirty()) {
      all_dirty_streams.insert(pPageObj->GetContentStream());
    }
  }
  std::set<int32_t> marked_dirty_streams = obj_holder_->TakeDirtyStreams();
  all_dirty_streams.insert(marked_dirty_streams.begin(),
                           marked_dirty_streams.end());

  // Start regenerating dirty streams.
  std::map<int32_t, fxcrt::ostringstream> streams;
  std::set<int32_t> empty_streams;
  std::unique_ptr<const CPDF_ContentMarks> empty_content_marks =
      std::make_unique<CPDF_ContentMarks>();
  std::map<int32_t, const CPDF_ContentMarks*> current_content_marks;

  for (int32_t dirty_stream : all_dirty_streams) {
    fxcrt::ostringstream buf;

    // Set the default graphic state values. Update CTM to be the identity
    // matrix for the duration of this stream, if it is not already.
    buf << "q\n";
    const CFX_Matrix ctm = obj_holder_->GetCTMAtBeginningOfStream(dirty_stream);
    if (!ctm.IsIdentity()) {
      WriteMatrix(buf, ctm.GetInverse()) << " cm\n";
    }

    ProcessDefaultGraphics(&buf);
    streams[dirty_stream] = std::move(buf);
    empty_streams.insert(dirty_stream);
    current_content_marks[dirty_stream] = empty_content_marks.get();
  }

  // Process the page objects, write into each dirty stream.
  for (auto& pPageObj : page_objects_) {
    if (!pPageObj->IsActive()) {
      continue;
    }

    int stream_index = pPageObj->GetContentStream();
    auto it = streams.find(stream_index);
    if (it == streams.end()) {
      continue;
    }

    fxcrt::ostringstream* buf = &it->second;
    empty_streams.erase(stream_index);
    current_content_marks[stream_index] =
        ProcessContentMarks(buf, pPageObj, current_content_marks[stream_index]);
    ProcessPageObject(buf, pPageObj);
  }

  // Finish dirty streams.
  for (int32_t dirty_stream : all_dirty_streams) {
    CFX_Matrix prev_ctm;
    CFX_Matrix ctm;
    bool affects_ctm;
    if (dirty_stream == 0) {
      // For the first stream, `prev_ctm` is the identity matrix.
      ctm = obj_holder_->GetCTMAtEndOfStream(dirty_stream);
      affects_ctm = !ctm.IsIdentity();
    } else if (dirty_stream > 0) {
      prev_ctm = obj_holder_->GetCTMAtEndOfStream(dirty_stream - 1);
      ctm = obj_holder_->GetCTMAtEndOfStream(dirty_stream);
      affects_ctm = prev_ctm != ctm;
    } else {
      CHECK_EQ(CPDF_PageObject::kNoContentStream, dirty_stream);
      // This is the last stream, so there is no subsequent stream that it can
      // affect.
      affects_ctm = false;
    }

    const bool is_empty = pdfium::Contains(empty_streams, dirty_stream);

    fxcrt::ostringstream* buf = &streams[dirty_stream];
    if (is_empty && !affects_ctm) {
      // Clear to show that this stream needs to be deleted.
      buf->str("");
      continue;
    }

    if (!is_empty) {
      FinishMarks(buf, current_content_marks[dirty_stream]);
    }

    // Return graphics to original state.
    *buf << "Q\n";

    if (affects_ctm) {
      // Update CTM so the next stream gets the expected value.
      CFX_Matrix ctm_difference = prev_ctm.GetInverse() * ctm;
      if (!ctm_difference.IsIdentity()) {
        WriteMatrix(*buf, ctm_difference) << " cm\n";
      }
    }
  }

  return streams;
}

void CPDF_PageContentGenerator::UpdateContentStreams(
    std::map<int32_t, fxcrt::ostringstream>&& new_stream_data) {
  CHECK(!new_stream_data.empty());

  // Make sure default graphics are created.
  default_graphics_name_ = GetOrCreateDefaultGraphics();

  CPDF_PageContentManager page_content_manager(obj_holder_, document_);
  for (auto& pair : new_stream_data) {
    int32_t stream_index = pair.first;
    fxcrt::ostringstream* buf = &pair.second;

    if (stream_index == CPDF_PageObject::kNoContentStream) {
      int new_stream_index =
          pdfium::checked_cast<int>(page_content_manager.AddStream(buf));
      UpdateStreamlessPageObjects(new_stream_index);
      continue;
    }

    if (page_content_manager.HasStreamAtIndex(stream_index)) {
      page_content_manager.UpdateStream(stream_index, buf);
    } else {
      page_content_manager.AddStream(buf);
    }
  }
}

void CPDF_PageContentGenerator::UpdateResourcesDict() {
  RetainPtr<CPDF_Dictionary> resources = obj_holder_->GetMutableResources();
  if (!resources) {
    return;
  }

  // Do not modify shared resource dictionaries. Give this page its own copy.
  if (IsPageResourceShared(document_, obj_holder_->GetDict(), resources)) {
    resources = pdfium::WrapRetain(resources->Clone()->AsMutableDictionary());
    const uint32_t clone_object_number =
        document_->AddIndirectObject(resources);
    obj_holder_->SetResources(resources);
    obj_holder_->GetMutableDict()->SetNewFor<CPDF_Reference>(
        pdfium::page_object::kResources, document_, clone_object_number);
  }

  // Even though `resources` itself is not shared, its dictionary entries may be
  // shared. Checked for that and clone those as well.
  CloneResourcesDictEntries(document_, resources);

  ResourcesMap seen_resources;
  for (auto& page_object : page_objects_) {
    if (!page_object->IsActive()) {
      continue;
    }
    RecordPageObjectResourceUsage(page_object, seen_resources);
  }
  if (!default_graphics_name_.IsEmpty()) {
    seen_resources["ExtGState"].insert(default_graphics_name_);
  }

  RemoveOrRestoreUnusedResources(std::move(resources), seen_resources,
                                 obj_holder_->all_removed_resources_map());
}

ByteString CPDF_PageContentGenerator::RealizeResource(
    const CPDF_Object* pResource,
    ByteStringView type) const {
  DCHECK(pResource);
  if (!obj_holder_->GetResources()) {
    obj_holder_->SetResources(document_->NewIndirect<CPDF_Dictionary>());
    obj_holder_->GetMutableDict()->SetNewFor<CPDF_Reference>(
        pdfium::page_object::kResources, document_,
        obj_holder_->GetResources()->GetObjNum());
  }

  RetainPtr<CPDF_Dictionary> resource_dict =
      obj_holder_->GetMutableResources()->GetOrCreateDictFor(type);
  const auto& all_removed_resources_map =
      obj_holder_->all_removed_resources_map();
  auto it = all_removed_resources_map.find(type);
  const CPDF_PageObjectHolder::RemovedResourceMap* removed_resource_map =
      it != all_removed_resources_map.end() ? &it->second : nullptr;
  ByteString name;
  int idnum = 1;
  while (true) {
    name = ByteString::Format("FX%c%d", type[0], idnum);
    // Avoid name collisions with existing `resource_dict` entries.
    if (resource_dict->KeyExist(name.AsStringView())) {
      idnum++;
      continue;
    }

    // Also avoid collisions with entries in `removed_resource_map`, since those
    // entries may move back into `resource_dict`.
    if (removed_resource_map && pdfium::Contains(*removed_resource_map, name)) {
      idnum++;
      continue;
    }

    resource_dict->SetNewFor<CPDF_Reference>(name, document_,
                                             pResource->GetObjNum());
    return name;
  }
}

bool CPDF_PageContentGenerator::ProcessPageObjects(fxcrt::ostringstream* buf) {
  bool bDirty = false;
  std::unique_ptr<const CPDF_ContentMarks> empty_content_marks =
      std::make_unique<CPDF_ContentMarks>();
  const CPDF_ContentMarks* content_marks = empty_content_marks.get();

  for (auto& pPageObj : page_objects_) {
    if (obj_holder_->IsPage() &&
        (!pPageObj->IsDirty() || !pPageObj->IsActive())) {
      continue;
    }

    bDirty = true;
    content_marks = ProcessContentMarks(buf, pPageObj, content_marks);
    ProcessPageObject(buf, pPageObj);
  }
  FinishMarks(buf, content_marks);
  return bDirty;
}

void CPDF_PageContentGenerator::UpdateStreamlessPageObjects(
    int new_content_stream_index) {
  for (auto& pPageObj : page_objects_) {
    if (!pPageObj->IsActive()) {
      continue;
    }

    if (pPageObj->GetContentStream() == CPDF_PageObject::kNoContentStream) {
      pPageObj->SetContentStream(new_content_stream_index);
    }
  }
}

const CPDF_ContentMarks* CPDF_PageContentGenerator::ProcessContentMarks(
    fxcrt::ostringstream* buf,
    const CPDF_PageObject* pPageObj,
    const CPDF_ContentMarks* pPrev) {
  const CPDF_ContentMarks* pNext = pPageObj->GetContentMarks();
  const size_t first_different = pPrev->FindFirstDifference(pNext);

  // Close all marks that are in prev but not in next.
  // Technically we should iterate backwards to close from the top to the
  // bottom, but since the EMC operators do not identify which mark they are
  // closing, it does not matter.
  for (size_t i = first_different; i < pPrev->CountItems(); ++i) {
    *buf << "EMC\n";
  }

  // Open all marks that are in next but not in prev.
  for (size_t i = first_different; i < pNext->CountItems(); ++i) {
    const CPDF_ContentMarkItem* item = pNext->GetItem(i);

    // Write mark tag.
    *buf << "/" << PDF_NameEncode(item->GetName()) << " ";

    // If there are no parameters, write a BMC (begin marked content) operator.
    if (item->GetParamType() == CPDF_ContentMarkItem::kNone) {
      *buf << "BMC\n";
      continue;
    }

    // If there are parameters, write properties, direct or indirect.
    switch (item->GetParamType()) {
      case CPDF_ContentMarkItem::kDirectDict: {
        CPDF_StringArchiveStream archive_stream(buf);
        item->GetParam()->WriteTo(&archive_stream, nullptr);
        *buf << " ";
        break;
      }
      case CPDF_ContentMarkItem::kPropertiesDict: {
        *buf << "/" << PDF_NameEncode(item->GetPropertyName()) << " ";
        break;
      }
      case CPDF_ContentMarkItem::kNone:
        NOTREACHED();
    }

    // Write BDC (begin dictionary content) operator.
    *buf << "BDC\n";
  }

  return pNext;
}

void CPDF_PageContentGenerator::FinishMarks(
    fxcrt::ostringstream* buf,
    const CPDF_ContentMarks* pContentMarks) {
  // Technically we should iterate backwards to close from the top to the
  // bottom, but since the EMC operators do not identify which mark they are
  // closing, it does not matter.
  for (size_t i = 0; i < pContentMarks->CountItems(); ++i) {
    *buf << "EMC\n";
  }
}

void CPDF_PageContentGenerator::ProcessPageObject(fxcrt::ostringstream* buf,
                                                  CPDF_PageObject* pPageObj) {
  if (CPDF_ImageObject* pImageObject = pPageObj->AsImage()) {
    ProcessImage(buf, pImageObject);
  } else if (CPDF_FormObject* pFormObj = pPageObj->AsForm()) {
    ProcessForm(buf, pFormObj);
  } else if (CPDF_PathObject* pPathObj = pPageObj->AsPath()) {
    ProcessPath(buf, pPathObj);
  } else if (CPDF_TextObject* pTextObj = pPageObj->AsText()) {
    ProcessText(buf, pTextObj);
  }
  pPageObj->SetDirty(false);
}

void CPDF_PageContentGenerator::ProcessImage(fxcrt::ostringstream* buf,
                                             CPDF_ImageObject* pImageObj) {
  const CFX_Matrix& matrix = pImageObj->matrix();
  if ((matrix.a == 0 && matrix.b == 0) || (matrix.c == 0 && matrix.d == 0)) {
    return;
  }

  RetainPtr<CPDF_Image> pImage = pImageObj->GetImage();
  if (pImage->IsInline()) {
    return;
  }

  RetainPtr<const CPDF_Stream> pStream = pImage->GetStream();
  if (!pStream) {
    return;
  }

  ProcessGraphics(buf, pImageObj);

  if (!matrix.IsIdentity()) {
    WriteMatrix(*buf, matrix) << " cm ";
  }

  bool bWasInline = pStream->IsInline();
  if (bWasInline) {
    pImage->ConvertStreamToIndirectObject();
  }

  ByteString name = RealizeResource(pStream, "XObject");
  pImageObj->SetResourceName(name);

  if (bWasInline) {
    auto* pPageData = CPDF_DocPageData::FromDocument(document_);
    pImageObj->SetImage(pPageData->GetImage(pStream->GetObjNum()));
  }

  *buf << "/" << PDF_NameEncode(name) << " Do";
  EndProcessGraphics(*buf);
}

void CPDF_PageContentGenerator::ProcessForm(fxcrt::ostringstream* buf,
                                            CPDF_FormObject* pFormObj) {
  CPDF_Form* form_xobject = pFormObj->form();
  if (form_xobject->HasDirtyStreams()) {
    // The Form XObject itself has modified content (e.g., an object was
    // removed). We need to regenerate its content stream before using it.
    CPDF_PageContentGenerator form_content_generator(form_xobject);
    form_content_generator.GenerateContent();
  }

  const CFX_Matrix& matrix = pFormObj->form_matrix();
  if ((matrix.a == 0 && matrix.b == 0) || (matrix.c == 0 && matrix.d == 0)) {
    return;
  }

  RetainPtr<const CPDF_Stream> pStream = pFormObj->form()->GetStream();
  CHECK(pStream);
  ByteString name = RealizeResource(pStream.Get(), "XObject");
  pFormObj->SetResourceName(name);

  ProcessGraphics(buf, pFormObj);

  if (!matrix.IsIdentity()) {
    WriteMatrix(*buf, matrix) << " cm ";
  }

  *buf << "/" << PDF_NameEncode(name) << " Do";
  EndProcessGraphics(*buf);
}

// Processing path construction with operators from Table 4.9 of PDF spec 1.7:
// "re" appends a rectangle (here, used only if the whole path is a rectangle)
// "m" moves current point to the given coordinates
// "l" creates a line from current point to the new point
// "c" adds a Bezier curve from current to last point, using the two other
// points as the Bezier control points
// Note: "l", "c" change the current point
// "h" closes the subpath (appends a line from current to starting point)
void CPDF_PageContentGenerator::ProcessPathPoints(fxcrt::ostringstream* buf,
                                                  CPDF_Path* pPath) {
  pdfium::span<const CFX_Path::Point> points = pPath->GetPoints();
  if (pPath->IsRect()) {
    CFX_PointF diff = points[2].point_ - points[0].point_;
    WritePoint(*buf, points[0].point_) << " ";
    WritePoint(*buf, diff) << " re";
    return;
  }
  for (size_t i = 0; i < points.size(); ++i) {
    if (i > 0) {
      *buf << " ";
    }

    WritePoint(*buf, points[i].point_);

    CFX_Path::Point::Type point_type = points[i].type_;
    if (point_type == CFX_Path::Point::Type::kMove) {
      *buf << " m";
    } else if (point_type == CFX_Path::Point::Type::kLine) {
      *buf << " l";
    } else if (point_type == CFX_Path::Point::Type::kBezier) {
      if (i + 2 >= points.size() ||
          !points[i].IsTypeAndOpen(CFX_Path::Point::Type::kBezier) ||
          !points[i + 1].IsTypeAndOpen(CFX_Path::Point::Type::kBezier) ||
          points[i + 2].type_ != CFX_Path::Point::Type::kBezier) {
        // If format is not supported, close the path and paint
        *buf << " h";
        break;
      }
      *buf << " ";
      WritePoint(*buf, points[i + 1].point_) << " ";
      WritePoint(*buf, points[i + 2].point_) << " c";
      i += 2;
    }
    if (points[i].close_figure_) {
      *buf << " h";
    }
  }
}

// Processing path painting with operators from Table 4.10 of PDF spec 1.7:
// Path painting operators: "S", "n", "B", "f", "B*", "f*", depending on
// the filling mode and whether we want stroking the path or not.
void CPDF_PageContentGenerator::ProcessPath(fxcrt::ostringstream* buf,
                                            CPDF_PathObject* pPathObj) {
  ProcessGraphics(buf, pPathObj);

  const CFX_Matrix& matrix = pPathObj->matrix();
  if (!matrix.IsIdentity()) {
    WriteMatrix(*buf, matrix) << " cm ";
  }

  ProcessPathPoints(buf, &pPathObj->path());

  if (pPathObj->has_no_filltype()) {
    *buf << (pPathObj->stroke() ? " S" : " n");
  } else if (pPathObj->has_winding_filltype()) {
    *buf << (pPathObj->stroke() ? " B" : " f");
  } else if (pPathObj->has_alternate_filltype()) {
    *buf << (pPathObj->stroke() ? " B*" : " f*");
  }
  EndProcessGraphics(*buf);
}

// This method supports color operators rg and RGB from Table 4.24 of PDF spec
// 1.7. A color will not be set if the colorspace is not DefaultRGB or the RGB
// values cannot be obtained. The method also adds an external graphics
// dictionary, as described in Section 4.3.4.
// "rg" sets the fill color, "RG" sets the stroke color (using DefaultRGB)
// "w" sets the stroke line width.
// "ca" sets the fill alpha, "CA" sets the stroke alpha.
// "W" and "W*" modify the clipping path using the nonzero winding rule and
// even-odd rules, respectively.
// "q" saves the graphics state, so that the settings can later be reversed
void CPDF_PageContentGenerator::ProcessGraphics(fxcrt::ostringstream* buf,
                                                CPDF_PageObject* pPageObj) {
  *buf << "q ";
  if (WriteColorToStream(*buf, pPageObj->color_state().GetFillColor())) {
    *buf << " rg ";
  }
  if (WriteColorToStream(*buf, pPageObj->color_state().GetStrokeColor())) {
    *buf << " RG ";
  }
  float line_width = pPageObj->graph_state().GetLineWidth();
  if (line_width != 1.0f) {
    WriteFloat(*buf, line_width) << " w ";
  }
  CFX_GraphStateData::LineCap lineCap = pPageObj->graph_state().GetLineCap();
  if (lineCap != CFX_GraphStateData::LineCap::kButt) {
    *buf << static_cast<int>(lineCap) << " J ";
  }
  CFX_GraphStateData::LineJoin lineJoin = pPageObj->graph_state().GetLineJoin();
  if (lineJoin != CFX_GraphStateData::LineJoin::kMiter) {
    *buf << static_cast<int>(lineJoin) << " j ";
  }
  std::vector<float> dash_array = pPageObj->graph_state().GetLineDashArray();
  if (dash_array.size()) {
    *buf << "[";
    for (size_t i = 0; i < dash_array.size(); ++i) {
      if (i > 0) {
        *buf << " ";
      }
      WriteFloat(*buf, dash_array[i]);
    }
    *buf << "] ";
    WriteFloat(*buf, pPageObj->graph_state().GetLineDashPhase()) << " d ";
  }

  const CPDF_ClipPath& clip_path = pPageObj->clip_path();
  if (clip_path.HasRef()) {
    for (size_t i = 0; i < clip_path.GetPathCount(); ++i) {
      CPDF_Path path = clip_path.GetPath(i);
      ProcessPathPoints(buf, &path);
      switch (clip_path.GetClipType(i)) {
        case CFX_FillRenderOptions::FillType::kWinding:
          *buf << " W ";
          break;
        case CFX_FillRenderOptions::FillType::kEvenOdd:
          *buf << " W* ";
          break;
        case CFX_FillRenderOptions::FillType::kNoFill:
          NOTREACHED();
      }

      // Use a no-op path-painting operator to terminate the path without
      // causing any marks to be placed on the page.
      *buf << "n ";
    }
  }

  GraphicsData graphD;
  graphD.fillAlpha = pPageObj->general_state().GetFillAlpha();
  graphD.strokeAlpha = pPageObj->general_state().GetStrokeAlpha();
  graphD.blendType = pPageObj->general_state().GetBlendType();
  if (graphD.fillAlpha == 1.0f && graphD.strokeAlpha == 1.0f &&
      graphD.blendType == BlendMode::kNormal) {
    return;
  }

  ByteString name;
  std::optional<ByteString> maybe_name = obj_holder_->GraphicsMapSearch(graphD);
  if (maybe_name.has_value()) {
    name = std::move(maybe_name.value());
  } else {
    auto gsDict = document_->New<CPDF_Dictionary>();
    if (graphD.fillAlpha != 1.0f) {
      gsDict->SetNewFor<CPDF_Number>("ca", graphD.fillAlpha);
    }

    if (graphD.strokeAlpha != 1.0f) {
      gsDict->SetNewFor<CPDF_Number>("CA", graphD.strokeAlpha);
    }

    if (graphD.blendType != BlendMode::kNormal) {
      gsDict->SetNewFor<CPDF_Name>("BM",
                                   pPageObj->general_state().GetBlendMode());
    }
    document_->AddIndirectObject(gsDict);
    name = RealizeResource(std::move(gsDict), "ExtGState");
    pPageObj->mutable_general_state().SetGraphicsResourceNames({name});
    obj_holder_->GraphicsMapInsert(graphD, name);
  }
  *buf << "/" << PDF_NameEncode(name) << " gs ";
}

void CPDF_PageContentGenerator::ProcessDefaultGraphics(
    fxcrt::ostringstream* buf) {
  *buf << "0 0 0 RG 0 0 0 rg 1 w "
       << static_cast<int>(CFX_GraphStateData::LineCap::kButt) << " J "
       << static_cast<int>(CFX_GraphStateData::LineJoin::kMiter) << " j\n";
  default_graphics_name_ = GetOrCreateDefaultGraphics();
  *buf << "/" << PDF_NameEncode(default_graphics_name_) << " gs ";
}

ByteString CPDF_PageContentGenerator::GetOrCreateDefaultGraphics() const {
  GraphicsData defaultGraphics;
  defaultGraphics.fillAlpha = 1.0f;
  defaultGraphics.strokeAlpha = 1.0f;
  defaultGraphics.blendType = BlendMode::kNormal;

  std::optional<ByteString> maybe_name =
      obj_holder_->GraphicsMapSearch(defaultGraphics);
  if (maybe_name.has_value()) {
    return maybe_name.value();
  }

  auto gsDict = document_->New<CPDF_Dictionary>();
  gsDict->SetNewFor<CPDF_Number>("ca", defaultGraphics.fillAlpha);
  gsDict->SetNewFor<CPDF_Number>("CA", defaultGraphics.strokeAlpha);
  gsDict->SetNewFor<CPDF_Name>("BM", "Normal");
  document_->AddIndirectObject(gsDict);
  ByteString name = RealizeResource(std::move(gsDict), "ExtGState");
  obj_holder_->GraphicsMapInsert(defaultGraphics, name);
  return name;
}

// This method adds text to the buffer, BT begins the text object, ET ends it.
// Tm sets the text matrix (allows positioning and transforming text).
// Tf sets the font name (from Font in Resources) and font size.
// Tr sets the text rendering mode.
// TJ sets the actual text, <####...> is used when specifying charcodes.
void CPDF_PageContentGenerator::ProcessText(fxcrt::ostringstream* buf,
                                            CPDF_TextObject* pTextObj) {
  ProcessGraphics(buf, pTextObj);
  *buf << "BT ";

  const CFX_Matrix& matrix = pTextObj->GetTextMatrix();
  if (!matrix.IsIdentity()) {
    WriteMatrix(*buf, matrix) << " Tm ";
  }

  RetainPtr<CPDF_Font> font(pTextObj->GetFont());
  if (!font) {
    font = CPDF_Font::GetStockFont(document_, "Helvetica");
  }

  FontData data;
  const CPDF_FontEncoding* pEncoding = nullptr;
  if (font->IsType1Font()) {
    data.type = "Type1";
    pEncoding = font->AsType1Font()->GetEncoding();
  } else if (font->IsTrueTypeFont()) {
    data.type = "TrueType";
    pEncoding = font->AsTrueTypeFont()->GetEncoding();
  } else if (font->IsCIDFont()) {
    data.type = "Type0";
  } else {
    return;
  }
  data.baseFont = font->GetBaseFontName();

  ByteString dict_name;
  std::optional<ByteString> maybe_name = obj_holder_->FontsMapSearch(data);
  if (maybe_name.has_value()) {
    dict_name = std::move(maybe_name.value());
  } else {
    RetainPtr<const CPDF_Object> pIndirectFont = font->GetFontDict();
    if (pIndirectFont->IsInline()) {
      // In this case we assume it must be a standard font
      auto font_dict = document_->New<CPDF_Dictionary>();
      font_dict->SetNewFor<CPDF_Name>("Type", "Font");
      font_dict->SetNewFor<CPDF_Name>("Subtype", data.type);
      font_dict->SetNewFor<CPDF_Name>("BaseFont", data.baseFont);
      if (pEncoding) {
        font_dict->SetFor("Encoding",
                          pEncoding->Realize(document_->GetByteStringPool()));
      }
      document_->AddIndirectObject(font_dict);
      pIndirectFont = std::move(font_dict);
    }
    dict_name = RealizeResource(std::move(pIndirectFont), "Font");
    obj_holder_->FontsMapInsert(data, dict_name);
  }
  pTextObj->SetResourceName(dict_name);

  *buf << "/" << PDF_NameEncode(dict_name) << " ";
  WriteFloat(*buf, pTextObj->GetFontSize()) << " Tf ";
  *buf << static_cast<int>(pTextObj->GetTextRenderMode()) << " Tr ";

  *buf << "[";
  ByteString text;
  for (auto [char_code, char_kerning] :
       fxcrt::Zip(pTextObj->GetCharCodes(), pTextObj->GetCharKernings())) {
    font->AppendChar(&text, char_code);
    if (char_kerning != 0) {
      *buf << PDF_HexEncodeString(text.AsStringView()) << " ";
      text.clear();
      WriteFloat(*buf, char_kerning) << " ";
    }
  }
  if (!text.IsEmpty()) {
    *buf << PDF_HexEncodeString(text.AsStringView());
  }
  *buf << "] TJ ET";
  EndProcessGraphics(*buf);
}
