Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Pinch zoom implementation for PDF.js viewer
<!-- Goes into viewer.html just before ending </body> -->
<script>
let pinchZoomEnabled = false;
function enablePinchZoom(pdfViewer) {
let startX = 0, startY = 0;
let initialPinchDistance = 0;
let pinchScale = 1;
const viewer = document.getElementById("viewer");
const container = document.getElementById("viewerContainer");
const reset = () => { startX = startY = initialPinchDistance = 0; pinchScale = 1; };
// Prevent native iOS page zoom
//document.addEventListener("touchmove", (e) => { if (e.scale !== 1) { e.preventDefault(); } }, { passive: false });
document.addEventListener("touchstart", (e) => {
if (e.touches.length > 1) {
startX = (e.touches[0].pageX + e.touches[1].pageX) / 2;
startY = (e.touches[0].pageY + e.touches[1].pageY) / 2;
initialPinchDistance = Math.hypot((e.touches[1].pageX - e.touches[0].pageX), (e.touches[1].pageY - e.touches[0].pageY));
} else {
initialPinchDistance = 0;
}
});
document.addEventListener("touchmove", (e) => {
if (initialPinchDistance <= 0 || e.touches.length < 2) { return; }
if (e.scale !== 1) { e.preventDefault(); }
const pinchDistance = Math.hypot((e.touches[1].pageX - e.touches[0].pageX), (e.touches[1].pageY - e.touches[0].pageY));
const originX = startX + container.scrollLeft;
const originY = startY + container.scrollTop;
pinchScale = pinchDistance / initialPinchDistance;
viewer.style.transform = `scale(${pinchScale})`;
viewer.style.transformOrigin = `${originX}px ${originY}px`;
}, { passive: false });
document.addEventListener("touchend", (e) => {
if (initialPinchDistance <= 0) { return; }
viewer.style.transform = `none`;
viewer.style.transformOrigin = `unset`;
PDFViewerApplication.pdfViewer.currentScale *= pinchScale;
const rect = container.getBoundingClientRect();
const dx = startX - rect.left;
const dy = startY - rect.top;
container.scrollLeft += dx * (pinchScale - 1);
container.scrollTop += dy * (pinchScale - 1);
reset();
});
}
document.addEventListener('webviewerloaded', () => {
if (!pinchZoomEnabled) {
pinchZoomEnabled = true;
enablePinchZoom();
}
});
</script>
@okj579
Copy link

okj579 commented Jan 29, 2020

Works great, thank you!

@shashankraj0202
Copy link

shashankraj0202 commented Jun 14, 2020

Thanks! Works perfectly.

@rmzetti
Copy link

rmzetti commented Jun 16, 2020

Thanks for doing this - it works and makes a big difference to pdfjs...

@ranunchius
Copy link

ranunchius commented Jun 26, 2020

Thanks a lot.
I've tested it, but after a while it gets stuck again. No possibility to scroll again.

@ismael-miguel
Copy link

ismael-miguel commented Jul 8, 2020

I've tested this and the ES6 version doesn't seem to trigger the event webviewerloaded.

I had to do this janky stuff:

if('ontouchstart' in document)
{
    pinchZoomEnabled = true;
    enablePinchZoom();
}

Since I'm using PDF.js as a fallback for mobile devices, this isn't a problem for me.

@d-vesely
Copy link

d-vesely commented Aug 4, 2020

The way you set the scrollLeft and scrollTop values (lines 40 and 41) does not work for me. However, the following code works. Essentially, what you want to do, is compute the point that will be in the center of the page after scaling in page coordinates. The scrollbars have then be set in a way, that said point is in fact centered. This solution assumes, that originX and originY can be accessed in the ``touchend` event listener as well.

The following code should replace the lines 36-41.

// Compute the current center point in page coordinates
const pageCenterX = this.rootElement.nativeElement.clientWidth/2 + this.rootElement.nativeElement.scrollLeft;
const pageCenterY = this.rootElement.nativeElement.clientHeight/2 + this.rootElement.nativeElement.scrollTop;

// Compute the next center point in page coordinates
const centerX = (pageCenterX - this.originX) / pinchScale + this.originX;
const centerY = (pageCenterY - this.originY) / pinchScale + this.originY;
          
// Compute the ratios of the center point to the total scrollWidth/scrollHeight
const px = centerX / this.rootElement.nativeElement.scrollWidth;
const py = centerY / this.rootElement.nativeElement.scrollHeight;

// Scale
PDFViewerApplication.pdfViewer.currentScale *= pinchScale;

// Set the scrollbar positions using the percentages and the new scrollWidth/scrollHeight
this.rootElement.nativeElement.scrollLeft = this.rootElement.nativeElement.scrollWidth * px - this.rootElement.nativeElement.clientWidth/2;
this.rootElement.nativeElement.scrollTop = this.rootElement.nativeElement.scrollHeight * py - this.rootElement.nativeElement.clientHeight/2;

@akbq2008
Copy link

akbq2008 commented Sep 16, 2020

Thank you !

@zoffyzhang
Copy link

zoffyzhang commented Jan 29, 2021

thx!

@ojuniour
Copy link

ojuniour commented Jan 29, 2021

thx!

Did it work for you?

@zoffyzhang
Copy link

zoffyzhang commented Jan 29, 2021

thx!

Did it work for you?

perfectly

@zoffyzhang
Copy link

zoffyzhang commented Feb 1, 2021

in iphone, the origin gist code works perfectly. but in android phone, it would get stuck if I pinch too quickly.
I tried some methods(time throttle, mask layer, pinch distance throttle) to fix the problem and found out pinch distance throttle is the most appropriate solution.

 let lastPinchDistance = 0;
 const pinchStepLength = 50;

 document.addEventListener("touchmove", (e) => {
    if (initialPinchDistance <= 0 || e.touches.length < 2) { return; }
    if (e.scale !== 1) { e.preventDefault(); }
    const pinchDistance = Math.hypot((e.touches[1].pageX - e.touches[0].pageX), (e.touches[1].pageY - e.touches[0].pageY));
    if (Math.abs(pinchDistance - lastPinchDistance) < pinchStepLength) {
      return;
    }
    lastPinchDistance = pinchDistance;
    const originX = startX + container.scrollLeft;
    const originY = startY + container.scrollTop;
    pinchScale = pinchDistance / initialPinchDistance;
    viewer.style.transform = `scale(${pinchScale})`;
    viewer.style.transformOrigin = `${originX}px ${originY}px`;
}, { passive: false });

@almalib
Copy link

almalib commented Mar 3, 2021

in iphone, the origin gist code works perfectly. but in android phone, it would get stuck if I pinch too quickly.
I tried some methods(time throttle, mask layer, pinch distance throttle) to fix the problem and found out pinch distance throttle is the most appropriate solution.

Hello. If I increase the pdf to the maximum size it causes the application to freeze on android. Have you faced such a problem?

@zoffyzhang
Copy link

zoffyzhang commented Mar 4, 2021

in iphone, the origin gist code works perfectly. but in android phone, it would get stuck if I pinch too quickly.
I tried some methods(time throttle, mask layer, pinch distance throttle) to fix the problem and found out pinch distance throttle is the most appropriate solution.

Hello. If I increase the pdf to the maximum size it causes the application to freeze on android. Have you faced such a problem?

Yes, I just verified this bug. I don't see pdfjs has a config of max scale limit, so I guess this is what cause the problem.
my poor solution is to add a max scale limit in touchend event, here's my modified code:

  const pinchMaxScale = 5;

  container.addEventListener("touchend", (e) => {
    if (initialPinchDistance <= 0) {
      return;
    }
    viewer.style.transform = `none`;
    viewer.style.transformOrigin = `unset`;
    const newPinchScale =
      PDFViewerApplication.pdfViewer.currentScale * pinchScale;
    if (newPinchScale <= pinchMaxScale) {
      PDFViewerApplication.pdfViewer.currentScale = newPinchScale;
      const rect = container.getBoundingClientRect();
      const dx = startX - rect.left;
      const dy = startY - rect.top;
      container.scrollLeft += dx * (pinchScale - 1);
      container.scrollTop += dy * (pinchScale - 1);
    }
    reset();
  });

@almalib
Copy link

almalib commented Mar 4, 2021

в iphone код origin gist работает отлично. но в телефоне Android он застрянет, если я ущипну его слишком быстро.
Я попробовал несколько методов (дроссель времени, слой маски, дроссель расстояния зажима), чтобы решить проблему, и обнаружил, что дроссель расстояния зажима является наиболее подходящим решением.

Привет. Если я увеличу pdf до максимального размера, это приведет к зависанию приложения на android. Вы сталкивались с такой проблемой?

Да, я только что проверил эту ошибку. Я не вижу, что pdfjs имеет конфигурацию максимального предела масштабирования, поэтому я предполагаю, что это причина проблемы.
Мое плохое решение - добавить ограничение максимального масштаба в событии touchend, вот мой измененный код:

  const  pinchMaxScale  =  5 ;

  контейнер . addEventListener ( "touchend" ,  ( e )  =>  { 
    if  ( initialPinchDistance <= 0 )  { 
      return ; 
    } 
    viewer . style . transform  =  `none` ; 
    viewer . style . transformOrigin  =  ` unset` ; 
    const  newPinchScale  = 
      PDFViewerApplication . pdfViewer . currentScale * pinchScale ;
    если  ( newPinchScale <= pinchMaxScale )  { 
      PDFViewerApplication . pdfViewer . currentScale  =  newPinchScale ; 
      const  rect  =  контейнер . getBoundingClientRect ( ) ; 
      const  dx  =  startX  -  прямоугольник . слева ; 
      const  dy  =  startY  -  прямоугольник . верх ; 
      контейнер . scrollLeft  + =  dx * (pinchScale  -  1 ) ; 
      контейнер . scrollTop  + =  dy * ( pinchScale  -  1 ) ; 
    } 
    reset ( ) ; 
  } ) ;

This is what I tried in the first place and which unfortunately didn't work for me. It seems to me that this is due to the fact that after each increase, the files are re-rendered.

@cepm-nate
Copy link

cepm-nate commented Apr 13, 2021

To fix the jerky rendering on touchend when using the viewer with the default 32px top bar, I used the code at the top of this page with line 39 modified like so:

 const dy = startY - rect.top + 32;

Perhaps this will help someone else. :-)

@EdvinasDul
Copy link

EdvinasDul commented Jun 16, 2021

Pinch Zoom on Android devices still doesn't work... :/ Works perfectly on iOS devices. I added this code to the end of viewer.html (before </body> )

Code inside <script></script> tags:

let pinchZoomEnabled = false;
function enablePinchZoom(pdfViewer) {
  let startX = 0, startY = 0;
  let initialPinchDistance = 0;
  let pinchScale = 1;
  const viewer = document.getElementById("viewer");
  const container = document.getElementById("viewerContainer");
  const reset = () => { startX = startY = initialPinchDistance = 0; pinchScale = 1; };

  // Prevent native iOS page zoom
  //document.addEventListener("touchmove", (e) => { if (e.scale !== 1) { e.preventDefault(); } }, { passive: false });
  document.addEventListener("touchstart", (e) => {
    if (e.touches.length > 1) {
      startX = (e.touches[0].pageX + e.touches[1].pageX) / 2;
      startY = (e.touches[0].pageY + e.touches[1].pageY) / 2;
      initialPinchDistance = Math.hypot((e.touches[1].pageX - e.touches[0].pageX), (e.touches[1].pageY - e.touches[0].pageY));
    } else {
      initialPinchDistance = 0;
    }
  });

  document.addEventListener("touchmove", (e) => {
    if (initialPinchDistance <= 0 || e.touches.length < 2) { return; }
    if (e.scale !== 1) { e.preventDefault(); }
    const pinchDistance = Math.hypot((e.touches[1].pageX - e.touches[0].pageX), (e.touches[1].pageY - e.touches[0].pageY));
    const originX = startX + container.scrollLeft;
    const originY = startY + container.scrollTop;
    pinchScale = pinchDistance / initialPinchDistance;
    viewer.style.transform = `scale(${pinchScale})`;
    viewer.style.transformOrigin = `${originX}px ${originY}px`;
  }, { passive: false });

  const pinchMaxScale = 5;

  container.addEventListener("touchend", (e) => {
    if (initialPinchDistance <= 0) {
      return;
    }
    viewer.style.transform = `none`;
    viewer.style.transformOrigin = `unset`;
    const newPinchScale =
      PDFViewerApplication.pdfViewer.currentScale * pinchScale;
    if (newPinchScale <= pinchMaxScale) {
      PDFViewerApplication.pdfViewer.currentScale = newPinchScale;
      const rect = container.getBoundingClientRect();
      const dx = startX - rect.left;
      const dy = startY - rect.top;
      container.scrollLeft += dx * (pinchScale - 1);
      container.scrollTop += dy * (pinchScale - 1);
    }
    reset();
  });
}
document.addEventListener('webviewerloaded', () => {
  if('ontouchstart' in document) {
    pinchZoomEnabled = true;
    enablePinchZoom();
  }
});

Any solutions? I also tried adding Hammer.js - unsuccessfully also...

@RDevR99
Copy link

RDevR99 commented Jun 30, 2021

So, it turns out 'webviewerloaded' event is not firing for me.

this works:

setTimeout(function(){
        if (!pinchZoomEnabled) {
              pinchZoomEnabled = true;
              enablePinchZoom();
          }
      }, 0)

The code inside would probably be executed after the render is finished. Any ideas for improvement?

And any reason why this event or on this note, anyother event may not be thrown by the library?

@soulsoulsoulsoulsoul
Copy link

soulsoulsoulsoulsoul commented Sep 22, 2021

If pdf is large in size(say 10-12MB ), the actual zooming occurs after you have completed the gesture while if pdf is small it zooms as soon as you start pinch gesture on android.

@adamabdulaev
Copy link

adamabdulaev commented Sep 24, 2021

Если PDF-файл имеет большой размер (скажем, 10–12 МБ), фактическое масштабирование происходит после того, как вы завершили жест, а если PDF-файл маленький, он масштабируется, как только вы начинаете жест сжатия на Android.

please tell me, have you found any solution to this problem?

@GAGANsinghmsitece
Copy link

GAGANsinghmsitece commented Sep 24, 2021

One solution would be to only zoom only the current page and save the scale value somewhere so that next pages renders with the new scale. Currently, I believe it's trying to resize whole pdf or part of the pdf which has been loaded which explains the delay. I do not have any idea on how to implement it.

@adamabdulaev
Copy link

adamabdulaev commented Oct 7, 2021

This solution does not work correctly if we render the entire document at once, rather than one page. Has anyone tried to solve this problem?

@GAGANsinghmsitece
Copy link

GAGANsinghmsitece commented Oct 7, 2021

Lazy loading would be the correct way. You would also need to ensure that if user scrolls a page after let's say 5 pages,free it from memory, but i believe pdfjs is already doing it. I better solution would be to just translate pdfjs when user is zooming and do not do actual zooming and store the value. When user has finished, do the actual zooming. Plus the code is resetting currentscale after every time. Instead we can set a maximum and minimum possible scale say 3 and 0.1. I noticed that google drive also do not do any zoom while user is pinching but it does when user has finished it.
It looks like the code is doing exactly the same thing except setting max and min scale

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment