gecko-dev/toolkit/components/pdfjs/content/GeckoViewPdfjsParent.sys.mjs

303 lines
7.2 KiB
JavaScript

/* Copyright 2022 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { GeckoViewActorParent } from "resource://gre/modules/GeckoViewActorParent.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
});
class FindHandler {
#browser;
#callbacks = null;
#state = null;
constructor(aBrowser) {
this.#browser = aBrowser;
}
cleanup() {
this.#state = null;
this.#callbacks = null;
}
onEvent(aEvent, aData, aCallback) {
if (
this.#browser.contentPrincipal.spec !==
"resource://pdf.js/web/viewer.html"
) {
return;
}
debug`onEvent: name=${aEvent}, data=${aData}`;
switch (aEvent) {
case "GeckoView:ClearMatches":
this.cleanup();
this.#browser.sendMessageToActor(
"PDFJS:Child:handleEvent",
{
type: "findbarclose",
detail: null,
},
"GeckoViewPdfjs"
);
break;
case "GeckoView:DisplayMatches":
if (!this.#state) {
return;
}
this.#browser.sendMessageToActor(
"PDFJS:Child:handleEvent",
{
type: "findhighlightallchange",
detail: this.#state,
},
"GeckoViewPdfjs"
);
break;
case "GeckoView:FindInPage":
const type = this.#getFindType(aData);
this.#browser.sendMessageToActor(
"PDFJS:Child:handleEvent",
{
type,
detail: this.#state,
},
"GeckoViewPdfjs"
);
this.#callbacks.push([aCallback, this.#state]);
break;
}
}
#getFindType(aData) {
const newState = {
query: this.#state?.query,
caseSensitive: !!aData.matchCase,
entireWord: !!aData.wholeWord,
highlightAll: !!aData.highlightAll,
findPrevious: !!aData.backwards,
matchDiacritics: !!aData.matchDiacritics,
};
if (!this.#state) {
// It's a new search.
newState.query = aData.searchString;
this.#state = newState;
this.#callbacks = [];
return "find";
}
if (aData.searchString && this.#state.query !== aData.searchString) {
// The searched string has changed.
newState.query = aData.searchString;
this.#state = newState;
return "find";
}
for (const [key, type] of [
["caseSensitive", "findcasesensitivitychange"],
["entireWord", "findentirewordchange"],
["matchDiacritics", "finddiacriticmatchingchange"],
]) {
if (this.#state[key] !== newState[key]) {
this.#state = newState;
return type;
}
}
this.#state = newState;
return "findagain";
}
updateMatchesCount(aData, aResult) {
if (
(aResult !== Ci.nsITypeAheadFind.FIND_FOUND &&
aResult !== Ci.nsITypeAheadFind.FIND_WRAPPED) ||
!this.#state
) {
return;
}
if (this.#callbacks.length === 0) {
warn`There are no callbacks to use to set the matches count.`;
return;
}
const [callback, state] = this.#callbacks.shift();
aData ||= { current: 0, total: -1 };
const response = {
found: aData.total !== 0,
wrapped:
aData.total === 0 || aResult === Ci.nsITypeAheadFind.FIND_WRAPPED,
current: aData.current,
total: aData.total,
searchString: state.query,
linkURL: null,
clientRect: null,
flags: {
backwards: state.findPrevious,
matchCase: state.caseSensitive,
wholeWord: state.entireWord,
},
};
callback.onSuccess(response);
}
}
class FileSaver {
#browser;
#callback = null;
constructor(aBrowser) {
this.#browser = aBrowser;
}
cleanup() {
this.#callback = null;
}
onEvent(aEvent, aData, aCallback) {
if (
this.#browser.contentPrincipal.spec !==
"resource://pdf.js/web/viewer.html"
) {
return;
}
this.#callback = aCallback;
this.#browser.sendMessageToActor(
"PDFJS:Child:handleEvent",
{
type: "save",
},
"GeckoViewPdfjs"
);
}
async save({ blobUrl, filename }) {
if (!this.#callback) {
warn`Save a PDF: No callback !`;
return;
}
try {
const isPrivate = lazy.PrivateBrowsingUtils.isBrowserPrivate(
this.#browser
);
const response = await fetch(blobUrl);
const buffer = await response.arrayBuffer();
const bytes = new Uint8Array(buffer);
this.#callback.onSuccess({
bytes,
filename,
isPrivate,
});
debug`Save a PDF: ${bytes.length} bytes sent.`;
} catch (e) {
this.#callback.onError(`Cannot save the pdf: ${e}.`);
} finally {
this.cleanup();
}
}
}
export class GeckoViewPdfjsParent extends GeckoViewActorParent {
#findHandler;
#fileSaver;
receiveMessage(aMsg) {
debug`receiveMessage: name=${aMsg.name}, data=${aMsg.data}`;
switch (aMsg.name) {
case "PDFJS:Parent:updateControlState":
return this.#updateControlState(aMsg);
case "PDFJS:Parent:updateMatchesCount":
return this.#updateMatchesCount(aMsg);
case "PDFJS:Parent:addEventListener":
return this.#addEventListener();
case "PDFJS:Parent:saveURL":
return this.#save(aMsg);
default:
break;
}
return undefined;
}
didDestroy() {
debug`didDestroy`;
if (!this.#findHandler) {
return;
}
this.eventDispatcher.unregisterListener(this.#findHandler, [
"GeckoView:ClearMatches",
"GeckoView:DisplayMatches",
"GeckoView:FindInPage",
]);
this.#findHandler.cleanup();
this.#findHandler = null;
this.eventDispatcher.unregisterListener(this.#fileSaver, [
"GeckoView:PDFSave",
]);
this.#fileSaver.cleanup();
this.#fileSaver = null;
}
#addEventListener() {
if (this.#findHandler) {
this.#findHandler.cleanup();
return;
}
this.#findHandler = new FindHandler(this.browser);
this.eventDispatcher.registerListener(this.#findHandler, [
"GeckoView:ClearMatches",
"GeckoView:DisplayMatches",
"GeckoView:FindInPage",
]);
this.#fileSaver = new FileSaver(this.browser);
this.eventDispatcher.registerListener(this.#fileSaver, [
"GeckoView:PDFSave",
]);
}
#updateMatchesCount({ data }) {
this.#findHandler.updateMatchesCount(data, Ci.nsITypeAheadFind.FIND_FOUND);
}
#updateControlState({ data: { matchesCount, result } }) {
this.#findHandler.updateMatchesCount(matchesCount, result);
}
#save({ data }) {
this.#fileSaver.save(data);
}
}
const { debug, warn } = GeckoViewPdfjsParent.initLogging(
"GeckoViewPdfjsParent"
);