หน้า PDF ส่วนใหญ่จะทำการแรสเตอร์ (rasterise) ภายในไม่กี่มิลลิวินาที และคุณแทบไม่ต้องคิดถึงมันเลย แต่แล้วผู้ใช้ก็เปิดแบบวาดวิศวกรรมขนาด A1 ซึ่งเป็นหน้าที่อัดแน่นไปด้วยลายเส้นเวกเตอร์หลายหมื่นเส้น หรือโปสเตอร์ที่เต็มไปด้วยกลุ่มความโปร่งใส (transparency groups) และซอฟต์มาสก์ (soft masks) และการเรียกฟังก์ชันวาดเพียงครั้งเดียวต้องใช้เวลาถึงสองหรือสามวินาที หากการเรียกฟังก์ชันนั้นรันบนเธรด UI หน้าต่างจะหยุดวาดหน้าจอใหม่ แถบชื่อเรื่องจะกลายเป็นสีเทา และระบบปฏิบัติการจะเสนอให้ปิดแอปพลิเคชัน การทำงานนั้นเป็นสิ่งที่ถูกต้องแล้ว หน้าดังกล่าวจำเป็นต้องใช้เวลานานขนาดนั้นจริงๆ ข้อบกพร่องคือการเรนเดอร์เป็นการเรียกฟังก์ชันที่บล็อกการทำงานแบบแบ่งแยกไม่ได้เพียงครั้งเดียว โดยไม่มีทางให้หยุดพักหรือช่องทางให้หยุดทำงานเลย
บทความนี้เกี่ยวกับหนึ่งในสองปัญหานั้นโดยเฉพาะ: การยกเลิกการเรนเดอร์หน้าเดียวที่ใช้เวลานานโดยไม่ทำให้ UI ค้าง ผู้ใช้คลิกหน้าถัดไป, ซูม หรือปิดเอกสาร และการเรนเดอร์ที่กำลังทำงานอยู่กลายเป็นงานที่สูญเปล่าซึ่งควรจะจบลงในโอกาสแรกที่เป็นไปได้แทนที่จะทำงานต่อไปจนเสร็จ การทำให้การเลื่อนและการซูมลื่นไหลขึ้นโดยการแคชสิ่งที่ถูกแรสเตอร์ไปแล้วเป็นอีกประเด็นหนึ่งที่มีการออกแบบของมันเอง ซึ่งครอบคลุมอยู่ในบทความที่เกี่ยวข้องซึ่งลิงก์ไว้ที่ส่วนท้าย ในที่นี้คำถามเดียวคือทำอย่างไรให้การเรนเดอร์แบบก้าวหน้าตอบสนองต่อคำขอยกเลิกได้อย่างรวดเร็วและหมดจด
API การเรนเดอร์แบบก้าวหน้าที่ PDFium มีมาให้แล้ว
PDFium ได้คาดการณ์ถึงปัญหาเรื่องอาการค้างนี้ไว้แล้ว นอกเหนือจากฟังก์ชัน FPDF_RenderPageBitmap ที่ทำงานรวดเดียวจบ ยังมีฟังก์ชันทางเลือกแบบก้าวหน้าที่แบ่งหน้าออกเป็นส่วนๆ ของการทำงาน คุณเรียกใช้ FPDF_RenderPageBitmap_Start หนึ่งครั้งเพื่อตั้งค่าการเรนเดอร์ลงบนบิตแมปปลายทาง จากนั้นเรียกใช้ FPDF_RenderPage_Continue ซ้ำๆ การเรียก Continue แต่ละครั้งจะแรสเตอร์ในส่วนที่กำหนดไว้และคืนค่าสถานะกลับมา FPDF_RENDER_TOBECONTINUED หมายถึงยังมีงานต้องทำต่อ FPDF_RENDER_DONE หมายถึงหน้าเสร็จสมบูรณ์แล้ว และ FPDF_RENDER_FAILED หมายถึงหยุดทำงานเนื่องจากเกิดข้อผิดพลาด เมื่อลูปสิ้นสุดลง คุณเรียก FPDF_RenderPage_Close เพื่อปลดปล่อยสถานะแบบก้าวหน้าของแต่ละหน้า เนื่องจากมีการคืนการควบคุมกลับมายังโค้ดของคุณในระหว่างส่วนที่แบ่งไว้ คุณจึงสามารถปั๊มข้อความ (pump messages) อัปเดตตัวบ่งชี้ความคืบหน้า หรือตรวจสอบว่ายังคงต้องการงานที่กำลังทำอยู่หรือไม่
กลไกที่ PDFium มีให้สำหรับตัดสินใจว่าเมื่อใดควรจะหยุดพักการทำงานชั่วคราวคือโครงสร้างคอลแบ็ก (callback struct) ที่ชื่อ IFSDK_PAUSE คุณส่งมันให้กับ Start และให้กับทุกๆ Continue หลังจากจบแต่ละส่วน PDFium จะเรียกใช้พอยน์เตอร์ฟังก์ชัน NeedToPauseNow ของมัน และถ้ามีการคืนค่าที่ไม่ใช่ศูนย์ การเรียก Continue ปัจจุบันจะหยุดก่อนกำหนดและคืนการควบคุมกลับมาพร้อมกับ FPDF_RENDER_TOBECONTINUED โครงสร้างนี้ยังมีฟิลด์ version ซึ่งต้องตั้งค่าเป็น 1 และพอยน์เตอร์ user แบบอิสระที่ PDFium จะไม่แตะต้องและส่งผ่านไปแบบไม่เปลี่ยนแปลง พอยน์เตอร์ที่ไม่ถูกแตะต้องนี้คือจุดศูนย์กลางทั้งหมดของการออกแบบที่กำลังจะกล่าวถึง
การปรับวัตถุประสงค์การหยุดพักชั่วคราวให้เป็นการยกเลิก
เจตนาเดิมของ NeedToPauseNow คือการแบ่งเวลาการทำงาน (time-slicing) ให้คืนค่าที่ไม่ใช่ศูนย์เมื่องบประมาณเวลาของเฟรมหมดไป และคืนค่าศูนย์เพื่อให้เรนเดอร์ต่อไป แล้ว PDFium จะหยุดพักเพื่อให้คุณสามารถทำสิ่งอื่นก่อนที่จะกลับมาเรนเดอร์ต่อ PDFium Component ได้นำสัญญาณเดียวกันนั้นมาใช้ซ้ำสำหรับกริยาที่ต่างออกไป แทนที่จะตอบว่า "ฉันควรหยุดพักเพื่อให้คุณกลับมาทำงานต่อหรือไม่" คอลแบ็กนี้จะตอบว่า "งานนี้ถูกยกเลิกแล้วหรือยัง" ทั้งสองอย่างนี้เข้ากันได้อย่างหมดจดเนื่องจากสิ่งที่ลูปทำเมื่อเห็นแฟล็ก การหยุดพักที่แท้จริงคาดหวังว่าจะมีการเรียก Continue ในภายหลัง แต่การยกเลิกไม่คาดหวังเช่นนั้น เมื่อลูปการเรียกสังเกตเห็นว่าโทเค็นถูกยกเลิก มันจะปิดบริบทการเรนเดอร์และจะไม่เรียก Continue อีก ดังนั้นการคืนค่าที่ไม่ใช่ศูนย์แบบเดียวกันซึ่ง PDFium อ่านได้ว่า "หยุดส่วนนี้" จึงกลายเป็น "หยุดถาวร" ไปในทางปฏิบัติ
การยกเลิกถูกแสดงออกผ่านอินเทอร์เฟซชื่อ IPdfCancellationToken ซึ่งพร็อพเพอร์ตี้ IsCancelled จะเปลี่ยนจากเท็จเป็นจริงเมื่อบางส่วนของโปรแกรมขอให้หยุดการเรนเดอร์ สะพานเชื่อมระหว่างอินเทอร์เฟซ Pascal และคอลแบ็กแบบ C ของ PDFium คือพอยน์เตอร์ตัวเดียว การอ้างอิงอินเทอร์เฟซของโทเค็นจะถูกเขียนลงใน IFSDK_PAUSE.user และคอลแบ็ก cdecl แบบสแตติกจะอ่านค่ากลับออกมาและสอบถามมัน นี่คือปัญหาแบบคลาสสิกของการให้ไลบรารี C เรียกกลับเข้ามาใน Pascal: คอลแบ็กจะต้องเป็นฟังก์ชันธรรมดาที่มีรูปแบบการเรียก (calling convention) แบบ C ไม่ใช่เมธอด เพราะ PDFium จัดเก็บและเรียกใช้พอยน์เตอร์ฟังก์ชันแบบเปล่าๆ ที่ไม่รู้อะไรเลยเกี่ยวกับออบเจกต์ Pascal หรือ Self
type
TPdfProgressivePause = record
Pause: IFSDK_PAUSE; // PDFium reads this; .user holds the token
Token: IPdfCancellationToken; // strong ref keeps the token alive
end;
function ProgressivePauseCallback(pThis: PIFSDK_PAUSE): FPDF_BOOL; cdecl;
var
Token: IPdfCancellationToken;
begin
Result := 0;
if (pThis = nil) or (pThis^.user = nil) then
Exit;
Token := IPdfCancellationToken(pThis^.user);
if Token.IsCancelled then
Result := 1; // non-zero: PDFium stops this chunk
end;
คอลแบ็กนี้กู้คืนโทเค็นโดยการแคสต์ pThis^.user กลับเป็นประเภทอินเทอร์เฟซและอ่าน IsCancelled ภายในไม่มีการจัดสรรหน่วยความจำ, การล็อก หรือการบล็อกใดๆ ซึ่งเป็นเรื่องสำคัญเนื่องจาก PDFium เรียกใช้มันบนเธรดการเรนเดอร์หลังจากเสร็จสิ้นในแต่ละส่วน และงานใดๆ ที่ทำที่นี่จะถูกเพิ่มเข้าไปในต้นทุนของการเรนเดอร์เอง การป้องกันไม่ให้โครงสร้างเป็น nil หรือฟิลด์ user เป็น nil หมายความว่าฟังก์ชันเดียวกันนี้ปลอดภัยที่จะติดตั้งแม้กระทั่งบนการเรนเดอร์ที่ไม่เคยได้รับโทเค็นจริงมาก่อน
การรักษาให้โทเค็นคงอยู่ตลอดลูปการทำงาน
การแคสต์พอยน์เตอร์อินเทอร์เฟซผ่าน Pointer แบบดิบๆ ไปและกลับเป็นจุดที่บั๊กอายุขัยมักจะเกิดขึ้น อินเทอร์เฟซ IInterface ใน Delphi มีการนับการอ้างอิง (reference counted) และการนับจะขยับก็ต่อเมื่อคอมไพเลอร์สามารถมองเห็นตัวแปรประเภทอินเทอร์เฟซกำลังถูกกำหนดค่า การเก็บโทเค็นไว้เป็นพอยน์เตอร์เปล่าๆ ไว้ภายใน IFSDK_PAUSE.user เพียงอย่างเดียวจะเป็นการซ่อนมันจากตัวนับการอ้างอิงโดยสมบูรณ์ หากการอ้างอิงอื่นเพียงแหล่งเดียวไปยังโทเค็นนั้นอยู่นอกขอบเขต (went out of scope) ในขณะที่ลูป Continue ยังคงทำงานอยู่ ออบเจกต์จะถูกปลดปล่อยอยู่ภายใต้คอลแบ็ก และส่วนถัดไปจะดึงค่าจากพอยน์เตอร์ที่สูญเสียการอ้างอิงไปแล้ว (dangling pointer)
นั่นคือเหตุผลที่ตัวอธิบาย (descriptor) เป็นเรคคอร์ด (record) ที่เก็บสองสิ่ง ไม่ใช่เพียงสิ่งเดียว ฟิลด์ Pause เป็นโครงสร้างที่ PDFium ใช้อ่าน ฟิลด์ Token เป็นการอ้างอิงประเภทอินเทอร์เฟซจริงที่คอมไพเลอร์ใช้ในการนับ และมันคงอยู่เพียงเพื่อวัตถุประสงค์เดียวคือปักหมุดโทเค็นไว้ในหน่วยความจำตราบนานเท่าที่เรคคอร์ดยังคงอยู่ เรคคอร์ดนี้เป็นตัวแปรโลคัลบนสแต็กของรูทีนการเรนเดอร์ ดังนั้นมันจะยังคงใช้การได้ตลอดระยะเวลาของลูปทั้งหมดและจะถูกทำลายเมื่อรูทีนสิ้นสุดลงเท่านั้น พอยน์เตอร์เปล่าๆ ใน user และการอ้างอิงที่มีการนับใน Token ชี้ไปยังออบเจกต์เดียวกัน สิ่งหนึ่งคือสิ่งที่ PDFium สามารถอ่านได้ ส่วนอีกสิ่งหนึ่งคือสิ่งที่ป้องกันไม่ให้ออบเจกต์นั้นถูกจัดเก็บ (collected)
var
Pause: TPdfProgressivePause;
EffectiveToken: IPdfCancellationToken;
begin
// ... choose EffectiveToken ...
// Strong ref first, then publish the same object to PDFium via .user.
Pause.Token := EffectiveToken;
Pause.Pause.version := 1;
Pause.Pause.NeedToPauseNow := ProgressivePauseCallback;
Pause.Pause.user := Pointer(EffectiveToken);
การปิดบริบทการเรนเดอร์ไม่ว่าลูปจะจบลงอย่างไร
ทุกครั้งที่เรียก FPDF_RenderPageBitmap_Start จะจัดสรรสถานะก้าวหน้าซึ่ง PDFium ผูกไว้กับหน้าเว็บนั้นๆ และสถานะนั้นจะถูกปลดปล่อยโดย FPDF_RenderPage_Close เท่านั้น มีสามวิธีในการออกจากลูประบบขับเคลื่อน หน้าเรนเดอร์เสร็จสิ้นและสถานะสุดท้ายคือ FPDF_RENDER_DONE โทเค็นถูกทริกเกอร์และลูปออกจากระบบล่วงหน้าเพื่อรายงานการยกเลิก บางสิ่งบางอย่างล้มเหลวและสถานะคือ FPDF_RENDER_FAILED ทั้งสามกรณีนี้ต้องเรียกใช้ Close และเส้นทางการยกเลิกนั้นง่ายที่สุดที่จะทำผิดพลาดได้ เนื่องจากรูปแบบตามธรรมชาติของ "พบการยกเลิก ออกจากการทำงาน" มักจะข้ามการล้างข้อมูล (cleanup) ไปในระหว่างทางออก การปล่อยให้ Close ไม่ถูกเรียกใช้จะทำให้เกิดการรั่วไหล (leak) ของสถานะในแต่ละหน้า และโปรแกรมแสดงไฟล์ที่อนุญาตให้ผู้ใช้ยกเลิกการเรนเดอร์ได้เรื่อยๆ จะสะสมการรั่วไหลนี้ในทุกๆ หน้าที่ถูกยกเลิก
รูปแบบที่แข็งแกร่งจะใส่ลูปและการจำแนกผลลัพธ์ไว้ภายในบล็อก try และใส่ FPDF_RenderPage_Close ในบล็อก finally ที่คู่กัน บิตแมปปลายทางจะถูกทำลายในบล็อกเดียวกัน การยกเลิกสามารถออกจากลูปผ่าน Exit แบบเร็ว แต่ finally ก็ยังคงทำงาน ดังนั้นจึงมีสถานที่เพียงแห่งเดียวที่ปลดปล่อยสถานะก้าวหน้าและมันไม่สามารถถูกข้ามไปได้
Status := FPDF_RenderPageBitmap_Start(PdfBmp, FPage, Left, Top,
Width, Height, Ord(Rotation), EncodeRenderOptions(Options), Pause.Pause);
try
while Status = FPDF_RENDER_TOBECONTINUED do
begin
if EffectiveToken.IsCancelled then
begin
Result := prsCancelled;
Exit;
end;
Status := FPDF_RenderPage_Continue(FPage, Pause.Pause);
end;
if EffectiveToken.IsCancelled then
Result := prsCancelled
else if Status = FPDF_RENDER_DONE then
Result := prsDone
else
Result := prsFailed;
finally
// Frees the progressive state Start allocated; mandatory on every path.
FPDF_RenderPage_Close(FPage);
FPDFBitmap_Destroy(PdfBmp);
end;
ลูปนี้จะตรวจสอบโทเค็นก่อนที่จะเรียก Continue แต่ละครั้ง รวมถึงพึ่งพาคอลแบ็กที่อยู่ข้างในด้วย คอลแบ็กจะทำให้การทำงานในส่วนปัจจุบันสั้นลง การตรวจสอบของลูปจะหยุดส่วนถัดไปไม่ให้เริ่มต้น เมื่อนำมารวมกัน พวกมันจะจำกัดระยะเวลาที่การยกเลิกจะมีผลให้เหลือประมาณระยะเวลาของหนึ่งส่วนการทำงาน
ผลลัพธ์ 3 แบบ และสิ่งที่บิตแมปเก็บไว้หลังจากการยกเลิก
จุดเริ่มต้นแบบสาธารณะ (public entry point) คือ TPdf.RenderPageProgressive และมันจะคืนค่า TPdfProgressiveStatus ซึ่งเป็นหนึ่งใน prsDone, prsCancelled หรือ prsFailed ค่าเหล่านี้สะท้อนค่าคงที่ FPDF_RENDER_* ของ PDFium ในรูปแบบของ Pascal แต่พับกรณีการยกเลิกเข้ามาเป็นผลลัพธ์ระดับเฟิร์สคลาสแทนที่จะเป็นข้อผิดพลาด
จุดที่ดึงดูดความสนใจคือสิ่งที่บิตแมปปลายทางมีอยู่หลังจาก prsCancelled มันไม่ได้ว่างเปล่า PDFium เรนเดอร์แบบก้าวหน้าลงในบิตแมปเดิมส่วนแล้วส่วนเล่า ดังนั้นเมื่อการยกเลิกหยุดลูป บิตแมปจะเก็บสิ่งที่ถูกวาดไว้จนถึงจุดเวลานั้น ซึ่งเป็นภาพบางส่วน: บางแถบเสร็จสมบูรณ์แล้ว ส่วนที่เหลือยังคงแสดงสีที่เติมไว้ ผลลัพธ์บางส่วนนี้จะมีประโยชน์หรือไม่ขึ้นอยู่กับผู้เรียก โปรแกรมดูไฟล์ที่กำลังจะทิ้งบิตแมปนั้นไปเนื่องจากผู้ใช้เปลี่ยนไปยังจุดอื่นสามารถเพิกเฉยได้เลย โปรแกรมดูไฟล์ที่ต้องการแสดงภาพตัวอย่างแบบประหยัดต้นทุนก็สามารถเก็บมันไว้ได้ สิ่งที่คุณไม่ควรทำคือการถือเอาว่า prsCancelled หมายถึงบิตแมปที่ว่างเปล่าหรือไม่ได้ถูกกำหนดขึ้น; มันหมายถึงสแนปชอตที่เป็นความจริงของการเรนเดอร์ที่ไม่เสร็จสมบูรณ์
var
Bmp: TBitmap;
Token: IPdfCancellationToken;
Status: TPdfProgressiveStatus;
begin
Bmp := TBitmap.Create;
try
// Token starts un-cancelled; flip Token.IsCancelled from elsewhere
// (a UI action, a navigation event) to abort the render in flight.
Status := Pdf.RenderPageProgressive(Bmp, 0, 0, PageW, PageH, Token);
case Status of
prsDone: Image1.Picture.Assign(Bmp); // fully rendered
prsCancelled: ; // partial bitmap, usually discarded
prsFailed: ShowMessage('Render failed');
end;
finally
Bmp.Free;
end;
end;
โทเค็นที่เป็น nil และเส้นทางคอลแบ็กที่ปราศจากการแตกแขนง
การยกเลิกเป็นการเลือกรับความสามารถ (opt-in) ผู้เรียกที่เพียงแค่ต้องการการเรนเดอร์แบบก้าวหน้าเพื่อประโยชน์ในการปั๊มข้อความ โดยไม่ได้ตั้งใจจะยกเลิก ควรสามารถส่งค่า nil สำหรับโทเค็นได้ วิธีที่ไร้เดียงสาในการสนับสนุนความสามารถนี้คือการกระจายการตรวจสอบ "ถ้ามีการส่งโทเค็นมาให้" ผ่านคอลแบ็กและลูป ซึ่งหมายความว่าจะมีการแตกแขนงในทุกๆ ส่วนและคอลแบ็กที่ต้องจัดการทั้งที่มีโทเค็นจริงและเมื่อไม่มีโทเค็น
การปรับใช้หลีกเลี่ยงสิ่งนั้นโดยการแทนที่อิงแบบเอกพจน์ (singleton) เมื่อผู้เรียกไม่ส่งค่าอะไรมาเลย โทเค็น nil จะถูกสลับเป็น PdfNoCancellationToken ซึ่งเป็นอินเทอร์เฟซที่มีพร็อพเพอร์ตี้ IsCancelled เป็นเท็จเสมอ จากจุดนั้นเป็นต้นไป คอลแบ็กและลูปจะมีโทเค็นเพื่อใช้สำหรับสืบค้นข้อมูลในทุกกรณี ดังนั้นทั้งสองส่วนจึงไม่จำเป็นต้องตรวจสอบค่า nil และไม่ต้องใช้เส้นทางพิเศษใดๆ โทเค็นแบบไม่ยกเลิกเลยนี้จะตอบว่าเท็จเสมอ คอลแบ็กก็จะคืนค่าเป็นศูนย์เสมอ และการเรนเดอร์จะทำงานจนเสร็จสมบูรณ์เหมือนกับการเรนเดอร์ที่ไม่สามารถยกเลิกได้ทุกประการ พฤติกรรมเสริมถูกจำลองเป็นโทเค็นที่ไม่เคยทำงานแทนที่จะเป็นการไม่มีโทเค็น ซึ่งช่วยรักษาให้ฮอตพาท (hot path) ทำงานเป็นรูปแบบเดียวกัน
// nil -> never-cancel singleton, so the callback path is identical
// whether or not the caller opted into cancellation.
if AToken <> nil then
EffectiveToken := AToken
else
EffectiveToken := PdfNoCancellationToken;
รูปแบบที่ปรากฏนั้นมีขนาดเล็กและคุ้มค่าที่จะเน้นย้ำ เพราะมันคือส่วนที่นำมาใช้ซ้ำได้ ไลบรารี C ที่รองรับคอลแบ็กมอบช่องทางให้คุณเพียงช่องทางเดียวในการส่งผ่านสถานะเข้าไปยังคอลแบ็กนั้น นั่นคือพอยน์เตอร์ user ที่ทึบแสง ให้นำการอ้างอิงอินเทอร์เฟซ Pascal ที่ถูกนับไว้หลังพอยน์เตอร์นั้น เก็บรักษาการอ้างอิงจริงที่สองให้คงอยู่ติดกับโครงสร้างเพื่อไม่ให้ออบเจกต์ถูกจัดเก็บทิ้งกลางคันในระหว่างที่ถูกเรียก และอ่านอินเทอร์เฟซนั้นกลับออกมาภายในฟังก์ชัน cdecl แบบสแตติก ห่อหุ้มลูประบบขับเคลื่อนทั้งหมดไว้ภายในบล็อก try และปลดปล่อยบริบทเนทีฟที่อยู่ใน finally แม่แบบ (template) เดียวกันนี้จะนำไปใช้กับการดำเนินการ PDFium แบบก้าวหน้าหรือที่ขับเคลื่อนด้วยคอลแบ็กใดๆ ซึ่งโค้ด Pascal จะต้องควบคุมอายุขัยการทำงานในขณะที่ C เก็บพอยน์เตอร์ไว้
การยกเลิกเป็นเพียงครึ่งหนึ่งของโปรแกรมดูไฟล์ที่ตอบสนองได้ดี อีกครึ่งหนึ่งคือการไม่เรนเดอร์หน้าซ้ำที่คุณวาดไปแล้ว และการทำให้การซูมและเลื่อนไหลลื่นไหลโดยการให้บริการบิตแมปที่แคชไว้ ซึ่งครอบคลุมอยู่ใน บทความของเราเกี่ยวกับการแคชการเรนเดอร์และประสิทธิภาพการซูม สำหรับวิธีการที่การเรนเดอร์ที่ยกเลิกได้จะสอดคล้องกับโปรแกรมดูไฟล์ที่สมบูรณ์ไปพร้อมกับการนำทาง, การเลือก และการค้นหา โปรดดู การสร้างโปรแกรมดู PDF ที่มีฟีเจอร์ครบครันด้วย PDFium Component การเรนเดอร์แบบก้าวหน้าที่อธิบายไว้ในที่นี้จัดส่งให้โดยเป็นส่วนหนึ่งของ PDFium Component สำหรับ Delphi และ Lazarus ควบคู่ไปกับการโหลด, การเรนเดอร์ และ API แบบฟอร์มที่ครอบคลุมในส่วนอื่นๆ ของบล็อกนี้