espresso3389 / pdfrx

pdfrx is yet another PDF viewer implementation that built on the top of PDFium. The plugin currently supports Android, iOS, Windows, macOS, Linux, and Web.
MIT License
115 stars 55 forks source link

`PdfDocumentRef` containing the same `hasCode` cannot be used by multiple `PdfViewer`s. #155

Open LiWenHui96 opened 6 months ago

LiWenHui96 commented 6 months ago

PdfViewer will provide me with the error message.

======== Exception caught by widgets library =======================================================
The following assertion was thrown building StreamBuilder<Matrix4>(dirty, state: _StreamBuilderBaseState<Matrix4, AsyncSnapshot<Matrix4>>#b68ce):
RenderBox was not laid out: RenderFractionalTranslation#fcc2e NEEDS-LAYOUT NEEDS-PAINT NEEDS-COMPOSITING-BITS-UPDATE
'package:flutter/src/rendering/box.dart':
Failed assertion: line 1972 pos 12: 'hasSize'

Either the assertion indicates an error in the framework itself, or we should provide substantially more information in this error message to help you determine and fix the underlying cause.
In either case, please report this assertion by filing a bug on GitHub:
  https://github.com/flutter/flutter/issues/new?template=2_bug.yml

The relevant error-causing widget was: 
  PdfViewer PdfViewer:file:///Users/liwenhui/WorkSpace/demo/lib/widget/pdf_view.dart:100:12
When the exception was thrown, this was the stack: 
#2      RenderBox.size (package:flutter/src/rendering/box.dart:1972:12)
#3      RenderFractionalTranslation.applyPaintTransform (package:flutter/src/rendering/proxy_box.dart:2952:24)
#4      RenderObject.getTransformTo (package:flutter/src/rendering/object.dart:3352:24)
#5      RenderBox.localToGlobal (package:flutter/src/rendering/box.dart:2619:39)
#6      _PdfViewerState._localToGlobal (package:pdfrx/src/widgets/pdf_viewer.dart:1446:22)
#7      _PdfViewerState._documentToGlobal (package:pdfrx/src/widgets/pdf_viewer.dart:1458:49)
#8      _PdfViewerState._documentToRenderBox (package:pdfrx/src/widgets/pdf_viewer.dart:787:16)
#9      _PdfViewerState._buildPageOverlayWidgets (package:pdfrx/src/widgets/pdf_viewer.dart:695:28)
#10     _PdfViewerState.build.<anonymous closure>.<anonymous closure> (package:pdfrx/src/widgets/pdf_viewer.dart:440:24)
#11     StreamBuilder.build (package:flutter/src/widgets/async.dart:437:81)
#12     _StreamBuilderBaseState.build (package:flutter/src/widgets/async.dart:120:48)
#13     StatefulElement.build (package:flutter/src/widgets/framework.dart:5592:27)
#14     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5480:15)
#15     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5643:11)
#16     Element.rebuild (package:flutter/src/widgets/framework.dart:5196:7)
#17     ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:5462:5)
#18     StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:5634:11)
#19     ComponentElement.mount (package:flutter/src/widgets/framework.dart:5456:5)
...     Normal element mounting (27 frames)
#46     Element.inflateWidget (package:flutter/src/widgets/framework.dart:4335:16)
#47     Element.updateChild (package:flutter/src/widgets/framework.dart:3846:18)
#48     _LayoutBuilderElement._layout.layoutCallback (package:flutter/src/widgets/layout_builder.dart:155:18)
#49     BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:2844:19)
#50     _LayoutBuilderElement._layout (package:flutter/src/widgets/layout_builder.dart:173:12)
#51     RenderObject.invokeLayoutCallback.<anonymous closure> (package:flutter/src/rendering/object.dart:2686:59)
#52     PipelineOwner._enableMutationsToDirtySubtrees (package:flutter/src/rendering/object.dart:1097:15)
#53     RenderObject.invokeLayoutCallback (package:flutter/src/rendering/object.dart:2686:14)
#54     RenderConstrainedLayoutBuilder.rebuildIfNecessary (package:flutter/src/widgets/layout_builder.dart:248:7)
#55     _RenderLayoutBuilder.performLayout (package:flutter/src/widgets/layout_builder.dart:331:5)
#56     RenderObject.layout (package:flutter/src/rendering/object.dart:2575:7)
#57     RenderBox.layout (package:flutter/src/rendering/box.dart:2389:11)
#58     MultiChildLayoutDelegate.layoutChild (package:flutter/src/rendering/custom_layout.dart:173:12)
#59     _ScaffoldLayout.performLayout (package:flutter/src/material/scaffold.dart:1063:7)
#60     MultiChildLayoutDelegate._callPerformLayout (package:flutter/src/rendering/custom_layout.dart:237:7)
#61     RenderCustomMultiChildLayoutBox.performLayout (package:flutter/src/rendering/custom_layout.dart:404:14)
#62     RenderObject.layout (package:flutter/src/rendering/object.dart:2575:7)
#63     RenderBox.layout (package:flutter/src/rendering/box.dart:2389:11)
#64     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:105:21)
#65     RenderObject.layout (package:flutter/src/rendering/object.dart:2575:7)
#66     RenderBox.layout (package:flutter/src/rendering/box.dart:2389:11)
#67     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:105:21)
#68     _RenderCustomClip.performLayout (package:flutter/src/rendering/proxy_box.dart:1440:11)
#69     RenderObject.layout (package:flutter/src/rendering/object.dart:2575:7)
#70     RenderBox.layout (package:flutter/src/rendering/box.dart:2389:11)
#71     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:105:21)
#72     RenderObject.layout (package:flutter/src/rendering/object.dart:2575:7)
#73     RenderBox.layout (package:flutter/src/rendering/box.dart:2389:11)
#74     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:105:21)
#75     RenderObject.layout (package:flutter/src/rendering/object.dart:2575:7)
#76     RenderBox.layout (package:flutter/src/rendering/box.dart:2389:11)
#77     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:105:21)
#78     RenderObject.layout (package:flutter/src/rendering/object.dart:2575:7)
#79     RenderBox.layout (package:flutter/src/rendering/box.dart:2389:11)
#80     ChildLayoutHelper.layoutChild (package:flutter/src/rendering/layout_helper.dart:52:11)
#81     RenderStack._computeSize (package:flutter/src/rendering/stack.dart:582:43)
#82     RenderStack.performLayout (package:flutter/src/rendering/stack.dart:609:12)
#83     RenderObject.layout (package:flutter/src/rendering/object.dart:2575:7)
#84     RenderBox.layout (package:flutter/src/rendering/box.dart:2389:11)
#85     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:105:21)
#86     RenderObject.layout (package:flutter/src/rendering/object.dart:2575:7)
#87     RenderBox.layout (package:flutter/src/rendering/box.dart:2389:11)
#88     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:105:21)
#89     RenderObject.layout (package:flutter/src/rendering/object.dart:2575:7)
#90     RenderBox.layout (package:flutter/src/rendering/box.dart:2389:11)
#91     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:105:21)
#92     RenderObject.layout (package:flutter/src/rendering/object.dart:2575:7)
#93     RenderBox.layout (package:flutter/src/rendering/box.dart:2389:11)
#94     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:105:21)
#95     RenderObject.layout (package:flutter/src/rendering/object.dart:2575:7)
#96     RenderBox.layout (package:flutter/src/rendering/box.dart:2389:11)
#97     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:105:21)
#98     RenderObject.layout (package:flutter/src/rendering/object.dart:2575:7)
#99     RenderBox.layout (package:flutter/src/rendering/box.dart:2389:11)
#100    RenderOffstage.performLayout (package:flutter/src/rendering/proxy_box.dart:3726:14)
#101    RenderObject.layout (package:flutter/src/rendering/object.dart:2575:7)
#102    RenderBox.layout (package:flutter/src/rendering/box.dart:2389:11)
#103    RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:105:21)
#104    RenderObject.layout (package:flutter/src/rendering/object.dart:2575:7)
#105    RenderBox.layout (package:flutter/src/rendering/box.dart:2389:11)
#106    _RenderTheaterMixin.layoutChild (package:flutter/src/widgets/overlay.dart:968:13)
#107    _RenderTheater.performLayout (package:flutter/src/widgets/overlay.dart:1282:9)
#108    RenderObject._layoutWithoutResize (package:flutter/src/rendering/object.dart:2414:7)
#109    PipelineOwner.flushLayout (package:flutter/src/rendering/object.dart:1051:18)
#110    PipelineOwner.flushLayout (package:flutter/src/rendering/object.dart:1064:15)
#111    RendererBinding.drawFrame (package:flutter/src/rendering/binding.dart:582:23)
#112    WidgetsBinding.drawFrame (package:flutter/src/widgets/binding.dart:991:13)
#113    RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:448:5)
#114    SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1386:15)
#115    SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1311:9)
#116    SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/binding.dart:1169:5)
#117    _invoke (dart:ui/hooks.dart:312:13)
#118    PlatformDispatcher._drawFrame (dart:ui/platform_dispatcher.dart:399:5)
#119    _drawFrame (dart:ui/hooks.dart:283:31)
(elided 2 frames from class _AssertionError)

Your support is needed, thank you very much!😄

Flutter doctor

Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.19.6, on macOS 14.4.1 23E224 darwin-x64, locale zh-Hans-CN)
[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 15.3)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2023.3)
[✓] IntelliJ IDEA Ultimate Edition (version 2024.1)
[✓] VS Code (version 1.88.0)
[✓] Proxy Configuration
[✓] Connected device (4 available)
[✓] Network resources

• No issues found!
LiWenHui96 commented 6 months ago

Although he seems to have no influence on the display effect, it is a problem after all!

UnluckyY1 commented 6 months ago

@LiWenHui96 can you share your pdf_view.dart code ?

LiWenHui96 commented 6 months ago

@UnluckyY1 这是一套我自己的方案,仅供参考

import 'dart:async';
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:pdfrx/pdfrx.dart';

export 'package:pdfrx/pdfrx.dart';

class PDFView extends StatefulWidget {
  /// Create [PdfViewer] from an asset.
  PDFView.asset(
    String assetName, {
    PdfPasswordProvider? passwordProvider,
    bool firstAttemptByEmptyPassword = true,
    super.key,
    this.controller,
    this.initialPageNumber = 1,
    this.isSafeArea = false,
    this.enableTextSelection = false,
    this.onViewerReady,
  })  : documentRef = CustomPdfDocumentRefAsset(
          assetName,
          passwordProvider: passwordProvider,
          firstAttemptByEmptyPassword: firstAttemptByEmptyPassword,
          hashCode: key != null ? key.hashCode : assetName.hashCode,
        ),
        sourceType = ResourcesSourceType.asset;

  /// Create [PdfViewer] from a file.
  PDFView.file(
    String filePath, {
    PdfPasswordProvider? passwordProvider,
    bool firstAttemptByEmptyPassword = true,
    super.key,
    this.controller,
    this.initialPageNumber = 1,
    this.isSafeArea = false,
    this.enableTextSelection = false,
    this.onViewerReady,
  })  : documentRef = CustomPdfDocumentRefFile(
          filePath,
          passwordProvider: passwordProvider,
          firstAttemptByEmptyPassword: firstAttemptByEmptyPassword,
          hashCode: key != null ? key.hashCode : filePath.hashCode,
        ),
        sourceType = ResourcesSourceType.file;

  /// Create [PdfViewer] from a URI.
  PDFView.uri(
    Uri uri, {
    PdfPasswordProvider? passwordProvider,
    bool firstAttemptByEmptyPassword = true,
    Map<String, String>? headers,
    super.key,
    this.controller,
    this.initialPageNumber = 1,
    this.isSafeArea = false,
    this.enableTextSelection = false,
    this.onViewerReady,
  })  : documentRef = CustomPdfDocumentRefUri(
          uri,
          passwordProvider: passwordProvider,
          firstAttemptByEmptyPassword: firstAttemptByEmptyPassword,
          preferRangeAccess: true,
          headers: headers,
          hashCode: key != null ? key.hashCode : uri.hashCode,
        ),
        sourceType = ResourcesSourceType.network;

  /// [PdfDocumentRef] that represents the PDF document.
  late final PdfDocumentRef documentRef;

  /// 资源来源方式
  final ResourcesSourceType sourceType;

  /// Controller to control the viewer.
  final PdfViewerController? controller;

  /// Page number to show initially.
  final int initialPageNumber;

  /// 是否采用安全区域
  final bool isSafeArea;

  /// Experimental: Enable text selection on pages.
  final bool enableTextSelection;

  /// 是否可以查看
  final ValueChanged<bool>? onViewerReady;

  @override
  State<PDFView> createState() => _PDFViewState();
}

class _PDFViewState extends State<PDFView> {
  PdfDocumentRef? _documentRef;

  /// 重试次数
  int _retryCount = 3;

  /// 加载时间过长的提示
  String? moreTimeTip;

  /// 加载时间过长的时间指示器
  Timer? _timer;

  @override
  void initState() {
    _getData();

    super.initState();
  }

  @override
  void dispose() {
    _cancelTime();

    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    if (_documentRef == null) return _buildLoading;
    return _buildBody(_documentRef!);
  }

  /// 主体
  Widget _buildBody(PdfDocumentRef documentRef) {
    return PdfViewer(
      documentRef,
      controller: widget.controller,
      params: PdfViewerParams(
        margin: 12,
        backgroundColor: Theme.of(context).scaffoldBackgroundColor,
        layoutPages: (List<PdfPage> pages, PdfViewerParams params) {
          final double width =
              pages.fold<double>(0, (_, __) => max(_, __.width)) +
                  params.margin * 2;
          final List<Rect> pageLayouts = <Rect>[];
          double y = params.margin;
          for (final PdfPage page in pages) {
            pageLayouts.add(
              Rect.fromLTWH(params.margin, y, page.width, page.height),
            );
            y += page.height + params.margin;
          }

          /// 用于添加安全区域
          if (widget.isSafeArea) y += MediaQuery.paddingOf(context).bottom;

          return PdfPageLayout(
            pageLayouts: pageLayouts,
            documentSize: Size(width, y),
          );
        },
        enableTextSelection: widget.enableTextSelection,
        pageDropShadow: const BoxShadow(
          color: Colors.black26,
          blurRadius: 4,
          spreadRadius: 2,
          offset: Offset(2, 2),
        ),
        onViewerReady: (_, __) {
          widget.onViewerReady?.call(true);
        },
        loadingBannerBuilder: (_, int count, int? total) => _buildLoading,
        errorBannerBuilder: (_, Object error, StackTrace? stackTrace, __) {
          /// 异常重试机制
          if (_retryCount > 0) {
            _retryCount--;
            _getData();
          } else {
            /// 异常上报
          }

          widget.onViewerReady?.call(false);

          return _buildError;
        },
      ),
      initialPageNumber: widget.initialPageNumber,
    );
  }

  /// 加载布局
  Widget get _buildLoading {
    return Center(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          const CircularProgressIndicator(),
          if (moreTimeTip != null)
            Container(
              margin: const EdgeInsets.only(top: 6),
              child: Text(moreTimeTip ?? ''),
            ),
        ],
      ),
    );
  }

  /// 异常布局
  Widget get _buildError {
    return const Center(child: Text('文档加载异常'));
  }

  /// 对于 [ResourcesSourceType.asset] 和 [ResourcesSourceType.file]
  /// 直接返回 widget.documentRef
  ///
  /// 对于 [ResourcesSourceType.network]
  /// 进行网络请求后,下载至本地,改用 [CustomPdfDocumentRefFile] 展示
  Future<void> _getData() async {
    _openTime();

    _documentRef = await _getPdfDocumentRef();
    if (mounted) setState(() {});
  }

  Future<PdfDocumentRef> _getPdfDocumentRef() async {
    final PdfDocumentRef documentRef = widget.documentRef;

    /// 当资源为网络资源时,且重试次数小于3次,则强制加载本地数据
    if (widget.sourceType == ResourcesSourceType.network &&
        documentRef is PdfDocumentRefUri &&
        _retryCount < 3) {
      /// 文件存储地址
      final Uri uri = documentRef.uri;
      final PdfFileCacheNative cache = await PdfFileCacheNative.fromUri(uri);
      if (cache.isInitialized) {
        return CustomPdfDocumentRefFile(
          cache.filePath,
          passwordProvider: documentRef.passwordProvider,
          firstAttemptByEmptyPassword: documentRef.firstAttemptByEmptyPassword,
          autoDispose: documentRef.autoDispose,
          hashCode: hashCode,
        );
      }
    }

    return documentRef;
  }

  /// 开启计时
  void _openTime() {
    if (_timer != null) _cancelTime();

    _timer = Timer.periodic(const Duration(seconds: 3), (Timer timer) {
      if (timer.isActive) {
        setState(() => moreTimeTip = '努力加载中,请稍候...');
        _cancelTime();
      }
    });
  }

  /// 取消计时
  void _cancelTime() {
    _timer?.cancel();
    _timer = null;
  }
}

@immutable
class CustomPdfDocumentRefAsset extends PdfDocumentRefAsset {
  const CustomPdfDocumentRefAsset(
    super.name, {
    super.passwordProvider,
    super.firstAttemptByEmptyPassword = true,
    super.autoDispose = true,
    required int hashCode,
  }) : _hashCode = hashCode;

  final int _hashCode;

  @override
  bool operator ==(Object other) =>
      other is CustomPdfDocumentRefAsset && name == other.name;

  @override
  int get hashCode => _hashCode;
}

@immutable
class CustomPdfDocumentRefUri extends PdfDocumentRefUri {
  const CustomPdfDocumentRefUri(
    super.uri, {
    super.passwordProvider,
    super.firstAttemptByEmptyPassword = true,
    super.autoDispose = true,
    super.preferRangeAccess,
    super.headers,
    required int hashCode,
  }) : _hashCode = hashCode;

  final int _hashCode;

  @override
  bool operator ==(Object other) =>
      other is CustomPdfDocumentRefUri && uri == other.uri;

  @override
  int get hashCode => _hashCode;
}

@immutable
class CustomPdfDocumentRefFile extends PdfDocumentRefFile {
  const CustomPdfDocumentRefFile(
    super.file, {
    super.passwordProvider,
    super.firstAttemptByEmptyPassword = true,
    super.autoDispose = true,
    required int hashCode,
  }) : _hashCode = hashCode;

  final int _hashCode;

  @override
  bool operator ==(Object other) =>
      other is CustomPdfDocumentRefFile && file == other.file;

  @override
  int get hashCode => _hashCode;
}

/// 资源的来源类型
enum ResourcesSourceType {
  /// 包含在应用程序的资产文件中
  asset,

  /// 从互联网上下载的
  network,

  /// 本地文件系统加载
  file;
}
EmreDET commented 4 months ago

This happens to me as well, but only when I push to a separate Widget containing my PDF View FROM a Bottom Sheet View. And as he said:

Although he seems to have no influence on the display effect, it is a problem after all!

It's only showing the error in the console, and the App does not become red in debug or gray in production.

Outline of my Widget Tree

Scaffold (Home Screen) > Bottom Sheet (Some User Input Process) > Scaffold with PdfViewer.file() Hope that helps.

Emre Y.