0

pdf: Add entry and exit of annotation-mode

* Adds a new viewer element to host the Ink component. For now this is
  just a dummy implementation of the expected API allowing us to test
  entry/exit.
* `ContentController` is extended with load/unload
* `InkController` is added and made the current controller when in
  annotation mode, delegating behavior to the viewer-ink-host rather
  than plugin.
* The plugin interface is extended to allow the PDF to be saved and
  sent back regardless of whether the PDF has been modified.
* The loaded state of PDFViewer is centralized and exposed via the
  `loaded` property. This allows the switching logic and tests to wait
  for the mode switch to complete.

Bug: 902646
Change-Id: I2922cb121e510e9e84c21b75faddc48fa90a195c
Reviewed-on: https://chromium-review.googlesource.com/c/1368846
Commit-Queue: dstockwell <dstockwell@chromium.org>
Reviewed-by: Lei Zhang <thestig@chromium.org>
Reviewed-by: Henrique Nakashima <hnakashima@chromium.org>
Reviewed-by: Demetrios Papadopoulos <dpapad@chromium.org>
Cr-Commit-Position: refs/heads/master@{#617686}
This commit is contained in:
dstockwell
2018-12-19 00:20:01 +00:00
committed by Commit Bot
parent 6c13fac529
commit f905e88821
17 changed files with 401 additions and 49 deletions

@ -119,6 +119,8 @@
<include name="IDR_PDF_VIEWER_BOOKMARKS_CONTENT_JS" file="pdf/elements/viewer-bookmarks-content/viewer-bookmarks-content.js" type="BINDATA" />
<include name="IDR_PDF_VIEWER_ERROR_SCREEN_HTML" file="pdf/elements/viewer-error-screen/viewer-error-screen.html" type="BINDATA" />
<include name="IDR_PDF_VIEWER_ERROR_SCREEN_JS" file="pdf/elements/viewer-error-screen/viewer-error-screen.js" type="BINDATA" />
<include name="IDR_PDF_VIEWER_INK_HOST_HTML" file="pdf/elements/viewer-ink-host/viewer-ink-host.html" type="BINDATA" />
<include name="IDR_PDF_VIEWER_INK_HOST_JS" file="pdf/elements/viewer-ink-host/viewer-ink-host.js" type="BINDATA" />
<include name="IDR_PDF_VIEWER_PAGE_INDICATOR_HTML" file="pdf/elements/viewer-page-indicator/viewer-page-indicator.html" type="BINDATA" />
<include name="IDR_PDF_VIEWER_PAGE_INDICATOR_JS" file="pdf/elements/viewer-page-indicator/viewer-page-indicator.js" type="BINDATA" flattenhtml="true" />
<include name="IDR_PDF_VIEWER_PAGE_SELECTOR_HTML" file="pdf/elements/viewer-page-selector/viewer-page-selector.html" type="BINDATA" />

@ -10,6 +10,7 @@ group("closure_compile") {
":pdf_resources",
"elements/viewer-bookmark:closure_compile",
"elements/viewer-error-screen:closure_compile",
"elements/viewer-ink-host:closure_compile",
"elements/viewer-page-indicator:closure_compile",
"elements/viewer-page-selector:closure_compile",
"elements/viewer-password-screen:closure_compile",

@ -0,0 +1,14 @@
# Copyright 2018 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import("//third_party/closure_compiler/compile_js.gni")
js_type_check("closure_compile") {
deps = [
":viewer-ink-host",
]
}
js_library("viewer-ink-host") {
}

@ -0,0 +1,13 @@
<link rel="import" href="chrome://resources/html/polymer.html">
<dom-module id="viewer-ink-host">
<template>
<style>
:host {
visibility: hidden;
}
</style>
[[dummyContent_]]
</template>
<script src="viewer-ink-host.js"></script>
</dom-module>

@ -0,0 +1,49 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* Hosts the Ink component which is responsible for both PDF rendering and
* annotation when in annotation mode.
*/
Polymer({
is: 'viewer-ink-host',
properties: {
/** @private */
dummyContent_: String,
},
/** @private {?string} */
dummyFileName_: null,
/** @private {ArrayBuffer} */
dummyData_: null,
/**
* Begins annotation mode with the document represented by `data`.
* When the return value resolves the Ink component will be ready
* to render immediately.
*
* @param {string} fileName The name of the PDF file.
* @param {ArrayBuffer} data The contents of the PDF document.
* @return {!Promise} void value.
*/
load: async function(fileName, data) {
this.dummyContent_ = `Annotating ${data.byteLength} bytes`;
this.dummyFileName_ = fileName;
this.dummyData_ = data;
this.style.visibility = 'visible';
},
/**
* @return {!Promise<{fileName: string, dataToSave: ArrayBuffer}>}
* The serialized PDF document including any annotations that were made.
*/
saveDocument: async function() {
return {
fileName: this.dummyFileName_,
dataToSave: this.dummyData_,
};
},
});

@ -125,6 +125,7 @@
<div id="buttons" class="invisible">
<template is="dom-if" if="[[pdfAnnotationsEnabled]]">
<paper-icon-button id="annotate" icon="pdf:create"
on-click="toggleAnnotation"
aria-label$="{{strings.tooltipAnnotate}}"
title$="{{strings.tooltipAnnotate}}">
</paper-icon-button>
@ -160,7 +161,7 @@
</div>
</div>
<div id="progress-container">
<paper-progress id="progress" value="{{loadProgress}}"></paper-progress>
<paper-progress id="progress" value="[[loadProgress]]"></paper-progress>
</div>
</div>
</template>

@ -9,7 +9,7 @@ Polymer({
/**
* The current loading progress of the PDF document (0 - 100).
*/
loadProgress: {type: Number, observer: 'loadProgressChanged'},
loadProgress: {type: Number, observer: 'loadProgressChanged_'},
/**
* The title of the PDF document.
@ -36,6 +36,14 @@ Polymer({
*/
opened: {type: Boolean, value: true},
/**
* Whether the viewer is currently in annotation mode.
*/
annotationMode: {
type: Boolean,
notify: true,
},
/**
* Whether the PDF Annotations feature is enabled.
*/
@ -44,11 +52,18 @@ Polymer({
strings: Object,
},
loadProgressChanged: function() {
if (this.loadProgress >= 100) {
this.$.pageselector.classList.toggle('invisible', false);
this.$.buttons.classList.toggle('invisible', false);
this.$.progress.style.opacity = 0;
/**
* @param {number} newProgress
* @param {number} oldProgress
* @private
*/
loadProgressChanged_: function(newProgress, oldProgress) {
const loaded = newProgress >= 100;
const progressReset = newProgress < oldProgress;
if (progressReset || loaded) {
this.$.pageselector.classList.toggle('invisible', !loaded);
this.$.buttons.classList.toggle('invisible', !loaded);
this.$.progress.style.opacity = loaded ? 0 : 1;
}
},
@ -126,6 +141,10 @@ Polymer({
print: function() {
this.fire('print');
},
toggleAnnotation: function() {
this.annotationMode = !this.annotationMode;
}
});
})();

@ -20,6 +20,7 @@ viewer-pdf-toolbar {
z-index: 4;
}
viewer-ink-host,
#plugin {
height: 100%;
position: fixed;

@ -3,6 +3,7 @@
<head>
<meta charset="utf-8">
<link rel="import" href="elements/viewer-error-screen/viewer-error-screen.html">
<link rel="import" href="elements/viewer-ink-host/viewer-ink-host.html">
<link rel="import" href="elements/viewer-page-indicator/viewer-page-indicator.html">
<link rel="import" href="elements/viewer-page-selector/viewer-page-selector.html">
<link rel="import" href="elements/viewer-password-screen/viewer-password-screen.html">
@ -38,6 +39,7 @@
<script src="pdf_scripting_api.js"></script>
<script src="chrome://resources/js/load_time_data.js"></script>
<script src="chrome://resources/js/util.js"></script>
<script src="chrome://resources/js/promise_resolver.js"></script>
<script src="browser_api.js"></script>
<script src="metrics.js"></script>
<script src="pdf_viewer.js"></script>

@ -125,9 +125,7 @@ function PDFViewer(browserApi) {
this.isFormFieldFocused_ = false;
this.beepCount_ = 0;
this.delayedScriptingMessages_ = [];
/** @private {!Set<string>} */
this.pendingTokens_ = new Set();
this.loaded_ = new PromiseResolver();
this.isPrintPreview_ = location.origin === 'chrome://print';
this.isPrintPreviewLoadingFinished_ = false;
@ -220,10 +218,12 @@ function PDFViewer(browserApi) {
} else {
this.plugin_.setAttribute('full-frame', '');
}
document.body.appendChild(this.plugin_);
this.pluginController_ =
new PluginController(this.plugin_, this, this.viewport_);
this.inkController_ = new InkController(this, this.viewport_);
this.currentController_ = this.pluginController_;
// Setup the button event listeners.
@ -252,6 +252,8 @@ function PDFViewer(browserApi) {
'print', () => this.currentController_.print());
this.toolbar_.addEventListener(
'rotate-right', () => this.currentController_.rotateClockwise());
this.toolbar_.addEventListener(
'annotation-mode-changed', e => this.annotationModeChanged_(e));
this.toolbar_.docTitle = getFilenameFromURL(this.originalUrl_);
}
@ -472,6 +474,52 @@ PDFViewer.prototype = {
}
},
/**
* Handles the annotation mode being toggled on or off.
*
* @param {CustomEvent} e
* @private
*/
annotationModeChanged_: async function(e) {
const annotationMode = e.detail.value;
if (annotationMode) {
// TODO(dstockwell): add assert lib and replace this with assert
if (this.currentController_ != this.pluginController_) {
throw new Error(
'Plugin controller is not current, cannot enter annotation mode');
}
// Enter annotation mode.
// TODO(dstockwell): set plugin read-only, begin transition
this.updateProgress(0);
// TODO(dstockwell): handle save failure
const result = await this.pluginController_.save(true);
// TODO(dstockwell): feed real progress data from the Ink component
this.updateProgress(50);
await this.inkController_.load(result.fileName, result.dataToSave);
this.currentController_ = this.inkController_;
this.pluginController_.unload();
this.updateProgress(100);
} else {
// Exit annotation mode.
// TODO(dstockwell): add assert lib and replace this with assert
if (this.currentController_ != this.inkController_) {
throw new Error(
'Ink controller is not current, cannot exit annotation mode');
}
// TODO(dstockwell): set ink read-only, begin transition
this.updateProgress(0);
// This runs separately to allow other consumers of `loaded` to queue
// up after this task.
this.loaded.then(() => {
this.currentController_ = this.pluginController_;
this.inkController_.unload();
});
// TODO(dstockwell): handle save failure
const result = await this.inkController_.save(true);
await this.pluginController_.load(result.fileName, result.dataToSave);
}
},
/**
* Request to change the viewport fitting type.
*
@ -559,9 +607,41 @@ PDFViewer.prototype = {
this.metrics.onFollowBookmark();
},
/**
* @return {Promise} Resolved when the load state reaches LOADED,
* rejects on FAILED.
*/
get loaded() {
return this.loaded_.promise;
},
/**
* Updates the load state and triggers completion of the `loaded`
* promise if necessary.
* @param {!LoadState} loadState
* @private
*/
setLoadState_(loadState) {
if (this.loadState_ == loadState) {
return;
}
if ((loadState == LoadState.SUCCESS || loadState == LoadState.FAILURE) &&
this.loadState_ != LoadState.LOADING) {
throw new Error('Internal error: invalid loadState transition.');
}
this.loadState_ = loadState;
if (loadState == LoadState.SUCCESS) {
this.loaded_.resolve();
} else if (loadState == LoadState.FAILED) {
this.loaded_.reject();
} else {
this.loaded_ = new PromiseResolver();
}
},
/**
* Update the loading progress of the document in response to a progress
* message being received from the plugin.
* message being received from the content controller.
*
* @param {number} progress the progress as a percentage.
*/
@ -577,7 +657,7 @@ PDFViewer.prototype = {
this.passwordScreen_.deny();
this.passwordScreen_.close();
}
this.loadState_ = LoadState.FAILED;
this.setLoadState_(LoadState.FAILED);
this.isPrintPreviewLoadingFinished_ = true;
this.sendDocumentLoadedMessage_();
} else if (progress == 100) {
@ -586,12 +666,14 @@ PDFViewer.prototype = {
this.viewport_.position = this.lastViewportPosition_;
this.paramsParser_.getViewportFromUrlParams(
this.originalUrl_, this.handleURLParams_.bind(this));
this.loadState_ = LoadState.SUCCESS;
this.setLoadState_(LoadState.SUCCESS);
this.sendDocumentLoadedMessage_();
while (this.delayedScriptingMessages_.length > 0)
this.handleScriptingMessage(this.delayedScriptingMessages_.shift());
this.toolbarManager_.hideToolbarsAfterTimeout();
} else {
this.setLoadState_(LoadState.LOADING);
}
},
@ -797,7 +879,7 @@ PDFViewer.prototype = {
this.pluginController_.postMessage(message.data);
return true;
case 'resetPrintPreviewMode':
this.loadState_ = LoadState.LOADING;
this.setLoadState_(LoadState.LOADING);
if (!this.inPrintPreviewMode_) {
this.inPrintPreviewMode_ = true;
this.isUserInitiatedEvent_ = false;
@ -1003,7 +1085,8 @@ PDFViewer.prototype = {
* Saves the current PDF document to disk.
*/
save: async function() {
const result = await this.currentController_.save();
// TODO(dstockwell): Report an error to user if this fails.
const result = await this.currentController_.save(false);
if (result == null) {
// The content controller handled the save internally.
return;
@ -1057,10 +1140,77 @@ class ContentController {
/**
* Requests that the current document be saved.
* @param {boolean} requireResult whether a response is required, otherwise
* the controller may save the document to disk internally.
* @return {Promise<{fileName: string, dataToSave: ArrayBuffer}}
* @abstract
*/
save() {}
save(requireResult) {}
/**
* Loads PDF document from `data` activates UI.
* @param {string} fileName
* @param {ArrayBuffer} data
* @return {Promise<void>}
* @abstract
*/
load(fileName, data) {}
/**
* Unloads the current document and removes the UI.
* @abstract
*/
unload() {}
}
class InkController extends ContentController {
/**
* @param {PDFViewer} viewer
* @param {Viewport} viewport
*/
constructor(viewer, viewport) {
super();
this.viewer_ = viewer;
this.viewport_ = viewport;
/** @type {ViewerInkHost} */
this.inkHost_ = null;
}
/** @override */
rotateClockwise() {
// TODO(dstockwell): implement rotation
}
/** @override */
rotateCounterClockwise() {
// TODO(dstockwell): implement rotation
}
/** @override */
print() {
// TODO(dstockwell): implement printing
}
/** @override */
save(requireResult) {
return this.inkHost_.saveDocument();
}
/** @override */
load(filename, data) {
if (!this.inkHost_) {
this.inkHost_ = document.createElement('viewer-ink-host');
document.body.appendChild(this.inkHost_);
}
return this.inkHost_.load(filename, data);
}
/** @override */
unload() {
this.inkHost_.remove();
this.inkHost_ = null;
}
}
class PluginController extends ContentController {
@ -1075,7 +1225,7 @@ class PluginController extends ContentController {
this.viewer_ = viewer;
this.viewport_ = viewport;
/** @private {!Map<string, Function>} */
/** @private {!Map<string, PromiseResolver>} */
this.pendingTokens_ = new Map();
this.plugin_.addEventListener(
'message', e => this.handlePluginMessage_(e), false);
@ -1162,13 +1312,30 @@ class PluginController extends ContentController {
}
/** @override */
save() {
return new Promise(resolve => {
const newToken = createToken();
this.pendingTokens_.set(newToken, resolve);
const force = false;
this.postMessage({type: 'save', token: newToken});
});
save(requireResult) {
const resolver = new PromiseResolver();
const newToken = createToken();
this.pendingTokens_.set(newToken, resolver);
this.postMessage({type: 'save', token: newToken, force: requireResult});
return resolver.promise;
}
/** @override */
async load(fileName, data) {
const url = URL.createObjectURL(new Blob([data]));
this.plugin_.removeAttribute('headers');
this.plugin_.setAttribute('stream-url', url);
this.plugin_.style.display = 'block';
try {
await this.viewer_.loaded;
} finally {
URL.revokeObjectURL(url);
}
}
/** @override */
unload() {
this.plugin_.style.display = 'none';
}
/**
@ -1250,15 +1417,21 @@ class PluginController extends ContentController {
* @private
*/
saveData_(messageData) {
if (!(loadTimeData.getBoolean('pdfFormSaveEnabled')))
if (!(loadTimeData.getBoolean('pdfFormSaveEnabled') ||
loadTimeData.getBoolean('pdfAnnotationsEnabled')))
throw new Error('Internal error: save not enabled.');
// Verify a token that was created by this instance is included to avoid
// being spammed.
const resolve = this.pendingTokens_.get(messageData.token);
const resolver = this.pendingTokens_.get(messageData.token);
if (!this.pendingTokens_.delete(messageData.token))
throw new Error('Internal error: save token not found, abort save.');
if (!messageData.dataToSave) {
resolver.reject();
return;
}
// Verify the file size and the first bytes to make sure it's a PDF. Cap at
// 100 MB. This cap should be kept in sync with and is also enforced in
// pdf/out_of_process_instance.cc.
@ -1275,6 +1448,6 @@ class PluginController extends ContentController {
throw new Error('Not a PDF file.');
}
resolve(messageData);
resolver.resolve(messageData);
}
}

@ -436,6 +436,10 @@ void SetupPrintPreviewPlugin(content::WebUIDataSource* source) {
source->AddResourcePath(
"pdf/elements/viewer-error-screen/viewer-error-screen.js",
IDR_PDF_VIEWER_ERROR_SCREEN_JS);
source->AddResourcePath("pdf/elements/viewer-ink-host/viewer-ink-host.html",
IDR_PDF_VIEWER_INK_HOST_HTML);
source->AddResourcePath("pdf/elements/viewer-ink-host/viewer-ink-host.js",
IDR_PDF_VIEWER_INK_HOST_JS);
source->AddResourcePath(
"pdf/elements/viewer-page-indicator/viewer-page-indicator.html",
IDR_PDF_VIEWER_PAGE_INDICATOR_HTML);

@ -2,6 +2,20 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
function contentElement() {
return document.elementFromPoint(innerWidth / 2, innerHeight / 2);
}
async function testAsync(f) {
try {
await f();
chrome.test.succeed();
} catch (e) {
chrome.test.fail(e);
}
}
chrome.test.runTests([
function testAnnotationsEnabled() {
const toolbar = document.body.querySelector('#toolbar');
@ -10,4 +24,20 @@ chrome.test.runTests([
toolbar.shadowRoot.querySelector('#annotate') != null);
chrome.test.succeed();
},
function testEnterAndExitAnnotationMode() {
testAsync(async () => {
chrome.test.assertEq('EMBED', contentElement().tagName);
// Enter annotation mode.
$('toolbar').toggleAnnotation();
await viewer.loaded;
chrome.test.assertEq(
'VIEWER-INK-HOST', contentElement().tagName);
// Exit annotation mode.
$('toolbar').toggleAnnotation();
await viewer.loaded;
chrome.test.assertEq('EMBED', contentElement().tagName);
});
}
]);

@ -102,6 +102,7 @@ constexpr char kJSPrintType[] = "print";
// Save (Page -> Plugin)
constexpr char kJSSaveType[] = "save";
constexpr char kJSToken[] = "token";
constexpr char kJSForce[] = "force";
// Save Data (Plugin -> Page)
constexpr char kJSSaveDataType[] = "saveData";
constexpr char kJSFileName[] = "fileName";
@ -666,11 +667,17 @@ void OutOfProcessInstance::HandleMessage(const pp::Var& message) {
} else if (type == kJSPrintType) {
Print();
} else if (type == kJSSaveType) {
if (!dict.Get(pp::Var(kJSToken)).is_string()) {
if (!(dict.Get(pp::Var(kJSToken)).is_string() &&
dict.Get(pp::Var(kJSForce)).is_bool())) {
NOTREACHED();
return;
}
Save(dict.Get(pp::Var(kJSToken)).AsString());
const bool force = dict.Get(pp::Var(kJSForce)).AsBool();
if (force) {
SaveToBuffer(dict.Get(pp::Var(kJSToken)).AsString());
} else {
SaveToFile(dict.Get(pp::Var(kJSToken)).AsString());
}
} else if (type == kJSRotateClockwiseType) {
RotateClockwise();
} else if (type == kJSRotateCounterclockwiseType) {
@ -1473,37 +1480,58 @@ void OutOfProcessInstance::GetDocumentPassword(
PostMessage(message);
}
void OutOfProcessInstance::Save(const std::string& token) {
engine_->KillFormFocus();
bool OutOfProcessInstance::ShouldSaveEdits() const {
return edit_mode_ &&
base::FeatureList::IsEnabled(features::kSaveEditedPDFForm);
}
if (!base::FeatureList::IsEnabled(features::kSaveEditedPDFForm) ||
!edit_mode_) {
ConsumeSaveToken(token);
pp::PDF::SaveAs(this);
return;
}
void OutOfProcessInstance::SaveToBuffer(const std::string& token) {
engine_->KillFormFocus();
GURL url(url_);
std::string file_name = url.ExtractFileName();
file_name = net::UnescapeURLComponent(file_name, net::UnescapeRule::SPACES);
std::vector<uint8_t> data = engine_->GetSaveData();
if (data.size() == 0u || data.size() > kMaximumSavedFileSize) {
// TODO(thestig): Add feedback to the user that a failure occurred.
ConsumeSaveToken(token);
return;
}
pp::VarDictionary message;
message.Set(kType, kJSSaveDataType);
message.Set(kJSToken, pp::Var(token));
message.Set(kJSFileName, pp::Var(file_name));
pp::VarArrayBuffer buffer(data.size());
std::copy(data.begin(), data.end(), reinterpret_cast<char*>(buffer.Map()));
message.Set(kJSDataToSave, buffer);
// This will be overwritten if the save is successful.
message.Set(kJSDataToSave, pp::Var(pp::Var::Null()));
if (ShouldSaveEdits()) {
std::vector<uint8_t> data = engine_->GetSaveData();
if (data.size() > 0 && data.size() <= kMaximumSavedFileSize) {
pp::VarArrayBuffer buffer(data.size());
std::copy(data.begin(), data.end(),
reinterpret_cast<char*>(buffer.Map()));
message.Set(kJSDataToSave, buffer);
}
} else {
DCHECK(base::FeatureList::IsEnabled(features::kPDFAnnotations));
uint32_t length = engine_->GetLoadedByteSize();
if (length > 0 && length <= kMaximumSavedFileSize) {
pp::VarArrayBuffer buffer(length);
if (engine_->ReadLoadedBytes(length, buffer.Map())) {
message.Set(kJSDataToSave, buffer);
}
}
}
PostMessage(message);
}
void OutOfProcessInstance::SaveToFile(const std::string& token) {
if (!ShouldSaveEdits()) {
engine_->KillFormFocus();
ConsumeSaveToken(token);
pp::PDF::SaveAs(this);
return;
}
SaveToBuffer(token);
}
void OutOfProcessInstance::ConsumeSaveToken(const std::string& token) {
pp::VarDictionary message;
message.Set(kType, kJSConsumeSaveTokenType);

@ -186,7 +186,9 @@ class OutOfProcessInstance : public pp::Instance,
// frame's origin.
pp::URLLoader CreateURLLoaderInternal();
void Save(const std::string& token);
bool ShouldSaveEdits() const;
void SaveToFile(const std::string& token);
void SaveToBuffer(const std::string& token);
void ConsumeSaveToken(const std::string& token);
void FormDidOpen(int32_t result);

@ -412,6 +412,9 @@ class PDFEngine {
// Remove focus from form widgets, consolidating the user input.
virtual void KillFormFocus() = 0;
virtual uint32_t GetLoadedByteSize() = 0;
virtual bool ReadLoadedBytes(uint32_t length, void* buffer) = 0;
};
// Interface for exports that wrap the PDF engine.

@ -535,7 +535,6 @@ gin::IsolateHolder* g_isolate_holder = nullptr;
void SetUpV8() {
const char* recommended = FPDF_GetRecommendedV8Flags();
v8::V8::SetFlagsFromString(recommended, strlen(recommended));
gin::IsolateHolder::Initialize(gin::IsolateHolder::kNonStrictMode,
gin::IsolateHolder::kStableV8Extras,
gin::ArrayBufferAllocator::SharedInstance());
@ -1227,6 +1226,15 @@ void PDFiumEngine::KillFormFocus() {
SetInFormTextArea(false);
}
uint32_t PDFiumEngine::GetLoadedByteSize() {
return doc_loader_->GetDocumentSize();
}
bool PDFiumEngine::ReadLoadedBytes(uint32_t length, void* buffer) {
DCHECK_LE(length, GetLoadedByteSize());
return doc_loader_->GetBlock(0, length, buffer);
}
void PDFiumEngine::SetFormSelectedText(FPDF_FORMHANDLE form_handle,
FPDF_PAGE page) {
unsigned long form_sel_text_len =

@ -136,6 +136,8 @@ class PDFiumEngine : public PDFEngine,
void OnDocumentCanceled() override;
void CancelBrowserDownload() override;
void KillFormFocus() override;
uint32_t GetLoadedByteSize() override;
bool ReadLoadedBytes(uint32_t length, void* buffer) override;
#if defined(PDF_ENABLE_XFA)
void UpdatePageCount();