鸿蒙next 下载图片实现教程(附带前后端代码)

360 阅读5分钟

前言导读

最近在研究图片上传和下这一块我们先讲下载 下一期我们再讲上传的逻辑。那么废话不多数我们正式开始。

效果图

image.png

image.png

image.png

具体实现

  • 客户端代码

// 根据url下载图片,获取的是ArrayBuffer对象


// 根据url下载图片,获取的是ArrayBuffer对象
public static async downLoadPic(url:string):Promise<ArrayBuffer|null> {
  let data = await http.createHttp().request(url);
  if (data.result instanceof ArrayBuffer) {
    let imageData:ArrayBuffer = data.result
    return imageData;
  }
  return null;
}

// 将ArrayBuffer转换为PixelMap对象,该对象可以直接传给Image组件使用

// 将ArrayBuffer转换为PixelMap对象,该对象可以直接传给Image组件使用
public  async arrayBufToPixelMap(buf:ArrayBuffer):Promise<PixelMap> {
  let imgSource:image.ImageSource = image.createImageSource(buf);
  let result:PixelMap = await imgSource.createPixelMap();
  return result;
}

// 将图片流保存到本地,路径由用户选择

// 将图片流保存到本地,路径由用户选择
public  saveToLocal(imageData:ArrayBuffer, contentType:string) {
  let photoSaveOption = new picker.AudioSaveOptions();
  let photoViewPicker = new picker.AudioViewPicker();
  photoSaveOption.newFileNames = [new Date().getTime() + '.jpg']
  if (contentType === 'video') {
    photoSaveOption.newFileNames = [new Date().getTime() + '.mp4']
  }
  // 此处会拉起系统的文件路径选择界面
  photoViewPicker.save(photoSaveOption).then(async (photoSaveResult) => {
    let uri = photoSaveResult[0];
    let file: fs.File = null!;
    try {
      file = await fs.open(uri, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
      await fs.write(file.fd, imageData);
    } finally {
      await fs.close(file.fd);
    }
  })
}

// 将图片流保存到相册


// 将图片流保存到相册
public  async savePicToGallery(context:common.UIAbilityContext, buf:ArrayBuffer){
  let helper = photoAccessHelper.getPhotoAccessHelper(context);
  let file: fs.File = null!;
  try {
    // 当点击SaveButton组件后10秒内通过createAsset接口创建图片文件,10秒后createAsset权限收回
    let uri = await helper.createAsset(photoAccessHelper.PhotoType.IMAGE,'jpg');
    file = await fs.open(uri, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
    await fs.write(file.fd, buf);
  } finally {
    await fs.close(file.fd);
  }

}

完整工具类代码

import { http } from '@kit.NetworkKit'
import { image } from '@kit.ImageKit';
import { picker } from '@kit.CoreFileKit';
import fs from '@ohos.file.fs'
import {common} from '@kit.AbilityKit'
import { photoAccessHelper } from '@kit.MediaLibraryKit';

export class DownloadUtil {
  public static instance: DownloadUtil | null = null
  // 设置方法为私有
  private constructor() {

  }

  public static getInstance() {
    if (!DownloadUtil.instance) {
      DownloadUtil.instance = new DownloadUtil()
    }
    return DownloadUtil.instance
  }




  // 根据url下载图片,获取的是ArrayBuffer对象
  public async downLoadPic(url:string):Promise<ArrayBuffer|null>{
    let data = await http.createHttp().request(url);
    if (data.result instanceof ArrayBuffer) {
      let imageData:ArrayBuffer = data.result
      return imageData;
    }
    return null;
  }

  // 将ArrayBuffer转换为PixelMap对象,该对象可以直接传给Image组件使用
  public  async arrayBufToPixelMap(buf:ArrayBuffer):Promise<PixelMap> {
    let imgSource:image.ImageSource = image.createImageSource(buf);
    let result:PixelMap = await imgSource.createPixelMap();
    return result;
  }

  // 将图片流保存到本地,路径由用户选择
  public  saveToLocal(imageData:ArrayBuffer, contentType:string) {
    let photoSaveOption = new picker.AudioSaveOptions();
    let photoViewPicker = new picker.AudioViewPicker();
    photoSaveOption.newFileNames = [new Date().getTime() + '.jpg']
    if (contentType === 'video') {
      photoSaveOption.newFileNames = [new Date().getTime() + '.mp4']
    }
    // 此处会拉起系统的文件路径选择界面
    photoViewPicker.save(photoSaveOption).then(async (photoSaveResult) => {
      let uri = photoSaveResult[0];
      let file: fs.File = null!;
      try {
        file = await fs.open(uri, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
        await fs.write(file.fd, imageData);
      } finally {
        await fs.close(file.fd);
      }
    })
  }

  // 将图片流保存到相册
  public  async savePicToGallery(context:common.UIAbilityContext, buf:ArrayBuffer){
    let helper = photoAccessHelper.getPhotoAccessHelper(context);
    let file: fs.File = null!;
    try {
      // 当点击SaveButton组件后10秒内通过createAsset接口创建图片文件,10秒后createAsset权限收回
      let uri = await helper.createAsset(photoAccessHelper.PhotoType.IMAGE,'jpg');
      file = await fs.open(uri, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
      await fs.write(file.fd, buf);
    } finally {
      await fs.close(file.fd);
    }

  }
}

  • ui调用

下载并显示
SaveButton()
  .onClick(async (event:ClickEvent, result: SaveButtonOnClickResult) => {
    if (result === SaveButtonOnClickResult.SUCCESS) {
      let buf = await DownloadUtil.getInstance().downLoadPic(this.url);
      if (buf) {
        let context = getContext(this) as common.UIAbilityContext
        DownloadUtil.getInstance().savePicToGallery(context, buf);
        DownloadUtil.getInstance().arrayBufToPixelMap(buf).then((data)=>{
          this.pixelMap=data;
        })
      }
    }
  })
显示图片
@State pixelMap:PixelMap|null = null
Image(this.pixelMap).
  height(200).
   width(100).
  margin({top:20,bottom:20})

完整UI代码

import { DownloadUtil } from './DownloadUtil'
import { common } from '@kit.AbilityKit'

@Entry
@Component
struct Index {
  @State pixelMap:PixelMap|null = null
  // 此处可以换成可下载的图片
  url:string = 'http://192.168.1.20:8085/api/file/download/bg_00.png'

  build() {
    Column(){
      Image(this.pixelMap).
      height(200).
      width(100).
      margin({top:20,bottom:20})
      Button('保存用户选择路径').onClick(async () => {
        let buf = await DownloadUtil.getInstance().downLoadPic(this.url);
        console.log("buf --- > "+buf)
        if (buf) {
          DownloadUtil.getInstance().saveToLocal(buf, 'image');
        }
      }).margin({top:20,bottom:20})


      SaveButton()
        .onClick(async (event:ClickEvent, result: SaveButtonOnClickResult) => {
          if (result === SaveButtonOnClickResult.SUCCESS) {
            let buf = await DownloadUtil.getInstance().downLoadPic(this.url);
            if (buf) {
              let context = getContext(this) as common.UIAbilityContext
              DownloadUtil.getInstance().savePicToGallery(context, buf);
              DownloadUtil.getInstance().arrayBufToPixelMap(buf).then((data)=>{
                this.pixelMap=data;
              })
            }
          }
        })

    }.height("100%")
    .width("100%")
  }
}

后端代码

package com.example.mybatis_demo.controller;
import com.example.mybatis_demo.util.FileUtil;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

/**
 * 文件下载控制器
 * 支持断点续传功能
 */
@RestController
@RequestMapping("/api/file")
public class FileDownloadController {

    // 定义文件上传的目录
    private static final String UPLOAD_DIR = "uploads/";

    /**
     * 支持断点续传的文件下载接口
     * 
     * @param filename 文件名
     * @param request  HTTP请求
     * @return 文件流
     */
    @GetMapping("/download/{filename}")
    public ResponseEntity<Resource> downloadFile(
            @PathVariable String filename,
            HttpServletRequest request) {

        try {
            // 构建文件路径
            Path filePath = Paths.get(UPLOAD_DIR).resolve(filename).normalize();

            // 检查文件是否存在
            if (!Files.exists(filePath)) {
                return ResponseEntity.notFound().build();
            }

            // 获取文件资源
            FileSystemResource fileResource = new FileSystemResource(filePath.toFile());

            // 获取文件大小
            long fileSize = fileResource.contentLength();

            // 解析Range请求头
            String rangeHeader = request.getHeader("Range");

            // 设置响应头
            HttpHeaders headers = new HttpHeaders();
            headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename);
            headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, HttpHeaders.CONTENT_RANGE);

            // 如果没有Range请求头,返回整个文件
            if (rangeHeader == null || rangeHeader.isEmpty()) {
                headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE);
                headers.add(HttpHeaders.CONTENT_LENGTH, String.valueOf(fileSize));

                return ResponseEntity.ok()
                        .headers(headers)
                        .body(fileResource);
            }

            // 解析Range参数 (格式: bytes=0-1023)
            if (!rangeHeader.startsWith("bytes=")) {
                return ResponseEntity.status(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE).build();
            }

            String rangeValue = rangeHeader.substring(6);
            String[] ranges = rangeValue.split("-");

            long start = 0;
            long end = fileSize - 1;

            try {
                if (ranges.length == 1) {
                    // 只有起始位置
                    start = Long.parseLong(ranges[0]);
                } else if (ranges.length == 2) {
                    // 有起始和结束位置
                    start = Long.parseLong(ranges[0]);
                    if (!ranges[1].isEmpty()) {
                        end = Long.parseLong(ranges[1]);
                    }
                }
            } catch (NumberFormatException e) {
                return ResponseEntity.status(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE).build();
            }

            // 验证范围
            if (start >= fileSize || end >= fileSize || start > end) {
                return ResponseEntity.status(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE).build();
            }

            // 计算实际传输的数据长度
            long contentLength = end - start + 1;

            // 设置断点续传响应头
            headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE);
            headers.add(HttpHeaders.CONTENT_LENGTH, String.valueOf(contentLength));
            headers.add(HttpHeaders.CONTENT_RANGE, "bytes " + start + "-" + end + "/" + fileSize);
            headers.add(HttpHeaders.ACCEPT_RANGES, "bytes");
            headers.add(HttpHeaders.CONNECTION, "keep-alive");

            // 创建部分文件流
            long finalEnd = end;
            long finalStart = start;
            InputStreamResource resource = new InputStreamResource(
                    new FileInputStream(fileResource.getFile()) {
                        @Override
                        public long skip(long n) throws IOException {
                            return super.skip(Math.min(n, finalEnd - pos + 1));
                        }

                        private long pos = 0;

                        @Override
                        public int read() throws IOException {
                            if (pos < finalStart) {
                                skip(finalStart - pos);
                                pos = finalStart;
                            }
                            if (pos > finalEnd) {
                                return -1;
                            }
                            int b = super.read();
                            if (b != -1) {
                                pos++;
                            }
                            return b;
                        }

                        @Override
                        public int read(byte[] b, int off, int len) throws IOException {
                            if (pos < finalStart) {
                                skip(finalStart - pos);
                                pos = finalStart;
                            }
                            if (pos > finalEnd) {
                                return -1;
                            }
                            int maxLen = (int) Math.min(len, finalEnd - pos + 1);
                            int result = super.read(b, off, maxLen);
                            if (result != -1) {
                                pos += result;
                            }
                            return result;
                        }
                    });

            return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT)
                    .headers(headers)
                    .body(resource);

        } catch (IOException e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }

    /**
     * 获取文件信息接口
     * 
     * @param filename 文件名
     * @return 文件信息
     */
    @GetMapping("/info/{filename}")
    public ResponseEntity<Map<String, Object>> getFileInfo(@PathVariable String filename) {
        try {
            Path filePath = Paths.get(UPLOAD_DIR).resolve(filename).normalize();

            if (!Files.exists(filePath)) {
                return ResponseEntity.notFound().build();
            }

            Map<String, Object> fileInfo = new HashMap<>();
            fileInfo.put("filename", filename);
            fileInfo.put("originalFilename", filename); // 简化处理
            fileInfo.put("size", Files.size(filePath));
            fileInfo.put("readableSize", FileUtil.getReadableFileSize(Files.size(filePath)));
            fileInfo.put("lastModified", Files.getLastModifiedTime(filePath).toMillis());

            return ResponseEntity.ok(createResponse(200, "获取文件信息成功", fileInfo));
        } catch (IOException e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body(createResponse(500, "获取文件信息失败: " + e.getMessage(), null));
        }
    }

    /**
     * 创建统一响应格式
     * 
     * @param code    状态码
     * @param message 消息
     * @param data    数据
     * @return 响应Map
     */
    private Map<String, Object> createResponse(int code, String message, Object data) {
        Map<String, Object> response = new HashMap<>();
        response.put("code", code);
        response.put("message", message);
        response.put("data", data);
        response.put("timestamp", System.currentTimeMillis());
        return response;
    }
}

启动服务

image.png

测试接口

image.png

image.png

最后总结

鸿蒙next 里面图片的下载我们还是可以基于我们的http请求来下载图片 成图片流 然后再把图片流转成 对象PixelMap 最后显示在我们的imgae 组件上面 然后我们也封装了直接将图片存储在相册里面或者是让用户看来选择存储在自定义的路径下面,如果是下载视频或者大图我们应该要做压缩和断点下载这些就留给同学们自己去完善, 今天的文章就讲到这里有兴趣的同学可以继续研究 如果你觉得文章还不错麻烦给我三连 关注点赞和转发 如果了解更多鸿蒙开发的知识 可以关注坚果派公众号 。 谢谢