2014-03-04
懷念舅公關傑才先生
在剛過去的農曆新年,和母親提起小時候會去澳門同外祖父母拜年。而且澳門還有一位舅公,他在澳門有一所英語學校,但我年紀小,沒有聽過他說英文,不過他曾送我一本他用中文寫的英文工具書,書名叫《英文解字》,這書的包裝很特別,是一本線裝書!
忽然間有點懷念這一位舅公,但對他的認識不深,只好靠互聯網找找有關他的資料。原本他也編寫過幾本書,其中一本《英譯廣東口語詞典》,在台灣博客來上的作者簡介如下:
關傑才
資深教育工作者,翻譯工作者。先後擔任過中學教務主任;中學高年級英文教師;公開大學特約講師及文字翻譯等職。有豐富的教中國人學英語和教外國人學廣東話的教學經驗。曾編譯出版《英文解字》一書,本書為其又一力作。
而另一編
趙汝能寫給關傑才《詞語疑音字正讀》的序
則見於: http://bbs.qoos.com/viewthread.php?tid=1544660
我在書櫃再找出他送我的《英文解字》線裝書,由於在網上再找不到此書的資料,故我決定將此孤本數碼化,以作紀念!
http://waihungmm.web.fc2.com/english/
另外,下載PDF可於https://drive.google.com/file/d/0Bzo1pJKfp9QELWFKTjlSNUUtZWM/view?usp=sharing
at
下午9:25
2014-01-22
Detecting errors in jpeg file using libjpeg-turbo
Recently I have the need to detect the integrity of a jpeg file. First I try to see the famous libjpeg (wiki) library can do the job. I am using Visual Studio (i.e. windows) to write my codes and I download the win32 version of libjpeg from internet. However, I keep on failure because the win32 libjpeg library keeps on emitting "Access Violation" at runtime. I try to solve the problem by googling solution but in vain.
Later I turn to use libjpeg-turbo although I really do not need to performance of SIMD instructions. Luckily libjpeg-turbo has a Visual Studio version of static library and I do to encounter the runtime error in libjpeg any more.
Basically my codes should be simply, as illustrated by the following pseudo-codes:
However, to my surprise, libjpeg use a setjmp/longjmp approach to detect and handle the errors. Frankly speaking, although I have been using C languages for years, I really do not like the GOTO style of handling errors. The libjpeg is emulating an Object-oriented approach by allowing programmer to build an error hander to intercept the errors.
I am interested in two kinds of errors:
The full source listing is as follows:
Postscript
I prepare two corrupted jpeg files. The first is truncated file that the exact image height is smaller than that is specified in the jpeg header. In this case, I can successfully intercept the error at scanline 313 with Error code = 120 and error message = "Premature end of JPEG file". The second is truncated that even the jpeg header cannot be successfully read. In this case, my my_error_exit() terminating route can capture the error_code = 51 and error message = "JPEG datastream contains no image".
Later I turn to use libjpeg-turbo although I really do not need to performance of SIMD instructions. Luckily libjpeg-turbo has a Visual Studio version of static library and I do to encounter the runtime error in libjpeg any more.
Basically my codes should be simply, as illustrated by the following pseudo-codes:
jpeg_create_decompress();
fopen(jpeg_file);
jpeg_stdio_src();
jpeg_read_header();
jpeg_start_decompress();
while loop {
jpeg_read_scanlines();
}
jpeg_finish_decompress();
jpeg_destroy_decompress();
fclose();
However, to my surprise, libjpeg use a setjmp/longjmp approach to detect and handle the errors. Frankly speaking, although I have been using C languages for years, I really do not like the GOTO style of handling errors. The libjpeg is emulating an Object-oriented approach by allowing programmer to build an error hander to intercept the errors.
I am interested in two kinds of errors:
- Fatal errors: the library will quit in this case
- Warnings: the library can continue with data corruption
my_error_exit ()
{
/* store the error code in the global variable */
/* store the error message in the global variable */
longjmp (myerr->setjmp_buffer, 1); /* Return control to the setjmp point */
}
my_output_message ()
{
/* store the error code in the global variable */
/* store the error message in the global variable */
}
The full source listing is as follows:
#include#include "stdafx.h" #include "jpeglib.h" #include #include int error_message_code; char error_message_buffer[JMSG_LENGTH_MAX]; struct my_error_mgr { struct jpeg_error_mgr pub; jmp_buf setjmp_buffer; }; typedef struct my_error_mgr * my_error_ptr; METHODDEF(void) my_error_exit (j_common_ptr cinfo) { my_error_ptr myerr = (my_error_ptr) cinfo->err; error_message_code = cinfo->err->msg_code; /* store the code in the global variable */ (*cinfo->err->format_message) (cinfo, error_message_buffer); /* store the message in the global variable */ printf ("Error intercepted in my_error_exit(): error_code = %d message = \'%s\'\n", error_message_code, error_message_buffer); longjmp(myerr->setjmp_buffer, 1); /* Return control to the setjmp point */ } METHODDEF(void) my_output_message (j_common_ptr cinfo) { error_message_code = cinfo->err->msg_code; (*cinfo->err->format_message) (cinfo, error_message_buffer); } int _tmain(int argc, _TCHAR* argv[]) { struct jpeg_decompress_struct cinfo; struct my_error_mgr jerr; FILE * infile; JSAMPARRAY ptr; /* Output row buffer */ int row_stride; /* physical row width in output buffer */ cinfo.err = jpeg_std_error(&jerr.pub); jerr.pub.error_exit = my_error_exit; jerr.pub.output_message = my_output_message; /* Establish the setjmp return context for my_error_exit to use. */ if (setjmp(jerr.setjmp_buffer)) { jpeg_destroy_decompress(&cinfo); fclose(infile); return 0; } jpeg_create_decompress(&cinfo); if ((infile = fopen("C:\\bin\\libjpeg-turbo\\b.jpg", "rb")) == NULL) { printf("cannot open jpeg file"); return 1; } jpeg_stdio_src(&cinfo, infile); (void) jpeg_read_header(&cinfo, TRUE); jpeg_start_decompress(&cinfo); row_stride = cinfo.output_width * cinfo.output_components; ptr = (*cinfo.mem->alloc_sarray)((j_common_ptr)&cinfo,JPOOL_IMAGE,row_stride,1); while (cinfo.output_scanline < cinfo.output_height) { error_message_code = 0; jpeg_read_scanlines(&cinfo, (JSAMPARRAY) ptr, 1); if (error_message_code) printf("Error occured at scanline %d: Error code = %d message= \'%s\'\n", cinfo.output_scanline, error_message_code, error_message_buffer); } // while (void) jpeg_finish_decompress(&cinfo); jpeg_destroy_decompress(&cinfo); fclose (infile); return 0; }
Postscript
I prepare two corrupted jpeg files. The first is truncated file that the exact image height is smaller than that is specified in the jpeg header. In this case, I can successfully intercept the error at scanline 313 with Error code = 120 and error message = "Premature end of JPEG file". The second is truncated that even the jpeg header cannot be successfully read. In this case, my my_error_exit() terminating route can capture the error_code = 51 and error message = "JPEG datastream contains no image".
at
下午2:19
2013-10-03
2013-10-01
2013-08-27
Creating a Full Blog list Gadget
I try to create a full blog list. Originally I thought Google should have a built-in gadget for this purpose but I was wrong. So finally after reviewing other blogger's proposal, I implement my way as follows:
(1) From the Layout menu, choose "Add a Gadget"

(2) Choose the "HTML/Javascript" Gadget

(3) Input the Title and enter the HTML in the Content box

-----
<div id="bp_toc"></div>
<script>
// ---------------------------------------------------
// BLOGTOC
// ---------------------------------------------------
// BlogToc creates a clickable Table Of Contents for
// Blogger Blogs.
// It uses the JSON post feed, and create a ToC of it.
// The ToC can be sorted by title or by date, both
// ascending and descending, and can be filtered by
// label.
// ---------------------------------------------------
// Author: Beautiful Beta
// Url: http://beautifulbeta.blogspot.com
// Version: 2
// Date: 2007-04-12
// ---------------------------------------------------
// Modified by Aneesh
// www.bloggerplugins.org
// Date : 02-08-2011
// ---------------------------------------------------
// Modified by waihungmm
// Date : 2013-08-21
var postTitle = new Array(); // array of posttitles
var postUrl = new Array(); // array of posturls
var postDate = new Array(); // array of post publish dates
var postSum = new Array(); // array of post summaries
var postLabels = new Array(); // array of post labels
// global variables
var sortBy = "titleasc"; // default value for sorting ToC
var tocLoaded = false; // true if feed is read and ToC can be displayed
var numChars = 250; // number of characters in post summary
var postFilter = ''; // default filter value
var tocdiv = document.getElementById("bp_toc"); //the toc container
var totalEntires =0; //Entries grabbed till now
var totalPosts =0; //Total number of posts in the blog.
// main callback function
function loadtoc(json) {
function getPostData() {
// this functions reads all postdata from the json-feed and stores it in arrays
if ("entry" in json.feed) {
var numEntries = json.feed.entry.length;
totalEntires = totalEntires + numEntries;
totalPosts=json.feed.openSearch$totalResults.$t
// main loop gets all the entries from the feed
for (var i = 0; i < numEntries; i++) {
// get the entry from the feed
var entry = json.feed.entry[i];
// get the posttitle from the entry
var posttitle = entry.title.$t;
// get the post date from the entry
var postdate = entry.published.$t.substring(0,10);
// get the post url from the entry
var posturl;
for (var k = 0; k < entry.link.length; k++) {
if (entry.link[k].rel == 'alternate') {
posturl = entry.link[k].href;
break;
}
}
// get the post contents from the entry
// strip all html-characters, and reduce it to a summary
if ("content" in entry) {
var postcontent = entry.content.$t;}
else
if ("summary" in entry) {
var postcontent = entry.summary.$t;}
else var postcontent = "";
// strip off all html-tags
var re = /<\S[^>]*>/g;
postcontent = postcontent.replace(re, "");
// reduce postcontent to numchar characters, and then cut it off at the last whole word
if (postcontent.length > numChars) {
postcontent = postcontent.substring(0,numChars);
var quoteEnd = postcontent.lastIndexOf(" ");
postcontent = postcontent.substring(0,quoteEnd) + '...';
}
// get the post labels from the entry
var pll = '';
if ("category" in entry) {
for (var k = 0; k < entry.category.length; k++) {
pll += '<a href="javascript:filterPosts(\'' + entry.category[k].term + '\');" title="Click here to select all posts with label \'' + entry.category[k].term + '\'">' + entry.category[k].term + '</a>, ';
}
var l = pll.lastIndexOf(',');
if (l != -1) { pll = pll.substring(0,l); }
}
// add the post data to the arrays
postTitle.push(posttitle);
postDate.push(postdate);
postUrl.push(posturl);
postSum.push(postcontent);
postLabels.push(pll);
}
}
if(totalEntires==totalPosts) {tocLoaded=true;showToc();}
} // end of getPostData
// start of showtoc function body
// get the number of entries that are in the feed
// numEntries = json.feed.entry.length;
// get the postdata from the feed
getPostData();
// sort the arrays
sortPosts(sortBy);
tocLoaded = true;
showToc(); // newly added
}
// filter and sort functions
function filterPosts(filter) {
// This function changes the filter
// and displays the filtered list of posts
// document.getElementById("bp_toc").scrollTop = document.getElementById("bp_toc").offsetTop;;
postFilter = filter;
displayToc(postFilter);
} // end filterPosts
function allPosts() {
// This function resets the filter
// and displays all posts
postFilter = '';
displayToc(postFilter);
} // end allPosts
function sortPosts(sortBy) {
// This function is a simple bubble-sort routine
// that sorts the posts
function swapPosts(x,y) {
// Swaps 2 ToC-entries by swapping all array-elements
var temp = postTitle[x];
postTitle[x] = postTitle[y];
postTitle[y] = temp;
var temp = postDate[x];
postDate[x] = postDate[y];
postDate[y] = temp;
var temp = postUrl[x];
postUrl[x] = postUrl[y];
postUrl[y] = temp;
var temp = postSum[x];
postSum[x] = postSum[y];
postSum[y] = temp;
var temp = postLabels[x];
postLabels[x] = postLabels[y];
postLabels[y] = temp;
} // end swapPosts
for (var i=0; i < postTitle.length-1; i++) {
for (var j=i+1; j<postTitle.length; j++) {
if (sortBy == "titleasc") { if (postTitle[i] > postTitle[j]) { swapPosts(i,j); } }
if (sortBy == "titledesc") { if (postTitle[i] < postTitle[j]) { swapPosts(i,j); } }
if (sortBy == "dateoldest") { if (postDate[i] > postDate[j]) { swapPosts(i,j); } }
if (sortBy == "datenewest") { if (postDate[i] < postDate[j]) { swapPosts(i,j); } }
}
}
} // end sortPosts
// displaying the toc
function displayToc(filter) {
// this function creates a three-column table and adds it to the screen
var numDisplayed = 0;
var tocTable = '';
tocTable += '<table>';
tocTable += '</tr>';
for (var i = 0; i < postTitle.length; i++) {
tocTable += '<tr><td class="toc-entry-col1"><a href="' + postUrl[i] + '" target=_blank>' + postTitle[i] + '</a></td></tr>';
numDisplayed++;
}
tocTable += '</table>';
tocdiv.innerHTML = tocTable;
} // end of displayToc
function showToc() {
if (tocLoaded) {
displayToc(postFilter);
var toclink = document.getElementById("toclink");
}
else { alert("Just wait... TOC is loading"); }
}
</script>
<script src="http://your_blog_name.blogspot.com/feeds/posts/default?alt=json-in-script&max-results=999&callback=loadtoc"></script>
-----
(4) The result will be as follows

(5) This is the final output:
(1) From the Layout menu, choose "Add a Gadget"
(2) Choose the "HTML/Javascript" Gadget
(3) Input the Title and enter the HTML in the Content box
-----
<div id="bp_toc"></div>
<script>
// ---------------------------------------------------
// BLOGTOC
// ---------------------------------------------------
// BlogToc creates a clickable Table Of Contents for
// Blogger Blogs.
// It uses the JSON post feed, and create a ToC of it.
// The ToC can be sorted by title or by date, both
// ascending and descending, and can be filtered by
// label.
// ---------------------------------------------------
// Author: Beautiful Beta
// Url: http://beautifulbeta.blogspot.com
// Version: 2
// Date: 2007-04-12
// ---------------------------------------------------
// Modified by Aneesh
// www.bloggerplugins.org
// Date : 02-08-2011
// ---------------------------------------------------
// Modified by waihungmm
// Date : 2013-08-21
var postTitle = new Array(); // array of posttitles
var postUrl = new Array(); // array of posturls
var postDate = new Array(); // array of post publish dates
var postSum = new Array(); // array of post summaries
var postLabels = new Array(); // array of post labels
// global variables
var sortBy = "titleasc"; // default value for sorting ToC
var tocLoaded = false; // true if feed is read and ToC can be displayed
var numChars = 250; // number of characters in post summary
var postFilter = ''; // default filter value
var tocdiv = document.getElementById("bp_toc"); //the toc container
var totalEntires =0; //Entries grabbed till now
var totalPosts =0; //Total number of posts in the blog.
// main callback function
function loadtoc(json) {
function getPostData() {
// this functions reads all postdata from the json-feed and stores it in arrays
if ("entry" in json.feed) {
var numEntries = json.feed.entry.length;
totalEntires = totalEntires + numEntries;
totalPosts=json.feed.openSearch$totalResults.$t
// main loop gets all the entries from the feed
for (var i = 0; i < numEntries; i++) {
// get the entry from the feed
var entry = json.feed.entry[i];
// get the posttitle from the entry
var posttitle = entry.title.$t;
// get the post date from the entry
var postdate = entry.published.$t.substring(0,10);
// get the post url from the entry
var posturl;
for (var k = 0; k < entry.link.length; k++) {
if (entry.link[k].rel == 'alternate') {
posturl = entry.link[k].href;
break;
}
}
// get the post contents from the entry
// strip all html-characters, and reduce it to a summary
if ("content" in entry) {
var postcontent = entry.content.$t;}
else
if ("summary" in entry) {
var postcontent = entry.summary.$t;}
else var postcontent = "";
// strip off all html-tags
var re = /<\S[^>]*>/g;
postcontent = postcontent.replace(re, "");
// reduce postcontent to numchar characters, and then cut it off at the last whole word
if (postcontent.length > numChars) {
postcontent = postcontent.substring(0,numChars);
var quoteEnd = postcontent.lastIndexOf(" ");
postcontent = postcontent.substring(0,quoteEnd) + '...';
}
// get the post labels from the entry
var pll = '';
if ("category" in entry) {
for (var k = 0; k < entry.category.length; k++) {
pll += '<a href="javascript:filterPosts(\'' + entry.category[k].term + '\');" title="Click here to select all posts with label \'' + entry.category[k].term + '\'">' + entry.category[k].term + '</a>, ';
}
var l = pll.lastIndexOf(',');
if (l != -1) { pll = pll.substring(0,l); }
}
// add the post data to the arrays
postTitle.push(posttitle);
postDate.push(postdate);
postUrl.push(posturl);
postSum.push(postcontent);
postLabels.push(pll);
}
}
if(totalEntires==totalPosts) {tocLoaded=true;showToc();}
} // end of getPostData
// start of showtoc function body
// get the number of entries that are in the feed
// numEntries = json.feed.entry.length;
// get the postdata from the feed
getPostData();
// sort the arrays
sortPosts(sortBy);
tocLoaded = true;
showToc(); // newly added
}
// filter and sort functions
function filterPosts(filter) {
// This function changes the filter
// and displays the filtered list of posts
// document.getElementById("bp_toc").scrollTop = document.getElementById("bp_toc").offsetTop;;
postFilter = filter;
displayToc(postFilter);
} // end filterPosts
function allPosts() {
// This function resets the filter
// and displays all posts
postFilter = '';
displayToc(postFilter);
} // end allPosts
function sortPosts(sortBy) {
// This function is a simple bubble-sort routine
// that sorts the posts
function swapPosts(x,y) {
// Swaps 2 ToC-entries by swapping all array-elements
var temp = postTitle[x];
postTitle[x] = postTitle[y];
postTitle[y] = temp;
var temp = postDate[x];
postDate[x] = postDate[y];
postDate[y] = temp;
var temp = postUrl[x];
postUrl[x] = postUrl[y];
postUrl[y] = temp;
var temp = postSum[x];
postSum[x] = postSum[y];
postSum[y] = temp;
var temp = postLabels[x];
postLabels[x] = postLabels[y];
postLabels[y] = temp;
} // end swapPosts
for (var i=0; i < postTitle.length-1; i++) {
for (var j=i+1; j<postTitle.length; j++) {
if (sortBy == "titleasc") { if (postTitle[i] > postTitle[j]) { swapPosts(i,j); } }
if (sortBy == "titledesc") { if (postTitle[i] < postTitle[j]) { swapPosts(i,j); } }
if (sortBy == "dateoldest") { if (postDate[i] > postDate[j]) { swapPosts(i,j); } }
if (sortBy == "datenewest") { if (postDate[i] < postDate[j]) { swapPosts(i,j); } }
}
}
} // end sortPosts
// displaying the toc
function displayToc(filter) {
// this function creates a three-column table and adds it to the screen
var numDisplayed = 0;
var tocTable = '';
tocTable += '<table>';
tocTable += '</tr>';
for (var i = 0; i < postTitle.length; i++) {
tocTable += '<tr><td class="toc-entry-col1"><a href="' + postUrl[i] + '" target=_blank>' + postTitle[i] + '</a></td></tr>';
numDisplayed++;
}
tocTable += '</table>';
tocdiv.innerHTML = tocTable;
} // end of displayToc
function showToc() {
if (tocLoaded) {
displayToc(postFilter);
var toclink = document.getElementById("toclink");
}
else { alert("Just wait... TOC is loading"); }
}
</script>
<script src="http://your_blog_name.blogspot.com/feeds/posts/default?alt=json-in-script&max-results=999&callback=loadtoc"></script>
-----
(4) The result will be as follows
(5) This is the final output:
at
下午12:24
2012-11-19
知魚之痛 (Metropop Issue 331 @ 2012-11-15)
惠子曾問「子非魚,安知魚之樂?」確實,對比起會跑會叫的陸上動物,我們一直不太相信魚類是有感受的動物,以至對牠們的同情心亦都相對較少:各國的動物權益條例一直把魚類豁免在外,釣魚仍被視為一種健康的休閒活動……但近十年愈來愈多科學家證實,魚其實與貓、狗、人一樣也有靈性時,我們仍可問心無愧地把快樂建築在牠們的痛苦之上嗎?
知魚知痛
魚的神經結構與人類及其他哺乳類動物相似,全身上下包括經常被魚?勾傷的唇部,都佈滿了能感知痛楚的神經末梢。更有研究指魚跟人類一樣,有一種具有鎮痛作用的腦分泌「安多芬」(endorphins),如果沒有痛覺,又哪來鎮痛?
放生仁慈嗎?
有些垂釣愛好者與「戰利品」拍照留念後,選擇把牠們放生回大海,這樣做比較仁慈嗎?首先,這過程已令魚類承受到很大的痛楚,如果目的純粹為自己高興,這種娛樂是否人道?每天無數的魚兒被纏困於魚網之中,或被金屬利?刺穿身體,再拖到水面無法呼吸,牠們的魚鰓經常因此而塌陷,魚鰾也會因突然驟變的壓力而破裂,又或失去保護身體的魚鱗而死去。美國奧克拉荷馬州野生動物保護局便發現,近43%被釣上後再放生的魚,會在六天內死去。
魚智慧
at
下午10:04
2012-10-26
Using Google Apps Scripts to create an automatic email subsscription service
Maintaining an email list for email subscription service is a headache. What I mean is a sincere service and not for spam email dissemination.
The steps will be as follows:
- to collect email address
- to confirm the email address is capable of receiving emails and the email address registration is intended (by sending a confirmation email and request acknowledgement)
- to process the acknowledgement
- to send email periodically based on the maintained email list
I shall use Google Spreadsheet to maintain the email list.
For step 2, there can be two alternatives (i) requiring user to send email to a designated address; (ii) allowing user to input an email address on a web page. Since I think some users may use receive-only email address, I decide to choose alternative (ii).
So, for step 1, the most straight forward method is to use Google form, as shown below:
Then I shall use Google Apps scripts to process the registration request. Google supports many methods to invoke Apps scripts (link), one of which is the form-submit event handler. Here I setup a method "send_confirmation" as follows:
I call this method send_confirmation because it will send an email to the inputted email address for confirmation purpose. The source is as follows:
Before sending the email, I will assign a random number for a unique key (which will be included in the email subject). This number will be recorded in the spreadsheet. However, I find the range passed into the method (e.range) has only the fields populated by Google form. Therefore I need to use the Spreadsheet object to do the update. Moreover, there is a tutorial to use sheet.getLastRow to return the row number for update. But I wonder whether there is any locking mechanism to prevent concurrent update. Therefore I use e.range.getRowIndex instead.
I would expect the user to reply the email to confirm the email address is for an intended registration.
To process the email reply (step 4), I use another trigger "checkGmail". I originally think whether there is any asynchronous event handler at Gmail for any event like emailReceived. But I fail. Therefore I use a periodic event (Time-driven) to periodically poll my Gmail inbox for an reply.
The source is as follows:
My email search criteria is 'in:inbox is:unread subject:"xxx"' to ensure that only new emails are processed. Again I use the Spreadsheet object to search for the email record (using the unique random number). If found, I will update the spreadsheet with a time-stamp to confirm the email address is geniune.
Finally I have included a method formatDate because I find there is not a dateFormatter in Javascript. But it is easily copied from internet.
at
下午1:55
訂閱:
文章 (Atom)



















