美文网首页iOS 技巧
flutter 截图和保存图片到本地和相册,并做分享

flutter 截图和保存图片到本地和相册,并做分享

作者: 冰棍儿好烫嘴 | 来源:发表于2022-03-16 19:59 被阅读0次

    一、截图功能

    使用RepaintBoundary实现
    具体实现:

    1、注册全局的key与RepaintBoundary匹配,来标明截图内容

    //全局key-截图key
    final GlobalKey boundaryKey = GlobalKey();
    

    2、将需要截图的widget包裹在RepaintBoundary组建中,加入key属性

    SingleChildScrollView(
                  child:RepaintBoundary(
                key: boundaryKey,
                child:Container(
                          color: MyColor.white,
                          child:Column(
                  children: [
                    Padding(
                      padding: EdgeInsets.fromLTRB(
                          MyDimens.margin, 10, MyDimens.margin, 10),
                      child: Row(
                        crossAxisAlignment: CrossAxisAlignment.center,
                        children: [
                          Visibility(
                              visible: result != null && result!.org.length > 0,
                              child: Image(
                                image: result != null &&
                                        result!.org.length > 0 &&
                                        result!.org[0].source!.jglx == "党政机关"
                                    ? AssetImage("assets/images/organ.png")
                                    : AssetImage("assets/images/institution.png"),
                                width: 20,
                                height: 20,
                              )),
                          Visibility(
                              visible: result != null && result!.org.length > 0,
                              child: SizedBox(
                                width: 5,
                              )),
                          Expanded(
                              child: Text(
                            _content,
                            style: MyStyle.text_style_bold,
                          ))
                        ],
                      ),
                    ),
                    Visibility(
                        visible: result != null &&
                            result!.org.length > 0 &&
                            result!.org[0].source != null &&
                            result!.web[0].source!.zwym != "--",
                        child: InkWell(
                          onTap: () {
                            launch('' + result!.web[0].source!.zwym.split(',')[0]);
                          },
                          child: Padding(
                              padding:
                                  EdgeInsets.fromLTRB(42, 0, MyDimens.margin, 10),
                              child: Text(
                                result != null &&
                                        result!.org[0].source != null &&
                                        result!.org.length > 0
                                    ? result!.web[0].source!.zwym.split(',')[0]
                                    : "",
                                style: MyStyle.text_style_link,
                              )),
                        )),
                    Container(
                      color: MyColor.background,
                      height: 0.5,
                    ),
                    Container(
                      color: MyColor.background,
                      height: 10,
                    ),
                    Padding(
                      padding: EdgeInsets.all(MyDimens.margin),
                      child: Text(
                        "基本信息",
                        style: MyStyle.text_style_bold,
                      ),
                    ),
                    Container(
                      color: MyColor.background,
                      height: 0.5,
                    ),
                    Padding(
                      padding: EdgeInsets.symmetric(horizontal: MyDimens.margin),
                      child: Column(
                        children: [
                          KeyValueSingle(
                              mKey: '机构职能',
                              mValue: result != null && result!.org.length > 0
                                  ? result!.org[0].source!.jgzz
                                  : ""),
                        ],
                      ),
                    ),
                    Container(
                      color: MyColor.background,
                      height: 10,
                    ),
                    Padding(
                      padding: EdgeInsets.all(MyDimens.margin),
                      child: Text(
                        "网站开办信息",
                        style: MyStyle.text_style_bold,
                      ),
                    ),
                    Container(
                      color: MyColor.background,
                      height: 0.5,
                    ),
                    Padding(
                      padding: EdgeInsets.symmetric(horizontal: MyDimens.margin),
                      child: Column(
                        children: [
                          KeyValueSingle(
                              mKey: '网站名称',
                              mValue: result != null && result!.org.length > 0
                                  ? result!.web[0].source!.wzmc
                                  : ""),
                          Container(
                            color: MyColor.background,
                            height: 0.5,
                          ),
                        
                        ],
                      ),
                    ),
                  ],
                ),
              ),
              ),
    )
    

    注意点:
    1、如果直接包裹listView则不能截取全部内容,最好用SingleChildScrollView + colum 结合实现列表 才能截取全部内容
    2、如果截取的内容超出屏幕,则必须将RepaintBoundary直接包裹在滑动内容上(colum上),否则屏幕外的内容截取不到
    满足以上两个条件,才能实现截取整个widget的内容
    3、如果RepaintBoundary包裹的widget没有背景色,在安卓上截图会是默认黑色背景,所以最好添加相应的背景色。

    二、图片保存

    /*
     * @Author: 王长春
     * @Date: 2022-03-14 09:24:34
     * @LastEditors: 王长春
     * @LastEditTime: 2022-03-17 10:01:41
     * @Description: 截图工具,生成截图,保存到相册或者保存本地cash文件夹返回文件路径(供分享使用)
     */
    
    import 'dart:io';
    import 'dart:typed_data';
    import 'dart:async';
    import 'dart:ui';
    import 'dart:ui' as ui;
    import 'package:flutter/material.dart';
    import 'package:flutter/rendering.dart';
    import 'package:flutter_easyloading/flutter_easyloading.dart';
    
    import 'package:image_gallery_saver/image_gallery_saver.dart';
    import 'package:path_provider/path_provider.dart';
    import 'package:permission_handler/permission_handler.dart';
    
    //全局key-截图key
    final GlobalKey boundaryKey = GlobalKey();
    
    class RepaintBoundaryUtils {
    //生成截图
      /// 截屏图片生成图片流ByteData
      Future<String> captureImage() async {
        RenderRepaintBoundary? boundary = boundaryKey.currentContext!
            .findRenderObject() as RenderRepaintBoundary?;
        double dpr = ui.window.devicePixelRatio; // 获取当前设备的像素比
        var image = await boundary!.toImage(pixelRatio: dpr);
        // 将image转化成byte
        ByteData? byteData = await image.toByteData(format: ImageByteFormat.png);
    
        var filePath = "";
    
        Uint8List pngBytes = byteData!.buffer.asUint8List();
        // 获取手机存储(getTemporaryDirectory临时存储路径)
        Directory applicationDir = await getTemporaryDirectory();
        // getApplicationDocumentsDirectory();
        // 判断路径是否存在
        bool isDirExist = await Directory(applicationDir.path).exists();
        if (!isDirExist) Directory(applicationDir.path).create();
        // 直接保存,返回的就是保存后的文件
        File saveFile = await File(
            applicationDir.path + "${DateTime.now().toIso8601String()}.jpg")
            .writeAsBytes(pngBytes);
        filePath = saveFile.path;
        // if (Platform.isAndroid) {
        //   // 如果是Android 的话,直接使用image_gallery_saver就可以了
        //   // 图片byte数据转化unit8
        //   Uint8List images = byteData!.buffer.asUint8List();
        //   // 调用image_gallery_saver的saveImages方法,返回值就是图片保存后的路径
        //   String result = await ImageGallerySaver.saveImage(images);
        //   // 需要去除掉file://开头。生成要使用的file
        //   File saveFile = new File(result.replaceAll("file://", ""));
        //   filePath = saveFile.path;
        //
        //
        // } else if (Platform.isIOS) {
        //   // 图片byte数据转化unit8
        //
        // }
    
        return filePath;
      }
    
    //申请存本地相册权限
      Future<bool> getPormiation() async {
        if (Platform.isIOS) {
          var status = await Permission.photos.status;
          if (status.isDenied) {
            Map<Permission, PermissionStatus> statuses = await [
              Permission.photos,
            ].request();
            // saveImage(globalKey);
          }
          return status.isGranted;
        } else {
          var status = await Permission.storage.status;
          if (status.isDenied) {
            Map<Permission, PermissionStatus> statuses = await [
              Permission.storage,
            ].request();
          }
          return status.isGranted;
        }
      }
    
    //保存到相册
      void savePhoto() async {
        RenderRepaintBoundary? boundary = boundaryKey.currentContext!
            .findRenderObject() as RenderRepaintBoundary?;
    
        double dpr = ui.window.devicePixelRatio; // 获取当前设备的像素比
        var image = await boundary!.toImage(pixelRatio: dpr);
        // 将image转化成byte
        ByteData? byteData = await image.toByteData(format: ImageByteFormat.png);
      //获取保存相册权限,如果没有,则申请改权限
        bool permition = await getPormiation();
        
        var status = await Permission.photos.status;
        if (permition) {
          if (Platform.isIOS) {
            if (status.isGranted) {
              Uint8List images = byteData!.buffer.asUint8List();
              final result = await ImageGallerySaver.saveImage(images,
                  quality: 60, name: "hello");
              EasyLoading.showToast("保存成功");
            }
            if (status.isDenied) {
              print("IOS拒绝");
            }
          } else {
            //安卓
            if (status.isGranted) {
              print("Android已授权");
              Uint8List images = byteData!.buffer.asUint8List();
              final result = await ImageGallerySaver.saveImage(images, quality: 60);
              if (result != null) {
                EasyLoading.showToast("保存成功");
              } else {
                print('error');
                // toast("保存失败");
              }
            }
          }
        }else{
          //重新请求--第一次请求权限时,保存方法不会走,需要重新调一次
          savePhoto();
        }
      }
    }
    
    

    调用方法:

    //保存本地 RepaintBoundaryUtils().savePhoto();
    
    

    说明:
    我这里涉及到分享以及保存图片到本地相册两个功能。
    涉及到的框架有:

      #分享
      share_plus: ^3.0.1
    
      #微信三方-分享、登录、小程序跳转等(不带支付)
      fluwx_no_pay: ^3.8.1
    
      #保存图片到相册
      image_gallery_saver: ^1.7.1
    
      #权限管理
      permission_handler: ^8.1.6
    
    
    iOS权限设置:

    在podfile中添加一下内容:

     target.build_configurations.each do |config|
          # You can remove unused permissions here
          # for more infomation: https://github.com/BaseflowIT/flutter-permission-handler/blob/master/permission_handler/ios/Classes/PermissionHandlerEnums.h
          # e.g. when you don't need camera permission, just add 'PERMISSION_CAMERA=0'
          config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
            '$(inherited)',
    
            ## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse]
            'PERMISSION_PHOTOS=1',
    
          ]
    
        end
    

    podfile整体做为参考:

    ENV['COCOAPODS_DISABLE_STATS'] = 'true'
    
    project 'Runner', {
      'Debug' => :debug,
      'Profile' => :release,
      'Release' => :release,
    }
    
    def flutter_root
      generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
      unless File.exist?(generated_xcode_build_settings_path)
        raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
      end
    
      File.foreach(generated_xcode_build_settings_path) do |line|
        matches = line.match(/FLUTTER_ROOT\=(.*)/)
        return matches[1].strip if matches
      end
      raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
    end
    
    require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
    
    flutter_ios_podfile_setup
    
    target 'Runner' do
      use_frameworks!
      use_modular_headers!
    
      flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
    end
    
    post_install do |installer|
      installer.pods_project.targets.each do |target|
        flutter_additional_ios_build_settings(target)
        target.build_configurations.each do |config|
          # You can remove unused permissions here
          # for more infomation: https://github.com/BaseflowIT/flutter-permission-handler/blob/master/permission_handler/ios/Classes/PermissionHandlerEnums.h
          # e.g. when you don't need camera permission, just add 'PERMISSION_CAMERA=0'
          config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
            '$(inherited)',
    
            ## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse]
            'PERMISSION_PHOTOS=1',
    
          ]
    
        end
      end
    end
    

    info.plist中添加字段

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
        <key>CFBundleDevelopmentRegion</key>
        <string>$(DEVELOPMENT_LANGUAGE)</string>
        <key>CFBundleExecutable</key>
        <string>$(EXECUTABLE_NAME)</string>
        <key>CFBundleIdentifier</key>
        <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
        <key>CFBundleInfoDictionaryVersion</key>
        <string>6.0</string>
        <key>CFBundleName</key>
        <string>organization</string>
        <key>CFBundlePackageType</key>
        <string>APPL</string>
        <key>CFBundleShortVersionString</key>
        <string>$(FLUTTER_BUILD_NAME)</string>
        <key>CFBundleSignature</key>
        <string>????</string>
        <key>CFBundleURLTypes</key>
        <array>
            <dict>
                <key>CFBundleTypeRole</key>
                <string>Editor</string>
                <key>CFBundleURLName</key>
                <string>weixin</string>
                <key>CFBundleURLSchemes</key>
                <array>
                    <string>wx543b04c9d1aa9a3a</string>
                </array>
            </dict>
        </array>
        <key>CFBundleVersion</key>
        <string>$(FLUTTER_BUILD_NUMBER)</string>
        <key>LSApplicationQueriesSchemes</key>
        <array>
            <string>weixinULAPI</string>
            <string>baidumap</string>
            <string>iosamap</string>
            <string>comgooglemaps</string>
            <string>qqmap</string>
            <string>mqzone</string>
            <string>mqqwpa</string>
            <string>mqzoneopensdkapi19</string>
            <string>mqzoneopensdkapi</string>
            <string>mqzoneopensdk</string>
            <string>mqzoneopensdkapiV2</string>
            <string>mqqapi</string>
            <string>mqq</string>
            <string>wtloginmqq2</string>
            <string>mqqopensdkapiV3</string>
            <string>mqqopensdkapiV2</string>
            <string>mqqOpensdkSSoLogin</string>
            <string>weixin</string>
            <string>wechat</string>
        </array>
        <key>LSRequiresIPhoneOS</key>
        <true/>
        <key>NSAppTransportSecurity</key>
        <dict>
            <key>NSAllowsArbitraryLoads</key>
            <true/>
        </dict>
        <key>NSCameraUsageDescription</key>
        <string>需要访问您的相机设置头像</string>
        <key>NSPhotoLibraryAddUsageDescription</key>
        <string>请允许APP保存图片到相册</string>
        <key>NSPhotoLibraryUsageDescription</key>
        <string>需要访问您的相册设置头像</string>
        <key>NSSupportsSuddenTermination</key>
        <false/>
        <key>UILaunchStoryboardName</key>
        <string>LaunchScreen</string>
        <key>UIMainStoryboardFile</key>
        <string>Main</string>
        <key>UISupportedInterfaceOrientations</key>
        <array>
            <string>UIInterfaceOrientationPortrait</string>
            <string>UIInterfaceOrientationLandscapeLeft</string>
            <string>UIInterfaceOrientationLandscapeRight</string>
        </array>
        <key>UISupportedInterfaceOrientations~ipad</key>
        <array>
            <string>UIInterfaceOrientationPortrait</string>
            <string>UIInterfaceOrientationPortraitUpsideDown</string>
            <string>UIInterfaceOrientationLandscapeLeft</string>
            <string>UIInterfaceOrientationLandscapeRight</string>
        </array>
        <key>UIViewControllerBasedStatusBarAppearance</key>
        <false/>
    </dict>
    </plist>
    

    添加urlSchemes

    截屏2022-03-16 19.48.50.png

    可以按我的添加,我这里除了基本功能,就是微信sdk分享以及相册相机权限。

    安卓权限

    AndroidManifest.xml添加一下权限

    <!-- 开启读写storage权限 -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    

    三、分享:

    分享这里提供两种方案;

    a、share_plus 分享:不用在第三方平台注册app,这种分享出去的图片,不带app图标以及名称,只有图片
    b、微信原生分享:fluwx_no_pay:需在微信开发者账号上注册app,这种分享出去的图片带app图标以及名称。

    微信原生分享代码:
    /*
     * @Author: 王长春
     * @Date: 2022-03-11 09:43:46
     * @LastEditors: 王长春
     * @LastEditTime: 2022-03-16 09:56:14
     * @Description: 
     */
    
    import 'dart:io';
    import 'dart:typed_data';
    import 'check.dart';
    import 'package:fluwx_no_pay/fluwx_no_pay.dart' as fluwx;
    
    
    class WxSdk {
      // static bool wxIsInstalled;
      static Future init() async {
        fluwx.registerWxApi(
            appId: "这里写你注册的appid",
            doOnAndroid: true,
            doOnIOS: true,
            universalLink: "这里写你注册的universalLink");
      
      }
    
      static Future<bool> wxIsInstalled() async {
        return await fluwx.isWeChatInstalled;
      }
    
    
    
      /**
       * 分享图片到微信,
       * file=本地路径
       * url=网络地址
       * asset=内置在app的资源图片
       * scene=分享场景,1好友会话,2朋友圈,3收藏
       */
      static void ShareImage(
          {String? title,
          String? decs,
          String? file,
          String? url,
          String? asset,
          int scene = 1}) async {
        fluwx.WeChatScene wxScene = fluwx.WeChatScene.SESSION;
        if (scene == 2) {
          wxScene = fluwx.WeChatScene.TIMELINE;
        } else if (scene == 3) {
          wxScene = fluwx.WeChatScene.FAVORITE;
        }
        fluwx.WeChatShareImageModel? model;
    
        if (file != null) {
          model = fluwx.WeChatShareImageModel(fluwx.WeChatImage.file(File(file)),
              title: title, description: decs, scene: wxScene);
        } else if (url != null) {
          model = fluwx.WeChatShareImageModel(fluwx.WeChatImage.network(url),
              title: title, description: decs, scene: wxScene);
        } else if (asset != null) {
          model = fluwx.WeChatShareImageModel(fluwx.WeChatImage.asset(asset),
              title: title, description: decs, scene: wxScene);
        } else {
          throw Exception("缺少图片资源信息");
        }
        fluwx.shareToWeChat(model);
      }
    
      /**
       * 分享文本
       * content=分享内容
       * scene=分享场景,1好友会话,2朋友圈,3收藏
       */
      static void ShareText(String content, {String? title, int scene = 1}) {
        fluwx.WeChatScene wxScene = fluwx.WeChatScene.SESSION;
        if (scene == 2) {
          wxScene = fluwx.WeChatScene.TIMELINE;
        } else if (scene == 3) {
          wxScene = fluwx.WeChatScene.FAVORITE;
        }
        fluwx.WeChatShareTextModel model =
            fluwx.WeChatShareTextModel(content, title: title, scene: wxScene);
        fluwx.shareToWeChat(model);
      }
    
    /***
     * 分享视频
     * videoUrl=视频网上地址
     * thumbFile=缩略图本地路径
       * scene=分享场景,1好友会话,2朋友圈,3收藏
     */
      static void ShareVideo(String videoUrl,
          {String? thumbFile, String? title, String? desc, int scene = 1}) {
        fluwx.WeChatScene wxScene = fluwx.WeChatScene.SESSION;
        if (scene == 2) {
          wxScene = fluwx.WeChatScene.TIMELINE;
        } else if (scene == 3) {
          wxScene = fluwx.WeChatScene.FAVORITE;
        }
        fluwx.WeChatImage? image;
        if (thumbFile != null) {
          image = fluwx.WeChatImage.file(File(thumbFile));
        }
        var model = fluwx.WeChatShareVideoModel(
            videoUrl: videoUrl,
            thumbnail: image,
            title: title,
            description: desc,
            scene: wxScene);
        fluwx.shareToWeChat(model);
      }
    
      /**
       * 分享链接
       * url=链接
       * thumbFile=缩略图本地路径
       * scene=分享场景,1好友会话,2朋友圈,3收藏
       */
      static void ShareUrl(String url,
          {String? thumbFile,
          Uint8List? thumbBytes,
          String? title,
          String? desc,
          int scene = 1,
          String? networkThumb,
          String? assetThumb}) {
        desc = desc ?? "";
        title = title ?? "";
        if (desc.length > 54) {
          desc = desc.substring(0, 54) + "...";
        }
        if (title.length > 20) {
          title = title.substring(0, 20) + "...";
        }
        fluwx.WeChatScene wxScene = fluwx.WeChatScene.SESSION;
        if (scene == 2) {
          wxScene = fluwx.WeChatScene.TIMELINE;
        } else if (scene == 3) {
          wxScene = fluwx.WeChatScene.FAVORITE;
        }
        fluwx.WeChatImage? image ;
        if (thumbFile != null) {
          image = fluwx.WeChatImage.file(File(thumbFile));
        } else if (thumbBytes != null) {
          image = fluwx.WeChatImage.binary(thumbBytes);
        } else if (strNoEmpty(networkThumb!)) {
          image = fluwx.WeChatImage.network(Uri.encodeFull(networkThumb));
        } else if (strNoEmpty(assetThumb!)) {
          image = fluwx.WeChatImage.asset(assetThumb, suffix: ".png");
        }
        var model = fluwx.WeChatShareWebPageModel(
          url,
          thumbnail: image,
          title: title,
          description: desc,
          scene: wxScene,
        );
        fluwx.shareToWeChat(model);
      }
    }
    
    分享功能封装:
    /*
     * @Author: 王长春
     * @Date: 2022-03-15 15:17:19
     * @LastEditors: 王长春
     * @LastEditTime: 2022-03-16 19:15:45
     * @Description: 分享工具
     */
    
    
    import 'package:flutter/material.dart';
    import 'package:flutter_easyloading/flutter_easyloading.dart';
    import 'package:organization/common/utils/repaintBoundary_utils.dart';
    import 'package:organization/common/utils/wechatSDK.dart';
    import 'package:share_plus/share_plus.dart';
    
    class ShareHelper {
    
      static bool wxIsInstalled = false;
    //微信分享截图
      static void onShareWx(BuildContext context) async {
    
        wxIsInstalled = await WxSdk.wxIsInstalled();
        //收回键盘,如果有输入的情况下,先收回键盘,再截图
        FocusScope.of(context).requestFocus(FocusNode());
        
        if (wxIsInstalled) {
          //分享
          // WxSdk.ShareText("sdgsfds",title:"标题");
          //获取截图地址
          String filePath = await RepaintBoundaryUtils().captureImage();
    
          print(filePath);
          WxSdk.ShareImage(title: "机构检索", decs: "", file: filePath);
        } else {
          //提示未安装微信
          EasyLoading.showToast("未安装微信");
        }
      }
    
      // static void checkWx() async {
      //   wxIsInstalled = await WxSdk.wxIsInstalled();
      // }
    
      //share_plus分享
      static void onSharePlusShare(BuildContext context) async {
        FocusScope.of(context).requestFocus(FocusNode());
        // A builder is used to retrieve the context immediately
        // surrounding the ElevatedButton.
        // The context's `findRenderObject` returns the first
        // RenderObject in its descendent tree when it's not
        // a RenderObjectWidget. The ElevatedButton's RenderObject
        // has its position and size after it's built.
        final box = context.findRenderObject() as RenderBox?;
        List<String> imagePaths = [];
    
        //获取截图地址
        String filePath = await RepaintBoundaryUtils().captureImage();
        //Share.shareFiles内可以传多张图片,里面是个数组,所以每次要将数组清空,再将新的截图添加到数组中
        imagePaths.clear();
        imagePaths.add(filePath);
        //分享
        await Share.shareFiles(imagePaths,
            text: "机构详情",
            subject: "",
            sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size);
      }
    }
    
    
    调用:
     //share_plus 分享
                    ShareHelper.onSharePlusShare(context);
                    //微信SDK分享
                    // ShareHelper.onShareWx(context);
    

    以上实现了图片截图以及保存本地分享内容,在安卓和iOS上亲测没问题。
    关于iOS微信原生分享,注册universalLink的内容,需要后台配合,我这里只是写了个demo,没具体实现。
    可以参考 : https://www.jianshu.com/p/4c96b54ef8d1

    相关文章

      网友评论

        本文标题:flutter 截图和保存图片到本地和相册,并做分享

        本文链接:https://www.haomeiwen.com/subject/qbapdrtx.html