איורי חיות בר היסטוריים הם דוגמה מועילה לדיון בהגשת תמונות ארכיון. לא אומתו כאן אוסף מסוים ששוחזר לאחרונה או דיווח חדשותי.
אבל כמפתחים, כשאנחנו רואים כותרת כזאת, אנחנו לא רק מעריכים את האמנות — אנחנו חושבים על התשתית שמאחוריה. איך קולטים, מעבדים, מאחסנים ומגישים אלפי תמונות ארכיון לא דחוסות ברזולוציה גבוהה מאוד, בלי להתיך את תקציב הענן או לחנוק את דפדפני המשתמשים?
סריקות גדולות יוצרות אתגרי עיבוד והגשה. שמרו מקור בלתי משונה, מידע מוצא, זכויות ומטא־נתונים רלוונטיים של צבע וטכניקה; צרו נגזרות לרשת בנפרד. נרמול ניגודיות אוטומטי הוא שינוי עריכתי, לא שחזור ניטרלי. המאמר מדגים חלוקה לאריחים, לא תוכנית שימור מלאה.
ארכיטקטורה של צינור תמונות ארכיוני
הפרידו בין קליטה ועיבוד להגשה. התרשים מציע תכנון אפשרי; שימוש בייצור עדיין דורש תורים, מגבלות משאבים, ניסיונות חוזרים, בקרת גישה ושלב פרסום מאומת.
סריקת מקור בלתי משונה → אחסון קליטה מוגבל משימה מאומתת → עובד מוגבל במשאבים — Lambda רק אם המגבלות מתאימות → אריחי DZI ותצוגה מקדימה באזור הכנה → בדיקת מלאות ושימור → מקור הגשה עם גישת CDN מבוקרת → OpenSeadragon ותצוגה מקדימה נגישה
כדי שהדבר יפעל ביעילות, נסתמך על שלושה עמודי תווך:
- פירמידות תמונה: DZI מתאר כמה רמות רזולוציה עם גודל אריח וחפיפה הניתנים להגדרה. בדוגמה אריחים של 256 פיקסלים וחפיפה של פיקסל אחד; TIFF פירמידלי הוא ייצוג אחר, לא אותו פורמט קובץ.
- libvips / Sharp:
libvipsמשתמש בעיבוד לפי דרישה, אך הזיכרון תלוי בקודקים, בפעולות, במטמון ובמקביליות. אין מכאן צריכת זיכרון זניחה או OOM בלתי נמנע בספריות אחרות. הגבילו משאבים ובדקו קלט מייצג. - OpenSeadragon: מציג לתמונות הניתנות להגדלה, שבוחר אריחים לפי התצוגה ורמת ההגדלה; התנהגות הטעינה והמטמון נקבעת בהגדרות.
שלב 1: הקמת מעבד אצווה עם Node.js ו־Sharp
דוגמת Node.js שלא הורצה יוצרת נגזרות רשת מקובץ TIFF מאומת יחיד. היא נמנעת בכוונה מנרמול ניגודיות אוטומטי, משאירה את הקלט ללא שינוי ומשתמשת במגבלות גודל מפורשות. המספרים הם מדיניות משאבים להמחשה, לא ערכים בטוחים בכל מצב.
ראשית ודאו שהספרייה הדרושה מותקנת:
npm install sharp
כעת נכתוב את צינור העיבוד ב־processor.js:
const sharp = require("sharp");
const path = require("node:path");
const fs = require("node:fs/promises");
const MAX_BYTES = 1024 * 1024 * 1024; // Illustrative input budget.
const MAX_PIXELS = 150_000_000;
async function processArchivalImage(inputPath, outputDir, name) {
if (!/^[a-z0-9_-]{1,80}$/.test(name)) throw new Error("Invalid asset name");
const stat = await fs.stat(inputPath);
if (!stat.isFile() || stat.size > MAX_BYTES) throw new Error("Input size rejected");
const pipeline = sharp(inputPath, { limitInputPixels: MAX_PIXELS,
failOn: "warning", pages: 1 });
const metadata = await pipeline.metadata();
if (metadata.format !== "tiff" || (metadata.pages || 1) !== 1 ||
!metadata.width || !metadata.height ||
metadata.width * metadata.height > MAX_PIXELS) {
throw new Error("Expected one bounded TIFF image");
}
// outputDir must be a fresh worker-owned staging directory.
await fs.mkdir(outputDir);
const tiles = path.join(outputDir, "tiles");
const web = path.join(outputDir, "web");
await fs.mkdir(tiles); await fs.mkdir(web);
// Master remains untouched. Color conversion is for access derivatives.
await pipeline.clone().rotate().withIccProfile("srgb")
.resize({ width: 1600, withoutEnlargement: true })
.webp({ quality: 80 }).timeout({ seconds: 120 })
.toFile(path.join(web, `${name}.webp`));
await pipeline.clone().rotate().withIccProfile("srgb")
.jpeg({ quality: 85 })
.tile({ size: 256, overlap: 1, layout: "dz", container: "fs" })
.timeout({ seconds: 120 }).toFile(path.join(tiles, `${name}.dz`));
// Sharp documents .dz output as name.dzi plus name_files/.
await fs.access(path.join(tiles, `${name}.dzi`));
await fs.access(path.join(tiles, `${name}_files`));
return { stagingDirectory: outputDir, sourceMetadata: metadata };
}
// Illustrative local paths; do not use untrusted filesystem paths.
processArchivalImage(
path.join(__dirname, "raw_input/wildlife_scan.tiff"),
path.join(__dirname, "processed_output"),
"pheasant_illustration_1880"
).then(() => console.log("Staged; validate all outputs before publication."))
.catch(error => { console.error(error.message); process.exitCode = 1; });
למה הגישה הזאת עובדת
pipeline.clone() מעתיק את מצב הצינור ויורש את הקלט. הוא יכול לשתף קלט זרימה, אך דוגמת נתיב הקובץ אינה מוכיחה פענוח יחיד בדיוק או צריכת זיכרון קבועה. מדדו זיכרון שיא וגודל פלט לפי הקודק והמקביליות בפועל.
שלב 2: פריסה כעובד serverless
משימה מוגבלת עשויה להתאים לעובד Lambda; סריקות או קבוצות אריחים גדולות עשויות לדרוש שירות אצווה ארוך־חיים. בדקו קודם פסק זמן, זיכרון, אחסון זמני, מספר פלטים ומקביליות. תלויות libvips/Sharp המקוריות צריכות להתאים למערכת ההפעלה, למעבד ולספריית C.
ארזו ובדקו את התלויות ליעד בפועל. ב־npm 10 ומעלה הדגלים המתועדים כוללים מערכת הפעלה, מעבד ו־libc; פקודת ההמחשה אינה התקנה שבוצעה כאן:
npm install --os=linux --cpu=x64 --libc=glibc sharp
בתכנון ענן שנבדק, סננו העלאות מורשות של .tiff תחת קידומת מפתח מפורשת כגון incoming/, ולא נתיב מערכת קבצים משוער /incoming. הכניסו לתור משימות אידמפוטנטיות, בודדו פענוח ופרסמו רק נגזרות שלמות. שמרו את דלי המקור פרטי מאחורי גישת CloudFront מבוקרת; אל תפרסמו מקורות כברירת מחדל. הסקריפט המקומי אינו מממש מטפל S3 או שלב העלאה.
שלב 3: הצגת מיליוני פיקסלים בצד הלקוח
טעינת כל האריחים ברזולוציה מלאה יחד עלולה לבזבז רוחב פס וזיכרון. מציג הגדלה טוען רמות ואזורים נבחרים בפירמידה; יציבות הדפדפן עדיין דורשת בדיקות ומגבלות סבירות.
OpenSeadragon הוא מציג קוד פתוח ב־JavaScript טהור לתמונות גדולות הניתנות להגדלה. הוא מזהה את אזור התצוגה ואת רמת ההגדלה, מחשב בדיוק אילו אריחים דרושים וטוען אותם לפי הצורך.
המציג שלהלן הוא מתווה שילוב שלא הורץ. ארזו מהדורת OpenSeadragon שנבדקה ואת תמונות הבקרה התואמות; שמרו תצוגת תמונה פשוטה ותיאור טקסט עבור מי שאינם יכולים להשתמש בממשק ההגדלה.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Archival Image Access Copy</title>
<script src="/vendor/openseadragon/openseadragon.min.js" defer></script>
<script src="/viewer.js" defer></script>
<style>#viewer { width:100%; height:65vh; min-height:300px; }</style>
</head>
<body>
<h1>Wildlife illustration: access copy</h1>
<p id="description">Example wildlife illustration. Add verified catalog,
rights, date and a meaningful visual description before publication.</p>
<img src="/web/pheasant_illustration_1880.webp"
alt="Preview of the example wildlife illustration">
<div id="viewer" tabindex="0" role="region"
aria-label="Zoomable illustration" aria-describedby="description"></div>
</body>
</html>
// viewer.js — separate file, not part of the HTML above
OpenSeadragon({
id: "viewer",
prefixUrl: "/vendor/openseadragon/images/",
tileSources: "/tiles/pheasant_illustration_1880.dzi",
showNavigator: true,
navigatorPosition: "BOTTOM_RIGHT",
constrainDuringPan: true,
visibilityRatio: 1.0
});
כיצד פועל המעגל לקוח–CDN–אחסון
לאחר מיפוי פלטי ההכנה שנבדקו לשורש ההגשה, OpenSeadragon מביא את /tiles/pheasant_illustration_1880.dzi. ה־XML שלהלן להמחשה: השתמשו בממדים שנוצרו מהנגזרת האמיתית, לא בממדי הדוגמה הבדויים.
<Image xmlns="http://schemas.microsoft.com/deepzoom/2008" TileSize="256" Overlap="1" Format="jpg">
<Size Width="14000" Height="9800"/>
</Image>
האריח המקביל עשוי להיות /tiles/pheasant_illustration_1880_files/12/3_4.jpg. פגיעות במטמון CDN יכולות לחסוך בקשת מקור, אך תפוגה, החטאות, מפתחות מטמון והשהיית רשת עדיין קיימים. הגשה ממקור אחר דורשת גם מדיניות CORS מתאימה.
שיפור ביצועים ומיטוב עלויות ענן
יש למדוד תועלות אפשריות מול בסיס השוואה ודפוס הצפייה הצפוי:
- רוחב פס: אריחים יכולים לחסוך הורדת פיקסלים ברזולוציה מלאה מחוץ לאזור הנצפה. הגדלה, הזזה, חפיפה וביקורים חוזרים משנים את הכמות הכוללת; לא נמדדה כאן תוצאה של מעבר מ־150 MB ל־3 MB.
- חוויית דף: תצוגה מקדימה בגודל מתאים וטעינה דחויה של המציג עשויות לשפר טעינה, אך LCP תלוי בדף כולו, ברשת ובמכשיר. אין תוצאת 1.2 שניות מאומתת או שיפור אוטומטי בדירוג חיפוש.
- הרחבה: אפשר להגיש אריחים סטטיים בנפרד מעובד העיבוד. עלות בקשות, מכסות, התנהגות מטמון, הרשאות וביטול מטמון עדיין דורשים תכנון קיבולת; לא הוכח מדד של מיליון בקשות.
סיכום והבמה שלכם
צינור libvips/Sharp מוגבל במשאבים ומציג אריחים יכולים להפוך עותקי גישה למעשיים. שימור עדיין תלוי במקורות שלא שונו, בתיעוד טרנספורמציות, במטא־נתונים, בזכויות ובאימות פלט; נוחות הנדסית אינה שחזור אוטומטי או הרחבה ללא מאמץ.
האם עבדתם על ארכוב ברזולוציה גבוהה או על צינורות לניהול נכסים דיגיטליים? באילו כלים השתמשתם כדי להתמודד עם גודל הקבצים? בואו נדבר בתגובות!
עד הפעם הבאה, המשיכו לתכנת.