You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

自定义JS PDF图片渲染器缩放时页面跳转问题解决方案咨询

问题

我有一套将PDF渲染为图片的自定义JavaScript遗留代码,现在遇到的问题是:执行放大(Zoom in)/缩小(Zoom out)操作时,页面会自动切换,我希望缩放过程中始终停留在当前页面。

我尝试用transformOrigin属性让缩放聚焦于中心,但这会导致包含所有渲染图片的.pages-container元素原始尺寸发生变化。请问有没有办法修复这个尺寸问题,或者有更优的实现方式确保缩放时页面不切换?

相关代码

render() {
  return (
    <ReactResizeDetector handleWidth onResize={() => this.handleResize()}>
      <div className='evidence-document-viewer-component vbox hfill vfill overflow-hidden evidence-document-viewer-ez'>

        <style dangerouslySetInnerHTML={this.evidenceStyle} />

        {this.renderDocHeaderContents()}

        <div data-test='ev-doc-scrollable'
          className='ev-doc-scrollable hfill vfill'>

          <div className='ev-doc-inline-contents overflow-auto'
            tabIndex={0}
            ref={this.inlineContentsRef}
            id='mainContainer'>

            {(this.props.noClinicalMessageFound && !this.props.loadedResult?.ezChartAsJson) &&
              <div className='no-document-msg'>
                ${this.props.noClinicalMessageFound}
              </div>
            }
            {(!this.props.noClinicalMessageFound && this.props.loadedResult?.ezChartAsJson) && this.renderPages()}
          </div>
        </div>
      </div>
    </ReactResizeDetector>);
}

renderPages() {
  if (!this.props.loadedResult?.ezChartAsJson?.pages) {
    return null;
  }

  window.setTimeout(() => {
    // for IE support, we will create canvases for all elements on this page if the page image is already loaded
    if (EvidenceDocUtils.useCanvasElements) {
      $('.page-container').each((index, page) => {
        EvidenceToCanvasUtil.createForPage($(page));
      });
    }
  });

  let { pageImagePath } = this.props.loadedResult.ezChartAsJson;
  if (pageImagePath) {
    pageImagePath = `${location.protocol}//${location.host}${pageImagePath}`;
  }

  const pages = this.props.loadedResult.ezChartAsJson.pages;
  const transform = `scale(${this.scaling}) translateX(${this.translateX}px`;
  const zeroPad = (num: number, places: number) => String(num).padStart(places, '0');
  const pageContainerStyle = {
    width: this.contentsWidth,
    height: this.contentsHeight,
    transformOrigin: 'center',
    transform
  };
  return <div className='pages-container' ref={this.pagesContainerRef} style={pageContainerStyle}>
    {pages.map((page, pageIndex) => {
      const pageBounds = this.pageBoundsArray[pageIndex];
      const isVisible = pageIndex >= this.pageIndexToRenderStart && pageIndex <= this.pageIndexToRenderEnd;
      const rotation = this.rotation;
      const style = {
        width: pageBounds.w,
        height: pageBounds.h,
        left: pageBounds.x,
        top: pageBounds.y,
        transform: `rotate(${rotation}deg)`
      };

      return (isVisible && <div
        className='page-container' data-visible={true} key={pageIndex} style={style}>

        <img src="/assets/images/loading_circle128.gif" className='page-image-loading' />
        {!this.suppressPageImages &&
          <img src={`${pageImagePath}${zeroPad(pageIndex, 4)}.jpg`} className='page-image' />
        }

        {page.evidences.map(evidence => {
          // get the index of the evidence in the results
          const conceptIndex = this.evidencesToInstancesMap.get(evidence);
          const key = `${evidence.conceptId}|${conceptIndex}`;
          const { conceptIdsAndInstancesToClassMap } = this;
          const evidenceClassName = (conceptIdsAndInstancesToClassMap[evidence.conceptId] ?? {})[conceptIndex] ?? '';
          if (evidenceClassName) {
            const toolTip = this.toolTipsForConceptIds[evidence.conceptId];
            const style = {
              left: evidence.x,
              top: evidence.y,
              width: evidence.w,
              height: evidence.h
            };
            return <span key={key} className={`nlp cg${evidence.conceptId} ${evidenceClassName}`}
              onClick={this.handleClickEvidence}
              title={toolTip} style={style} data-concept-id={evidence.conceptId} data-concept-index={conceptIndex}></span>;
          } else {
            return null;
          }
        })}

      </div>);
    })}

  </div>;
}

@action evidenceZoomIn() {
  this.scaleToFit = false;
  this.scaleToFitPage = false;
  this.evidenceDocScaling *= 1.1;
}

@action evidenceZoomOut() {
  this.scaleToFit = false;
  this.scaleToFitPage = false;
  this.evidenceDocScaling *= .9;
}

解决思路与方案

核心问题分析

缩放时页面切换的本质是:transform: scale()仅改变容器视觉尺寸,但滚动容器的滚动位置基于原始布局尺寸计算,缩放后内容实际滚动范围错位,导致视觉上的页面跳变。同时transformOrigin: center固定容器中心为缩放点,而非当前视口中心,进一步加剧滚动偏移。

具体修复方案

1. 固定.pages-container的原始尺寸

容器始终保持文档的真实原始尺寸(所有页面拼接后的总宽高),仅通过transform: scale()实现视觉缩放,避免滚动范围混乱:

// 先提前计算文档原始尺寸(比如纵向排版时,高度为所有页面高度之和,宽度取最大页面宽度)
// 示例计算逻辑(可根据实际排版调整)
const calculateOriginalDocumentSize = () => {
  const pages = this.props.loadedResult.ezChartAsJson.pages;
  let maxWidth = 0;
  let totalHeight = 0;
  pages.forEach(page => {
    const bounds = this.pageBoundsArray[pages.indexOf(page)];
    maxWidth = Math.max(maxWidth, bounds.w);
    totalHeight += bounds.h;
  });
  return { width: maxWidth, height: totalHeight };
};

// 修改pageContainerStyle
const { width: originalDocumentWidth, height: originalDocumentHeight } = calculateOriginalDocumentSize();
const pageContainerStyle = {
  width: originalDocumentWidth,
  height: originalDocumentHeight,
  transformOrigin: '0 0', // 改为左上角原点,便于后续滚动计算
  transform: `scale(${this.scaling})`
};

2. 缩放时锁定当前视口中心

缩放前后记录当前视口对应的文档原始坐标,缩放后调整滚动位置,确保该坐标始终在视口中心:

@action evidenceZoomIn() {
  this.scaleToFit = false;
  this.scaleToFitPage = false;
  this.adjustScrollPositionBeforeZoom();
  this.evidenceDocScaling *= 1.1;
  this.adjustScrollPositionAfterZoom();
}

@action evidenceZoomOut() {
  this.scaleToFit = false;
  this.scaleToFitPage = false;
  this.adjustScrollPositionBeforeZoom();
  this.evidenceDocScaling *= .9;
  this.adjustScrollPositionAfterZoom();
}

// 缩放前记录视口中心对应的原始文档坐标
adjustScrollPositionBeforeZoom() {
  const scrollContainer = this.inlineContentsRef.current;
  if (!scrollContainer) return;

  const viewportCenterX = scrollContainer.scrollLeft + scrollContainer.clientWidth / 2;
  const viewportCenterY = scrollContainer.scrollTop + scrollContainer.clientHeight / 2;
  // 转换为原始文档坐标(除以当前缩放比例)
  this.preZoomDocCenterX = viewportCenterX / this.scaling;
  this.preZoomDocCenterY = viewportCenterY / this.scaling;
}

// 缩放后调整滚动位置,让原始坐标回到视口中心
adjustScrollPositionAfterZoom() {
  const scrollContainer = this.inlineContentsRef.current;
  if (!scrollContainer) return;

  const newViewportCenterX = this.preZoomDocCenterX * this.scaling;
  const newViewportCenterY = this.preZoomDocCenterY * this.scaling;
  // 设置新的滚动位置
  scrollContainer.scrollLeft = newViewportCenterX - scrollContainer.clientWidth / 2;
  scrollContainer.scrollTop = newViewportCenterY - scrollContainer.clientHeight / 2;
}

3. 简化替代方案:使用CSS zoom属性

如果项目目标浏览器兼容(Chrome、Edge、IE支持良好,Firefox需额外配置),可直接用zoom替代transform: scale(),它会直接缩放元素且不改变布局尺寸,滚动位置自动保持:

const pageContainerStyle = {
  width: originalDocumentWidth,
  height: originalDocumentHeight,
  zoom: this.scaling // 替换transform为zoom
};

额外优化

  • 移除transform中的translateX硬编码(无特殊平移需求时无需保留)
  • 确保pageIndexToRenderStart/pageIndexToRenderEnd基于原始页面位置计算,避免渲染错误页面

内容的提问来源于stack exchange,提问作者Akash Singh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 05:38:11