Chrome RenderText分析(1)

 

先从一些基础的类开始

1.Range

// A Range contains two integer values that represent a numeric range, like the
// range of characters in a text selection. A range is made of a start and end
// position; when they are the same, the Range is akin to a caret. Note that
// |start_| can be greater than |end_| to respect the directionality of the
// range.

文字绘制前后有索引,比如

“hello你好”便是hello |0,4|5,6|

image

一段简单的测试代码

TEST(RangeTest, StartEndInit) {
  ui::Range r(10, 15);
  EXPECT_EQ(10U, r.start());
  EXPECT_EQ(15U, r.end());
  EXPECT_EQ(5U, r.length());
  EXPECT_FALSE(r.is_reversed());
  EXPECT_FALSE(r.is_empty());
  EXPECT_TRUE(r.IsValid());
  EXPECT_EQ(10U, r.GetMin());
  EXPECT_EQ(15U, r.GetMax());
}

2.Font

// Font provides a wrapper around an underlying font. Copy and assignment
// operators are explicitly allowed, and cheap.

由于是跨平台的,所以其GetNativeFont返回则HFONT

image

3.RenderTextWin

// RenderTextWin is the Windows implementation of RenderText using Uniscribe.

Lay Out Text Using Uniscribe

Your application can use the following steps to lay out out a text paragraph with Uniscribe. This procedure assumes that the application has already divided the paragraph into runs.

  1. Call ScriptRecordDigitSubstitution only when starting or when receiving a WM_SETTINGCHANGE message.
  2. (Optional) Call ScriptIsComplex to determine if the paragraph requires complex processing.
  3. (Optional) If using Uniscribe to handle bidirectional text and/or digit substitution, call ScriptApplyDigitSubstitution to prepare the SCRIPT_CONTROL and SCRIPT_STATE structures as inputs to ScriptItemize. If skipping this step, but still requiring digit substitution, substitute national digits for Unicode U+0030 through U+0039 (European digits). For information about digit substitution, see Digit Shapes.
  4. Call ScriptItemize to divide the paragraph into items. If not using Uniscribe for digit substitution and the bidirectional order is known, for example, because of the keyboard layout used to enter the character, call ScriptItemize. In the call, provide null pointers for the SCRIPT_CONTROL and SCRIPT_STATE structures. This technique generates items by use of the shaping engine only, and the items can be reordered using the engine information.

    Note   Typically, applications that work only with left-to-right scripts and without any digit substitution should pass null pointers for the SCRIPT_CONTROL and SCRIPT_STATE structures.

  5. Merge the item information with the run information to produce ranges.
  6. Call ScriptShape to identify clusters and generate glyphs.
  7. If ScriptShape returns the code USP_E_SCRIPT_NOT_IN_FONT or S_OK with the output containing missing glyphs, select characters from a different font. Either substitute another font or disable shaping by setting the eScript member of the SCRIPT_ANALYSIS structure passed to ScriptShape to SCRIPT_UNDEFINED. For more information, see Using Font Fallback.
  8. Call ScriptPlace to generate advance widths and x and y positions for the glyphs in each successive range. This is the first step for which text size becomes a consideration.
  9. Sum the range sizes until the line overflows.
  10. Break the range on a word boundary by using the fSoftBreak and fWhiteSpace members in the logical attributes. To break a single character cluster off the run, use the information returned by calling ScriptBreak.

    Note  Decide if the first code point of a range should be a word break point because the last character of the previous range requires it. For example, if one range ends in a comma, consider the first character of the next range to be a word break point.

  11. Repeat steps 6 through 10 for each line in the paragraph. However, if breaking the last run on the line, call ScriptShape to reshape the remaining part of the run as the first run on the next line.

 

4.TextRun

用来描述每小段文字

image

重点来看RenderTextWin的流程

GetStringSize用于获取文字的尺寸

Size RenderTextWin::GetStringSize() {
  EnsureLayout();
  return string_size_;
}

首先要通过ScriptItemize方法来把文字分段,封装成TextRun数组

void RenderTextWin::ItemizeLogicalText() {


  if (script_items_count <= 0)
    return;

  // Temporarily apply composition underlines and selection colors.
  ApplyCompositionAndSelectionStyles();

  // Build the list of runs from the script items and ranged colors/styles.
  // TODO(msw): Only break for bold/italic, not color etc. See TextRun comment.
  internal::StyleIterator style(colors(), styles());
  SCRIPT_ITEM* script_item = &script_items[0];
  const size_t layout_text_length = GetLayoutText().length();
  for (size_t run_break = 0; run_break < layout_text_length;) {
    internal::TextRun* run = new internal::TextRun();
    run->range.set_start(run_break);
    run->font = GetFont();
    run->font_style = (style.style(BOLD) ? Font::BOLD : 0) |
                      (style.style(ITALIC) ? Font::ITALIC : 0);
    DeriveFontIfNecessary(run->font.GetFontSize(), run->font.GetHeight(),
                          run->font_style, &run->font);
    run->foreground = style.color();
    run->strike = style.style(STRIKE);
    run->diagonal_strike = style.style(DIAGONAL_STRIKE);
    run->underline = style.style(UNDERLINE);
    run->script_analysis = script_item->a;

    // Find the next break and advance the iterators as needed.
    const size_t script_item_break = (script_item + 1)->iCharPos;
    run_break = std::min(script_item_break,
                         TextIndexToLayoutIndex(style.GetRange().end()));
    style.UpdatePosition(LayoutIndexToTextIndex(run_break));
    if (script_item_break == run_break)
      script_item++;
    run->range.set_end(run_break);
    runs_.push_back(run);
  }

  // Undo the temporarily applied composition underlines and selection colors.
  UndoCompositionAndSelectionStyles();
}

注意点:

The function returns E_OUTOFMEMORY if the value of cMaxItems is insufficient. As in all error cases, no items are fully processed and no part of the output array contains defined values. If the function returns E_OUTOFMEMORY, the application can call it again with a larger pItems buffer.

 

1.先看斜体部分,通过ScriptItemize,得到一个SCRIPT_ITEM数组

2.再通过SCRIPT_ITEM数组封装成TextRun数组

5.TextRun的文字样式(BreakList)

看如下文字样式
image

文字的样式有颜色,格式(斜体,下划线)

想要呈现”[set]”上面的文字需要把文字拆成几个分段

1.颜色分成5段[0,1,2,3,4]
2.字体分段: 每个文字可能有多个格式,比如斜体和粗体是同时出现的

为了储存以上样式信息,就出现一个BreakList的数据结构

// BreakLists manage ordered, non-overlapping, and non-repeating ranged values.
// These may be used to apply ranged colors and styles to text, for an example.
//
// Each break stores the start position and value of its associated range.
// A solitary break at position 0 applies to the entire space [0, max_).
// |max_| is initially 0 and should be set to match the available ranged space.
// The first break always has position 0, to ensure all positions have a value.
// The value of the terminal break applies to the range [break.first, max_).
// The value of other breaks apply to the range [break.first, (break+1).first).

  typedef std::pair<size_t, T> Break;
  std::vector<Break> breaks_;
  size_t max_;

上面颜色的存储就是[0,green],[1,blue],[2,red],[3,blue],[4,green]

字体分段则是一个列表的BreakList:[[0,true],[1,true]],[[0,true],[1,true]]

看一下类成员

image

 

  1. SetValue方法设置一个默认值
  2. ApplyValue则设置区间的start和end值,同时清除区间的值

BreakList的设置逻辑是区间相邻不会出现重复的值.
即不会出现[0,true][1,true],[2,true]这种情况

看几个单元测试用例

1.SetValue测试

TEST_F(BreakListTest, SetValue) {
  // Check the default values applied to new instances.
  BreakList<bool> style_breaks(false);
  EXPECT_TRUE(style_breaks.EqualsValueForTesting(false));
  style_breaks.SetValue(true);
  EXPECT_TRUE(style_breaks.EqualsValueForTesting(true));

  // Ensure that setting values works correctly.
  BreakList<SkColor> color_breaks(SK_ColorRED);
  EXPECT_TRUE(color_breaks.EqualsValueForTesting(SK_ColorRED));
  color_breaks.SetValue(SK_ColorBLACK);
  EXPECT_TRUE(color_breaks.EqualsValueForTesting(SK_ColorBLACK));
}

重新设置SetValue回清空所有段落

2.ApplyValue

  BreakList<bool> breaks(false);
  const size_t max = 99;
  breaks.SetMax(max);
  // Apply a value to a valid range, check breaks; repeating should be no-op.
  std::vector<std::pair<size_t, bool> > expected;
  expected.push_back(std::pair<size_t, bool>(0, false));
  expected.push_back(std::pair<size_t, bool>(2, true));
  expected.push_back(std::pair<size_t, bool>(3, false));
  for (size_t i = 0; i < 2; ++i) {
    breaks.ApplyValue(true, ui::Range(2, 3));
    EXPECT_TRUE(breaks.EqualsForTesting(expected));
  }

ApplyValue区间的end如果不是max值则是SetValue的默认值

  // Ensure applying a value over [0, |max|) is the same as setting a value.
  breaks.ApplyValue(false, ui::Range(0, max));
  EXPECT_TRUE(breaks.EqualsValueForTesting(false));

ApplyValue区间的end如果是max值则段区域只有1段

  // Ensure applying a value that is already applied has no effect.
  breaks.ApplyValue(false, ui::Range(0, 2));
  breaks.ApplyValue(false, ui::Range(3, 6));
  breaks.ApplyValue(false, ui::Range(7, max));
  EXPECT_TRUE(breaks.EqualsValueForTesting(false));

同理,设置区域的Value都一样的话,那还是在同一个区域,也还只有1段

看完BreakList的用法之后,我们回头来看RenderTextWin中关于color和style的应用

变量

  // Color and style breaks, used to color and stylize ranges of text.
  // BreakList positions are stored with text indices, not layout indices.
  // TODO(msw): Expand to support cursor, selection, background, etc. colors.
  BreakList<SkColor> colors_;
  std::vector<BreakList<bool> > styles_;

方法

void RenderText::SetColor(SkColor value) {
  colors_.SetValue(value);
}

void RenderText::ApplyColor(SkColor value, const ui::Range& range) {
  colors_.ApplyValue(value, range);

}

void RenderText::SetStyle(TextStyle style, bool value) {
  styles_[style].SetValue(value);

 }

void RenderText::ApplyStyle(TextStyle style,
                            bool value,
                            const ui::Range& range) {
  styles_[style].ApplyValue(value, range);

}

回到ItemizeLogicalText方法中看StyleIterator类,其均是取BreakList的首个值

StyleIterator::StyleIterator(const BreakList<SkColor>& colors,
                             const std::vector<BreakList<bool> >& styles)
    : colors_(colors),
      styles_(styles) {
  color_ = colors_.breaks().begin();
  for (size_t i = 0; i < styles_.size(); ++i)
    style_.push_back(styles_[i].breaks().begin());
}
详细分析解释一下chromium源码中的下面的函数: class OmniboxViewViews : public OmniboxView, public views::Textfield, #if BUILDFLAG(IS_CHROMEOS) public ash::input_method::InputMethodManager::CandidateWindowObserver, #endif public views::TextfieldController, public ui::CompositorObserver, public TemplateURLServiceObserver { METADATA_HEADER(OmniboxViewViews, views::Textfield) public: // Max width of the gradient mask used to smooth ElideAnimation edges. static const int kSmoothingGradientMaxWidth = 15; OmniboxViewViews(std::unique_ptr<OmniboxClient> client, bool popup_window_mode, LocationBarView* location_bar_view, const gfx::FontList& font_list); OmniboxViewViews(const OmniboxViewViews&) = delete; OmniboxViewViews& operator=(const OmniboxViewViews&) = delete; ~OmniboxViewViews() override; // Initialize, create the underlying views, etc. void Init(); // Exposes the RenderText for tests. #if defined(UNIT_TEST) gfx::RenderText* GetRenderText() { return views::Textfield::GetRenderText(); } #endif // For use when switching tabs, this saves the current state onto the tab so // that it can be restored during a later call to Update(). void SaveStateToTab(content::WebContents* tab); // Called when the window's active tab changes. void OnTabChanged(const content::WebContents* web_contents); // Called to clear the saved state for |web_contents|. void ResetTabState(content::WebContents* web_contents); // Installs the placeholder text with the name of the current default search // provider. For example, if Google is the default search provider, this shows // "Search Google or type a URL" when the Omnibox is empty and unfocused. void InstallPlaceholderText(); // Indicates if the cursor is at the end of the input. Requires that both // ends of the selection reside there. bool GetSelectionAtEnd() const; // Returns the width in pixels needed to display the current text. The // returned value includes margins. int GetTextWidth() const; // Returns the width in pixels needed to display the current text unelided. int GetUnelidedTextWidth() const; // Returns the omnibox's width in pixels. int GetWidth() const; // OmniboxView: void EmphasizeURLComponents() override; void Update() override; std::u16string GetText() const override; using OmniboxView::SetUserText; void SetUserText(const std::u16string& text, bool update_popup) override; void SetWindowTextAndCaretPos(const std::u16string& text, size_t caret_pos, bool update_popup, bool notify_text_changed) override; void SetAdditionalText(const std::u16string& additional_text) override; void EnterKeywordModeForDefaultSearchProvider() override; bool IsSelectAll() const override; void GetSelectionBounds(std::u16string::size_type* start, std::u16string::size_type* end) const override; void SelectAll(bool reversed) override; void RevertAll() override; void SetFocus(bool is_user_initiated) override; bool IsImeComposing() const override; gfx::NativeView GetRelativeWindowForPopup() const override; bool IsImeShowingPopup() const override; // views::Textfield: gfx::Size GetMinimumSize() const override; bool OnMousePressed(const ui::MouseEvent& event) override; bool OnMouseDragged(const ui::MouseEvent& event) override; void OnMouseReleased(const ui::MouseEvent& event) override; void OnPaint(gfx::Canvas* canvas) override; void ExecuteCommand(int command_id, int event_flags) override; void OnInputMethodChanged() override; void AddedToWidget() override; void RemovedFromWidget() override; std::u16string GetLabelForCommandId(int command_id) const override; bool IsCommandIdEnabled(int command_id) const override; // For testing only. OmniboxPopupView* GetPopupViewForTesting() const; protected: // OmniboxView: void UpdateSchemeStyle(const gfx::Range& range) override; // views::Textfield: void OnThemeChanged() override; bool IsDropCursorForInsertion() const override; // Wrappers around Textfield methods that tests can override. virtual void ApplyColor(SkColor color, const gfx::Range& range); virtual void ApplyStyle(gfx::TextStyle style, bool value, const gfx::Range& range); private: friend class TestingOmniboxView; FRIEND_TEST_ALL_PREFIXES(OmniboxPopupViewViewsTest, EmitAccessibilityEvents); // TODO(tommycli): Remove the rest of these friends after porting these // browser tests to unit tests. FRIEND_TEST_ALL_PREFIXES(OmniboxViewViewsTest, CloseOmniboxPopupOnTextDrag); FRIEND_TEST_ALL_PREFIXES(OmniboxViewViewsTest, FriendlyAccessibleLabel); FRIEND_TEST_ALL_PREFIXES(OmniboxViewViewsTest, DoNotNavigateOnDrop); FRIEND_TEST_ALL_PREFIXES(OmniboxViewViewsTest, AyncDropCallback); FRIEND_TEST_ALL_PREFIXES(OmniboxViewViewsTest, AccessibleTextSelectBoundTest); enum class UnelisionGesture { HOME_KEY_PRESSED, MOUSE_RELEASE, OTHER, }; // Update the field with |text| and set the selection. |ranges| should not be // empty; even text with no selections must have at least 1 empty range in // |ranges| to indicate the cursor position. void SetTextAndSelectedRange(const std::u16string& text, const gfx::Range& selection); // Returns the selected text. std::u16string_view GetSelectedText() const; void UpdateAccessibleTextSelection() override; // Paste text from the clipboard into the omnibox. // Textfields implementation of Paste() pastes the contents of the clipboard // as is. We want to strip whitespace and other things (see GetClipboardText() // for details). The function invokes OnBefore/AfterPossibleChange() as // necessary. void OnOmniboxPaste(); // Handle keyword hint tab-to-search and tabbing through dropdown results. bool HandleEarlyTabActions(const ui::KeyEvent& event); void ClearAccessibilityLabel(); void SetAccessibilityLabel(const std::u16string& display_text, const AutocompleteMatch& match, bool notify_text_changed) override; // Returns true if the user text was updated with the full URL (without // steady-state elisions). |gesture| is the user gesture causing unelision. bool UnapplySteadyStateElisions(UnelisionGesture gesture); #if BUILDFLAG(IS_MAC) void AnnounceFriendlySuggestionText(); #endif // Get the preferred text input type, this checks the IME locale on Windows. ui::TextInputType GetPreferredTextInputType() const; // OmniboxView: void SetCaretPos(size_t caret_pos) override; void UpdatePopup() override; void ApplyCaretVisibility() override; void OnTemporaryTextMaybeChanged(const std::u16string& display_text, const AutocompleteMatch& match, bool save_original_selection, bool notify_text_changed) override; void OnInlineAutocompleteTextMaybeChanged( const std::u16string& user_text, const std::u16string& inline_autocompletion) override; void OnInlineAutocompleteTextCleared() override; void OnRevertTemporaryText(const std::u16string& display_text, const AutocompleteMatch& match) override; void OnBeforePossibleChange() override; bool OnAfterPossibleChange(bool allow_keyword_ui_change) override; void OnKeywordPlaceholderTextChange() override; gfx::NativeView GetNativeView() const override; void ShowVirtualKeyboardIfEnabled() override; void HideImeIfNeeded() override; int GetOmniboxTextLength() const override; void SetEmphasis(bool emphasize, const gfx::Range& range) override; // views::View void OnMouseMoved(const ui::MouseEvent& event) override; void OnMouseExited(const ui::MouseEvent& event) override; // views::Textfield: bool IsItemForCommandIdDynamic(int command_id) const override; void OnGestureEvent(ui::GestureEvent* event) override; bool SkipDefaultKeyEventProcessing(const ui::KeyEvent& event) override; bool HandleAccessibleAction(const ui::AXActionData& action_data) override; void OnFocus() override; void OnBlur() override; std::u16string GetSelectionClipboardText() const override; void DoInsertChar(char16_t ch) override; bool IsTextEditCommandEnabled(ui::TextEditCommand command) const override; void ExecuteTextEditCommand(ui::TextEditCommand command) override; bool ShouldShowPlaceholderText() const override; void UpdateAccessibleValue() override; // ash::input_method::InputMethodManager::CandidateWindowObserver: #if BUILDFLAG(IS_CHROMEOS) void CandidateWindowOpened( ash::input_method::InputMethodManager* manager) override; void CandidateWindowClosed( ash::input_method::InputMethodManager* manager) override; #endif // views::TextfieldController: void ContentsChanged(views::Textfield* sender, const std::u16string& new_contents) override; bool HandleKeyEvent(views::Textfield* sender, const ui::KeyEvent& key_event) override; void OnBeforeUserAction(views::Textfield* sender) override; void OnAfterUserAction(views::Textfield* sender) override; void OnAfterCutOrCopy(ui::ClipboardBuffer clipboard_buffer) override; void OnWriteDragData(ui::OSExchangeData* data) override; void OnGetDragOperationsForTextfield(int* drag_operations) override; void AppendDropFormats( int* formats, std::set<ui::ClipboardFormatType>* format_types) override; ui::mojom::DragOperation OnDrop(const ui::DropTargetEvent& event) override; views::View::DropCallback CreateDropCallback( const ui::DropTargetEvent& event) override; void UpdateContextMenu(ui::SimpleMenuModel* menu_contents) override; // ui::SimpleMenuModel::Delegate: bool IsCommandIdChecked(int id) const override; // ui::CompositorObserver: void OnCompositingDidCommit(ui::Compositor* compositor) override; void OnCompositingStarted(ui::Compositor* compositor, base::TimeTicks start_time) override; void OnDidPresentCompositorFrame( uint32_t frame_token, const gfx::PresentationFeedback& feedback) override; void OnCompositingShuttingDown(ui::Compositor* compositor) override; // TemplateURLServiceObserver: void OnTemplateURLServiceChanged() override; // Permits launch of the external protocol handler after user actions in // the omnibox. The handler needs to be informed that omnibox input should // always be considered "user gesture-triggered", lest it always return BLOCK. void PermitExternalProtocolHandler(); // Drops dragged text and updates `output_drag_op` accordingly. void PerformDrop(const ui::DropTargetEvent& event, ui::mojom::DragOperation& output_drag_op, std::unique_ptr<ui::LayerTreeOwner> drag_image_layer_owner); // Helper method to construct part of the context menu. void MaybeAddSendTabToSelfItem(ui::SimpleMenuModel* menu_contents); // Called when the popup view becomes visible. void OnPopupOpened(); // Helper for updating placeholder color depending on whether its a keyword or // DSE placeholder. void UpdatePlaceholderTextColor(); // When true, the location bar view is read only and also is has a slightly // different presentation (smaller font size). This is used for popups. bool popup_window_mode_; // Owns either an OmniboxPopupViewViews or an OmniboxPopupViewWebUI. std::unique_ptr<OmniboxPopupView> popup_view_; base::CallbackListSubscription popup_view_opened_subscription_; // Selection persisted across temporary text changes, like popup suggestions. gfx::Range saved_temporary_selection_; // Holds the user's selection across focus changes. There is only a saved // selection if this range IsValid(). gfx::Range saved_selection_for_focus_change_; // Tracking state before and after a possible change. State state_before_change_; bool ime_composing_before_change_ = false; // |location_bar_view_| can be NULL in tests. raw_ptr<LocationBarView> location_bar_view_; #if BUILDFLAG(IS_CHROMEOS) // True if the IME candidate window is open. When this is true, we want to // avoid showing the popup. So far, the candidate window is detected only // on Chrome OS. bool ime_candidate_window_open_ = false; #endif // True if any mouse button is currently depressed. bool is_mouse_pressed_ = false; // Applies a minimum threshold to drag events after unelision. Because the // text shifts after unelision, we don't want unintentional mouse drags to // change the selection. bool filter_drag_events_for_unelision_ = false; // Should we select all the text when we see the mouse button get released? // We select in response to a click that focuses the omnibox, but we defer // until release, setting this variable back to false if we saw a drag, to // allow the user to select just a portion of the text. bool select_all_on_mouse_release_ = false; // Indicates if we want to select all text in the omnibox when we get a // GESTURE_TAP. We want to select all only when the textfield is not in focus // and gets a tap. So we use this variable to remember focus state before tap. bool select_all_on_gesture_tap_ = false; // Whether the user should be notified if the clipboard is restricted. bool show_rejection_ui_if_any_ = false; // Keep track of the word that would be selected if URL is unelided between // a single and double click. This is an edge case where the elided URL is // selected. On the double click, unelision is performed in between the first // and second clicks. This results in both the wrong word to be selected and // the wrong selection length. For example, if example.com is shown and you // try to double click on the "x", it unelides to https://example.com after // the first click, resulting in "https" being selected. size_t next_double_click_selection_len_ = 0; size_t next_double_click_selection_offset_ = 0; // The time of the first character insert operation that has not yet been // painted. Used to measure omnibox responsiveness with a histogram. base::TimeTicks insert_char_time_; // The state machine for logging the Omnibox.CharTypedToRepaintLatency // histogram. enum { NOT_ACTIVE, // Not currently tracking a char typed event. CHAR_TYPED, // Character was typed. ON_PAINT_CALLED, // Character was typed and OnPaint() called. COMPOSITING_COMMIT, // Compositing was committed after OnPaint(). COMPOSITING_STARTED, // Compositing was started. } latency_histogram_state_; // The currently selected match, if any, with additional labelling text // such as the document title and the type of search, for example: // "Google https://google.com location from bookmark", or // "cats are liquid search suggestion". std::u16string friendly_suggestion_text_; // The number of added labelling characters before editable text begins. // For example, "Google https://google.com location from history", // this is set to 7 (the length of "Google "). int friendly_suggestion_text_prefix_length_; base::ScopedObservation<ui::Compositor, ui::CompositorObserver> scoped_compositor_observation_{this}; base::ScopedObservation<TemplateURLService, TemplateURLServiceObserver> scoped_template_url_service_observation_{this}; PrefChangeRegistrar pref_change_registrar_; base::WeakPtrFactory<OmniboxViewViews> weak_factory_{this}; };
最新发布
08-02
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值