From 0564ac341f92d0d892d046a9096ac50fb0a9a27d Mon Sep 17 00:00:00 2001 From: David Mathog Date: Sat, 18 Jan 2014 00:24:11 +0100 Subject: libUEMF update; EMF linear gradient bug fixes (bug #1263242) Fixed bugs: - https://launchpad.net/bugs/1263242 (bzr r12953) --- src/extension/internal/emf-print.cpp | 417 ++++++++++++++++++++++++++++++----- 1 file changed, 356 insertions(+), 61 deletions(-) (limited to 'src/extension/internal/emf-print.cpp') diff --git a/src/extension/internal/emf-print.cpp b/src/extension/internal/emf-print.cpp index 0df643130..3c27ac1b8 100644 --- a/src/extension/internal/emf-print.cpp +++ b/src/extension/internal/emf-print.cpp @@ -70,9 +70,8 @@ namespace Internal { /* globals */ -static double PX2WORLD = 20.0f; -static U_XFORM worldTransform; -static bool FixPPTCharPos, FixPPTDashLine, FixPPTGrad2Polys, FixPPTPatternAsHatch, FixImageRot; +static double PX2WORLD; +static bool FixPPTCharPos, FixPPTDashLine, FixPPTGrad2Polys, FixPPTLinGrad, FixPPTPatternAsHatch, FixImageRot; static EMFTRACK *et = NULL; static EMFHANDLES *eht = NULL; @@ -125,9 +124,12 @@ unsigned int PrintEmf::begin(Inkscape::Extension::Print *mod, SPDocument *doc) char *rec; gchar const *utf8_fn = mod->get_param_string("destination"); + // Typially PX2WORLD is 1200/90, using inkscape's default dpi + PX2WORLD = 1200.0 / Inkscape::Util::Quantity::convert(1.0, "in", "px"); FixPPTCharPos = mod->get_param_bool("FixPPTCharPos"); FixPPTDashLine = mod->get_param_bool("FixPPTDashLine"); FixPPTGrad2Polys = mod->get_param_bool("FixPPTGrad2Polys"); + FixPPTLinGrad = mod->get_param_bool("FixPPTLinGrad"); FixPPTPatternAsHatch = mod->get_param_bool("FixPPTPatternAsHatch"); FixImageRot = mod->get_param_bool("FixImageRot"); @@ -179,12 +181,12 @@ unsigned int PrintEmf::begin(Inkscape::Extension::Print *mod, SPDocument *doc) float dwInchesX = d.width(); float dwInchesY = d.height(); - // dwInchesX x dwInchesY in micrometer units, dpi=90 -> 3543.3 dpm - (void) drawing_size((int) ceil(dwInchesX * 25.4), (int) ceil(dwInchesY * 25.4), 3.543307, &rclBounds, &rclFrame); + // dwInchesX x dwInchesY in micrometer units, 1200 dpi/25.4 -> dpmm + (void) drawing_size((int) ceil(dwInchesX * 25.4), (int) ceil(dwInchesY * 25.4),1200.0/25.4, &rclBounds, &rclFrame); // set up the reference device as 100 X A4 horizontal, (1200 dpi/25.4 -> dpmm). Extra digits maintain dpi better in EMF - int MMX = 21600; - int MMY = 27900; + int MMX = 216; + int MMY = 279; (void) device_size(MMX, MMY, 1200.0 / 25.4, &szlDev, &szlMm); int PixelsX = szlDev.cx; int PixelsY = szlDev.cy; @@ -220,13 +222,16 @@ unsigned int PrintEmf::begin(Inkscape::Extension::Print *mod, SPDocument *doc) } - // Correct for dpi in EMF (1200) vs dpi in Inkscape (always 90). - // Also correct for the scaling in PX2WORLD, which is set to 20. + // In earlier versions this was used to scale from inkscape's dpi of 90 to + // the files 1200 dpi, taking into account PX2WORLD which was 20. Now PX2WORLD + // is set so that this matrix is unitary. The usual value of PX2WORLD is 1200/90, + // but might be different if the internal dpi is changed. - worldTransform.eM11 = 1200. / (90.0 * PX2WORLD); + U_XFORM worldTransform; + worldTransform.eM11 = 1.0; worldTransform.eM12 = 0.0; worldTransform.eM21 = 0.0; - worldTransform.eM22 = 1200. / (90.0 * PX2WORLD); + worldTransform.eM22 = 1.0; worldTransform.eDx = 0; worldTransform.eDy = 0; @@ -420,7 +425,7 @@ int PrintEmf::create_brush(SPStyle const *style, PU_COLORREF fcolor) hatchColor = avg_stop_color(rg); } } else if (lg) { - if (FixPPTGrad2Polys) { + if (FixPPTGrad2Polys || FixPPTLinGrad) { return hold_gradient(lg, fill_mode); } else { hatchColor = avg_stop_color(lg); @@ -696,7 +701,7 @@ int PrintEmf::create_pen(SPStyle const *style, const Geom::Affine &transform) n_dash = style->stroke_dash.n_dash; dash = new uint32_t[n_dash]; for (i = 0; i < style->stroke_dash.n_dash; i++) { - dash[i] = (uint32_t)(style->stroke_dash.dash[i]); + dash[i] = (uint32_t)(Inkscape::Util::Quantity::convert(1, "mm", "px") * style->stroke_dash.dash[i]); } } } @@ -780,6 +785,182 @@ void PrintEmf::destroy_pen() } } +/* Return a Path consisting of just the corner points of the single path in a a PathVector. If the +PathVector has more than one path, or that one path is open, or any of its segments are curved, then the +returned PathVector is . If the input path is already just straight lines and vertices the output will be the +same as the sole path in the input. */ + +Geom::Path PrintEmf::pathv_to_simple_polygon(Geom::PathVector const &pathv, int *vertices) +{ + Geom::Point P1_trail; + Geom::Point P1; + Geom::Point P1_lead; + Geom::Point v1,v2; + Geom::Path output; + Geom::Path bad; + Geom::PathVector pv = pathv_to_linear_and_cubic_beziers(pathv); + Geom::PathVector::const_iterator pit = pv.begin(); + Geom::PathVector::const_iterator pit2 = pv.begin(); + ++pit2; + *vertices = 0; + if(pit->end_closed() != pit->end_default())return(bad); // path must be closed + if(pit2 != pv.end())return(bad); // there may only be one path + P1_trail = pit->finalPoint(); + Geom::Path::const_iterator cit = pit->begin(); + P1 = cit->initialPoint(); + for(;cit != pit->end_closed();++cit) { + if (!is_straight_curve(*cit)) { + *vertices = 0; + return(bad); + } + P1_lead = cit->finalPoint(); + if(Geom::are_near(P1_lead,P1))continue; // duplicate points at the same coordinate + v1 = unit_vector(P1 - P1_trail); + v2 = unit_vector(P1_lead - P1 ); + if(Geom::are_near(dot(v1,v2),1.0)){ // P1 is within a straight line + P1 = P1_lead; + continue; + } + // P1 is the center point of a turn of some angle + if(!*vertices){ + output.start( P1 ); + output.close( pit->closed() ); + } + *vertices += 1; + Geom::LineSegment ls(P1_trail, P1); + output.append(ls); + P1_trail = P1; + P1 = P1_lead; + } + return(output); +} + +/* Returns the simplified PathVector (no matter what). + Sets is_rect if it is a rectangle. + Sets angle that will rotate side closest to horizontal onto horizontal. +*/ +Geom::Path PrintEmf::pathv_to_rect(Geom::PathVector const &pathv, bool *is_rect, double *angle) +{ + Geom::Point P1_trail; + Geom::Point P1; + Geom::Point P1_lead; + Geom::Point v1,v2; + int vertices; + Geom::Path pR = pathv_to_simple_polygon(pathv, &vertices); + *is_rect = false; + if(vertices==4){ // or else it cannot be a rectangle rectangle + int vertex_count=0; + /* Get the ends of the LAST line segment. + Find minimum rotation to align rectangle with X,Y axes. (Very degenerate if it is rotated 45 degrees.) */ + *angle = 10.0; /* must be > than the actual angle in radians. */ + for(Geom::Path::const_iterator cit = pR.begin(); cit != pR.end_open(); ++cit){ + P1_trail = cit->initialPoint(); + P1 = cit->finalPoint(); + v1 = unit_vector(P1 - P1_trail); + if(v1[Geom::X] > 0){ // only check the 1 or 2 points on vectors aimed the same direction as unit X + double ang = asin(v1[Geom::Y]); // because component is rotation by ang of {1,0| vector + if(fabs(ang) < fabs(*angle))*angle = -ang; // y increases down, flips sign on angle + } + } + + for(Geom::Path::const_iterator cit = pR.begin(); cit != pR.end_open();++cit) { + P1_lead = cit->finalPoint(); + v1 = unit_vector(P1 - P1_trail); + v2 = unit_vector(P1_lead - P1 ); + if(!Geom::are_near(dot(v1,v2),0.0))break; // P1 is center of a turn that is not 90 degrees + P1_trail = P1; + P1 = P1_lead; + vertex_count++; + } + if(vertex_count == 4){ + *is_rect=true; + } + } + return(pR); +} + +/* Compare a vector with a rectangle's orientation (angle needed to rotate side(s) + closest to horizontal to exactly horizontal) and return: + 0 none of the following + 1 parallel to horizontal + 2 parallel to vertical + 3 antiparallel to horizontal + 4 antiparallel to vertical +*/ +int PrintEmf::vector_rect_alignment(double angle, Geom::Point vtest){ + int stat = 0; + Geom::Point v1 = Geom::unit_vector(vtest); // unit vector to test alignment + Geom::Point v2 = Geom::Point(1,0) * Geom::Rotate(-angle); // unit horizontal side (sign change because Y increases DOWN) + if( Geom::are_near(dot(v1,v2), 1.0)){ stat = 1; } + else if(Geom::are_near(dot(v1,v2),-1.0)){ stat = 2; } + if(!stat){ + v2 = Geom::Point(0,1) * Geom::Rotate(-angle); // unit vertical side + if( Geom::are_near(dot(v1,v2), 1.0)){ stat = 3; } + else if(Geom::are_near(dot(v1,v2),-1.0)){ stat = 4; } + } + return(stat); +} + +/* retrieve the point at the indicated corner: + 0 UL (and default) + 1 UR + 2 LR + 3 LL + Needed because the start can be any point, and the direction could run either + clockwise or counterclockwise. This should work even if the corners of the rectangle + are slightly displaced. +*/ +Geom::Point PrintEmf::get_pathrect_corner(Geom::Path pathRect, double angle, int corner){ + Geom::Point v1 = Geom::Point(1,0) * Geom::Rotate(-angle); // unit horizontal side (sign change because Y increases DOWN) + Geom::Point v2 = Geom::Point(0,1) * Geom::Rotate(-angle); // unit vertical side (sign change because Y increases DOWN) + Geom::Point center, P1; + int LR; // 1 if Left, 0 if Right + int UL; // 1 if Lower, 0 if Upper (as viewed on screen, y coordinates increase downwards) + center = Geom::Point(0,0); + Geom::Path::const_iterator cit = pathRect.begin(); + for(; cit != pathRect.end_open();++cit) { + center += cit->initialPoint()/4.0; + } + + switch(corner){ + case 1: //UR + LR = 0; + UL = 0; + break; + case 2: //LR + LR = 0; + UL = 1; + break; + case 3: //LL + LR = 1; + UL = 1; + break; + default: //UL + LR = 1; + UL = 0; + break; + } + cit = pathRect.begin(); + for(int i; cit != pathRect.end_open();++cit,i++) { + P1 = cit->initialPoint(); + if((LR == (dot(P1 - center,v1)> 0 ? 0 : 1)) && + (UL == (dot(P1 - center,v2)> 0 ? 1 : 0)))break; + } + return(P1); +} + +U_TRIVERTEX PrintEmf::make_trivertex(Geom::Point Pt, U_COLORREF uc){ + U_TRIVERTEX tv; + using Geom::X; + using Geom::Y; + tv.x = (int32_t) round(Pt[X]); + tv.y = (int32_t) round(Pt[Y]); + tv.Red = uc.Red << 8; + tv.Green = uc.Green << 8; + tv.Blue = uc.Blue << 8; + tv.Alpha = uc.Reserved << 8; // EMF will ignore this + return(tv); +} unsigned int PrintEmf::fill( Inkscape::Extension::Print * /*mod*/, @@ -795,8 +976,27 @@ unsigned int PrintEmf::fill( use_stroke = false; fill_transform = tf; + + int brush_stat = create_brush(style, NULL); + + /* native linear gradients are only used if the object is a rectangle AND the gradient is parallel to the sides of the object */ + bool is_Rect = false; + double angle; + int rectDir=0; + Geom::Path pathRect; + if(FixPPTLinGrad && brush_stat && gv.mode == DRAW_LINEAR_GRADIENT){ + pathRect = pathv_to_rect(pathv, &is_Rect, &angle); + if(is_Rect){ + /* Gradientfill records can only be used if the gradient is parallel to the sides of the rectangle. + That must be checked here so that we can fall back to another form of gradient fill if it is not + the case. */ + rectDir = vector_rect_alignment(angle, gv.p2 - gv.p1); + if(!rectDir)is_Rect = false; + } + if(!is_Rect && !FixPPTGrad2Polys)brush_stat=0; // fall all the way back to a solid fill + } - if (create_brush(style, NULL)) { // only happens if the style is a gradient + if (brush_stat) { // only happens if the style is a gradient /* Handle gradients. Uses modified livarot as 2geom boolops is currently broken. Can handle gradients with multiple stops. @@ -819,10 +1019,10 @@ unsigned int PrintEmf::fill( SPRadialGradient *tg = (SPRadialGradient *)(gv.grad); // linear/radial are the same here nstops = tg->vector.stops.size(); sp_color_get_rgb_floatv(&tg->vector.stops[0].color, rgb); - opa = tg->vector.stops[0].opacity; + opa = tg->vector.stops[0].opacity; // first stop c1 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); sp_color_get_rgb_floatv(&tg->vector.stops[nstops - 1].color, rgb); - opa = tg->vector.stops[nstops - 1].opacity; + opa = tg->vector.stops[nstops - 1].opacity; // last stop c2 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); doff = 0.0; @@ -868,9 +1068,10 @@ unsigned int PrintEmf::fill( pathvr = sp_pathvector_boolop(pathvc, pathv, bool_op_inters, (FillRule) fill_nonZero, frb); print_pathv(pathvr, fill_transform); // show the intersection - if (doff >= doff_range - doff_base) { + if (doff >= doff_range) { istop++; if (istop >= nstops) { + istop = nstops - 1; continue; // could happen on a rounding error } doff_base = doff_range; @@ -882,56 +1083,145 @@ unsigned int PrintEmf::fill( } } } else if (gv.mode == DRAW_LINEAR_GRADIENT) { - Geom::Point uv = Geom::unit_vector(gv.p2 - gv.p1); // unit vector - Geom::Point puv = uv.cw(); // perp. to unit vector - double range = Geom::distance(gv.p1, gv.p2); // length along the gradient - double step = range / divisions; // adequate approximation for gradient - double overlap = step / 4.0; // overlap slices slightly - double start; - double stop; - Geom::PathVector pathvc, pathvr; - - /* before lower end of gradient, overlap first slice position */ - wc = weight_opacity(c1); - (void) create_brush(style, &wc); - pathvc = rect_cutter(gv.p1, uv * (overlap), uv * (-50000.0), puv * 50000.0); - pathvr = sp_pathvector_boolop(pathvc, pathv, bool_op_inters, (FillRule) fill_nonZero, frb); - print_pathv(pathvr, fill_transform); - - /* after high end of gradient, overlap last slice poosition */ - wc = weight_opacity(c2); - (void) create_brush(style, &wc); - pathvc = rect_cutter(gv.p2, uv * (-overlap), uv * (50000.0), puv * 50000.0); - pathvr = sp_pathvector_boolop(pathvc, pathv, bool_op_inters, (FillRule) fill_nonZero, frb); - print_pathv(pathvr, fill_transform); + if(is_Rect){ + char *rec; + int gMode; + Geom::Point ul, ur, lr; + Geom::Point outUL, outLR; // UL,LR corners of a stop rectangle, in OUTPUT coordinates + U_TRIVERTEX ut[2]; + U_GRADIENT4 ug4; + U_RECTL rcb; + Geom::Affine tf2; + U_XFORM tmpTransform; + double wRect, hRect; + + /* coordinates, w,h and transform for the ENTIRE retangle */ + ul = get_pathrect_corner(pathRect, angle, 0) * fill_transform * PX2WORLD; + ur = get_pathrect_corner(pathRect, angle, 1) * fill_transform * PX2WORLD; + lr = get_pathrect_corner(pathRect, angle, 2) * fill_transform * PX2WORLD; + wRect = Geom::distance(ul,ur); + hRect = Geom::distance(ur,lr); + + /* The basic rectangle for all of these is placed with its UL corner at 0,0 with a size wRect,hRect. + Apply a world transform to place/scale it into the appropriate position on the drawing. + Actual gradientfill records are either this entire rectangle or slices of it as defined by the stops. + */ + + tf2 = Geom::Rotate(-angle); + tmpTransform.eM11 = tf2[0]; + tmpTransform.eM12 = tf2[1]; + tmpTransform.eM21 = tf2[2]; + tmpTransform.eM22 = tf2[3]; + tmpTransform.eDx = (ul)[Geom::X]; + tmpTransform.eDy = (ul)[Geom::Y]; + + rec = U_EMRSAVEDC_set(); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::image at U_EMRSAVEDC_set"); + } - sp_color_get_rgb_floatv(&tg->vector.stops[istop].color, rgb); - opa = tg->vector.stops[istop].opacity; - c2 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); + rec = U_EMRMODIFYWORLDTRANSFORM_set(tmpTransform, U_MWT_LEFTMULTIPLY); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::image at EMRMODIFYWORLDTRANSFORM"); + } + + for(;istopvector.stops[istop].offset; // next or last stop + if(rectDir == 1 || rectDir == 2){ + outUL = Geom::Point(doff_base *wRect, 0 ); + outLR = Geom::Point(doff_range*wRect, hRect); + gMode = U_GRADIENT_FILL_RECT_H; + } + else { + outUL = Geom::Point(0, doff_base *hRect); + outLR = Geom::Point(wRect,doff_range*hRect); + gMode = U_GRADIENT_FILL_RECT_V; + } + doff_base = doff_range; + rcb.left = outUL[X]; + rcb.top = outUL[Y]; + rcb.right = outLR[X]; + rcb.bottom = outLR[Y]; + sp_color_get_rgb_floatv(&tg->vector.stops[istop].color, rgb); + opa = tg->vector.stops[istop].opacity; + c2 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); + + if(rectDir == 2 || rectDir == 4){ // gradient is reversed, so swap colors + ut[0] = make_trivertex(outUL, c2); + ut[1] = make_trivertex(outLR, c1); + } + else { + ut[0] = make_trivertex(outUL, c1); + ut[1] = make_trivertex(outLR, c2); + } + c1 = c2; // for next stop + ug4.UpperLeft = 0; + ug4.LowerRight= 1; + /* NEED to push world transform here for rotations */ + rec = U_EMRGRADIENTFILL_set(rcb, 2, 1, gMode, ut, (uint32_t *) &ug4 ); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::fill at U_EMRGRADIENTFILL_set"); + } + } - for (start = 0.0; start < range; start += step, doff += 1. / divisions) { - stop = start + step + overlap; - if (stop > range) { - stop = range; + rec = U_EMRRESTOREDC_set(-1); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::fill at U_EMRRESTOREDC_set"); } - pathvc = rect_cutter(gv.p1, uv * start, uv * stop, puv * 50000.0); + } + else { + Geom::Point uv = Geom::unit_vector(gv.p2 - gv.p1); // unit vector + Geom::Point puv = uv.cw(); // perp. to unit vector + double range = Geom::distance(gv.p1, gv.p2); // length along the gradient + double step = range / divisions; // adequate approximation for gradient + double overlap = step / 4.0; // overlap slices slightly + double start; + double stop; + Geom::PathVector pathvc, pathvr; + + /* before lower end of gradient, overlap first slice position */ + wc = weight_opacity(c1); + (void) create_brush(style, &wc); + pathvc = rect_cutter(gv.p1, uv * (overlap), uv * (-50000.0), puv * 50000.0); + pathvr = sp_pathvector_boolop(pathvc, pathv, bool_op_inters, (FillRule) fill_nonZero, frb); + print_pathv(pathvr, fill_transform); - wc = weight_colors(c1, c2, (doff - doff_base) / (doff_range - doff_base)); + /* after high end of gradient, overlap last slice position */ + wc = weight_opacity(c2); (void) create_brush(style, &wc); - Geom::PathVector pathvr = sp_pathvector_boolop(pathvc, pathv, bool_op_inters, (FillRule) fill_nonZero, frb); - print_pathv(pathvr, fill_transform); // show the intersection + pathvc = rect_cutter(gv.p2, uv * (-overlap), uv * (50000.0), puv * 50000.0); + pathvr = sp_pathvector_boolop(pathvc, pathv, bool_op_inters, (FillRule) fill_nonZero, frb); + print_pathv(pathvr, fill_transform); - if (doff >= doff_range - doff_base) { - istop++; - if (istop >= nstops) { - continue; // could happen on a rounding error + sp_color_get_rgb_floatv(&tg->vector.stops[istop].color, rgb); + opa = tg->vector.stops[istop].opacity; + c2 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); + + for (start = 0.0; start < range; start += step, doff += 1. / divisions) { + stop = start + step + overlap; + if (stop > range) { + stop = range; + } + pathvc = rect_cutter(gv.p1, uv * start, uv * stop, puv * 50000.0); + + wc = weight_colors(c1, c2, (doff - doff_base) / (doff_range - doff_base)); + (void) create_brush(style, &wc); + Geom::PathVector pathvr = sp_pathvector_boolop(pathvc, pathv, bool_op_inters, (FillRule) fill_nonZero, frb); + print_pathv(pathvr, fill_transform); // show the intersection + + if (doff >= doff_range) { + istop++; + if (istop >= nstops) { + istop = nstops - 1; + continue; // could happen on a rounding error + } + doff_base = doff_range; + doff_range = tg->vector.stops[istop].offset; // next or last stop + c1 = c2; + sp_color_get_rgb_floatv(&tg->vector.stops[istop].color, rgb); + opa = tg->vector.stops[istop].opacity; + c2 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); } - doff_base = doff_range; - doff_range = tg->vector.stops[istop].offset; // next or last stop - c1 = c2; - sp_color_get_rgb_floatv(&tg->vector.stops[istop].color, rgb); - opa = tg->vector.stops[istop].opacity; - c2 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); } } } else { @@ -1301,6 +1591,10 @@ unsigned int PrintEmf::image( U_POINTL cDest = pointl_set(round(dw * PX2WORLD), round(dh * PX2WORLD)); U_POINTL Src = pointl_set(0, 0); U_POINTL cSrc = pointl_set(w, h); + /* map the integer Dest coordinates back into pLL2, so that the rounded part does not destabilize the transform offset below */ + pLL2[Geom::X] = Dest.x; + pLL2[Geom::Y] = Dest.y; + pLL2 /= PX2WORLD; if (!FixImageRot) { /* Rotate images - some programs cannot read them in correctly if they are rotated */ tf[4] = tf[5] = 0.0; // get rid of the offset in the transform Geom::Point pLL2prime = pLL2 * tf; @@ -1309,7 +1603,7 @@ unsigned int PrintEmf::image( tmpTransform.eM12 = tf[1]; tmpTransform.eM21 = tf[2]; tmpTransform.eM22 = tf[3]; - tmpTransform.eDx = (pLL2[Geom::X] - pLL2prime[Geom::X]) * PX2WORLD; //map pLL2 (now in EMF coordinates) back onto itself after the rotation + tmpTransform.eDx = (pLL2[Geom::X] - pLL2prime[Geom::X]) * PX2WORLD; tmpTransform.eDy = (pLL2[Geom::Y] - pLL2prime[Geom::Y]) * PX2WORLD; rec = U_EMRSAVEDC_set(); @@ -1734,6 +2028,7 @@ void PrintEmf::init(void) "false\n" "false\n" "false\n" + "false\n" "false\n" "false\n" "\n" -- cgit v1.2.3 From 61737a54a16f2434f26b1ccfa5e1e082464d65c0 Mon Sep 17 00:00:00 2001 From: David Mathog Date: Wed, 22 Jan 2014 00:44:34 +0100 Subject: EMF export: fix handling of transformed rectangular gradients (bug #1263242) Fixed bugs: - https://launchpad.net/bugs/1263242 (bzr r12968) --- src/extension/internal/emf-print.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) (limited to 'src/extension/internal/emf-print.cpp') diff --git a/src/extension/internal/emf-print.cpp b/src/extension/internal/emf-print.cpp index 3c27ac1b8..2680718cf 100644 --- a/src/extension/internal/emf-print.cpp +++ b/src/extension/internal/emf-print.cpp @@ -814,10 +814,10 @@ Geom::Path PrintEmf::pathv_to_simple_polygon(Geom::PathVector const &pathv, int return(bad); } P1_lead = cit->finalPoint(); - if(Geom::are_near(P1_lead,P1))continue; // duplicate points at the same coordinate + if(Geom::are_near(P1_lead, P1, 1e-5))continue; // duplicate points at the same coordinate v1 = unit_vector(P1 - P1_trail); v2 = unit_vector(P1_lead - P1 ); - if(Geom::are_near(dot(v1,v2),1.0)){ // P1 is within a straight line + if(Geom::are_near(dot(v1,v2), 1.0, 1e-5)){ // P1 is within a straight line P1 = P1_lead; continue; } @@ -867,7 +867,7 @@ Geom::Path PrintEmf::pathv_to_rect(Geom::PathVector const &pathv, bool *is_rect, P1_lead = cit->finalPoint(); v1 = unit_vector(P1 - P1_trail); v2 = unit_vector(P1_lead - P1 ); - if(!Geom::are_near(dot(v1,v2),0.0))break; // P1 is center of a turn that is not 90 degrees + if(!Geom::are_near(dot(v1,v2), 0.0, 1e-5))break; // P1 is center of a turn that is not 90 degrees P1_trail = P1; P1 = P1_lead; vertex_count++; @@ -891,12 +891,12 @@ int PrintEmf::vector_rect_alignment(double angle, Geom::Point vtest){ int stat = 0; Geom::Point v1 = Geom::unit_vector(vtest); // unit vector to test alignment Geom::Point v2 = Geom::Point(1,0) * Geom::Rotate(-angle); // unit horizontal side (sign change because Y increases DOWN) - if( Geom::are_near(dot(v1,v2), 1.0)){ stat = 1; } - else if(Geom::are_near(dot(v1,v2),-1.0)){ stat = 2; } + if( Geom::are_near(dot(v1,v2), 1.0, 1e-5)){ stat = 1; } + else if(Geom::are_near(dot(v1,v2),-1.0, 1e-5)){ stat = 2; } if(!stat){ v2 = Geom::Point(0,1) * Geom::Rotate(-angle); // unit vertical side - if( Geom::are_near(dot(v1,v2), 1.0)){ stat = 3; } - else if(Geom::are_near(dot(v1,v2),-1.0)){ stat = 4; } + if( Geom::are_near(dot(v1,v2), 1.0, 1e-5)){ stat = 3; } + else if(Geom::are_near(dot(v1,v2),-1.0, 1e-5)){ stat = 4; } } return(stat); } @@ -1091,7 +1091,6 @@ unsigned int PrintEmf::fill( U_TRIVERTEX ut[2]; U_GRADIENT4 ug4; U_RECTL rcb; - Geom::Affine tf2; U_XFORM tmpTransform; double wRect, hRect; @@ -1107,7 +1106,8 @@ unsigned int PrintEmf::fill( Actual gradientfill records are either this entire rectangle or slices of it as defined by the stops. */ - tf2 = Geom::Rotate(-angle); + Geom::Affine tf2 = Geom::Rotate(-angle); // the rectangle may be drawn skewed to the coordinate system + tf2 *= tf; // the coordinate system of the rectangular path may be rotated tmpTransform.eM11 = tf2[0]; tmpTransform.eM12 = tf2[1]; tmpTransform.eM21 = tf2[2]; -- cgit v1.2.3 From 042cc81dd7c3ca2f0d42b68356f4b450074c8473 Mon Sep 17 00:00:00 2001 From: David Mathog Date: Sat, 8 Feb 2014 08:17:30 +0100 Subject: EMF/WMF support: Various changes (see bug #1263242 c35 for details) EMF support: - linear gradients: improve numeric stability on round-trip editing - linear gradients: fix scaling issue libTERE/text_reassemble update: - improve handling of whitespace created with large x kerns - fix handling of absolute RTL-positioning of LTR text in EMF files WMF support: - fix support for 'textout' records and Placeable headers - add support for CREATEPATTERNBRUSH - fix offset on load if opened via GUI (bug #1250250) Fixed bugs: - https://launchpad.net/bugs/1263242 - https://launchpad.net/bugs/1250250 (bzr r13008) --- src/extension/internal/emf-print.cpp | 40 +++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 17 deletions(-) (limited to 'src/extension/internal/emf-print.cpp') diff --git a/src/extension/internal/emf-print.cpp b/src/extension/internal/emf-print.cpp index 2680718cf..6d5bb6f4b 100644 --- a/src/extension/internal/emf-print.cpp +++ b/src/extension/internal/emf-print.cpp @@ -124,7 +124,7 @@ unsigned int PrintEmf::begin(Inkscape::Extension::Print *mod, SPDocument *doc) char *rec; gchar const *utf8_fn = mod->get_param_string("destination"); - // Typially PX2WORLD is 1200/90, using inkscape's default dpi + // Typically PX2WORLD is 1200/90, using inkscape's default dpi PX2WORLD = 1200.0 / Inkscape::Util::Quantity::convert(1.0, "in", "px"); FixPPTCharPos = mod->get_param_bool("FixPPTCharPos"); FixPPTDashLine = mod->get_param_bool("FixPPTDashLine"); @@ -848,7 +848,7 @@ Geom::Path PrintEmf::pathv_to_rect(Geom::PathVector const &pathv, bool *is_rect, int vertices; Geom::Path pR = pathv_to_simple_polygon(pathv, &vertices); *is_rect = false; - if(vertices==4){ // or else it cannot be a rectangle rectangle + if(vertices==4){ // or else it cannot be a rectangle int vertex_count=0; /* Get the ends of the LAST line segment. Find minimum rotation to align rectangle with X,Y axes. (Very degenerate if it is rotated 45 degrees.) */ @@ -862,12 +862,17 @@ Geom::Path PrintEmf::pathv_to_rect(Geom::PathVector const &pathv, bool *is_rect, if(fabs(ang) < fabs(*angle))*angle = -ang; // y increases down, flips sign on angle } } + + /* For increased numerical stability, snap the angle to the nearest 1/100th of a degree. */ + double convert = 36000.0/ (2.0 * M_PI); + *angle = round(*angle * convert)/convert; for(Geom::Path::const_iterator cit = pR.begin(); cit != pR.end_open();++cit) { P1_lead = cit->finalPoint(); v1 = unit_vector(P1 - P1_trail); v2 = unit_vector(P1_lead - P1 ); - if(!Geom::are_near(dot(v1,v2), 0.0, 1e-5))break; // P1 is center of a turn that is not 90 degrees + // P1 is center of a turn that is not 90 degrees. Limit comes from cos(89.9) = .001745 + if(!Geom::are_near(dot(v1,v2), 0.0, 2e-3))break; P1_trail = P1; P1 = P1_lead; vertex_count++; @@ -985,12 +990,13 @@ unsigned int PrintEmf::fill( int rectDir=0; Geom::Path pathRect; if(FixPPTLinGrad && brush_stat && gv.mode == DRAW_LINEAR_GRADIENT){ - pathRect = pathv_to_rect(pathv, &is_Rect, &angle); + Geom::PathVector pvr = pathv * fill_transform; + pathRect = pathv_to_rect(pvr, &is_Rect, &angle); if(is_Rect){ /* Gradientfill records can only be used if the gradient is parallel to the sides of the rectangle. That must be checked here so that we can fall back to another form of gradient fill if it is not the case. */ - rectDir = vector_rect_alignment(angle, gv.p2 - gv.p1); + rectDir = vector_rect_alignment(angle, (gv.p2 - gv.p1) * fill_transform); if(!rectDir)is_Rect = false; } if(!is_Rect && !FixPPTGrad2Polys)brush_stat=0; // fall all the way back to a solid fill @@ -1094,26 +1100,27 @@ unsigned int PrintEmf::fill( U_XFORM tmpTransform; double wRect, hRect; - /* coordinates, w,h and transform for the ENTIRE retangle */ - ul = get_pathrect_corner(pathRect, angle, 0) * fill_transform * PX2WORLD; - ur = get_pathrect_corner(pathRect, angle, 1) * fill_transform * PX2WORLD; - lr = get_pathrect_corner(pathRect, angle, 2) * fill_transform * PX2WORLD; + /* coordinates: upper left, upper right, and lower right corners of the rectangle. + inkscape transform already applied, but needs to be scaled to EMF coordinates. */ + ul = get_pathrect_corner(pathRect, angle, 0) * PX2WORLD; + ur = get_pathrect_corner(pathRect, angle, 1) * PX2WORLD; + lr = get_pathrect_corner(pathRect, angle, 2) * PX2WORLD; wRect = Geom::distance(ul,ur); hRect = Geom::distance(ur,lr); /* The basic rectangle for all of these is placed with its UL corner at 0,0 with a size wRect,hRect. Apply a world transform to place/scale it into the appropriate position on the drawing. Actual gradientfill records are either this entire rectangle or slices of it as defined by the stops. + This rectangle has already been transformed by tf (whatever rotation/scale) Inkscape had applied to it. */ Geom::Affine tf2 = Geom::Rotate(-angle); // the rectangle may be drawn skewed to the coordinate system - tf2 *= tf; // the coordinate system of the rectangular path may be rotated tmpTransform.eM11 = tf2[0]; tmpTransform.eM12 = tf2[1]; tmpTransform.eM21 = tf2[2]; tmpTransform.eM22 = tf2[3]; - tmpTransform.eDx = (ul)[Geom::X]; - tmpTransform.eDy = (ul)[Geom::Y]; + tmpTransform.eDx = round((ul)[Geom::X]); // use explicit round for better stability + tmpTransform.eDy = round((ul)[Geom::Y]); rec = U_EMRSAVEDC_set(); if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { @@ -1138,10 +1145,10 @@ unsigned int PrintEmf::fill( gMode = U_GRADIENT_FILL_RECT_V; } doff_base = doff_range; - rcb.left = outUL[X]; - rcb.top = outUL[Y]; - rcb.right = outLR[X]; - rcb.bottom = outLR[Y]; + rcb.left = round(outUL[X]); // use explicit round for better stability + rcb.top = round(outUL[Y]); + rcb.right = round(outLR[X]); + rcb.bottom = round(outLR[Y]); sp_color_get_rgb_floatv(&tg->vector.stops[istop].color, rgb); opa = tg->vector.stops[istop].opacity; c2 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); @@ -1157,7 +1164,6 @@ unsigned int PrintEmf::fill( c1 = c2; // for next stop ug4.UpperLeft = 0; ug4.LowerRight= 1; - /* NEED to push world transform here for rotations */ rec = U_EMRGRADIENTFILL_set(rcb, 2, 1, gMode, ut, (uint32_t *) &ug4 ); if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { g_error("Fatal programming error in PrintEmf::fill at U_EMRGRADIENTFILL_set"); -- cgit v1.2.3 From 12e03b939f58c6b6dc5495e0559fe47d7a73bb48 Mon Sep 17 00:00:00 2001 From: David Mathog Date: Sat, 22 Feb 2014 10:03:23 +0100 Subject: Fix 'preserveAspectRatio' attribute of bitmap images in EMF/WMF import/export (bug #1278645). Fixed bugs: - https://launchpad.net/bugs/1278645 (bzr r13049) --- src/extension/internal/emf-print.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'src/extension/internal/emf-print.cpp') diff --git a/src/extension/internal/emf-print.cpp b/src/extension/internal/emf-print.cpp index 6d5bb6f4b..c103100b3 100644 --- a/src/extension/internal/emf-print.cpp +++ b/src/extension/internal/emf-print.cpp @@ -1562,7 +1562,7 @@ unsigned int PrintEmf::image( unsigned int w, /** width of bitmap */ unsigned int h, /** height of bitmap */ unsigned int rs, /** row stride (normally w*4) */ - Geom::Affine const &/*tf_ignore*/, /** WRONG affine transform, use the one from m_tr_stack */ + Geom::Affine const &tf_rect, /** affine transform only used for defining location and size of rect, for all other tranforms, use the one from m_tr_stack */ SPStyle const *style) /** provides indirect link to image object */ { double x1, y1, dw, dh; @@ -1574,10 +1574,10 @@ unsigned int PrintEmf::image( g_error("Fatal programming error in PrintEmf::image at EMRHEADER"); } - x1 = atof(style->object->getAttribute("x")); - y1 = atof(style->object->getAttribute("y")); - dw = atof(style->object->getAttribute("width")); - dh = atof(style->object->getAttribute("height")); + x1 = tf_rect[4]; + y1 = tf_rect[5]; + dw = ((double) w) * tf_rect[0]; + dh = ((double) h) * tf_rect[3]; Geom::Point pLL(x1, y1); Geom::Point pLL2 = pLL * tf; //location of LL corner in Inkscape coordinates -- cgit v1.2.3 From 1d31728fb7399f48d272560a290dc990b75a197e Mon Sep 17 00:00:00 2001 From: Tavmjong Bah Date: Tue, 11 Mar 2014 15:52:08 +0100 Subject: Change stroke-dasharray and stroke-dashoffset handling to match other properties. Split style.h into more manageable size files. (bzr r13135) --- src/extension/internal/emf-print.cpp | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) (limited to 'src/extension/internal/emf-print.cpp') diff --git a/src/extension/internal/emf-print.cpp b/src/extension/internal/emf-print.cpp index c103100b3..9cc662a27 100644 --- a/src/extension/internal/emf-print.cpp +++ b/src/extension/internal/emf-print.cpp @@ -683,25 +683,24 @@ int PrintEmf::create_pen(SPStyle const *style, const Geom::Affine &transform) linejoin = U_PS_JOIN_BEVEL; } - if (style->stroke_dash.n_dash && - style->stroke_dash.dash) { + if (!style->stroke_dasharray.values.empty()) { if (FixPPTDashLine) { // will break up line into many smaller lines. Override gradient if that was set, cannot do both. brushStyle = U_BS_SOLID; hatchType = U_HS_HORIZONTAL; } else { - int i = 0; - while ((linestyle != U_PS_USERSTYLE) && (i < style->stroke_dash.n_dash)) { - if (style->stroke_dash.dash[i] > 0.00000001) { + unsigned i = 0; + while ((linestyle != U_PS_USERSTYLE) && (i < style->stroke_dasharray.values.size())) { + if (style->stroke_dasharray.values[i] > 0.00000001) { linestyle = U_PS_USERSTYLE; } i++; } if (linestyle == U_PS_USERSTYLE) { - n_dash = style->stroke_dash.n_dash; + n_dash = style->stroke_dasharray.values.size(); dash = new uint32_t[n_dash]; - for (i = 0; i < style->stroke_dash.n_dash; i++) { - dash[i] = (uint32_t)(Inkscape::Util::Quantity::convert(1, "mm", "px") * style->stroke_dash.dash[i]); + for (i = 0; i < n_dash; i++) { + dash[i] = (uint32_t)(Inkscape::Util::Quantity::convert(1, "mm", "px") * style->stroke_dasharray.values[i]); } } } @@ -1259,7 +1258,7 @@ unsigned int PrintEmf::fill( } if ( (style->stroke.isNone() || style->stroke.noneSet || style->stroke_width.computed == 0.0) || - (style->stroke_dash.n_dash && style->stroke_dash.dash && FixPPTDashLine) || + (!style->stroke_dasharray.values.empty() && FixPPTDashLine) || !all_closed ) { print_pathv(pathv, fill_transform); // do any fills. side effect: clears fill_pathv @@ -1287,13 +1286,13 @@ unsigned int PrintEmf::stroke( return 0; } - if (style->stroke_dash.n_dash && style->stroke_dash.dash && FixPPTDashLine) { + if (!style->stroke_dasharray.values.empty() && FixPPTDashLine) { // convert the path, gets its complete length, and then make a new path with parameter length instead of t Geom::Piecewise > tmp_pathpw; // pathv-> sbasis Geom::Piecewise > tmp_pathpw2; // sbasis using arc length parameter Geom::Piecewise > tmp_pathpw3; // new (discontinuous) path, composed of dots/dashes Geom::Piecewise > first_frag; // first fragment, will be appended at end - int n_dash = style->stroke_dash.n_dash; + int n_dash = style->stroke_dasharray.values.size(); int i = 0; //dash index double tlength; // length of tmp_pathpw double slength = 0.0; // start of gragment @@ -1306,7 +1305,7 @@ unsigned int PrintEmf::stroke( // go around the dash array repeatedly until the entire path is consumed (but not beyond). while (slength < tlength) { - elength = slength + style->stroke_dash.dash[i++]; + elength = slength + style->stroke_dasharray.values[i++]; if (elength > tlength) { elength = tlength; } @@ -1317,7 +1316,7 @@ unsigned int PrintEmf::stroke( first_frag = fragment; } slength = elength; - slength += style->stroke_dash.dash[i++]; // the gap + slength += style->stroke_dasharray.values[i++]; // the gap if (i >= n_dash) { i = 0; } -- cgit v1.2.3 From b74a8597149d17529201fb8c9d9a92e29cc821ad Mon Sep 17 00:00:00 2001 From: "Jon A. Cruz" Date: Mon, 17 Mar 2014 18:52:59 -0700 Subject: Warning cleanups. (bzr r13162) --- src/extension/internal/emf-print.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/extension/internal/emf-print.cpp') diff --git a/src/extension/internal/emf-print.cpp b/src/extension/internal/emf-print.cpp index 9cc662a27..4bb892821 100644 --- a/src/extension/internal/emf-print.cpp +++ b/src/extension/internal/emf-print.cpp @@ -1562,7 +1562,7 @@ unsigned int PrintEmf::image( unsigned int h, /** height of bitmap */ unsigned int rs, /** row stride (normally w*4) */ Geom::Affine const &tf_rect, /** affine transform only used for defining location and size of rect, for all other tranforms, use the one from m_tr_stack */ - SPStyle const *style) /** provides indirect link to image object */ + SPStyle const * /*style*/) /** provides indirect link to image object */ { double x1, y1, dw, dh; char *rec = NULL; -- cgit v1.2.3 From de8450cc3161988edb0ffb713d94fe10bb5412e6 Mon Sep 17 00:00:00 2001 From: "Johan B. C. Engelen" Date: Sun, 23 Mar 2014 17:02:42 +0100 Subject: emf-print.cpp : fix coding... :'( (bzr r13189) --- src/extension/internal/emf-print.cpp | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) (limited to 'src/extension/internal/emf-print.cpp') diff --git a/src/extension/internal/emf-print.cpp b/src/extension/internal/emf-print.cpp index 4bb892821..ed25bf767 100644 --- a/src/extension/internal/emf-print.cpp +++ b/src/extension/internal/emf-print.cpp @@ -915,17 +915,13 @@ int PrintEmf::vector_rect_alignment(double angle, Geom::Point vtest){ are slightly displaced. */ Geom::Point PrintEmf::get_pathrect_corner(Geom::Path pathRect, double angle, int corner){ - Geom::Point v1 = Geom::Point(1,0) * Geom::Rotate(-angle); // unit horizontal side (sign change because Y increases DOWN) - Geom::Point v2 = Geom::Point(0,1) * Geom::Rotate(-angle); // unit vertical side (sign change because Y increases DOWN) - Geom::Point center, P1; - int LR; // 1 if Left, 0 if Right - int UL; // 1 if Lower, 0 if Upper (as viewed on screen, y coordinates increase downwards) - center = Geom::Point(0,0); - Geom::Path::const_iterator cit = pathRect.begin(); - for(; cit != pathRect.end_open();++cit) { + Geom::Point center(0,0); + for(Geom::Path::const_iterator cit = pathRect.begin(); cit != pathRect.end_open(); ++cit) { center += cit->initialPoint()/4.0; } + int LR; // 1 if Left, 0 if Right + int UL; // 1 if Lower, 0 if Upper (as viewed on screen, y coordinates increase downwards) switch(corner){ case 1: //UR LR = 0; @@ -944,11 +940,15 @@ Geom::Point PrintEmf::get_pathrect_corner(Geom::Path pathRect, double angle, int UL = 0; break; } - cit = pathRect.begin(); - for(int i; cit != pathRect.end_open();++cit,i++) { + + Geom::Point v1 = Geom::Point(1,0) * Geom::Rotate(-angle); // unit horizontal side (sign change because Y increases DOWN) + Geom::Point v2 = Geom::Point(0,1) * Geom::Rotate(-angle); // unit vertical side (sign change because Y increases DOWN) + Geom::Point P1; + for(Geom::Path::const_iterator cit = pathRect.begin(); cit != pathRect.end_open(); ++cit) { P1 = cit->initialPoint(); - if((LR == (dot(P1 - center,v1)> 0 ? 0 : 1)) && - (UL == (dot(P1 - center,v2)> 0 ? 1 : 0)))break; + + if ( ( LR == (dot(P1 - center,v1) > 0 ? 0 : 1) ) + && ( UL == (dot(P1 - center,v2) > 0 ? 1 : 0) ) ) break; } return(P1); } -- cgit v1.2.3 From 2cadb5d282a9371129a28a142508120bd065454a Mon Sep 17 00:00:00 2001 From: David Mathog Date: Tue, 25 Mar 2014 21:14:20 +0100 Subject: Patch for several issues in libuemf. ----- The patch: 1. Removes half a dozen or so code lines that didn't do anything. 2. Fixes a couple of missing tests for pointers which cannot be NULL. (Note that in many instances the pseudoobject functions in upmf.c do accept null pointers because the objects being constructed have optional parts.) 3. There was one "real" potential memory leak (line 6276 in upmf.c, numbers before the patch). I fixed the others too, but they just never happen (iconv would have to fail in some strange way.) There are no (real) memory access violations or memory leaks in any of this either in standalone libUEMF testing or in use as part of Inkscape as determined from valgrind runs. (bzr r13211) --- src/extension/internal/emf-print.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'src/extension/internal/emf-print.cpp') diff --git a/src/extension/internal/emf-print.cpp b/src/extension/internal/emf-print.cpp index ed25bf767..f4f7f08cb 100644 --- a/src/extension/internal/emf-print.cpp +++ b/src/extension/internal/emf-print.cpp @@ -366,11 +366,13 @@ int PrintEmf::create_brush(SPStyle const *style, PU_COLORREF fcolor) if (!fcolor && style) { if (style->fill.isColor()) { fill_mode = DRAW_PAINT; +#if 0 +// opacity not supported by EMF float opacity = SP_SCALE24_TO_FLOAT(style->fill_opacity.value); if (opacity <= 0.0) { opacity = 0.0; // basically the same as no fill } - +#endif sp_color_get_rgb_floatv(&style->fill.value.color, rgb); hatchColor = U_RGB(255 * rgb[0], 255 * rgb[1], 255 * rgb[2]); -- cgit v1.2.3