/********************************************************************** * File: blobbox.cpp (Formerly blobnbox.c) * Description: Code for the textord blob class. * Author: Ray Smith * Created: Thu Jul 30 09:08:51 BST 1992 * * (C) Copyright 1992, Hewlett-Packard Ltd. ** Licensed under the Apache License, Version 2.0 (the "License"); ** you may not use this file except in compliance with the License. ** You may obtain a copy of the License at ** http://www.apache.org/licenses/LICENSE-2.0 ** Unless required by applicable law or agreed to in writing, software ** distributed under the License is distributed on an "AS IS" BASIS, ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ** See the License for the specific language governing permissions and ** limitations under the License. * **********************************************************************/ #include "mfcpch.h" #include "blobbox.h" #include "helpers.h" #define PROJECTION_MARGIN 10 //arbitrary #define EXTERN ELISTIZE (BLOBNBOX) ELIST2IZE (TO_ROW) ELISTIZE (TO_BLOCK) // Upto 30 degrees is allowed for rotations of diacritic blobs. const double kCosSmallAngle = 0.866; // Min aspect ratio for a joined word to indicate an obvious flow direction. const double kDefiniteAspectRatio = 2.0; // Multiple of short length in perimeter to make a joined word. const double kComplexShapePerimeterRatio = 1.5; void BLOBNBOX::rotate(FCOORD rotation) { cblob_ptr->rotate(rotation); rotate_box(rotation); compute_bounding_box(); } // Rotate the box by the angle given by rotation. // If the blob is a diacritic, then only small rotations for skew // correction can be applied. void BLOBNBOX::rotate_box(FCOORD rotation) { if (IsDiacritic()) { ASSERT_HOST(rotation.x() >= kCosSmallAngle) ICOORD top_pt((box.left() + box.right()) / 2, base_char_top_); ICOORD bottom_pt(top_pt.x(), base_char_bottom_); top_pt.rotate(rotation); base_char_top_ = top_pt.y(); bottom_pt.rotate(rotation); base_char_bottom_ = bottom_pt.y(); box.rotate(rotation); } else { box.rotate(rotation); set_diacritic_box(box); } } /********************************************************************** * BLOBNBOX::merge * * Merge this blob with the given blob, which should be after this. **********************************************************************/ void BLOBNBOX::merge( //merge blobs BLOBNBOX *nextblob //blob to join with ) { box += nextblob->box; //merge boxes set_diacritic_box(box); nextblob->joined = TRUE; } // Merge this with other, taking the outlines from other. // Other is not deleted, but left for the caller to handle. void BLOBNBOX::really_merge(BLOBNBOX* other) { if (cblob_ptr != NULL && other->cblob_ptr != NULL) { C_OUTLINE_IT ol_it(cblob_ptr->out_list()); ol_it.add_list_after(other->cblob_ptr->out_list()); } compute_bounding_box(); } /********************************************************************** * BLOBNBOX::chop * * Chop this blob into equal sized pieces using the x height as a guide. * The blob is not actually chopped. Instead, fake blobs are inserted * with the relevant bounding boxes. **********************************************************************/ void BLOBNBOX::chop( //chop blobs BLOBNBOX_IT *start_it, //location of this BLOBNBOX_IT *end_it, //iterator FCOORD rotation, //for landscape float xheight //of line ) { inT16 blobcount; //no of blobs BLOBNBOX *newblob; //fake blob BLOBNBOX *blob; //current blob inT16 blobindex; //number of chop inT16 leftx; //left edge of blob float blobwidth; //width of each float rightx; //right edge to scan float ymin, ymax; //limits of new blob float test_ymin, test_ymax; //limits of part blob ICOORD bl, tr; //corners of box BLOBNBOX_IT blob_it; //blob iterator //get no of chops blobcount = (inT16) floor (box.width () / xheight); if (blobcount > 1 && cblob_ptr != NULL) { //width of each blobwidth = (float) (box.width () + 1) / blobcount; for (blobindex = blobcount - 1, rightx = box.right (); blobindex >= 0; blobindex--, rightx -= blobwidth) { ymin = (float) MAX_INT32; ymax = (float) -MAX_INT32; blob_it = *start_it; do { blob = blob_it.data (); find_cblob_vlimits(blob->cblob_ptr, rightx - blobwidth, rightx, /*rotation, */ test_ymin, test_ymax); blob_it.forward (); UpdateRange(test_ymin, test_ymax, &ymin, &ymax); } while (blob != end_it->data ()); if (ymin < ymax) { leftx = (inT16) floor (rightx - blobwidth); if (leftx < box.left ()) leftx = box.left (); //clip to real box bl = ICOORD (leftx, (inT16) floor (ymin)); tr = ICOORD ((inT16) ceil (rightx), (inT16) ceil (ymax)); if (blobindex == 0) box = TBOX (bl, tr); //change box else { newblob = new BLOBNBOX; //box is all it has newblob->box = TBOX (bl, tr); //stay on current newblob->base_char_top_ = tr.y(); newblob->base_char_bottom_ = bl.y(); end_it->add_after_stay_put (newblob); } } } } } // Returns the box gaps between this and its neighbours_ in an array // indexed by BlobNeighbourDir. void BLOBNBOX::NeighbourGaps(int gaps[BND_COUNT]) const { for (int dir = 0; dir < BND_COUNT; ++dir) { gaps[dir] = MAX_INT16; BLOBNBOX* neighbour = neighbours_[dir]; if (neighbour != NULL) { TBOX n_box = neighbour->bounding_box(); if (dir == BND_LEFT || dir == BND_RIGHT) { gaps[dir] = box.x_gap(n_box); } else { gaps[dir] = box.y_gap(n_box); } } } } // Returns the min and max horizontal and vertical gaps (from NeighbourGaps) // modified so that if the max exceeds the max dimension of the blob, and // the min is less, the max is replaced with the min. // The objective is to catch cases where there is only a single neighbour // and avoid reporting the other gap as a ridiculously large number void BLOBNBOX::MinMaxGapsClipped(int* h_min, int* h_max, int* v_min, int* v_max) const { int max_dimension = MAX(box.width(), box.height()); int gaps[BND_COUNT]; NeighbourGaps(gaps); *h_min = MIN(gaps[BND_LEFT], gaps[BND_RIGHT]); *h_max = MAX(gaps[BND_LEFT], gaps[BND_RIGHT]); if (*h_max > max_dimension && *h_min < max_dimension) *h_max = *h_min; *v_min = MIN(gaps[BND_ABOVE], gaps[BND_BELOW]); *v_max = MAX(gaps[BND_ABOVE], gaps[BND_BELOW]); if (*v_max > max_dimension && *v_min < max_dimension) *v_max = *v_min; } // Returns positive if there is at least one side neighbour that has a similar // stroke width and is not on the other side of a rule line. int BLOBNBOX::GoodTextBlob() const { int score = 0; for (int dir = 0; dir < BND_COUNT; ++dir) { BlobNeighbourDir bnd = static_cast(dir); if (good_stroke_neighbour(bnd)) ++score; } return score; } // Returns true, and sets vert_possible/horz_possible if the blob has some // feature that makes it individually appear to flow one way. // eg if it has a high aspect ratio, yet has a complex shape, such as a // joined word in Latin, Arabic, or Hindi, rather than being a -, I, l, 1 etc. bool BLOBNBOX::DefiniteIndividualFlow() { int box_perimeter = 2 * (box.height() + box.width()); if (box.width() > box.height() * kDefiniteAspectRatio) { // Attempt to distinguish a wide joined word from a dash. // If it is a dash, then its perimeter is approximately // 2 * (box width + stroke width), but more if the outline is noisy, // so perimeter - 2*(box width + stroke width) should be close to zero. // A complex shape such as a joined word should have a much larger value. int perimeter = cblob()->perimeter(); if (vert_stroke_width() > 0) perimeter -= 2 * vert_stroke_width(); else perimeter -= 4 * cblob()->area() / perimeter; perimeter -= 2 * box.width(); // Use a multiple of the box perimeter as a threshold. if (perimeter > kComplexShapePerimeterRatio * box_perimeter) { set_vert_possible(false); set_horz_possible(true); return true; } } if (box.height() > box.width() * kDefiniteAspectRatio) { // As above, but for a putative vertical word vs a I/1/l. int perimeter = cblob()->perimeter(); if (horz_stroke_width() > 0) perimeter -= 2 * horz_stroke_width(); else perimeter -= 4 * cblob()->area() / perimeter; perimeter -= 2 * box.height(); if (perimeter > kComplexShapePerimeterRatio * box_perimeter) { set_vert_possible(true); set_horz_possible(false); return true; } } return false; } // Returns true if there is no tabstop violation in merging this and other. bool BLOBNBOX::ConfirmNoTabViolation(const BLOBNBOX& other) const { if (box.left() < other.box.left() && box.left() < other.left_rule_) return false; if (other.box.left() < box.left() && other.box.left() < left_rule_) return false; if (box.right() > other.box.right() && box.right() > other.right_rule_) return false; if (other.box.right() > box.right() && other.box.right() > right_rule_) return false; return true; } // Returns true if other has a similar stroke width to this. bool BLOBNBOX::MatchingStrokeWidth(const BLOBNBOX& other, double fractional_tolerance, double constant_tolerance) const { // The perimeter-based width is used as a backup in case there is // no information in the blob. double p_width = area_stroke_width(); double n_p_width = other.area_stroke_width(); float h_tolerance = horz_stroke_width_ * fractional_tolerance + constant_tolerance; float v_tolerance = vert_stroke_width_ * fractional_tolerance + constant_tolerance; double p_tolerance = p_width * fractional_tolerance + constant_tolerance; bool h_zero = horz_stroke_width_ == 0.0f || other.horz_stroke_width_ == 0.0f; bool v_zero = vert_stroke_width_ == 0.0f || other.vert_stroke_width_ == 0.0f; bool h_ok = !h_zero && NearlyEqual(horz_stroke_width_, other.horz_stroke_width_, h_tolerance); bool v_ok = !v_zero && NearlyEqual(vert_stroke_width_, other.vert_stroke_width_, v_tolerance); bool p_ok = h_zero && v_zero && NearlyEqual(p_width, n_p_width, p_tolerance); // For a match, at least one of the horizontal and vertical widths // must match, and the other one must either match or be zero. // Only if both are zero will we look at the perimeter metric. return p_ok || ((v_ok || h_ok) && (h_ok || h_zero) && (v_ok || v_zero)); } // Returns a bounding box of the outline contained within the // given horizontal range. TBOX BLOBNBOX::BoundsWithinLimits(int left, int right) { FCOORD no_rotation(1.0f, 0.0f); float top, bottom; if (cblob_ptr != NULL) { find_cblob_limits(cblob_ptr, static_cast(left), static_cast(right), no_rotation, bottom, top); } if (top < bottom) { top = box.top(); bottom = box.bottom(); } FCOORD bot_left(left, bottom); FCOORD top_right(right, top); TBOX shrunken_box(bot_left); TBOX shrunken_box2(top_right); shrunken_box += shrunken_box2; return shrunken_box; } #ifndef GRAPHICS_DISABLED ScrollView::Color BLOBNBOX::TextlineColor(BlobRegionType region_type, BlobTextFlowType flow_type) { switch (region_type) { case BRT_HLINE: return ScrollView::BROWN; case BRT_VLINE: return ScrollView::DARK_GREEN; case BRT_RECTIMAGE: return ScrollView::RED; case BRT_POLYIMAGE: return ScrollView::ORANGE; case BRT_UNKNOWN: return flow_type == BTFT_NONTEXT ? ScrollView::CYAN : ScrollView::WHITE; case BRT_VERT_TEXT: if (flow_type == BTFT_STRONG_CHAIN || flow_type == BTFT_TEXT_ON_IMAGE) return ScrollView::GREEN; if (flow_type == BTFT_CHAIN) return ScrollView::LIME_GREEN; return ScrollView::YELLOW; case BRT_TEXT: if (flow_type == BTFT_STRONG_CHAIN) return ScrollView::BLUE; if (flow_type == BTFT_TEXT_ON_IMAGE) return ScrollView::LIGHT_BLUE; if (flow_type == BTFT_CHAIN) return ScrollView::MEDIUM_BLUE; if (flow_type == BTFT_LEADER) return ScrollView::WHEAT; return ScrollView::MAGENTA; default: return ScrollView::GREY; } } // Keep in sync with BlobRegionType. ScrollView::Color BLOBNBOX::BoxColor() const { return TextlineColor(region_type_, flow_); } #endif /********************************************************************** * find_cblob_limits * * Scan the outlines of the cblob to locate the y min and max * between the given x limits. **********************************************************************/ void find_cblob_limits( //get y limits C_BLOB *blob, //blob to search float leftx, //x limits float rightx, FCOORD rotation, //for landscape float &ymin, //output y limits float &ymax) { inT16 stepindex; //current point ICOORD pos; //current coords ICOORD vec; //rotated step C_OUTLINE *outline; //current outline //outlines C_OUTLINE_IT out_it = blob->out_list (); ymin = (float) MAX_INT32; ymax = (float) -MAX_INT32; for (out_it.mark_cycle_pt (); !out_it.cycled_list (); out_it.forward ()) { outline = out_it.data (); pos = outline->start_pos (); //get coords pos.rotate (rotation); for (stepindex = 0; stepindex < outline->pathlength (); stepindex++) { //inside if (pos.x () >= leftx && pos.x () <= rightx) { UpdateRange(pos.y(), &ymin, &ymax); } vec = outline->step (stepindex); vec.rotate (rotation); pos += vec; //move to next } } } /********************************************************************** * find_cblob_vlimits * * Scan the outlines of the cblob to locate the y min and max * between the given x limits. **********************************************************************/ void find_cblob_vlimits( //get y limits C_BLOB *blob, //blob to search float leftx, //x limits float rightx, float &ymin, //output y limits float &ymax) { inT16 stepindex; //current point ICOORD pos; //current coords ICOORD vec; //rotated step C_OUTLINE *outline; //current outline //outlines C_OUTLINE_IT out_it = blob->out_list (); ymin = (float) MAX_INT32; ymax = (float) -MAX_INT32; for (out_it.mark_cycle_pt (); !out_it.cycled_list (); out_it.forward ()) { outline = out_it.data (); pos = outline->start_pos (); //get coords for (stepindex = 0; stepindex < outline->pathlength (); stepindex++) { //inside if (pos.x () >= leftx && pos.x () <= rightx) { UpdateRange(pos.y(), &ymin, &ymax); } vec = outline->step (stepindex); pos += vec; //move to next } } } /********************************************************************** * find_cblob_hlimits * * Scan the outlines of the cblob to locate the x min and max * between the given y limits. **********************************************************************/ void find_cblob_hlimits( //get x limits C_BLOB *blob, //blob to search float bottomy, //y limits float topy, float &xmin, //output x limits float &xmax) { inT16 stepindex; //current point ICOORD pos; //current coords ICOORD vec; //rotated step C_OUTLINE *outline; //current outline //outlines C_OUTLINE_IT out_it = blob->out_list (); xmin = (float) MAX_INT32; xmax = (float) -MAX_INT32; for (out_it.mark_cycle_pt (); !out_it.cycled_list (); out_it.forward ()) { outline = out_it.data (); pos = outline->start_pos (); //get coords for (stepindex = 0; stepindex < outline->pathlength (); stepindex++) { //inside if (pos.y () >= bottomy && pos.y () <= topy) { UpdateRange(pos.x(), &xmin, &xmax); } vec = outline->step (stepindex); pos += vec; //move to next } } } /********************************************************************** * crotate_cblob * * Rotate the copy by the given vector and return a C_BLOB. **********************************************************************/ C_BLOB *crotate_cblob( //rotate it C_BLOB *blob, //blob to search FCOORD rotation //for landscape ) { C_OUTLINE_LIST out_list; //output outlines //input outlines C_OUTLINE_IT in_it = blob->out_list (); //output outlines C_OUTLINE_IT out_it = &out_list; for (in_it.mark_cycle_pt (); !in_it.cycled_list (); in_it.forward ()) { out_it.add_after_then_move (new C_OUTLINE (in_it.data (), rotation)); } return new C_BLOB (&out_list); } /********************************************************************** * box_next * * Compute the bounding box of this blob with merging of x overlaps * but no pre-chopping. * Then move the iterator on to the start of the next blob. **********************************************************************/ TBOX box_next( //get bounding box BLOBNBOX_IT *it //iterator to blobds ) { BLOBNBOX *blob; //current blob TBOX result; //total box blob = it->data (); result = blob->bounding_box (); do { it->forward (); blob = it->data (); if (blob->cblob() == NULL) //was pre-chopped result += blob->bounding_box (); } //until next real blob while ((blob->cblob() == NULL) || blob->joined_to_prev()); return result; } /********************************************************************** * box_next_pre_chopped * * Compute the bounding box of this blob with merging of x overlaps * but WITH pre-chopping. * Then move the iterator on to the start of the next pre-chopped blob. **********************************************************************/ TBOX box_next_pre_chopped( //get bounding box BLOBNBOX_IT *it //iterator to blobds ) { BLOBNBOX *blob; //current blob TBOX result; //total box blob = it->data (); result = blob->bounding_box (); do { it->forward (); blob = it->data (); } //until next real blob while (blob->joined_to_prev ()); return result; } /********************************************************************** * TO_ROW::TO_ROW * * Constructor to make a row from a blob. **********************************************************************/ TO_ROW::TO_ROW ( //constructor BLOBNBOX * blob, //first blob float top, //corrected top float bottom, //of row float row_size //ideal ) { clear(); y_min = bottom; y_max = top; initial_y_min = bottom; float diff; //in size BLOBNBOX_IT it = &blobs; //list of blobs it.add_to_end (blob); diff = top - bottom - row_size; if (diff > 0) { y_max -= diff / 2; y_min += diff / 2; } //very small object else if ((top - bottom) * 3 < row_size) { diff = row_size / 3 + bottom - top; y_max += diff / 2; y_min -= diff / 2; } } /********************************************************************** * TO_ROW:add_blob * * Add the blob to the end of the row. **********************************************************************/ void TO_ROW::add_blob( //constructor BLOBNBOX *blob, //first blob float top, //corrected top float bottom, //of row float row_size //ideal ) { float allowed; //allowed expansion float available; //expansion BLOBNBOX_IT it = &blobs; //list of blobs it.add_to_end (blob); allowed = row_size + y_min - y_max; if (allowed > 0) { available = top > y_max ? top - y_max : 0; if (bottom < y_min) //total available available += y_min - bottom; if (available > 0) { available += available; //do it gradually if (available < allowed) available = allowed; if (bottom < y_min) y_min -= (y_min - bottom) * allowed / available; if (top > y_max) y_max += (top - y_max) * allowed / available; } } } /********************************************************************** * TO_ROW:insert_blob * * Add the blob to the row in the correct position. **********************************************************************/ void TO_ROW::insert_blob( //constructor BLOBNBOX *blob //first blob ) { BLOBNBOX_IT it = &blobs; //list of blobs if (it.empty ()) it.add_before_then_move (blob); else { it.mark_cycle_pt (); while (!it.cycled_list () && it.data ()->bounding_box ().left () <= blob->bounding_box ().left ()) it.forward (); if (it.cycled_list ()) it.add_to_end (blob); else it.add_before_stay_put (blob); } } /********************************************************************** * TO_ROW::compute_vertical_projection * * Compute the vertical projection of a TO_ROW from its blobs. **********************************************************************/ void TO_ROW::compute_vertical_projection() { //project whole row TBOX row_box; //bound of row BLOBNBOX *blob; //current blob TBOX blob_box; //bounding box BLOBNBOX_IT blob_it = blob_list (); if (blob_it.empty ()) return; row_box = blob_it.data ()->bounding_box (); for (blob_it.mark_cycle_pt (); !blob_it.cycled_list (); blob_it.forward ()) row_box += blob_it.data ()->bounding_box (); projection.set_range (row_box.left () - PROJECTION_MARGIN, row_box.right () + PROJECTION_MARGIN); projection_left = row_box.left () - PROJECTION_MARGIN; projection_right = row_box.right () + PROJECTION_MARGIN; for (blob_it.mark_cycle_pt (); !blob_it.cycled_list (); blob_it.forward ()) { blob = blob_it.data(); if (blob->cblob() != NULL) vertical_cblob_projection(blob->cblob(), &projection); } } /********************************************************************** * TO_ROW::clear * * Zero out all scalar members. **********************************************************************/ void TO_ROW::clear() { all_caps = 0; used_dm_model = 0; projection_left = 0; projection_right = 0; pitch_decision = PITCH_DUNNO; fixed_pitch = 0.0; fp_space = 0.0; fp_nonsp = 0.0; pr_space = 0.0; pr_nonsp = 0.0; spacing = 0.0; xheight = 0.0; xheight_evidence = 0; ascrise = 0.0; descdrop = 0.0; min_space = 0; max_nonspace = 0; space_threshold = 0; kern_size = 0.0; space_size = 0.0; y_min = 0.0; y_max = 0.0; initial_y_min = 0.0; m = 0.0; c = 0.0; error = 0.0; para_c = 0.0; para_error = 0.0; y_origin = 0.0; credibility = 0.0; num_repeated_sets_ = -1; } /********************************************************************** * vertical_cblob_projection * * Compute the vertical projection of a cblob from its outlines * and add to the given STATS. **********************************************************************/ void vertical_cblob_projection( //project outlines C_BLOB *blob, //blob to project STATS *stats //output ) { //outlines of blob C_OUTLINE_IT out_it = blob->out_list (); for (out_it.mark_cycle_pt (); !out_it.cycled_list (); out_it.forward ()) { vertical_coutline_projection (out_it.data (), stats); } } /********************************************************************** * vertical_coutline_projection * * Compute the vertical projection of a outline from its outlines * and add to the given STATS. **********************************************************************/ void vertical_coutline_projection( //project outlines C_OUTLINE *outline, //outline to project STATS *stats //output ) { ICOORD pos; //current point ICOORD step; //edge step inT32 length; //of outline inT16 stepindex; //current step C_OUTLINE_IT out_it = outline->child (); pos = outline->start_pos (); length = outline->pathlength (); for (stepindex = 0; stepindex < length; stepindex++) { step = outline->step (stepindex); if (step.x () > 0) { stats->add (pos.x (), -pos.y ()); } else if (step.x () < 0) { stats->add (pos.x () - 1, pos.y ()); } pos += step; } for (out_it.mark_cycle_pt (); !out_it.cycled_list (); out_it.forward ()) { vertical_coutline_projection (out_it.data (), stats); } } /********************************************************************** * TO_BLOCK::TO_BLOCK * * Constructor to make a TO_BLOCK from a real block. **********************************************************************/ TO_BLOCK::TO_BLOCK( //make a block BLOCK *src_block //real block ) { clear(); block = src_block; } static void clear_blobnboxes(BLOBNBOX_LIST* boxes) { BLOBNBOX_IT it = boxes; // A BLOBNBOX generally doesn't own its blobs, so if they do, you // have to delete them explicitly. for (it.mark_cycle_pt(); !it.cycled_list(); it.forward()) { BLOBNBOX* box = it.data(); if (box->cblob() != NULL) delete box->cblob(); } } /********************************************************************** * TO_BLOCK::clear * * Zero out all scalar members. **********************************************************************/ void TO_BLOCK::clear() { block = NULL; pitch_decision = PITCH_DUNNO; line_spacing = 0.0; line_size = 0.0; max_blob_size = 0.0; baseline_offset = 0.0; xheight = 0.0; fixed_pitch = 0.0; kern_size = 0.0; space_size = 0.0; min_space = 0; max_nonspace = 0; fp_space = 0.0; fp_nonsp = 0.0; pr_space = 0.0; pr_nonsp = 0.0; key_row = NULL; } TO_BLOCK::~TO_BLOCK() { // Any residual BLOBNBOXes at this stage own their blobs, so delete them. clear_blobnboxes(&blobs); clear_blobnboxes(&underlines); clear_blobnboxes(&noise_blobs); clear_blobnboxes(&small_blobs); clear_blobnboxes(&large_blobs); } #ifndef GRAPHICS_DISABLED // Draw the blobs on the various lists in the block in different colors. void TO_BLOCK::plot_graded_blobs(ScrollView* to_win) { plot_blob_list(to_win, &noise_blobs, ScrollView::CORAL, ScrollView::BLUE); plot_blob_list(to_win, &small_blobs, ScrollView::GOLDENROD, ScrollView::YELLOW); plot_blob_list(to_win, &large_blobs, ScrollView::DARK_GREEN, ScrollView::YELLOW); plot_blob_list(to_win, &blobs, ScrollView::WHITE, ScrollView::BROWN); } /********************************************************************** * plot_blob_list * * Draw a list of blobs. **********************************************************************/ void plot_blob_list(ScrollView* win, // window to draw in BLOBNBOX_LIST *list, // blob list ScrollView::Color body_colour, // colour to draw ScrollView::Color child_colour) { // colour of child BLOBNBOX_IT it = list; for (it.mark_cycle_pt(); !it.cycled_list(); it.forward()) { it.data()->plot(win, body_colour, child_colour); } } #endif // GRAPHICS_DISABLED