גיליון אלקטרוני מחזיק עמודה של שמות לקוחות. חלקם בסינית, חלקם בקירילית, כמה נושאים אומלאוט גרמני או מבטא צרפתי. אתה מייצא אותו ל-CSV ופותח את התוצאה, וכל תו נשאר שלם. אתה מייצא את אותה חוברת עבודה ל-RTF עבור תבנית מיזוג דואר (mail-merge), פותח אותה במעבד תמלילים, והשמות שאינם ASCII קרסו לשורות של סימני שאלה. הנתונים מעולם לא השתנו. מה שהשתנה הוא חוזה הקידוד (encoding contract) של הפורמט שכתבת, וכל נתיב ייצוא נושא חוזה אחר
זו המלכודת שתופסת ספרייה שנראית מודעת לחלוטין ל-Unicode על פני השטח. טקסט התא מוחזק פנימית כ-WideString, כך שהמודל מעולם לא מאבד תו. האובדן מתרחש בגבול, בכותב (writer) שחייב לעשות סריאליזציה לטקסט הזה לפורמט בעל חוקים משלו לגבי אילו בתים הם חוקיים וכיצד יש לקודד כל דבר מחוץ לטווח החוקי. תקן כותב אחד ועדיין תוכל לשלוח כותב אחר שמחרבש את אותו טקסט. התיקון אינו מתג גלובלי. זו החלטה נפרדת ונכונה בכל נתיב
RTF הוא פורמט בטוח ל-7 סיביות בעיצובו
Rich Text Format קודם ל-Unicode והוגדר כדי לשרוד העברות שמעבירות רק ASCII הניתן להדפסה. מסמך RTF מצהיר על דף קוד (code page) בכותרת שלו, וכל תו שהכותב אינו יכול לייצג בדף הקוד ההוא חייב להיפלט כמילוט (escape) ולא כבית גולמי. המילוט הרלוונטי הוא \u, הנושא יחידת קוד חתומה של 16 סיביות ולאחריה תו גיבוי (fallback) ב-ASCII עבור קוראים ישנים מדי מכדי להבין את המילוט כלל
HotXLS כותבת RTF בצורה זו. כותרת המסמך נפתחת בהצהרה על דף הקוד, בצורה \ansi\ansicpg1252\uc1, והכותב ביחידת lxRTF עובר על כל מחרוזת ופולט כל תו מעל ASCII פשוט כמילוט \u כך שזרם הבתים נשאר נקי ב-7 סיביות ללא קשר למה שדף הקוד המוצהר יכול להחזיק. נקודת קוד כגון U+4E2D הופכת לרצף המילולי 3?, ולא לבית גולמי שמציג ינסה לאחר מכן לפרש דרך דף קוד כלשהו שהוא מניח. ללא משמעת זו, לכל דבר מחוץ לדף הקוד המוצהר אין ייצוג בתים חוקי, וכותב הפולט את הערך הגולמי מייצר את סימני השאלה שהתחילו מאמר זה
הפרט שיש לזכור הוא שדף הקוד המוצהר והמילוטים הם שני חצאים של חוזה אחד. הצהרת דף הקוד לבדה אינה עוזרת לטקסט שנמצא מחוצה לו. פליטת מילוטים ללא דף קוד מוצהר משאירה את תווי הגיבוי מעורפלים. שניהם חייבים להיות נכונים יחד, וזו הסיבה שכותב המטפל רק באחד מהם עדיין נכשל בחוברת העבודה הרב-לשונית הראשונה
HTML escaping is about more than angle brackets
ייצוא ה-HTML מייצר מסמך רב-גיליונות שפריסות הניווט שלו נושאות את שמות הגיליונות כטקסט גלוי. השמות הללו הם מחרוזות בשליטת המחבר שיכולות להכיל כל תו, כולל אלו המשמעותיים לסימון (markup-significant). גיליון המכונה מילולית Q1 & Q2 <draft> חייב להגיע לעמוד כיישויות מילוט (escaped entities), אחרת הסוגריים הזוויתיים יפתחו תג רפאים והאמפרסנד יתחיל הפניית יישות שלעולם לא הייתה מכוונת. זהו מילוט HTML רגיל, ודילוג עליו בתווית פריסה הוא סוג ההשמטה שעובר כל בדיקה שנבנתה משמות גיליונות ב-ASCII בלבד
שאלת הקידוד יושבת שכבה אחת מתחת לכך. כאשר תווים שאינם ASCII נוחתים בהקשר שאינו מובטח להיות מוגש כ-UTF-8, הייצוג הבטוח הוא הפניה לתו מספרי (numeric character reference), כך ש-U+00E9 נכתב כ-é ולא כבית גולמי שמשמעותו תלויה בקידוד התווים (charset) של התגובה. תמונת המראה של כלל זה חלה בדרך פנימה. חוברת עבודה שנקראת בחזרה מ-XLSX נושאת מחרוזות משותפות שבהן תו עשוי כבר להיות שמור כיישות XML מספרית, והיישות הזו חייבת להיות מפורשת לתו שלם אחד לפני שהיא נכנסת למודל התא. פתח אותה בחוסר זהירות, תוך פיצול נקודת קוד לבתים נפרדים, ותו בודד יופיע מחדש כשתי פיסות ג'יבריש (mojibake) שום ייצוא מאוחר יותר לא יוכל לתקן
מכל ה-XLSX הוא ZIP, ול-ZIP יש קידוד שמות משלו
קובץ XLSX הוא ארכיון ZIP, והארכיון שומר שם לכל חבר שהוא מחזיק. פורמט ZIP ישן מספיק כדי שהמפרט המקורי שלו לא אמר דבר על קידוד השמות הללו, כך שקורא שלא מוצא סימן מניח את דף הקוד המקומי של הארכיון. ההנחה הזו שגויה ברגע ששם חבר מכיל תו שאינו ASCII, מה שקורה עם שמות חלקי גיליון עבודה מקומיים ועם מדיה מוטמעת ששמות הקבצים שלה נושאים סימני מבטא או כתב שאינו לטיני
התיקון הוא סיבית (bit) בודדת. סיבית 11 לשימוש כללי בכל כותרת קובץ מקומית מצהירה ששם החבר מקודד כ-UTF-8. HotXLS בודק בדיוק את הסיבית הזו כשהוא קורא ארכיון, ומאמת את דגלי השימוש הכללי מול המסיכה $0800, וקורא או כותב שמתעלם ממנה יקרא לא נכון שם שמימוש נכון שמר כ-UTF-8. קל להגדיר את הסיבית וקל לכבד אותה, וזה כל ההבדל בין שם חבר ששורד את הסבב המלא לבין כזה שמגיע פגום עוד לפני שתוכן הגיליון אלקטרוני מפענח בכלל
קיפול אותיות (Case folding) וסריקת מספרים מסתירים את אותה סכנה
הערכת נוסחאות היא השלב שבו בטיחות Unicode מפסיקה לעסוק בסריאליזציה ומתחילה לעסוק בהשוואה. הפונקציה SEARCH אינה רגישה לאותיות רישיות (case-insensitive), מה שפירושו שהיא חייבת לקפל את האותיות (fold case) לפני שהיא מחפשת מחרוזת משנה. הדרך השגויה לקפל היא דרך דף הקוד של ANSI, מכיוון שהפיכה לאותיות גדולות (uppercasing) של טקסט שאינו ASCII בצורה זו מנתבת את התווים דרך דף קוד צר ומעוותת כל דבר מחוצה לו. הדרך הנכונה היא הפיכה לאותיות גדולות של wide-string, השומרת על טווח ה-UTF-16 המלא. HotXLS מקפל עם WideUpperCase בדיוק מסיבה זו, כך שחיפוש אחר טקסט עם סימני מבטא או טקסט שאינו לטיני יתאים לאותם תווים שניתנו במקום קירוב מעוות של דף קוד
הטוקנייזר (tokenizer) של הנוסחאות נושא התחייבות קשורה שאין לה שום קשר לאותיות והכל קשור למקום שבו טוקן (token) מסתיים. כתיב מדעי כגון 1E3 או 2.5E-3 הוא ליטרל מספרי יחיד, והסורק חייב לזהות את ה-E, סימן אופציונלי והספרות הבאות כחלק מהמספר במקום לשבור את הקלט לשם ולאחריו מספר נפרד. סורק שמטפל בכך לא נכון הופך קבוע תקין לחלוטין לשגיאת פענוח או, גרוע מכך, לביטוי שגוי בשקט. זה שייך לאותו דיון מכיוון ששני המקרים עוסקים בקורא המקבל החלטה נכונה ברמת התו: אחת לגבי אופן קיפול תו להשוואה, והשנייה לגבי האם תו ממשיך את הטוקן הנוכחי
בנייה וייצוא של חוברת עבודה רב-לשונית
ה-API הציבורי אינו מבקש ממך לחשב על דבר מזה. אתה בונה את חוברת העבודה מערכי תאים של WideString וקורא לנקודת הכניסה של הייצוא שאתה רוצה. החלטות הקידוד מתרחשות בתוך כל כותב. הדוגמה להלן מזינה גיליון עם טקסט בכמה כתבים, ואז כותבת הן קובץ RTF והן קובץ HTML מאותה חוברת עבודה, כך ששני הנתיבים רצים מול קלט זהה
uses
lxHandle;
procedure ExportMultilingualWorkbook;
var
Book: IXLSWorkbook;
Sheet: IXLSWorksheet;
begin
Book := TXLSWorkbook.Create;
try
Sheet := Book.Sheets.Add('Customers');
Sheet.Cells[1, 1].Value := 'Name';
Sheet.Cells[1, 2].Value := 'City';
// Cell text is held as WideString, so every script survives the model.
Sheet.Cells[2, 1].Value := '王伟'; // Chinese
Sheet.Cells[2, 2].Value := '北京';
Sheet.Cells[3, 1].Value := 'Müller'; // German umlaut
Sheet.Cells[3, 2].Value := 'Köln';
Sheet.Cells[4, 1].Value := 'Иванов'; // Cyrillic
Sheet.Cells[4, 2].Value := 'Москва';
Sheet.Cells[5, 1].Value := 'Désirée'; // French accents
Sheet.Cells[5, 2].Value := 'Montréal';
// RTF: the lxRTF writer declares the code page and emits every
// non-ASCII character as a \u escape, keeping the file 7-bit clean.
Book.SaveAsRTF('Customers.rtf');
// HTML: sheet names are HTML-escaped and non-ASCII text is written
// so it does not depend on a guessed response charset.
Book.SaveAsHTML('Customers.html');
finally
Book := nil;
end;
end;
שתי הקריאות מחזירות סטטוס Integer, ושתיהן צורכות את אותו טקסט שבזיכרון. שום דבר בקוד הקורא אינו מצהיר על דף קוד או מולט תו, מכיוון שהאחריות מוטלת על הכותב שמכיר את הפורמט של עצמו. הפונקציה SaveAsCSV ברמת חוברת העבודה עוקבת אחר אותה צורה אם אתה זקוק לייצוא מופרד מאותו מקור זהה
// Same workbook, a third export path with its own encoding rules.
Book.SaveAsCSV('Customers.csv');
בטיחות Unicode היא לכל נתיב, לא לכל ספרייה
בטיחות Unicode היא לכל נתיב, לא לכל ספרייה. הלקח שראוי לקחת הוא שאין מקום יחיד להיות בטוח ל-Unicode. RTF זקוק לדף קוד מוצהר פלוס מילוטים של \u. HTML זקוק למילוט ישויות (entity escaping) עבור תווים משמעותיים לסימון והפניות מספריות שבהן קידוד התווים אינו מובטח, פלוס פיענוח נכון של ישויות שמגיעות במחרוזות משותפות. מכל ה-ZIP זקוק להגדרת סיבית 11 לשימוש כללי כך ששם חבר ב-UTF-8 ייקרא כ-UTF-8. הערכת נוסחאות זקוקה לקיפול אותיות של wide-string וטוקנייזר השומר על כתיב מדעי בחתיכה אחת. כל אחד מאלה הוא חוזה שונה, וספרייה יכולה לעמוד באחד תוך הפרה שקטה של אחר. זו הסיבה שכלי שמבצע CSV נכון עדיין יכול למסור לך RTF מלא בסימני שאלה
אם הייצוא שלך נשען על הפורמטים המופרדים, הפשרות ביניהם מכוסות במדריך שלנו על ייצוא CSV, TSV ו-HTML, וכאשר המקור הוא ערכת תוצאות ולא גיליון שנבנה ידנית, התבניות בייצוא מסד נתונים עבור דוחות Delphi מצטרפות באופן טבעי לחוקי הקידוד המתוארים כאן. כל זה נשלח כחלק מרכיב גיליונות אלקטרוניים של HotXLS עבור Delphi ו-C++Builder, לצד ממשקי ה-API לקריאה, נוסחאות ועיצוב המכוסים במקומות אחרים בבלוג זה