How do I make these pages?

I have made software to do so. It would be near impossible to hand craft these pages. The steps are:

  1. Make a selection
  2. Extract EXIF data from the pictures
  3. Resize and watermark the picture
  4. Create the HTML code
  5. Clean up the directory
  6. Create the index.html file
  7. Create a caption image
  8. Make an entry in the main index.html page
  9. Upload the lot to the webhost

Make the selection

I use GwenView to judge the pictures and with F7 I copy each (group of) picture file(s) to the final target directory.

Extract EXIF

I use a script for doing this:

#!/usr/bin/bash

exiv2 *.jpg | grep 'Exposure' 		>  props
exiv2 *.jpg | grep 'Image size'		>> props
exiv2 *.jpg | grep 'Image timestamp'	>> props
exiv2 *.jpg | grep 'Camera model'	>> props
exiv2 *.jpg | grep 'Focal length'	>> props
exiv2 *.jpg | grep 'ISO speed'		>> props
exiv2 *.jpg | grep 'Aperture  '		>> props

sort <props >props.sorted
mv props.sorted props
echo 'Done'
   
The script creates a file named 'props' that contains the most important EXIF data for each jpeg file. Beware, that some SONY pictures have superfluous EOF tokens in it and the script chokes on that. A simple way to check on this is: Suppose you have 40 images, then the props file must be 9 . 40 = 360 lines.

Resize the images

Resizing is easy. I just use the standard Linux command 'convert' which is part of Image Magick:

convert -resize 900x600 DSC_0001.jpg DSC_0001k.jpg
The smaller file gets the letter 'k' added to its base-name (the name without the file extension). This way I keep the original files as long as possible.

Add a watermark

When the small image is ready, I add a watermark to it, using the 'composite' command, in the lower left corner. Why not in the center? That would ruin the pictures. OK, now users can just download an image, tinker out the watermark and have a free print. So be it.

Automate the last two steps

It is a lot of work to resize and watermark all the images. So I made some software to ease things up. The program is called 'Fprep' and it is used as follows:

ls -1 *jpg | Fprep >run
The 'ls' command lists all jpeg files. These are piped to the Fprep program, that does some magic on the filenames and outputs two lines of text for each picture file:
convert -resize 900x600 DSC_1519.jpg DSC_1519k.jpg
composite -gravity southwest -dissolve 80% ../bottomleft.png DSC_1519k.jpg DSC_1519kw.jpg
   
These lines are redirected to the file 'run' and later, this file is executed with the command
bash run

The Fprep program that does all this was written in OBC oberon. The source is open and here it is:

MODULE Fprep;

IMPORT	In, Out, Err, Strings;

CONST	TAB	= 09X;

TYPE	PictureName	= ARRAY  32  OF CHAR;

VAR	image	: PictureName;
	images	: INTEGER;


PROCEDURE InsertLetter (src  : PictureName; char  : ARRAY OF CHAR; VAR dest   : PictureName);

VAR	pos	: INTEGER;

BEGIN
  dest := src;
  pos := Strings.Pos ('.jpg', dest, 0);
  Strings.Insert (char, pos, dest)
END InsertLetter;


PROCEDURE Process (pict : PictureName);

VAR	pictk, pictkf	: PictureName;

BEGIN
  Out.String ("convert -resize 900x600 ");			(*  Produce 600 x 900 copy	*)
  Out.String (pict);
  InsertLetter (pict, 'k', pictk);
  Out.Char (" ");
  Out.String (pictk);	Out.Ln;
  
  Out.String ("composite -gravity southwest -dissolve 80% ../bottomleft.png ");
  Out.String (pictk);		
  InsertLetter (pictk, 'w', pictkf);			(*  Add watermark in bottomleft	*)
  Out.Char (' ');
  Out.String (pictkf);
  Out.Ln
END Process;


BEGIN
  images := 0;
  Err.String ("Syntax: ls -1 *jpg | Fprep >outfile");	Err.Ln;
  LOOP
    In.Line (image);
    IF  In.Done = FALSE  THEN  EXIT  END;
    INC (images);
    Process (image);
    Out.Ln
  END;
  Err.String ("Processed ");	Err.Int (images, 4);	Err.String (" images. Have a nice day.");	Err.Ln
END Fprep.
   
Compile with
obc -o Fprep Fprep.mod

Create the HTML code

The index.html file needs to know which pictures to display and the EXIF data must be in the right place. This is done with the file 'Fgallery' which also is a compiled program written in OBC oberon. The program is used as in:

Fgallery -i props -o file
The props file, created with exivtract, is processed and after fully processing, for each picture around 11 lines of HTML code are produced that formats the pictures on the webpage. You can just import this file in the right place in the index.html file and you're done.

The source of Fgallery:

MODULE Fgall;

IMPORT	Args, Out, Err, Files, Strings;

CONST	AsciiLF 	= 0AX;
	AsciiCR 	= 0DX;
	AsciiHT 	= 09X;
	open		= 0;
	close		= 1;
	maxPict 	= 250;
	maxProps	= 9;
	maxPropLength	= 24;
	
TYPE	PictureName	= ARRAY  32  OF CHAR;
	PropValue	= ARRAY  maxPropLength  OF CHAR;
	Properties	= ARRAY  maxProps  OF  PropValue;

VAR	inF, outF	: Files.File;
	InputFile,
	EOF, DebugMode	: BOOLEAN;
	Fcode		: ARRAY  40  OF  CHAR;
	picture, pickw	: PictureName;
	Pnames		: ARRAY  maxPict  OF  PictureName;
	Props		: ARRAY  maxPict  OF  Properties;
	PicturesFound,
	index		: INTEGER;
(*	Pdates		: ARRAY  maxPict, 3  OF INTEGER;	*)
	

PROCEDURE Abort (nr : INTEGER);         (* Abort program in a controlled way, with an   *)
                                        (* error message and shutdown procedure         *)
BEGIN
  CASE  nr  OF
    1 : Err.String ("Please supply arguments and options")	|
    2 : Err.String ("No input-file specified")         		|
    3 : Err.String ("File cannot be opened")			|
    4 : Err.String ("Cannot create output file")		|
    5 : Err.String ("Cannot append to file")
  END;
  Err.String (", aborting.");								Err.Ln;
  Err.String ("Syntax : Fgallery -i props-file -o output-file -a append-file -d (debug)");		Err.Ln;
  ShutDown;
  HALT (nr)
END Abort;


PROCEDURE StringCopy (src : ARRAY OF CHAR; VAR dest : ARRAY OF CHAR);

VAR     i, k, l         : INTEGER;
        ch              : CHAR;

BEGIN
  k := Strings.Length (src);    l := LEN (dest) - 1;
  i := 0;
  LOOP
    ch := src [i];
    IF  i < l  THEN  dest [i] := ch  ELSE  EXIT  END;
    INC (i);
    IF  i = k  THEN  EXIT  END
  END;
  dest [i] := 0X
END StringCopy;


PROCEDURE Read (VAR c : CHAR);
	(*  Read one token from file	*)

BEGIN
  IF  Files.Eof (inF)  THEN  EOF := TRUE  ELSE  EOF := FALSE  END;
  Files.ReadChar (inF, c)
END Read;


PROCEDURE UnRead (n : INTEGER);
	(*  Move 'n' characters back	*)

BEGIN
  Files.Seek (inF, -n, Files.SeekCur)
END UnRead;
      

PROCEDURE SkipWs;
	(*  Skip over WhiteSpace		*)

VAR     ch      : CHAR;

BEGIN
  REPEAT  Read (ch)  UNTIL  (ch > ' ')  OR  EOF;
  UnRead (1)
END SkipWs;
	  
	  
PROCEDURE SkipTo (token : CHAR);
	(*  Skip all characters until 'token' is found	*)

VAR     ch      : CHAR;

BEGIN
  REPEAT  Read (ch)  UNTIL  (ch = token) OR EOF
END SkipTo;


PROCEDURE ReadString (VAR  dest  : ARRAY OF CHAR;  delim  : CHAR);
	(*  Read a string from file, UNTIL the delimiter token	*)

VAR	i, j	: INTEGER;
	ch	: CHAR;

BEGIN
  i := 0;	j := LEN (dest) - 1;
  SkipWs;
  Read (ch);
  REPEAT
    dest [i] := ch;
    INC (i);
    Read (ch)
  UNTIL  (ch = delim) OR (i = j);
  dest [i] := 0X
END ReadString;


PROCEDURE InsertLetters (src  : PictureName; chars  : ARRAY OF CHAR; VAR dest   : PictureName);

VAR     pos     : INTEGER;

BEGIN
  dest := src;
  pos := Strings.Pos ('.jpg', dest, 0);
  Strings.Insert (chars, pos, dest)
END InsertLetters;


PROCEDURE BackStrip (VAR str : ARRAY OF CHAR);	
	(*  Remove spaces from end of string	*)

VAR	i	: INTEGER;

BEGIN
  i := Strings.Length (str) - 1;
  WHILE  str [i] = ' '  DO  DEC (i)  END;
  INC (i);
  str [i] := 0X
END BackStrip;


PROCEDURE Store (name	: PictureName) : INTEGER;
	(*  Store or Find picturename index	*)

VAR	i	: INTEGER;

BEGIN
  i := 0;
  LOOP
    IF  Pnames [i] = '' THEN
      Pnames [i] := name;
      INC (PicturesFound);
      EXIT
    END;
    IF  name = Pnames [i]  THEN  EXIT  END;
    INC (i)
  END;
  RETURN i
END Store;


PROCEDURE DivHeader (mode  : INTEGER; pict  : ARRAY OF CHAR);
	(*  Output HTML div header and footer code		*)

BEGIN
  IF  mode = open  THEN
    Files.WriteString (outF, '<img class="left" src="');
    Files.WriteString (outF, pict);
    Files.WriteString (outF, '"> <p> <pre>')
  ELSE
    Files.WriteString (outF, '</pre> <script> ShowKofi () </script></p> <br clear="all"> <hr>');
    Files.WriteLn (outF)
  END;
  Files.WriteLn (outF)
END DivHeader;


PROCEDURE Process (n	: INTEGER);
	(*  Take care of storing the EXIF data in an array	*)

VAR	i	: INTEGER;
	option	: PropValue;

BEGIN
  ReadString (option, ':');
  BackStrip (option);
  IF     option = 'Aperture'  		THEN
    i := 0;
    ReadString (option, AsciiLF)
  ELSIF  option = 'Camera model'	THEN
    i := 1;
    ReadString (option, AsciiLF)
  ELSIF  option = 'Exposure bias'	THEN
    i := 2;
    ReadString (option, AsciiLF)
  ELSIF  option = 'Exposure mode'	THEN
    i := 3;
    ReadString (option, AsciiLF)
  ELSIF  option = 'Exposure time'	THEN
    i := 4;
    ReadString (option, AsciiLF)
  ELSIF  option = 'Focal length'	THEN
    i := 5;
    ReadString (option, '(');
    SkipTo (AsciiLF)
  ELSIF  option = 'ISO speed'		THEN
    i := 6;
    ReadString (option, AsciiLF)
  ELSIF  option = 'Image size'		THEN
    i := 7;
    ReadString (option, AsciiLF)
  ELSIF  option = 'Image timestamp'	THEN
    i := 8;
    ReadString (option, ' ');
    SkipTo (AsciiLF)
  ELSE
    i:= -1
  END;
  Props [n, i] := option
END Process;


PROCEDURE ShowProps;
	(*  Dump the contents of the EXIF arrays to screen	*)

VAR	i, j	: INTEGER;

BEGIN
  FOR  i := 0  TO  PicturesFound - 1  DO
    Out.String (Pnames [i]);		Out.Ln;
    FOR  j := 0  TO  maxProps - 1  DO
      Out.String (Props [i, j]);
      Out.Char (AsciiHT)
    END;
    Out.Ln
  END;
  Out.Ln
END ShowProps;


PROCEDURE Init;

VAR     i, j	: INTEGER;
	option	: ARRAY 32 OF CHAR;

BEGIN
  i := 1;
  DebugMode := FALSE;
  InputFile := FALSE;
  IF  Args.argc # 5  THEN  Abort (1)  END;
  WHILE  i < Args.argc  DO
    Args.GetArg (i, option);
    IF     option = '-i'	THEN
      INC (i);
      Args.GetArg (i, option);
      inF := Files.Open (option, "r");
      IF  inF = NIL  THEN  Abort (3)  END;
      InputFile := TRUE
    
    ELSIF  option = '-o'  	THEN
      INC (i);
      Args.GetArg (i, option);
      outF := Files.Open (option, "w");
      IF  outF = NIL  THEN  Abort (4)  END
    
    ELSIF  option = '-a'  THEN
      INC (i);
      Args.GetArg (i, option);
      outF := Files.Open (option, "a");
      IF  outF = NIL  THEN  Abort (5)  END
      
    ELSIF  option = '-d'	THEN
      DebugMode := TRUE
    END;
    INC (i)
  END;
  IF  InputFile = FALSE  THEN  Abort (2)  END;
  PicturesFound := 0;
  FOR  i := 0  TO  maxPict - 1  DO
    Pnames [i] := '';
    FOR  j := 0  TO  maxProps - 1  DO
      Props [i, j] := ''
    END
  END
END Init;
										      

PROCEDURE ShutDown;

BEGIN
  IF  inF # NIL  THEN  Files.Close (inF)  END;
  IF  outF # NIL  THEN  Files.Close (outF)  END;
  Out.Ln;
  Out.String ("Files closed, shutting down.");
  Out.Ln
END ShutDown;


BEGIN
  Init;
  LOOP
    ReadString (picture, ' ');
    IF  EOF  THEN  EXIT  END;
    index := Store (picture);
    Process (index)
  END;
  IF  DebugMode  THEN  ShowProps  END;
  FOR  index := 0  TO  PicturesFound - 1  DO
    picture := Pnames [index];
    InsertLetters (picture, 'kw', pickw);
    DivHeader (open, pickw);
    Files.WriteString (outF, Props [index] [1]);		(*  Camera		*)
    Files.WriteLn (outF);
    Files.WriteString (outF, Props [index] [3]);		(*  Exposure mode	*)
    Files.WriteLn (outF);
    Files.WriteString (outF, Props [index] [5]);		(*  Focal length	*)
    Files.WriteLn (outF);
    Files.WriteString (outF, Props [index] [4]);		(*  Shutter speed	*)
    Files.WriteString (outF, "   at  ");
    Files.WriteString (outF, Props [index] [0]);		(*  Aperture		*)
    IF  Props [index][2][0] # '0'  THEN
      Files.WriteString (outF, "  and  ");
      Files.WriteString (outF, Props [index][2])		(*  Exp compensation	*)
    END;
    Files.WriteLn (outF);
    Files.WriteString (outF, Props [index] [6]);		(*  ISO			*)
    Files.WriteString (outF, " ISO");
    Files.WriteLn (outF);
    Files.WriteString (outF, Props [index] [7]);		(*  Resolution		*)
    Files.WriteLn (outF);
    Files.WriteString (outF, Props [index] [8]);		(*  Date		*)
    Files.WriteLn (outF);
    Files.WriteLn (outF);
    
    StringCopy (Props [index] [8], Fcode);
    Fcode [4] := '/';
    Fcode [7] := '/';	Fcode [8] := 0X;
    Strings.Append (Pnames [index], Fcode);
    
    Files.WriteString (outF, "<h2>");
    Files.WriteString (outF, Fcode);
    Files.WriteString (outF, "</h2>");
    DivHeader (close, Pnames [index])		(*  picturename is not used here but the
						    parameter must match the TYPE of the function	*)
  END;
  ShutDown 
END Fgall.
   
Oberon is a programmig language ANYONE can read and understand without being fluent in it..

Check the number of lines in the target file and compare it with the number of pictures:

jan@fluor:~/internet/knipser/galleries/tocht10$ wc file
 1705  4495 35733 file
jan@fluor:~/internet/knipser/galleries/tocht10$ ls *jpg | wc
 155     155    2015
jan@fluor:~/internet/knipser/galleries/tocht10$ calc
155 11 * =
1705
Done
   
Yes, calc is an obc program too.
MODULE calc;

IMPORT	In, Out;

CONST	maxSP		= 64;

VAR	option		: ARRAY 32 OF CHAR;
	digits		: ARRAY 20 OF CHAR;
	stack		: ARRAY maxSP + 2 OF LONGINT;
	operation	: CHAR;
	radix, sp	: INTEGER;		(* number base, stackpointer *)
	Done		: BOOLEAN;


PROCEDURE Init;

BEGIN
  sp := 0;
  Done := FALSE;
  radix := 10;
  digits := '0123456789ABCDEF'
END Init;


PROCEDURE GetOption () : CHAR;

VAR	i	: INTEGER;
	ch	: CHAR;
	sign	: BOOLEAN;

BEGIN
  i := 0;
  REPEAT  In.Char (ch)  UNTIL ch > ' ';
  IF  ch = '-'  THEN  
    sign := TRUE;
    In.Char (ch)
  ELSE  
    sign := FALSE
  END;
  IF  ch = ' '  THEN  
    RETURN ('-')
  ELSIF  (ch < '0') OR (ch > '9')  THEN  
    RETURN (ch)
  END;
  IF  sign  THEN
    option [i] := '-';
    INC (i)
  END;
  REPEAT
    option [i] := ch;
    INC (i);
    In.Char (ch)
  UNTIL ch <= ' ';
  option [i] := 0X;
  RETURN ('#')
END GetOption;


PROCEDURE Push (number : LONGINT);

BEGIN
  INC (sp);
  IF  sp > maxSP THEN
    Out.String ("Stack overflow; Aborting.");
    Out.Ln;
    HALT (3)
  ELSE
    stack [sp] := number
  END
END Push;


PROCEDURE Pop (VAR  number : LONGINT);

BEGIN
  IF  sp = 0  THEN
    Out.String ("Stack underflow; Aborting.");
    Out.Ln;
    HALT (4)
  ELSE
    number := stack [sp];
    DEC (sp)
  END
END Pop;


PROCEDURE Swap;

VAR	num1, num2	: LONGINT;

BEGIN
  Pop (num1);
  Pop (num2);
  Push (num1);
  Push (num2)
END Swap;


PROCEDURE Add;

VAR	num1, num2	: LONGINT;

BEGIN
  Pop (num2);
  Pop (num1);
  Push (num1 + num2)
END Add;


PROCEDURE Sub;

VAR	num1, num2	: LONGINT;

BEGIN
  Pop (num2);
  Pop (num1);
  Push (num1 - num2)
END Sub;


PROCEDURE Mul;

VAR	num1, num2	: LONGINT;

BEGIN
  Pop (num2);
  Pop (num1);
  Push (num1 * num2)
END Mul;


PROCEDURE Div;

VAR	num1, num2	: LONGINT;

BEGIN
  Pop (num2);
  Pop (num1);
  Push (num1 DIV num2)
END Div;


PROCEDURE Power; 

VAR	num1, num2, res	: LONGINT;

BEGIN
  Pop (num2);
  Pop (num1);
  res := 1;
  WHILE num2 > 0  DO  
    res := res * num1;
    DEC (num2)
  END;
  Push (res)
END Power;


PROCEDURE Radix;

VAR	num1	: LONGINT;

BEGIN
  Pop (num1);
  radix := SHORT (num1 MOD 17)
END Radix;


PROCEDURE Find (token : CHAR; text : ARRAY OF CHAR) : INTEGER;

VAR	i	: INTEGER;

BEGIN
  i := 0;
  WHILE text [i] # 0X  DO
    IF  token = text [i]  THEN  RETURN (i)  END;
    INC (i)
  END;
  RETURN (-1)
END Find;


PROCEDURE Number;

VAR	num, sign	: LONGINT;
	pos, i, j	: INTEGER;
	ch		: CHAR;

BEGIN
  i := 0;	j := 0;		num := 0;
  IF  option [0] = '-'  THEN
    sign := -1;
    i := 1
  ELSE
    sign := 1
  END;
  REPEAT
    ch := CAP (option [i]);
    pos := Find (ch, digits);
    IF  (pos < 0) OR (pos >= radix)  THEN
      Out.String ("Illegal token in number : ");
      Out.String (option);
      Out.String (" using radix");	Out.Int (radix, 3);
      Out.Ln;
      HALT (5)
    END;
    num := num * radix + pos;
    INC (i)
  UNTIL option [i] = 0X;
  num := num * sign;
  Push (num)
END Number;


PROCEDURE Print;

VAR	num	: LONGINT;
	res	: ARRAY 66 OF CHAR;
	i, p	: INTEGER;
	sign	: BOOLEAN;

BEGIN
  sign := FALSE;
  Pop (num);
  i := 0;
  IF  num = -9223372036854775808  THEN  
    Out.String ("-9223372036854775808")
  ELSIF  num = 0  THEN  
    Out.Char ('0')
  ELSE
    IF  num < 0  THEN
      sign := TRUE;
      num := ABS (num)
    END;
    WHILE  num # 0  DO
      p := SHORT (num MOD radix);
      num := num DIV radix;
      res [i] := digits [p];
      INC (i)
    END;
    IF  sign  THEN  Out.Char ('-')  END;
    DEC (i);
    WHILE  i >= 0  DO
      Out.Char (res [i]);
      DEC (i)
    END
  END;
  Out.Ln;
  Done := TRUE
END Print;


BEGIN
  Init;
  REPEAT
    operation := GetOption ();
    CASE  operation  OF
      '#' : Number	|
      'R',
      'r' : Radix	|
      '+' : Add 	|
      'x' : Swap	|
      '-' : Sub 	|
      '*' : Mul 	|
      '/' : Div 	|
      '^' : Power	|
      '=' : Print
    ELSE
      Out.String ("Illegal parameter : ");
      Out.String (option);
      Out.Ln;
      HALT (1)
    END
  UNTIL Done;
  Out.String ("Done");		Out.Ln
END calc.
   
It mimics a small RPN calculator without floating point math

Cleaning up

At this point you are ready to clean up the no longer required files:

When you started out with 100 images of 4 MB each, totalling 400 MB, you now end up with a total disk usage of 25 MB. Quite a reduction in disk usage and the 600 x 900 images are still big enough to see what is going on in them.

Make a caption image

After cleaning up, go through the resized and watermarked files and select one image. Resize it to 300x200 pixels and use KolourPaint to add some text in the lower right corner. Save the file as 'cap.jpg'.

Create the index.html file

Spoiler: I use a template file, stored in the system directory:

cp ../sy<TAB>/i<TAB> .
If you come from Windows: do not even try to understand this line.

The template index.html file is as follows:

<!doctype html>
<html lang="en">

  <head> 
    <script src="../../libs/knipser.js"> </script>
    <meta charset = "utf-8">
    <title> </title> 
    <link type="text/css" rel="stylesheet" href="../../knipser.css">
  </head>
  
  <body>

    <h1 class="center"> </h1>
    <h2>Foto's of prints bestellen? Noteer het fotonummer en stuur een mail naar foto@knipser.nl</h2>
    <p>
     De foto's zoals ze hier staan zijn vrij te downloaden en gebruiken MITS het watermerk intact gelaten wordt. 
    </p>
    
    <script> ShowKofi1 () </script>

    <p>
     
    </p>
    
    <div>



    </div>
    
    <script> ShowKofi1 () </script>

  </body>
</html>
   
Some text needs to be entered explaining what this collection is about. After that, the file output by Fgallery is inserted in the div section in the bottom of the file.

Make an entry in the main page

In the required section, add a line

Publish ('gallery')
and save the index.html file

'Publish' is a piece of javascript code:

  function Publish (name)
  {
    document.write ('<a href="galleries/' + name + '" target="_top"> ');
    document.write ('<img src="galleries/' + name + '/cap.jpg"></a>');
  }
   
that inserts one line of HTML to publish the cap.jpg file.

Upload to the webhost

The dialog is simple:

ncftp
open knipser
put index.html (overwrite)
cd galleries
put -R galleries/gallery
exit
   
Things may be different when using Windows.

Page created 28 Apr 2022,