1 /*
2  * Copyright (C) 2000-2020 the xine project
3  *
4  * This file is part of xine, a free video player.
5  *
6  * xine is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * xine is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110, USA
19  *
20  * Contains common code to calculate video scaling parameters.
21  * In short, it will map frame dimensions to screen/window size.
22  * Takes into account aspect ratio correction and zooming.
23  */
24 
25 #ifdef HAVE_CONFIG_H
26 #include "config.h"
27 #endif
28 
29 #include <stdio.h>
30 #include <string.h>
31 #include <math.h>
32 
33 #define LOG_MODULE "vo_scale"
34 #define LOG_VERBOSE
35 /*
36 #define LOG
37 */
38 
39 #include <xine/xine_internal.h>
40 #include <xine/vo_scale.h>
41 
42 /*
43  * convert delivered height/width to ideal width/height
44  * taking into account aspect ratio and zoom factor
45  */
46 
_x_vo_scale_compute_ideal_size(vo_scale_t * this)47 void _x_vo_scale_compute_ideal_size (vo_scale_t *this) {
48 
49   if (this->scaling_disabled & ~1) {
50 
51     this->video_pixel_aspect = (this->scaling_disabled & 1) ? 1.0 : this->gui_pixel_aspect;
52 
53   } else {
54     double image_ratio, desired_ratio;
55 
56     /*
57      * aspect ratio
58      */
59 
60     image_ratio = (double) (this->delivered_width - (this->crop_left + this->crop_right)) /
61                   (double) (this->delivered_height - (this->crop_top + this->crop_bottom));
62 
63     switch (this->user_ratio) {
64     case XINE_VO_ASPECT_AUTO:
65       desired_ratio = this->delivered_ratio;
66       break;
67     case XINE_VO_ASPECT_ANAMORPHIC:
68       desired_ratio = 16.0 / 9.0;
69       break;
70     case XINE_VO_ASPECT_DVB:
71       desired_ratio = 2.0 / 1.0;
72       break;
73     case XINE_VO_ASPECT_SQUARE:
74       desired_ratio = image_ratio;
75       break;
76     case XINE_VO_ASPECT_4_3:
77     default:
78       desired_ratio = 4.0 / 3.0;
79     }
80 
81     this->video_pixel_aspect = desired_ratio / image_ratio;
82 
83     _x_assert(this->gui_pixel_aspect != 0.0);
84 
85     /* dont scale just for tiny pixel aspect shift */
86     if (this->scaling_disabled & 1) {
87       if (fabs (this->video_pixel_aspect - 1.0) < 0.01)
88         this->video_pixel_aspect = 1.0;
89     } else {
90       if (fabs (this->video_pixel_aspect / this->gui_pixel_aspect - 1.0) < 0.01)
91         this->video_pixel_aspect = this->gui_pixel_aspect;
92     }
93 
94 #if 0
95 
96     /* onefield_xv divide by 2 the number of lines */
97     if (this->deinterlace_enabled
98 	&& (this->deinterlace_method == DEINTERLACE_ONEFIELDXV)
99 	&& (this->cur_frame->format == XINE_IMGFMT_YV12)) {
100       this->displayed_height  = this->displayed_height / 2;
101       this->displayed_yoffset = this->displayed_yoffset / 2;
102     }
103 #endif
104   }
105 }
106 
107 
108 /*
109  * make ideal width/height "fit" into the gui
110  */
111 
_x_vo_scale_compute_output_size(vo_scale_t * this)112 void _x_vo_scale_compute_output_size (vo_scale_t *this) {
113 
114   int    cropped_width, cropped_height;
115 
116   cropped_width  = this->delivered_width - (this->crop_left + this->crop_right);
117   cropped_height = this->delivered_height - (this->crop_top + this->crop_bottom);
118 
119 
120   if (this->scaling_disabled & ~1) {
121 
122     this->output_width   = cropped_width;
123     this->output_height  = cropped_height;
124     this->displayed_width = cropped_width;
125     this->displayed_height = cropped_height;
126 
127   } else {
128     int    sw, sh;
129     double aspect;
130 
131     aspect = this->video_pixel_aspect;
132     if (!(this->scaling_disabled & 1))
133       aspect /= this->gui_pixel_aspect;
134 
135     sw = (double)(cropped_width * this->gui_height) * aspect / (double)cropped_height + 0.5;
136     sh = (double)(cropped_height * this->gui_width) / ((double)cropped_width * aspect) + 0.5;
137 
138     if ( this->support_zoom ) {
139 
140       /* zoom behaviour:
141        * - window size never changes due zooming
142        * - output image shall be increased whenever there are
143        *   black borders to use.
144        * - exceding zoom shall be accounted by reducing displayed image.
145        */
146       if ((this->gui_width - sw) < (this->gui_height - sh)) {
147         int zh;
148         this->output_width = this->gui_width;
149         this->displayed_width = (double)cropped_width / this->zoom_factor_x + 0.5;
150 
151         this->output_height = sh;
152         zh = (double)this->output_height * this->zoom_factor_y + 0.5;
153         if( zh <= this->gui_height ) {
154           this->displayed_height = cropped_height;
155           this->output_height = zh;
156         } else {
157           this->displayed_height = (double)cropped_height *
158             this->gui_height / this->output_height / this->zoom_factor_y + 0.5;
159           this->output_height = this->gui_height;
160         }
161       } else {
162         int zw;
163         this->output_height = this->gui_height;
164         this->displayed_height = (double)cropped_height / this->zoom_factor_y + 0.5;
165 
166         this->output_width = sw;
167         zw = (double)this->output_width * this->zoom_factor_x + 0.5;
168         if( zw <= this->gui_width ) {
169           this->displayed_width = cropped_width;
170           this->output_width = zw;
171         } else {
172           this->displayed_width = (double)cropped_width *
173             this->gui_width / this->output_width / this->zoom_factor_x + 0.5;
174           this->output_width = this->gui_width;
175         }
176       }
177 
178     } else {
179       if ((this->gui_width - sw) < (this->gui_height - sh)) {
180         this->output_width   = this->gui_width;
181         this->output_height  = sh;
182       } else {
183         this->output_width   = sw;
184         this->output_height  = this->gui_height;
185       }
186       this->displayed_width = cropped_width;
187       this->displayed_height = cropped_height;
188     }
189   }
190 
191   /* make sure displayed values are sane, that is, we are not trying to display
192    * something outside the delivered image. may happen when zoom < 100%.
193    */
194   if( this->displayed_width > this->delivered_width ) {
195     int w;
196     w  = this->output_width * this->delivered_width;
197     w += this->displayed_width >> 1;
198     w /= this->displayed_width;
199     this->output_width = w;
200     this->displayed_width = this->delivered_width;
201   }
202   if( this->displayed_height > this->delivered_height ) {
203     int h;
204     h  = this->output_height * this->delivered_height;
205     h += this->displayed_height >> 1;
206     h /= this->displayed_height;
207     this->output_height = h;
208     this->displayed_height = this->delivered_height;
209   }
210 
211   this->output_xoffset =
212     (this->gui_width - this->output_width) * this->output_horizontal_position + this->gui_x;
213   this->output_yoffset =
214     (this->gui_height - this->output_height) * this->output_vertical_position + this->gui_y;
215 
216   this->displayed_xoffset = ((cropped_width -  this->displayed_width) / 2) + this->crop_left;
217   this->displayed_yoffset = ((cropped_height - this->displayed_height) / 2) + this->crop_top;
218 
219   lprintf ("frame source %d x %d (%d x %d) => screen output %d x %d\n",
220 	   this->delivered_width, this->delivered_height,
221 	   this->displayed_width, this->displayed_height,
222 	   this->output_width, this->output_height);
223 
224   /* calculate borders */
225   if (this->output_height < this->gui_height) {
226     /* top */
227     this->border[0].x = 0;
228     this->border[0].y = 0;
229     this->border[0].w = this->gui_width;
230     this->border[0].h = this->output_yoffset;
231     /* bottom */
232     this->border[1].x = 0;
233     this->border[1].y = this->output_yoffset + this->output_height;
234     this->border[1].w = this->gui_width;
235     this->border[1].h = this->gui_height - this->border[1].y;
236   } else {
237     /* no top/bottom borders */
238     this->border[0].w = this->border[0].h = 0;
239     this->border[1].w = this->border[1].h = 0;
240   }
241 
242   if (this->output_width < this->gui_width) {
243     /* left */
244     this->border[2].x = 0;
245     this->border[2].y = 0;
246     this->border[2].w = this->output_xoffset;
247     this->border[2].h = this->gui_height;
248     /* right */
249     this->border[3].x = this->output_xoffset + this->output_width;
250     this->border[3].y = 0;
251     this->border[3].w = this->gui_width - this->border[3].x;
252     this->border[3].h = this->gui_height;
253   } else {
254     /* no left/right borders */
255     this->border[2].w = this->border[2].h = 0;
256     this->border[3].w = this->border[3].h = 0;
257   }
258 }
259 
260 /*
261  * return true if a redraw is needed due resizing, zooming,
262  * aspect ratio changing, etc.
263  */
264 
_x_vo_scale_redraw_needed(vo_scale_t * this)265 int _x_vo_scale_redraw_needed (vo_scale_t *this) {
266   int gui_x, gui_y, gui_width, gui_height, gui_win_x, gui_win_y;
267   double gui_pixel_aspect, video_pixel_aspect;
268   int ret = 0;
269 
270   _x_assert(this->frame_output_cb);
271   if ( ! this->frame_output_cb )
272     return 0;
273 
274   video_pixel_aspect = this->video_pixel_aspect;
275   /* Nasty: tweaking GUI into different output window size will work from 2nd attempt on only... */
276   if ( this->scaling_disabled & 1 )
277     video_pixel_aspect *= this->gui_pixel_aspect;
278 
279   this->frame_output_cb (this->user_data,
280 			 this->delivered_width - (this->crop_left + this->crop_right),
281 			 this->delivered_height - (this->crop_top + this->crop_bottom),
282 			 video_pixel_aspect,
283 			 &gui_x, &gui_y, &gui_width, &gui_height,
284 			 &gui_pixel_aspect, &gui_win_x, &gui_win_y );
285 
286   if ( (gui_x != this->gui_x) || (gui_y != this->gui_y)
287       || (gui_width != this->gui_width) || (gui_height != this->gui_height)
288       || (gui_pixel_aspect != this->gui_pixel_aspect)
289       || (gui_win_x != this->gui_win_x) || (gui_win_y != this->gui_win_y) ) {
290 
291     this->gui_x      = gui_x;
292     this->gui_y      = gui_y;
293     this->gui_width  = gui_width;
294     this->gui_height = gui_height;
295     this->gui_win_x  = gui_win_x;
296     this->gui_win_y  = gui_win_y;
297     this->gui_pixel_aspect = gui_pixel_aspect;
298 
299     ret = 1;
300   }
301   else
302     ret = this->force_redraw;
303 
304   this->force_redraw = 0;
305   return ret;
306 }
307 
308 /*
309  *
310  */
311 
_x_vo_scale_translate_gui2video(vo_scale_t * this,int x,int y,int * vid_x,int * vid_y)312 void _x_vo_scale_translate_gui2video(vo_scale_t *this,
313 				 int x, int y,
314 				 int *vid_x, int *vid_y) {
315 
316   if (this->output_width > 0 && this->output_height > 0) {
317     /*
318      * 1.
319      * the driver may center a small output area inside a larger
320      * gui area.  This is the case in fullscreen mode, where we often
321      * have black borders on the top/bottom/left/right side.
322      */
323     x -= this->output_xoffset;
324     y -= this->output_yoffset;
325 
326     /*
327      * 2.
328      * the driver scales the delivered area into an output area.
329      * translate output area coordianates into the delivered area
330      * coordiantes.
331      */
332 
333     x = x * this->displayed_width  / this->output_width  + this->displayed_xoffset;
334     y = y * this->displayed_height / this->output_height + this->displayed_yoffset;
335   }
336 
337   *vid_x = x;
338   *vid_y = y;
339 }
340 
341 
_x_vo_scale_map(vo_scale_t * this,vo_scale_map_t * map)342 vo_scale_map_res_t _x_vo_scale_map (vo_scale_t *this, vo_scale_map_t *map) {
343   double fx, fy, t;
344   int vw, vh, ax, ay;
345 
346   if (!this || !map)
347     return VO_SCALE_MAP_WRONG_ARGS;
348 
349   if ((this->displayed_width <= 0) || (this->displayed_height <= 0))
350     return VO_SCALE_MAP_ERROR;
351 
352   vw = this->delivered_width - (this->crop_left + this->crop_right);
353   vh = this->delivered_height - (this->crop_top + this->crop_bottom);
354 
355   if ((map->out.x1 <= 0) || (map->out.y1 <= 0)) {
356     map->out.x1 = vw;
357     map->out.y1 = vh;
358     if ((map->out.x1 <= 0) || (map->out.y1 <= 0))
359       return VO_SCALE_MAP_ERROR;
360   }
361 
362   t = (this->output_width == this->displayed_width) ? 1.0 : (double)this->output_width / (double)this->displayed_width;
363   fx = (map->out.x1 == vw) ? 1.0 : (double)vw / (double)map->out.x1;
364   ax = fx * (this->output_xoffset - (t * this->displayed_xoffset));
365   fx *= t;
366 
367   t = (this->output_height == this->displayed_height) ? 1.0 : (double)this->output_height / (double)this->displayed_height;
368   fy = (map->out.y1 == vh) ? 1.0 : (double)vh / (double)map->out.y1;
369   ay = fy * (this->output_yoffset - (t * this->displayed_yoffset));
370   fy *= t;
371 
372   map->out.x1 = ax + (fx * (map->out.x0 + map->in.x1));
373   map->out.x0 = ax + (fx * map->out.x0);
374   map->out.y1 = ay + (fy * (map->out.y0 + map->in.y1));
375   map->out.y0 = ay + (fy * map->out.y0);
376 
377   map->in.x0 = 0;
378   ax = this->output_xoffset;
379   if (map->out.x0 < ax) {
380     map->in.x0 = (ax - map->out.x0) / fx;
381     if (map->in.x0 >= map->in.x1)
382       return VO_SCALE_MAP_OUTSIDE;
383     map->out.x0 = ax;
384   }
385   map->in.y0 = 0;
386   ay = this->output_yoffset;
387   if (map->out.y0 < ay) {
388     map->in.y0 = (ay - map->out.y0) / fy;
389     if (map->in.y0 >= map->in.y1)
390       return VO_SCALE_MAP_OUTSIDE;
391     map->out.y0 = ay;
392   }
393 
394   ax = this->output_xoffset + this->output_width;
395   if (map->out.x1 > ax) {
396     map->in.x1 -= (map->out.x1 - ax) / fx;
397     if (map->in.x1 <= map->in.x0)
398       return VO_SCALE_MAP_OUTSIDE;
399     map->out.x1 = ax;
400   }
401   ay = this->output_yoffset + this->output_height;
402   if (map->out.y1 > ay) {
403     map->in.y1 -= (map->out.y1 - ay) / fy;
404     if (map->in.y1 <= map->in.y0)
405       return VO_SCALE_MAP_OUTSIDE;
406     map->out.y1 = ay;
407   }
408 
409   return VO_SCALE_MAP_OK;
410 }
411 
412 /*/
413  * @brief Table for description of a given ratio code.
414  *
415  * @note changing the size of the elements of the array will break
416  * ABI, so please don't do that unless you absolutely can't continue
417  * with the current size.
418  */
419 const char _x_vo_scale_aspect_ratio_name_table[][8] = {
420   "auto",   /* XINE_VO_ASPECT_AUTO */
421   "square", /* XINE_VO_ASPECT_SQUARE */
422   "4:3",    /* XINE_VO_ASPECT_4_3 */
423   "16:9",   /* XINE_VO_ASPECT_ANAMORPHIC */
424   "2:1",    /* XINE_VO_ASPECT_DVB */
425   "unknown" /* All the rest */
426 };
427 
428 /*
429  * config callbacks
430  */
vo_scale_horizontal_pos_changed(void * data,xine_cfg_entry_t * entry)431 static void vo_scale_horizontal_pos_changed(void *data, xine_cfg_entry_t *entry) {
432   vo_scale_t *this = (vo_scale_t *)data;
433 
434   this->output_horizontal_position = entry->num_value / 100.0;
435   this->force_redraw = 1;
436 }
437 
vo_scale_vertical_pos_changed(void * data,xine_cfg_entry_t * entry)438 static void vo_scale_vertical_pos_changed(void *data, xine_cfg_entry_t *entry) {
439   vo_scale_t *this = (vo_scale_t *)data;
440 
441   this->output_vertical_position = entry->num_value / 100.0;
442   this->force_redraw = 1;
443 }
444 
vo_scale_disable_scaling_changed(void * data,xine_cfg_entry_t * entry)445 static void vo_scale_disable_scaling_changed(void *data, xine_cfg_entry_t *entry) {
446   vo_scale_t *this = (vo_scale_t *)data;
447 
448   this->scaling_disabled &= ~2;
449   this->scaling_disabled |= (entry->num_value & 1) << 1;
450   this->force_redraw = 1;
451 }
452 
vo_scale_square_pixels_changed(void * data,xine_cfg_entry_t * entry)453 static void vo_scale_square_pixels_changed(void *data, xine_cfg_entry_t *entry) {
454   vo_scale_t *this = (vo_scale_t *)data;
455 
456   this->scaling_disabled &= ~1;
457   this->scaling_disabled |= entry->num_value & 1;
458   this->force_redraw = 1;
459 }
460 
461 /*
462  * initialize rescaling struct
463  */
464 
_x_vo_scale_cleanup(vo_scale_t * self,config_values_t * config)465 void _x_vo_scale_cleanup(vo_scale_t *self, config_values_t *config) {
466   config->unregister_callbacks (config, NULL, NULL, self, sizeof (*self));
467 }
468 
_x_vo_scale_init(vo_scale_t * this,int support_zoom,int scaling_disabled,config_values_t * config)469 void _x_vo_scale_init(vo_scale_t *this, int support_zoom, int scaling_disabled,
470                    config_values_t *config ) {
471 
472   memset( this, 0, sizeof(vo_scale_t) );
473   this->support_zoom = support_zoom;
474   this->force_redraw = 1;
475   this->zoom_factor_x = 1.0;
476   this->zoom_factor_y = 1.0;
477   this->gui_pixel_aspect = 1.0;
478   this->user_ratio = XINE_VO_ASPECT_AUTO;
479   this->delivered_ratio = 0.0;
480 
481   this->crop_left   = 0;
482   this->crop_right  = 0;
483   this->crop_top    = 0;
484   this->crop_bottom = 0;
485 
486   this->output_horizontal_position =
487     config->register_range(config, "video.output.horizontal_position", 50, 0, 100,
488       _("horizontal image position in the output window"),
489       _("If the video window's horizontal size is bigger than the actual image "
490 	"to show, you can adjust the position where the image will be placed.\n"
491 	"The position is given as a percentage, so a value of 50 means \"in the "
492 	"middle\", while 0 means \"at the very left\" and 100 \"at the very right\"."),
493       10, vo_scale_horizontal_pos_changed, this) / 100.0;
494   this->output_vertical_position =
495     config->register_range(config, "video.output.vertical_position", 50, 0, 100,
496       _("vertical image position in the output window"),
497       _("If the video window's vertical size is bigger than the actual image "
498 	"to show, you can adjust the position where the image will be placed.\n"
499 	"The position is given as a percentage, so a value of 50 means \"in the "
500 	"middle\", while 0 means \"at the top\" and 100 \"at the bottom\"."),
501       10, vo_scale_vertical_pos_changed, this) / 100.0;
502   this->scaling_disabled = scaling_disabled << 2;
503   this->scaling_disabled |=
504     config->register_bool(config, "video.output.disable_scaling", 0,
505       _("disable all video scaling"),
506       _("If you want the video image to be always shown at its original resolution, "
507 	"you can disable all image scaling here.\n"
508 	"This of course means that the image will no longer adapt to the size of the "
509 	"video window and that videos with a pixel aspect ratio other than 1:1, like "
510 	"anamorphic DVDs, will be shown distorted. But on the other hand, with some "
511 	"video output drivers like XShm, where the image scaling is not hardware "
512 	"accelerated, this can dramatically reduce CPU usage."),
513       10, vo_scale_disable_scaling_changed, this) << 1;
514   this->scaling_disabled |=
515     config->register_bool(config, "video.output.square_pixels", 0,
516       _("treat screen pixels as exactly square"),
517       _("Many screens have \"only\" almost square pixels, like 94x93 dpi.\n"
518 	"This little deviation is important for true size graphics applications.\n"
519 	"For video, however, it often just means unnecessary black bars and less "
520 	"sharpness.\n"),
521       10, vo_scale_square_pixels_changed, this);
522 }
523 
524