Skip to content

Instantly share code, notes, and snippets.

@larsneo
Forked from jsprpalm/viewer.html
Last active April 22, 2024 22:14
Show Gist options
  • Star 40 You must be signed in to star a gist
  • Fork 15 You must be signed in to fork a gist
  • Save larsneo/bb75616e9426ae589f50e8c8411020f6 to your computer and use it in GitHub Desktop.
Save larsneo/bb75616e9426ae589f50e8c8411020f6 to your computer and use it in GitHub Desktop.
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>
@shashankraj0202
Copy link

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

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

@ismael-miguel
Copy link

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

Thank you !

@zoffyzhang
Copy link

thx!

@ojuniour
Copy link

thx!

Did it work for you?

@zoffyzhang
Copy link

thx!

Did it work for you?

perfectly

@zoffyzhang
Copy link

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 });

@adamabdulaev
Copy link

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

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();
  });

@adamabdulaev
Copy link

в 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

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.

@adam-abdulaev
Copy link

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

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

@GAGANsinghmsitece
Copy link

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.

@adam-abdulaev
Copy link

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

@naynara87
Copy link

I have a question. As I touch to zoom out, I want to set my pdf page scale for min size (page fit)
How can I do. I waiting for your ask.

@BekNaji
Copy link

BekNaji commented Jun 21, 2022

So I have set minScale and maxScale like this. Just replace touchend event

const pinchMaxScale = 5;
const pinchMinScale = 0.3;
      document.addEventListener("touchend", (e) => {
            if (initialPinchDistance <= 0) { return; }
            viewer.style.transform = `none`;
            viewer.style.transformOrigin = `unset`;
            
            const newPinchScale = PDFViewerApplication.pdfViewer.currentScale * pinchScale;

            if (newPinchScale <= pinchMaxScale && newPinchScale >= pinchMinScale) {
                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);
            }else{
            	if(newPinchScale >= pinchMaxScale){
            		PDFViewerApplication.pdfViewer.currentScale = pinchMaxScale;
            	}else{
            		PDFViewerApplication.pdfViewer.currentScale = pinchMinScale;
            	}
            }
            reset();
        });

I hope it will help someone else :-)

@Ikau
Copy link

Ikau commented Jun 22, 2022

So this code is working fine but it may need a little update if you're using recent versions of mozilla/pdf.js

webviewerloaded was not firing on iOS 13+ and Android 10+ when I tested because the event was fired using deprecated code.

In this case, you should be listening on DOMContentLoaded instead of webviewerloaded

Replace

document.addEventListener('webviewerloaded', () => {

With

document.addEventListener('DOMContentLoaded', () => {

@meghrathod
Copy link

Hence the final code should be this:

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("DOMContentLoaded", () => {
    if (!pinchZoomEnabled) {
        pinchZoomEnabled = true;
        enablePinchZoom();
    }
});

It can be placed in a file called zoom.js and you can just add this line before the final </body> tag in viewer.html file in the web folder.

<body>
    ---
    ---
    <script src="zoom.js"></script>
</body>

@yc-cui
Copy link

yc-cui commented Aug 15, 2022

Is this just suitable for viewer.html? Can I utilize it within my customed viewer? Like a viewer in a div using canvas backend?

@sergiobellini
Copy link

sergiobellini commented Sep 13, 2022

Unfortunately pinch-to-zoom doesn't work if I put the viewer in an iframe :( Is there any kind of solution?

@meghrathod
Copy link

Is this just suitable for viewer.html? Can I utilize it within my customed viewer? Like a viewer in a div using canvas backend?

I think it should work but you would have to make appropriate changes as required by the back end that you make.

@azazel404
Copy link

hello guys , when i implement and test it on my application ,it doesn't work

@duandre
Copy link

duandre commented Apr 15, 2023

Thank you @meghrathod - It's working for BlazorWebView in MAUI Blazor

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