This derives directly from the cookie cutter / press stack, so check that series for more background and explanation. Some height map thoughts and preliminary doodling led up to this.
We start with a tiny grayscale image file that defines the height of each point in the mold:

Feed that file into a Bash script:
./MakeMold.sh Tux.png
And a corresponding STL file pops out:

The MakeMold Bash script orchestrates the whole thing:
#!/bin/bash
DotsPerMM=3.0
MapHeight=5
ImageName="${1%%.*}"
rm ${ImageName}_* ${ImageName}-positive.stl
echo Normalize and prepare grayscale image...
convert $1 -type Grayscale -depth 8 -trim +repage -flip +set comment ${ImageName}_prep.png
echo Create PGM files...
convert ${ImageName}_prep.png -compress none ${ImageName}_map.pgm
convert ${ImageName}_prep.png -white-threshold 1 -compress none ${ImageName}_plate.pgm
echo Create height map data files...
ImageX=`identify -format '%[fx:w]' ${ImageName}_map.pgm`
ImageY=`identify -format '%[fx:h]' ${ImageName}_map.pgm`
echo Width: ${ImageX} x Height: ${ImageY}
cat ${ImageName}_map.pgm | tr -s ' \012' '\012' | tail -n +5 | column -x -c $((8*$ImageX)) > ${ImageName}_map.dat
cat ${ImageName}_plate.pgm | tr -s ' \012' '\012' | tail -n +5 | column -x -c $((8*$ImageX)) > ${ImageName}_plate.dat
echo Create mold positive...
time openscad -D fnPlate=\"${ImageName}_plate.dat\" \
-D fnMap=\"${ImageName}_map.dat\" -D Height=$MapHeight \
-D ImageX=$ImageX -D ImageY=$ImageY -D DotsPerMM=$DotsPerMM \
-o ${ImageName}-positive.stl MoldPositive.scad
The first convert normalizes the grayscale file and produces a PNG file in a standard format.
The next two convert operations translate that PNG file into uncompressed PGM files with the data as ASCII text required by OpenSCAD’s surface() function. It’s not in the proper format, however, so a few lines of Bash-fu rearrange the data into DAT files; the extension is arbitrary.
Then OpenSCAD eats those files along with a bunch of configuration settings and spits out a solid model of the positive mold in STL format.
The MakePositive.scad OpenSCAD source code:
// Mold positive pattern from grayscale height map using Minkowski sum
// Ed Nisley KE4ZNU - February 2014 - adapted from cookie press, added alignment pins
//-----------------
// Mold files
fnMap = "SqWr_map.dat"; // override with -D 'fnMap="whatever.dat"'
fnPlate = "SqWr_plate.dat"; // override with -D 'fnPlate="whatever.dat"'
DotsPerMM = 3.0; // overrride with -D DotsPerMM=number
MapHeight = 5.0; // overrride with -D MapHeight=number
ImageX = 100; // overrride with -D ImageX=whatever
ImageY = 100;
MapScaleXYZ = [1/DotsPerMM,1/DotsPerMM,MapHeight/255];
PlateScaleXYZ = [1/DotsPerMM,1/DotsPerMM,1.0];
echo("Press File: ",fnMap);
echo("Plate File: ",fnPlate);
echo(str("ImageX:",ImageX," ImageY: ", ImageY));
echo(str("Map Height: ",MapHeight));
echo(str("Dots/mm: ",DotsPerMM));
echo(str("Scale Map: ",MapScaleXYZ," Plate: ",PlateScaleXYZ));
//- Extrusion parameters - must match reality!
ThreadThick = 0.25;
ThreadWidth = 2.0 * ThreadThick;
//- Buid parameters
PlateThick = IntegerMultiple(1.0,ThreadThick); // solid plate under press relief
PinOD = 1.75; // locating pin diameter
PinDepth = PlateThick; // ... depth into bottom surface = total length/2
PinOC = 20.0; // spacing within mold item
echo(str("Pin depth: ",PinDepth," spacing: ",PinOC));
//- Useful info
function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
HoleWindage = 0.2;
Protrusion = 0.1; // make holes & unions work correctly
MaxConvexity = 5; // used for F5 previews in OpenSCAD GUI
ZFuzz = 0.2; // numeric chaff just above height map Z=0 plane
//-----------------
// Import plate height map, slice off a slab to define outline
module Slab(Thick=1.0) {
intersection() {
translate([0,0,Thick/2])
cube([2*ImageX,2*ImageY,Thick],center=true);
scale(PlateScaleXYZ)
difference() {
translate([0,0,-ZFuzz])
surface(fnPlate,center=true,convexity=MaxConvexity);
translate([0,0,-1])
cube([2*ImageX,2*ImageY,2],center=true);
}
}
}
//- Put peg grid on build surface
module ShowPegGrid(Space = 10.0,Size = 1.0) {
Range = floor(50 / Space);
for (x=[-Range:Range])
for (y=[-Range:Range])
translate([x*Space,y*Space,Size/2])
%cube(Size,center=true);
}
//-- convert cylinder to low-count polygon
module PolyCyl(Dia,Height,ForceSides=0) { // based on nophead's polyholes
Sides = (ForceSides != 0) ? ForceSides : (ceil(Dia) + 2);
FixDia = Dia / cos(180/Sides);
cylinder(r=(FixDia + HoleWindage)/2,
h=Height,
$fn=Sides);
}
//-- Locating pin hole with glue recess
// Default length is two pin diameters on each side of the split
module LocatingPin(Dia=PinOD,Len=0.0) {
PinLen = (Len != 0.0) ? Len : (4*Dia);
translate([0,0,-ThreadThick])
PolyCyl((Dia + 2*ThreadWidth),2*ThreadThick,4);
translate([0,0,-2*ThreadThick])
PolyCyl((Dia + 1*ThreadWidth),4*ThreadThick,4);
translate([0,0,-(Len/2 + ThreadThick)])
PolyCyl(Dia,(Len + 2*ThreadThick),4);
}
//- Build it
//ShowPegGrid();
echo("Building mold");
union() {
difference() {
Slab(PlateThick + Protrusion);
for (i=[-1,1])
translate([0,i*PinOC/2,0])
rotate(180/4) LocatingPin(Len=2*PinDepth);
}
translate([0,0,PlateThick]) // cookie press height map
scale(MapScaleXYZ)
difference() {
translate([0,0,-ZFuzz])
surface(fnMap,center=true,convexity=MaxConvexity);
translate([0,0,-1])
cube([2*ImageX,2*ImageY,2],center=true);
}
}
The molds have alignment pin holes in the back:

That match up with the holes in a baseplate:
The plate holds the molds in place, perhaps with tapeless sticky, while you’re slathering silicone goop to make the negative mold:

As you might expect, the OpenSCAD file that generates the plate-with-holes can also embed the positive molds atop the plate, so you could get a solid (well, infilled at 20%) chunk of plastic without attaching the molds. I’d rather do the plate separately from the molds, so you can recycle the plate for many different molds. Your mileage may vary.
The Positive Mold Framework.scad OpenSCAD source code:
// Positive mold framework for chocolate slabs
// Ed Nisley - KE4ZNU - January 2014
Layout = "FramePins"; // FramePins FrameMolds Pin
//- Extrusion parameters must match reality!
// Print with 2 shells and 3 solid layers
ThreadThick = 0.20;
ThreadWidth = 0.40;
Protrusion = 0.1; // make holes end cleanly
HoleWindage = 0.2;
//----------------------
// Dimensions
FileName = "Tux-positive.stl"; // overrride with -D
Molds = [2,3]; // count of molds within framework
MoldOC = [40.0,45.0]; // on-center spacing of molds
MoldSlab = 1.0; // thickness of slab under molds
BaseThick = 5.0;
BaseSize = [(Molds[0]*MoldOC[0] + 0),(Molds[1]*MoldOC[1] + 0),BaseThick];
echo(str("Overall base: ",BaseSize));
PinOD = 1.75; // locating pin diameter
PinLength = 2.0; // ... total length
PinOC = 20.0; // spacing within mold item
//----------------------
// Useful routines
//- Put peg grid on build surface
module ShowPegGrid(Space = 10.0,Size = 1.0) {
RangeX = floor(100 / Space);
RangeY = floor(125 / Space);
for (x=[-RangeX:RangeX])
for (y=[-RangeY:RangeY])
translate([x*Space,y*Space,Size/2])
%cube(Size,center=true);
}
module PolyCyl(Dia,Height,ForceSides=0) { // based on nophead's polyholes
Sides = (ForceSides != 0) ? ForceSides : (ceil(Dia) + 2);
FixDia = Dia / cos(180/Sides);
cylinder(r=(FixDia + HoleWindage)/2,
h=Height,
$fn=Sides);
}
// Locating pin hole with glue recess
// Default length is two pin diameters on each side of the split
module LocatingPin(Dia=PinOD,Len=0.0) {
PinLen = (Len != 0.0) ? Len : (4*Dia);
translate([0,0,-ThreadThick])
PolyCyl((Dia + 2*ThreadWidth),2*ThreadThick,4);
translate([0,0,-2*ThreadThick])
PolyCyl((Dia + 1*ThreadWidth),4*ThreadThick,4);
translate([0,0,-(Len/2 + ThreadThick)])
PolyCyl(Dia,(Len + 2*ThreadThick),4);
}
module LocatingPins(Length) {
for (i=[-1,1])
translate([0,i*PinOC/2,0])
rotate(180/4)
LocatingPin(Len=Length);
}
//-- import a single mold item
module MoldItem() {
import(FileName,convexity=10);
}
//-- Overall frame shape
module Frame() {
// translate([0,0,BaseSize[2]/2]) // platform under molds
// cube(BaseSize,center=true);
difference() {
hull()
for (i=[-1,1], j=[-1,1])
translate([i*BaseSize[0]/2,j*BaseSize[1]/2,0])
sphere(r=BaseThick);
translate([0,0,-BaseThick])
cube(2*BaseSize,center=true);
}
}
//- Build it
ShowPegGrid();
if (Layout == "Pin")
LocatingPin(Len=PinLength);
if (Layout == "Frame")
Frame();
if (Layout == "FramePins")
difference() {
Frame();
translate([-MoldOC[0]*(Molds[0] - 1)/2,-MoldOC[1]*(Molds[1] - 1)/2,0])
for (i=[0:Molds[0]-1],j=[0:Molds[1]-1])
translate([i*MoldOC[0],j*MoldOC[1],BaseSize[2]])
LocatingPins(BaseThick);
}
if (Layout == "FrameMolds") {
Frame();
translate([-MoldOC[0]*(Molds[0] - 1)/2,-MoldOC[1]*(Molds[1] - 1)/2,0])
for (i=[0:Molds[0]-1],j=[0:Molds[1]-1])
translate([i*MoldOC[0],j*MoldOC[1],BaseThick - MoldSlab + Protrusion])
MoldItem();
}
And then it’s time to pour some chocolate… which someone else knows how to do much better than I!



























