Flutter: การแสดงภาพ Jpeg ที่กำลังโหลดจากอินเทอร์เน็ตยังไม่เสร็จ

บันทึกผลการทดลอง แสดงภาพ Jpeg ในขณะที่ยังโหลดไม่เสร็จ เพื่อแสดงภาพแบบ Progressive Jpeg เพื่อให้ผู้ใช้งานเห็นภาพร่างก่อนที่ภาพจะโหลดมาเสร็จสมบูรณ์

ปัญหาที่พบ

หากเราเอาข้อมูลภาพ Jpeg ที่โหลดมาแต่ยังไม่เสร็จมาแสดงใน Widget ผลที่ได้คือ Exception Error ที่แจ้งว่า ImageException: Invalid progressive encoding หลังจากพยายามหาวิธีการโดยใช้ dependencies ที่มีพบว่า

  1. ตัว Flutter ไม่รองรับข้อมูลภาพ Jpeg ที่ไม่สมบูรณ์ และไม่มี option ที่จะให้แสดงภาพบางส่วนก่อน
  2. ลองหาใน dependencies แล้ว หาไม่เจอ เจอแต่เอาภาพความละเอียดต่ำมาแสดงก่อนแล้วเมื่อโหลดเสร็จค่อยเอาภาพจริงที่ต้องการมาแสดง
  3. ในตัว iOS และ Android มี libraries ที่ช่วยเรื่องนี้อยู่คือ (มาจาก issues ของ flutter)
  4. ถ้าเขียนเพื่อใช้งานบน Web หรือ Windows  ต้องหาตัวอื่นมาใช้ ซึ่งขี้เกียจหาแล้ว หลังจากการหามาหลายครั้งก็ไม่เจอตัวเลือกที่ดี

วิธีแก้ไขปัญหา

หลังจากลองพยายามใช้ตัว ImageProvider ของ Flutter และ image สรุปว่าไม่เจอทางออกว่าจะทำยังไงก็ปัญหาตรงนี้ดี จากการศึกษารูปแบบของไฟล์ Jpeg พบว่าเมื่อจบไฟล์จะมีข้อมูล 2 byte คือ 0xFF และ 0xD9 เพื่อบอกว่าเป็น End of Image

JFIF file structure

เพื่อบอกตัวถอดรหัสภาพ Jpeg ว่าข้อมูลภาพจบแล้ว แม้ว่าภาพจะไม่สมบูรณ์ก็ตาม เลยลองใส่ข้อมูล EOI ต่อท้ายเข้าไป ผลที่ได้คือตัวถอดรหัสสามารถถอดรหัสภาพที่มีได้ และสามารถเอาข้อมูลดังกว่ามาแสดงบนหน้าจอได้

ในตัวอย่าง ได้ลองทำไฟล์ test.jpg ขึ้นมา แล้วลองนำบางส่วนของไฟล์มาแสดง โดยทำการเพิ่ม EOI ต่อท้ายเข้าไป


    int testPercent = 30; // test load only 30% of jpeg image file
    File jpeg = File('D:\\test.jpg');
    Uint8List content = jpeg.readAsBytesSync();
    int lengthTest = ((content.length / 100) * testPercent).truncate(); // calculate size of content
    Uint8List jpegContext = Uint8List.fromList([...content.sublist(0, lengthTest), 0xFF, 0xD9]); // add EOI after jpeg data
    MemoryImage? outputImage; // ImageProvider for Image() widget
    try {
      outputImage = MemoryImage(jpegContext);
      setState(() {
        debugPrint('size of jpeg is ${jpegContext!.length}, $testPercent');
      });
    } catch (e) {
      outputImage = null;
    }

ในส่วนของการแสดงภาพ ใช้ Image widget ธรรมดาแสดงภาพ


    if(outputImage != null) {
        Image(image: outputImage!, gaplessPlayback: true);
    }

เนื่องจากต้องการ update ภาพทุกครั้งที่มีข้อมูลใหม่มาจากอินเทอร์เน็ต ก็ให้กำหนด gaplessPlayback เป็น true เพื่อให้ค้างภาพเดิมไว้แล้วเขียนภาพใหม่ทับ

ปัญหาที่พบหลังจากที่ใช้ EOI

เนื่องจากการเพิ่ม EOI เข้าไปต่อท้ายเป็นการแก้ไขแบบ hack ซึ่งไม่ใช่วิธีที่ถูกต้อง และอาจทำให้บางครั้งตัวถอดรหัสภาพไม่สามารถถอดรหัสภาพได้ ดังนั้นอย่าลืมใช้ try catch เพื่อจัดการปัญหาเมื่อถอดรหัสไม่ได้ด้วย ซึ่งจากที่ใช้อยู่ก็คือหากถอดรหัสไม่ได้ก็ไม่ไป update ตัว Image widget และรอข้อมูลมาเพิ่มแล้วลองใหม่ หากไม่เกิด error ก็แสดงภาพที่โหลดมาได้ตามปกติ