PDF.js Express Version
Development environment
Frontend: React@^17.0.2
Backend NodeJs
Detailed description of issue
Hi there,
Here is the problem I’ve been facing: I followed the official documentation and the advices that I’ve found in the forum in order to load a document as a Blob in the viewer.
I’m using react router as my router gateway.
When you land on this route /sessions/:session_id/editor/folder/:folder_id/doc/:doc_id
, the WebViewer parent’s component grabs the session_id, folder_id and doc_id, make a call to our server and return the doc infos (mainly the xfdf string and the doc as a base64 string).
Then, with the received data, I pass two props:
- doc_data: contains my file data as a Blob
- xfdf: contains the xfdf string from whom I import annotations
In the WebViewer component, I’m using a useEffect that checks if instance is defined and if doc_data is defined then I load the doc_data into the viewer using instance.loadDocument(doc_data)
There is also another useEffect that attach the documentLoaded eventListenner to the docViewer (I use the .one() instead of .on() else the eventListenner is called multiple times on the same doc)
The last useEffect that I use is for setting the annotationChanged eventListenner, in there I simply call an endpoint on our server in order to save the xfdf string.
To be noted that the more I switch files the more onAnnotationChanged is called.
The problem is that whenever I switch between files, the annotation from the previous doc are re-rendered in the newly loaded document. To be noted that it also imports the new document annotations.
Below I’ll attach a video demonstrating my problem and I’ll add some code snippets regarding the two components that I’ve talked about.
Expected behaviour
I expect the viewer to render only the annotations that belong to the currently loaded document
Does your issue happen with every document, or just one?
All documents
Link to document
Not possible, files are loaded as Blob into the viewer
Code snippet
WebViewer component (child component)
import React, { useRef, useEffect, useMemo, useLayoutEffect } from 'react';
import WebViewer from '@pdftron/pdfjs-express';
import Fab from '@material-ui/core/Fab';
import GetAppIcon from '@material-ui/icons/GetApp';
import { useDispatch, useSelector } from 'react-redux';
import { stopIsLoading, setIsLoading } from '../../features/loader/loadingSlice';
import { makeStyles } from '@material-ui/core/styles';
import BackEnd from '../../api';
const useStyles = makeStyles((theme) => ({
fab: {
position: 'fixed',
bottom: theme.spacing(2),
right: theme.spacing(2),
}
}));
export default function WebViewerComponent(props) {
const { instance, download_file, doc_data, xfdf, setInstance, session_id, user_id, setSnackBarMessage, t } = props;
const currentDoc = useSelector((state) => state.editor.currentDoc);
const dispatch = useDispatch();
const memoInstance = useMemo(() => instance, [instance]);
const viewerDiv = useRef();
useLayoutEffect(() => {
WebViewer({
licenseKey: 'My_license_Key',
path: '/pdfjsexpress',
disableFlattenedAnnotations: true,
}, viewerDiv.current).then(inst => {
setInstance(inst);
});
}, []);
useEffect(() => {
if (memoInstance && doc_data) {
memoInstance.loadDocument(doc_data);
}
}, [memoInstance, doc_data]);
useEffect(() => {
if (memoInstance && user_id && session_id && currentDoc && currentDoc.id) {
const annotManager = memoInstance.docViewer.getAnnotationManager();
annotManager.on('annotationChanged', async (annotations, action, { imported }) => {
// If the event is triggered by importing then it can be ignored
// This will happen when importing the initial annotations from the server or individual changes from other users
if (imported) {
return;
} else {
console.log('annotationChanged IN');
annotManager.exportAnnotations().then((xfdfString) => {
if (xfdfString !== xfdf) {
dispatch(setIsLoading());
console.log('called merge');
setAnnotation(session_id, user_id, currentDoc.id, xfdfString);
}
});
}
});
}
}, [memoInstance, currentDoc]);
useEffect(() => {
if (memoInstance && xfdf) {
const annotManager = memoInstance.docViewer.getAnnotationManager();
memoInstance.docViewer.one('documentLoaded', async () => {
console.log('imported annotations');
if (xfdf !== '') {
// Get rid of the <xml></xml>
const xfdf_ = xfdf.split('<?xml version=\"1.0\" encoding=\"UTF-8\" ?>')[1];
annotManager.importAnnotations(xfdf_).then(importedAnnotations => { });
}
dispatch(stopIsLoading());
});
} else {
dispatch(stopIsLoading());
}
}, [memoInstance, xfdf, doc_data]);
useEffect(() => {
return () => {
if (memoInstance) {
memoInstance.annotManager.off();
}
}
}, [memoInstance]);
const setAnnotation = async (sessionId, userId, docId, xfdf) => {
if (sessionId && userId && docId) {
try {
const response = await BackEnd.post(`/medias/set_annotations`, {
payload: {
local_user_id: userId,
session_id: sessionId,
xfdf_string: xfdf,
doc_id: docId
}
});
if (response.status === 200) {
console.log('success saving');
} else {
setSnackBarMessage(t('Generic.oops'));
}
dispatch(stopIsLoading());
} catch (e) {
const { response } = e;
const { data } = response;
if (response.status === 403) {
if (data && data.message) {
setSnackBarMessage(t('Generic.oops', { err: data.message }));
} else {
setSnackBarMessage(t('Generic.oops', { err: 'Access forbidden' }));
}
} else if (response.status === 404) {
setSnackBarMessage(t('Generic.oops', { err: data.message }));
} else if (response.status === 401) {
setSnackBarMessage(t('Generic.oops', { err: e.message }));
} else {
setSnackBarMessage(t('Generic.oops', { err: e.message }));
}
dispatch(stopIsLoading());
}
}
}
const classes = useStyles();
return (
<div className='component' style={{ marginTop: 12 }}>
<div className="webviewer" ref={viewerDiv} style={{ height: "100vh" }}></div>
<Fab aria-label={'download'} className={classes.fab} disabled={!memoInstance} color={'primary'} onClick={download_file}>
<GetAppIcon />
</Fab>
</div>
)
}
SessionEditor component (parent component)
import React, { useEffect, useState } from 'react';
import FileSaver from 'file-saver';
import ExpressUtils from '@pdftron/pdfjs-express-utils';
import GenBackDrop from './Gen/GeneralBackDrop';
import SnackbarComponent from './Gen/SnackBar';
import socketIOClient from "socket.io-client";
import { useDispatch, useSelector } from 'react-redux';
import { setIsLoading, stopIsLoading } from '../features/loader/loadingSlice';
import WebViewerComponent from './WebViewer';
import { setShowFoldersSelect, setCurrentDoc } from '../features/editor/editorSlice';
import BackEnd from '../api';
import ScrollableTabsEditorDocs from './Gen/EditorFilesTabs'
const ENDPOINT = 'our_endpoint';
const utils = new ExpressUtils();
export default function SessionEditor(props) {
const { t, match, history } = props;
const foldersDocs = useSelector((state) => state.editor.foldersDocs);
const [instance, setInstance] = useState(null)
const [loading, setLoading] = useState(false);
const [loadingMessage, setloadingMessage] = useState(null);
const [currentDocData, setCurrentDocData] = useState(null);
const [currentDoc, setCurrentDoc_] = useState({});
const [currentUser, setCurrentUser] = useState(null);
const [sessionId, setSessionId] = useState(null);
const [userId, setUserId] = useState(null);
const [docId, setDocId] = useState(null);
const [userRole, setUserRole] = useState(null);
const [snackBarMessage, setSnackBarMessage] = useState(null);
const [currentSocket, setCurrentSocket] = useState(null);
const dispatch = useDispatch();
/**
* @main
* Create a socket connection to our server
*/
useEffect(() => {
const socket = socketIOClient(ENDPOINT, { transports: ["websocket"] });
setCurrentSocket(socket);
}, [])
useEffect(() => {
setLoading(true);
dispatch(setIsLoading());
const local_user_id = localStorage.getItem('local_user_id') || null;
if (!match.params.session_id || !match.params.folder_id || !match.params.doc_id || !local_user_id) {
history.push('/');
} else if (match.params.session_id && match.params.folder_id && match.params.doc_id && local_user_id) {
setSessionId(match.params.session_id);
setUserId(local_user_id);
setDocId(match.params.doc_id);
}
}, [match.params.session_id, match.params.doc_id, match.params.folder_id]);
useEffect(() => {
if (sessionId && userId && docId) {
fetchDocData(sessionId, userId, docId);
}
}, [sessionId, userId, docId]);
useEffect(() => {
if (sessionId && docId && userId && currentSocket) {
currentSocket.emit('createSessionSocket', { sessionId, docId, userId });
}
}, [sessionId, docId, userId, currentSocket]);
useEffect(() => {
if (instance && currentUser && userRole) {
set_config(instance);
set_current_user_and_authorization(instance, currentUser, userRole);
}
}, [instance, currentUser, userRole]);
const set_config = (instance) => {
const locale_not_sterialized = window.localStorage.i18nextLng || 'en-US';
const sterialized_locale = locale_not_sterialized.split('-')[0];
// Set language to the browser language. fallback to english if no language or if not currently supported
instance.setLanguage(sterialized_locale);
instance.enableMeasurement();
instance.enableFeatures([instance.Feature.Search]);
}
const set_current_user_and_authorization = (instance, currUser, userRole) => {
const { annotManager } = instance;
annotManager.setCurrentUser(currUser.f_name);
annotManager.setIsAdminUser(userRole === 'Admin' ? true : false);
annotManager.setReadOnly(userRole === 'Viewer' ? true : false)
}
function base64ToBlob(base64) {
const binaryString = window.atob(base64);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; ++i) {
bytes[i] = binaryString.charCodeAt(i);
}
return new Blob([bytes], { type: 'application/pdf' });
};
const download_file = async () => {
setLoading(true);
setloadingMessage(t('Session_Editor.wait_for_download'));
const xfdf = await instance.annotManager.exportAnnotations();
const fileData = await instance.docViewer.getDocument().getFileData({});
// Set the annotations and document into the Utility SDK, then merge them together
const resp = await utils
.setFile(fileData)
.setXFDF(xfdf)
.set();
// Get the resulting blob from the merge operation
const mergedBlob = await resp.getBlob();
// currentSocket.emit('edits', mergedBlob);
const file_name = `${currentDoc.file_name.split('.')[0]}.pdf`;
// trigger a download for the user!
FileSaver.saveAs(mergedBlob, file_name);
setLoading(false);
setloadingMessage(null);
setSnackBarMessage(t('Session_Editor.file_downloaded', { file_name: file_name }));
}
async function fetchDocData(session_id, local_user_id, doc_id) {
try {
const response = await BackEnd.get(`/medias/retrieve_media?session_id=${session_id}&local_user_id=${local_user_id}&doc_id=${doc_id}`);
const { data } = response;
if (response.status === 200) {
setCurrentDocData(base64ToBlob(data.buffer));
dispatch(setCurrentDoc(data.doc));
setCurrentDoc_(data.doc);
setCurrentUser(data.user);
setUserRole(data.user_role);
dispatch(setShowFoldersSelect());
} else {
setSnackBarMessage(t('Generic.oops'));
}
} catch (e) {
dispatch(stopIsLoading());
const { response } = e;
const { data } = response;
if (response.status === 403) {
if (data && data.message) {
setSnackBarMessage(t('Generic.oops', { err: data.message }));
} else {
setSnackBarMessage(t('Generic.oops', { err: 'Access forbidden' }));
}
} else if (response.status === 404) {
setSnackBarMessage(t('Generic.oops', { err: data.message }));
} else if (response.status === 400) {
setSnackBarMessage(t('Generic.oops', { err: data.message }));
} else {
setSnackBarMessage(t('Generic.oops', { err: e.message }));
}
}
setLoading(false);
}
const dismiss_snackar = () => {
setSnackBarMessage(null);
}
return (
<div>
<ScrollableTabsEditorDocs session_id={sessionId} h={history} docs={foldersDocs} match={match}/>
<WebViewerComponent setInstance={setInstance} session_id={sessionId} setSnackBarMessage={setSnackBarMessage} t={t} user_id={userId} doc_id={docId} doc={currentDoc} doc_data={currentDocData} instance={instance} download_file={download_file} xfdf={currentDoc && currentDoc.xfdf_string} />
<GenBackDrop message={loadingMessage} isOpen={loading} />
<SnackbarComponent error_and_message={snackBarMessage} dismiss={dismiss_snackar} />
</div>
)
}
Thanks for your time